snibgo's ImageMagick pages

Filling holes

A process module estimates colours for transparent pixels by cloning colours from elsewhere in the image.

The general technique is also known as infilling or inpainting. The process is similar to darning a sock or invisibly-mending a hole in a jacket.

This page describes a process module, fillholes, and a variety of alternative techniques that don't use process modules.

The module fills pixels in "onion-skin" order, from the boundaries of holes inwards. For each pixel to be filled, a surrounding window of known pixels is searched for within the input image. When the best match ("exemplar") is found, the pixel centred at the found window is used to fill the unknown pixel. This is easy to understand and to code, and is highly effective. Sadly it is slow. We can't have everything.

(An alternative process module fills pixels in a different order. See Filling holes in priority order.)

This is a process module. The source code is provided below: fillholes.c. For details of integrating this with IM, see my page Process modules.

Description

The module fillholes.c operates on all images in the list. For each image, it calls function fillholes() which fills the holes in one image, and returns it.

Function fillholes() clones the input image into holed_image and new_image. For each pass of the algorithm, it traverses all pixels in new_image, using a read-only view of three rows at a time. Each pixel that is transparent and has at least one non-transparent neighbour is processed.

When a destination pixel in new_image is selected for processing, we define a window around the corresponding pixel in holed_image, and search for that window in input_image. The search is similar to IM's usual -subimage-search: the window around the destination pixel is a subimage, and we have a window of the same size that slides around the larger image, to find the closest match. Unlike IM's usual -subimage-search, for comparison purposes we ignore pixels that are fully transparent. If the central pixel in the sliding window in input_image is transparent, the search returns the maximum possible search score, indicating "not found". Otherwise, we get a location and a search score at that location. The minimum score for all searches for this pixel gives the best match. Now we know the exemplar position, we copy either:

When comparing windows, a pixel that is transparent in input_image but not in holed_image results in "not found".

When comparing a subimage window with a potential exemplar window, the code returns "not found" when any of the following are true:

In the fillholes module, a single pass of the above will process all the pixels around the edges of all the holes. Within each pass, processing is in scanline order. Passes are repeated until no more pixels are changed. The module processes pixels in "onion skin" order.

Throughout this module, pixels with alpha <= 0 are taken as transparent, while pixels with alpha > 0 are taken as non-transparent. (HDRI IM permits negative alpha values.)

Any semi-transparent pixels in the input image will be untouched. This generally looks horrible, so alpha should first be made binary, for example by -channel A -threshold 50% +channel.

Each pixel in a hole is filled only once. When a value has been copied in, it isn't changed. (This is true of both fillholes and fillholespri.) An alternative strategy might fill pixels tentatively.

Strategy note: the subimage window used for searching is from holed_image, which is the result from the previous pass. An alternative would be to use new_image, which would do all the processing in a single pass, so it would work from top to bottom instead of outside to inside.

Programming note: holed_image and the input_image are both used for two purposes, so they each have two calls to GetCacheViewVirtualPixels(). Even though these are read-only, two calls to GetCacheViewVirtualPixels() on the same view causes problems, with incorrect values in a pixel array. By creating a cloned view with CloneCacheView(), pixel arrays are derived from individual views, and array values are correct.

If a hole is at an edge of the image, or within WindowRadius of an edge, the module will use virtual pixels in the subimage search. Hence "-virtual-pixel None" is useful, so these will be ignored.

If the input image is entirely transparent, it won't be filled at all.

Arguments

The process can take many arguments. The important parameter for good quality is window_radius. The other parameters are mostly to get good quality quickly.

Option Description
Short
form
Long form
wr N window_radius N Radius of window for searches, >= 1.
Window will be D*D square where D = radius*2+1.
The minimum radius is one.
Default 1.
lsr N limit_search_radius N Limit radius from transparent pixel to search for source, >= 0.
Default = 0 = no limit
als X auto_limit_search X Automatically limit the search radius.
X is on or off.
Default = on
s X search X Mode for searching, where X is entire or random or skip.
Default = entire.
rs N rand_searches N For random searches, the number to make per attempted match.
Default = 0 = minimum of search field width and height
sn N skip_num N For skip searches, the number of positions to skip in x and y directions.
Default = 0 = window radius
hc N hom_chk N Homogeneity check.
N is either a number, typically 0 < N < 0.5, or off to remove the check.
Default = 0.1
e X search_to_edges X Whether to search for matches to image edges, so the result will be influenced by virtual pixels.
Set to on or off.
Default = on
st N similarity_threshold N Stop searching for a window when RMSE <= N, when a match is "good enough".
Typically use 0.0 <= N <= 1.0, eg 0.01.
Default = 0 = keep going to find the best match.
dt N dissimilarity_threshold N When a best match is found, if score > N, reject it, leaving pixels unfilled.
Typically use 0.0 <= N <= 1.0, eg 0.05.
If zero, only perfect matches will be filled.
Default = no threshold, so no rejections from bad scores.
cp X copy X Mode for copying, where X is either onepixel or window.
Default = onepixel.
cr N copy_radius N Radius of pixels to be copied from a matching window, >= 1.
Window will be D*D square where D = radius*2+1.
Default: the value of window_radius.
c favour_close Favour search results close to destination pixel. EXPERIMENTAL.
w filename write filename Write the input image, then one frame per pass.
Example filename: frame_%06d.png
w2 filename write2 filename Write the input image, then one frame per changed pixel.
v verbose Write some text output to stderr.

The window_radius should be at least as large as the level of detail that matters. It must be at least one, which gives a 3x3 window. The default is one, but this is often too low. For ordinary photographs, five to ten often works well, but a regular repeating pattern may need more.

For increased performance, limit_search_radius can be set to a positive number, either as pixels or a percentage. lsr limits the area within the input image that will be searched for a pixel value. The value defines a square centred on the transparent pixel; only pixels within this square will be considered. Smaller values increase performance, but may give worse results. If any holes are larger than limit_search_radius, they will not be completely filled. If this happens, and the verbose option is used, the module sends a warning to stderr. (FUTURE: perhaps the module could also take remedial action. For example, it could repeat the operation, which would search within the already-filled area.)

The numeric values to either or both window_radius and limit_search_radius can have a percent (%) suffix. When they do, the radius is calculated as a percentage of the minimum of the image width and height, rounded to the nearest integer. For example, for a 400x300 pixel image and limit_search_radius 2.6%, the actual radius will be 2.6/100*300, which is 8, to the nearest integer.

As a rule of thumb, limit_search_radius 5% often works well, provided no holes are larger than this.

The auto_limit_search option is on by default. When it is on, the bounding box of the matches found in the first pass is recorded, and subsequent passes will search only within that box. It may slightly reduce quality, so it can be turned off. For very small values of lsr, eg 1 or 2 pixels, the boundary box will be artificially small, so auto_limit_search should be turned off. (The fillholespri module doesn't work in passes, so the mechanism is slightly different.)

By default (search entire), when searching for an exemplar, the search is made in all possible positions of the window within the image, or within the defined search radius. The search terminates when a perfect match is found, or "good enough" as defined by the threshold, or every position has been examined. The search space is large, with approximately 1 million positions available in a 1000x1000 pixel image.

Two alternative search modes are provided that can greatly reduce the number of searches made. These are search random and search skip. They may not find the best match, but will be much faster.

search random is very loosely based on the PatchMatch paper. It searches n random positions in the search field, uniformly distributed in x and y. The search terminates when the perfect or "good enough" score is found, or all n positions have been examined. By default, n is the smallest of the search field width and height, but can be set with search random rand_searches n. In the extreme, n can be set to 1, which quickly does just one random search, which will probably be a poor match. If the rand_searches number is suffixed '%' or 'c', it is read as a percentage of the default, eg "500c" for five times as much as the default.

When the best random match has been found, the search is refined by testing all positions near it, plus or minus the window radius.

The second alternative is search skip, which is like search entire but skips over n positions in the x and y directions. When the best match is found, it then searches the positions around that match, plus or minus n. This is helpful when the window is large. For example with window_radius 10, a search skip will improve the speed of the initial search by a factor of 100, though it will then do a secondary search of 441 (=21*21) positions. If the skip_num is set to a value greater than the window_radius, there is less chance of finding the best possible match.

By default, a candidate window will fail to match if any of its pixels fall significantly outside the range of the subimage pixels. This is a check for homogeneity, reducing any tendency to fill holes with colours that (to humans) clearly don't belong there, even if pairwise corresponding opaque pixels match perfectly. This check is important (ironically) for inhomogenous images such as the green square and church, and less important for homogenous images such as the grass. This is controlled by the hom_chk or hc option. By default, hc is 0.1. Values in each of the channels are allowed fall outside the range, provided they are within 10% of the range. Any values outside that limit will cause the candidate window to be rejected. If hc is set to 0.0, no candidate pixels will fall outside the range. hc can even be negative, ensuring that candidate pixels fall somewhat inside the range. To disable the test, use off instead of a number. When the homogeneity test is disabled, the score is simply the MSE (mean squared error) between corresponding pixels that are opaque in the subimage and candidate window.

By default, the window is searched for within the image. Specifying search_to_edges will slightly extend the search area so that although the central pixel of the searched window will be within the image, other window pixels may be beyond it, so these will be virtual pixels.

The similarity_threshold is a "good enough" setting. Be default, the setting is 0.0, which means searches will test all positions. If a larger number is used (eg 0.01, 0.1, up to 1.0) the search will stop as soon as a match is found that is better than this number. For photos, a value of 0.01 works fine.

By default, the module searches for the best match, and copies pixels from there, however good or bad the match is. Setting a large window radius and copying the entire window is quick but can give bad quality with blocky artifacts. The dissimilarity_threshold or dt places a lower bound on the required quality, and is useful for getting high quality quickly by using multiple passes. The trick is to set a threshold of, say, 0.05. This will quickly copy good matches, but will leave some areas unfilled. Repeat the process with a smaller window radius. Keep going as much as desired, but don't set a threshold on the last call.

The default dissimilarity_threshold is 1.0, which means a very bad match may be copied, if none better are available. If holes are filled badly, consider reducing this value, eg to 0.5 or 0.1. Then, if the best match is poor, pixels will remain unfilled, and these could be filled in a later pass with a larger lsr.

I think you would always want similarity_threshold < dissimilarity_threshold.

The copy mode determines how many pixels are copied after a successful match. With mode onepixel, only the central pixel is copied from the exemplar. With mode window, all pixels that are not transparent in the exemplar, but the corresponding subimage pixels are transparent, are copied. This dramatically increases performance but reduces quality. For a compromise, copy the window but set copy_radius to some size smaller than the window_radius, for example "wr 10 cr 5". Or set a dissimilarity_threshold and repeat the process.

The module is not sensitive to IM's -channel setting.

Examples

Make a test image, with a hole.

%IMG7%magick ^
  -size 100x100 xc:lime ^
  -stroke Red -draw "line 0,0 99,99" ^
  -stroke Blue -strokewidth 3 -draw "line 0,20 69,99" ^
  ( +clone -fill White -colorize 100 ^
    -fill Black -draw "Rectangle 30,20 79,69" ^
    -threshold 50%% ^
  ) ^
  -alpha off ^
  -compose CopyOpacity -composite ^
  fh_src1.png
fh_src1.png

Fill any holes.

%IM7DEV%magick ^
  fh_src1.png ^
  -virtual-pixel None ^
  -process 'fillholes' ^
  fh_src1_fh.png

The blue line hasn't infilled well because the window radius is too small.

fh_src1_fh.png

As previous, but with homogeneity check off.

%IM7DEV%magick ^
  fh_src1.png ^
  -virtual-pixel None ^
  -process 'fillholes hc off' ^
  fh_src1_fhnhc.png

This makes no difference.

fh_src1_fhnhc.png

A larger radius cures the problem with the blue line.

%IM7DEV%magick ^
  fh_src1.png ^
  -virtual-pixel None ^
  -process 'fillholes window_radius 2' ^
  fh_src1_fh2.png
fh_src1_fh2.png

We can make an animation of the infill process.

%IM7DEV%magick ^
  fh_src1.png ^
  -virtual-pixel None ^
  -process 'fillholes wr 2 write fh_x_fr_%%06d.png' ^
  null:

%IMG7%magick ^
  fh_x_fr_*.png ^
  -scale 400%% -layers optimize ^
  fh_x.gif
fh_x.gif

A more complex graphic, with five holes.

%IMG7%magick ^
  -size 180x180 xc:lime ^
  -fill Black ^
  -draw "translate  60,60  circle 0,0 0,25" ^
  -draw "translate  60,120 circle 0,0 0,25" ^
  -draw "translate 120,60  circle 0,0 0,25" ^
  -draw "translate 120,120 circle 0,0 0,25" ^
  ( +clone ^
    -fill White -colorize 100 ^
    -fill Black ^
    +antialias ^
    -draw "translate  30,30  circle 0,0 0,20" ^
    -draw "translate 150,30  circle 0,0 0,22" ^
    -draw "translate  30,150 circle 0,0 0,24" ^
    -draw "translate 150,150 circle 0,0 0,26" ^
    -draw "rectangle 60,60 119,119" ^
  ) ^
  -alpha off -compose CopyOpacity -composite ^
  fh_src2.png
fh_src2.png
%IM7DEV%magick ^
  fh_src2.png ^
  -virtual-pixel None ^
  -process ^
    'fillholes' ^
  fh_src2a_fh.png

The window radius is too small.

fh_src2a_fh.png

The smallest radius to get the desired result is 6.
(The top, bottom and sides of the circles have 13 non-lime pixels.)

%IM7DEV%magick ^
  fh_src2.png ^
  -virtual-pixel None ^
  -process ^
    'fillholes window_radius 6' ^
  fh_src2_fh.png

The result looks like exactly what we would expect.

fh_src2_fh.png

The module's verbose option writes some text information to stderr:

%IM7DEV%magick ^
  fh_src2.png ^
  -virtual-pixel None ^
  -process 'fillholes window_radius 6 verbose' ^
  null: >fh_verb.lis 2^>^&1
fillhole options:   window_radius 6  limit_search_radius 0  auto_lsr on  search entire  hom_chk 0.1  refine on  similarity_threshold 0  copy onepixel  copy_radius 0  verbose
fillholes: Input image [fh_src2.png] 180x180 depth is 16
pfh->WindowRadPix = 6
pfh->Match.limSrchRad = 0x0
pfh->CopyRadPix = 0
pfh->CompWind.WorstCompare = 1.61611e+27
pfh->sqDim = 13
pfh->sqCopyDim = 1
pfh->Match.RandSearchesI = 0
pfh->Match.SkipNumI = 0
pfh->Match.simThresholdSq = 0
pfh->Match.dissimThresholdSq = 1.61611e+27
Finished fillholes nIter=31

Photographs

A reduced-size photo for tests.

set FH_PHSRC=fh_phsrc.png

if not exist %FH_PHSRC% %IMG7%magick ^
  %PICTLIB%20130117\AGA_1135.jpg ^
  -resize 400x400 ^
  +depth ^
  %FH_PHSRC%
fh_phsrc.pngjpg

Add holes, and fill them, with default settings.

%IM7DEV%magick ^
  %FH_PHSRC% ^
  ( +clone ^
    -fill white -colorize 100 ^
    -fill Black +antialias ^
    -draw "translate 40,40 circle 0,0 0,20" ^
    -draw "translate 120,135 circle 0,0 0,20" ^
    -draw "translate 200,300 circle 0,0 0,20" ^
    -draw "translate 40,280 circle 0,0 0,20" ^
    -draw "translate 240,180 circle 0,0 0,20" ^
    -draw "translate 0,400 circle 0,0 0,20" ^
  ) ^
  -alpha off -compose CopyOpacity -composite ^
  +write fh_ph0.png ^
  -process 'fillholes' ^
  fh_ph0_fh.png

call StopWatch 
0 00:03:13

StopWatch shows the elapsed time in days hours:minutes:seconds.

The result looks horrible.

fh_ph0.pngjpg fh_ph0_fh.pngjpg

Increase the window_radius.

%IM7DEV%magick ^
  fh_ph0.png ^
  -process 'fillholes wr 5' ^
  fh_ph1_fh.png
0 00:12:23

The visual result is good but performance is bad.

fh_ph0.pngjpg fh_ph1_fh.pngjpg

Copy the entire window_radius.

%IM7DEV%magick ^
  fh_ph0.png ^
  -process 'fillholes wr 5 copy window' ^
  fh_ph1a_fh.png
0 00:00:33

Better performance but lower quality, eg roof line and left edge of tower.

fh_ph0.pngjpg fh_ph1a_fh.pngjpg

Copy the central part of the window.

%IM7DEV%magick ^
  fh_ph0.png ^
  -process 'fillholes wr 5 copy window cr 3' ^
  fh_ph1b_fh.png
0 00:00:59

Slower but slightly better quality than the previous.

fh_ph0.pngjpg fh_ph1b_fh.pngjpg

Limit the search radius.

 %IM7DEV%magick ^
  fh_ph0.png ^
  -process 'fillholes wr 5 lsr 10%%' ^
  fh_ph2_fh.png
0 00:00:14

Limiting the search radius improved performance by a factor of about 30.
It has also improved the infill on the left of the tower.

fh_ph0.pngjpg fh_ph2_fh.pngjpg

As previous but with random-search.

 %IM7DEV%magick ^
  -seed 1234 ^
  fh_ph0.png ^
  -process 'fillholes wr 5 lsr 10%% search random' ^
  fh_ph2r_fh.png
0 00:00:01

Very quick. Slightly worse result.

fh_ph0.pngjpg fh_ph2r_fh.pngjpg

As previous but with skip-search.

 %IM7DEV%magick ^
  fh_ph0.png ^
  -process 'fillholes wr 5 lsr 10%% search skip' ^
  fh_ph2k_fh.png
0 00:00:02

Quicker than random, and slightly better result.

fh_ph0.pngjpg fh_ph2k_fh.pngjpg

Another example, with noise that removes half the pixels.

When holes are close together, we need a small window_radius.

%IM7DEV%magick ^
  -seed 1234 ^
  %FH_PHSRC% ^
  ( +clone ^
    +noise Random -modulate 100,0,100 ^
    -threshold 50%% ^
  ) ^
  -alpha off -compose CopyOpacity -composite ^
  +write fh_ph3.png ^
  -process 'fillholes lsr 10%%' ^
  fh_ph3_fh.png
0 00:00:31

The output contains noise, most evident in the flagpole.
But the result is impressive.

fh_ph3.pngjpg fh_ph3_fh.pngjpg

In the current algorithm, transparent pixels within a search window create a "not found" condition, so we need small window_radius when holes are close together. But large holes need a large window_radius.

When an image has widely-spaced large holes and close-spaced small holes, there is no ideal window_radius. The module can be run multiple times, with increasing limit_srch_radius or window_radius.

Increment limit_srch_radius.
For such small values of lsr, we turn auto_limit_search off.

 %IM7DEV%magick ^
  -seed 1234 ^
  %FH_PHSRC% ^
  ( +clone ^
    -fill white -colorize 100 ^
    +noise Random -modulate 100,0,100 ^
    -threshold 50%% ^
    -fill Black +antialias ^
    -draw "translate 40,40 circle 0,0 0,20" ^
    -draw "translate 120,135 circle 0,0 0,20" ^
    -draw "translate 200,300 circle 0,0 0,20" ^
    -draw "translate 40,280 circle 0,0 0,20" ^
    -draw "translate 240,180 circle 0,0 0,20" ^
    -draw "translate 0,400 circle 0,0 0,20" ^
  ) ^
  -alpha off -compose CopyOpacity -composite ^
  +write fh_ph4.png ^
  -process 'fillholes wr 1 lsr 1 als off' ^
  -process 'fillholes wr 1 lsr 2 als off' ^
  -process 'fillholes wr 1 lsr 3 als off' ^
  -process 'fillholes wr 1 lsr 4 als off' ^
  -process 'fillholes wr 1 lsr 5 als off' ^
  -process 'fillholes wr 1 lsr 6 als off' ^
  -process 'fillholes wr 1 lsr 7 als off' ^
  fh_ph4_fh.png
0 00:00:04

Very fast, but the edges are noisy.

fh_ph4.png fh_ph4_fh.pngjpg

Increment window_radius.

 %IM7DEV%magick ^
  fh_ph4.png ^
  -process 'fillholes wr 1 lsr 10%%' ^
  -process 'fillholes wr 2 lsr 10%%' ^
  -process 'fillholes wr 3 lsr 10%%' ^
  -process 'fillholes wr 4 lsr 10%%' ^
  -process 'fillholes wr 5 lsr 10%%' ^
  -process 'fillholes wr 6 lsr 10%%' ^
  -process 'fillholes wr 7 lsr 10%%' ^
  fh_ph5_fh.png
0 00:00:32

Slower, but the result is less noisy.

fh_ph4.pngjpg fh_ph5_fh.pngjpg

A "hole" doesn't need to be surrounded by image pixels. We can extend an image, for example to the right and bottom:

%IM7DEV%magick ^
  %FH_PHSRC% ^
  -background None ^
  -size 10x1 xc:None +append +repage ^
  -size 1x10 xc:None -append +repage ^
  -process 'fillholes wr 5 lsr 10%%' ^
  fh_ph6_fh.png
0 00:00:12
fh_ph6_fh.pngjpg

An example with a more "natural" image:

toes.png.

toes.pngjpg

With Gimp, erase (make transparent) all non-grass pixels.

toes_holed.png.

toes_holed.png

The erasing process does not need to be precise, and can overflow into the grass.

The image has a slight coincidental pattern at the hole edge. Processes will tend to repeat the pattern in the fill. I show this pattern circled in red:

%IM7DEV%magick ^
  toes_holed.png ^
  -stroke red -fill none ^
  -draw "translate 108,180 circle 0,0 0,20" ^
  fh_red_pat.png
fh_red_pat.pngjpg

The hole covers most of the image, so limit_search_radius would leave many pixels unfilled. We might search the entire image, or use limit_search_radius but repeat the process until no unfilled pixels remain.

Search the entire image.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 5' ^
  fh_th1_fh.png
0 00:23:21

Visually quite good. Horribly slow.

fh_th1_fh.pngjpg

Limit the search radius, and repeat.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 5 lsr 10%%' ^
  -process 'fillholes wr 5 lsr 10%%' ^
  -process 'fillholes wr 5 lsr 10%%' ^
  -process 'fillholes wr 5 lsr 10%%' ^
  -process 'fillholes wr 5 lsr 10%%' ^
  -process 'fillholes wr 5 lsr 10%%' ^
  -process 'fillholes wr 5 lsr 10%%' ^
  fh_th2_fh.png
0 00:00:39

Quicker but looks horrible, with many repeated patterns.

fh_th2_fh.pngjpg

Search entire image, window mode.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 5 copy window' ^
  fh_th3_fh.png
0 00:00:52

Even quicker and looks better.

fh_th3_fh.pngjpg

Search entire image, window mode, with reduced copy radius.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 5 cr 3 copy window' ^
  fh_th3a_fh.png
0 00:01:40

Reducing copy radius takes longer but improves quality.

fh_th3a_fh.pngjpg

Limit the search radius, window mode, and repeat.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 5 lsr 10%% copy window' ^
  -process 'fillholes wr 5 lsr 10%% copy window' ^
  -process 'fillholes wr 5 lsr 10%% copy window' ^
  -process 'fillholes wr 5 lsr 10%% copy window' ^
  -process 'fillholes wr 5 lsr 10%% copy window' ^
  -process 'fillholes wr 5 lsr 10%% copy window' ^
  -process 'fillholes wr 5 lsr 10%% copy window' ^
  fh_th4_fh.png
0 00:00:06

Very quick but looks horrible.

fh_th4_fh.pngjpg

Search entire image with a larger window, window mode.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 10 copy window' ^
  fh_th5_fh.png
0 00:01:01

The long grass blade has been replicated a few times.

fh_th5_fh.pngjpg

As previous, with homogeneity check off.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 10 copy window hc off' ^
  fh_th6_fh.png
0 00:00:56

Turning the check off is slightly faster. This image doesn't have homogenous areas, so the check does more harm than good.

fh_th6_fh.pngjpg

As previous, but search random.
The seed makes it repeatable.

%IM7DEV%magick ^
  -seed 1234 ^
  toes_holed.png ^
  -process 'fillholes wr 10 search random hc off copy window' ^
  fh_thr1_fh.png
0 00:00:01

Very fast, but not great result.

fh_thr1_fh.pngjpg

As previous, but reduce the copy radius.

%IM7DEV%magick ^
  -seed 1234 ^
  toes_holed.png ^
  -process 'fillholes wr 10 search random hc off copy window cr 1' ^
  fh_thr2_fh.png
0 00:00:13

Yuck.

fh_thr2_fh.pngjpg

Instead, a smaller window.

%IM7DEV%magick ^
  -seed 1234 ^
  toes_holed.png ^
  -process 'fillholes wr 5 search random hc off copy window' ^
  fh_thr3_fh.png
0 00:00:01

Very fast, and slightly improved quality.

fh_thr3_fh.pngjpg

As previous but with ten times as many random searches.

%IM7DEV%magick ^
  -seed 1234 ^
  toes_holed.png ^
  -process 'fillholes wr 5 search random rand_searches 1000%% hc off copy window' ^
  fh_thr4_fh.png
0 00:00:03

Very fast, but still fairly horrible.

fh_thr4_fh.pngjpg

As previous, but search skip.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 10 search skip hc off copy window' ^
  fh_ths1_fh.png
0 00:00:01

Very fast, and not quite so horrible.

fh_ths1_fh.pngjpg

As previous, but repeated, with dissimilarity threshold.

%IM7DEV%magick ^
  toes_holed.png ^
  -process 'fillholes wr 10 search skip dt 0.05 hc off copy window' ^
  -process 'fillholes wr 5 search skip dt 0.05 hc off copy window' ^
  -process 'fillholes wr 2 search skip hc off copy window' ^
  fh_ths2_fh.png
0 00:00:08

Very fast, and the result is credible, aside from being over-sharp.

fh_ths2_fh.pngjpg

The process has introduced an artificial sharpness within the filled area. We can blur a result, placing it behind the original image so only the filled area is blurred.

%IMG7%magick ^
  fh_th3_fh.png -blur 0x1 ^
  toes_holed.png ^
  -compose SrcOver -composite ^
  fh_th3_fh_bl.png
fh_th3_fh_bl.pngjpg

It is now very hard to identify which pixels have been filled, even at increased magnification. Of course, blurring does not remove the repeated pattern where fillholes has become trapped.

Cleanup

We don't need to keep the frame files, so delete them.

del fh_x_fr_*.png
del fh_x_fr2_*.png

Possible improvements

The module currently doesn't multi-thread. I don't know why, but hope to fix this. (Perhaps my Cygwin build doesn't have multi-threading at all.)

The module is not fast. For the copy onepixel mode, it runs a custom-built "subimage-search" across the entire image for every single transparent pixel. For the linear dimension d, the complexity is of order O(d 6). Yes, that's the sixth power of the linear dimension. Double the image size and window size, and it will take sixty-four times as long. Processing time is proportional to the number of transparent pixels, the number of pixels in the window, and the number of pixels in the search area (which by default is the entire image).

For the copy window mode, the number of searches is reduced by a factor of approximately window_radius * window_radius, so the complexity is of order "only" O(d 4) (and this is independent, roughly, of window size).

A number of performance improvements might be made:

The functionality could also be enhanced:

The "onion-skin" order of pixel-filling can hit problems when holes are not convex. We could add gradient-based priority, which seems especially important when copying windows rather than just pixels. For this, I have created a new process module, fillholespri. See the page Filling holes in priority order.

The homogeneity test is crude. It might be improved by testing mean and standard deviation.

The module fillholes searches for a matching exemplar, and translates it to fill the hole. For man-made scenes with strong perspective, hence the scene is not fronto-parallel, a good match might need a perspective transformation applied before the search, and applied in reverse after filling. The Huang paper suggests an approach.

Aside: with Hough lines, perhaps IM can fairly easily find the required perspective transformations.

For homogenous images (such as the grass) there is no obvious "right" answer, and many possibilities are not obviously "wrong". For these, "copy window" works well.

For inhomogenous images (such as the church), there is a narrow range of "right" answers, and most "wrong" answers are clearly wrong. For these, "copy window" is less successful, but limit_search_radius can be beneficial.

The module could create a displacement map as output.

The PatchMatch paper suggests a somewhat different approach. We want to find displacement offsets for all the patches (or pixels). We start by assigning random offsets, and check how good they are (the score of each). One of them will be the best. Provided there are many patches (or pixels), the chances are good that the best score is actually very good. Its neighbours will probably have very similar offsets. Thus we propagate good mappings. For each patch, we also trial random searches at offsets at radiating distances from the currently assumed offset, looking for an improvement, eg 10 searches for a 1000x1000 pixel image. Each cycle does a propagation and random search for each patch, and about five cycles are made.

That approach can't (I think) be applied to fillholes. It is a successive approximation technique, for when the patches are all known and we want to search for them in a source. For fillholes, we initially know only patches at edges, and others are unknown until we start filling in pixels. But the PatchMatch technique could be used for image analogies, etc, where we know the patches from the outset.

The modules could readily be made invariant to brightness and contrast. Before searching, calculate the mean and SD of the subimage. For each candidate window, calculate its mean and SD, thus the required gain and bias to match with the subimage. Apply this gain and bias to each pixel when calculating differences. See Gain and bias. Put this as a compile-time option in match.inc and compwind.inc.

But what would the copying do? I suppose it would adjust the values according to the gain and bias.

The paper "Grayscale Template-Matching Invariant to Rotation, Scale, Translation, Brightness and Contrast", Hae Yong Kim and Sidnei Alves de Araújo, contains many ineresting ideas. One is the concept of "don't bother testing candidate windows that will certainly fail". The paper describes succesive filters for finding a subimage invariant to rotation, scale, translation, brightness and contrast. Processing at each stage is naturally faster if a previous stage has eliminated possibilities. Both fillholes (and pixmatch) could use a mask image that meant "don't bother testing this postion". They already ignore candidate windows that have transparent central pixels, but transparent pixels are also ignored for all windows they appear in.

Acknowledgments

Something I read on the Internet inspired this process module and this page. Sadly, I've forgotten what it was. It may have been the Efros and Leung paper below, though my method is different to theirs.

Interesting sources include:

As far as I know, "search skip" is my own invention.

Fill by blur

For comparison, blurFill.bat fills by blurring. It blurs the image, and composes the original over the blur. It repeats this until there are no transparent pixels. See also Selective fill: blur fill.

The process is successful for small holes, such as the penultimate of the following examples (fh_ph3.png).

set bfTHRESH_PC=10

call %PICTBAT%blurFill fh_src1.png
fh_src1_bf.png
call %PICTBAT%blurFill fh_src2.png
fh_src2_bf.png
call %PICTBAT%blurFill fh_ph0.png
fh_ph0_bf.pngjpg
call %PICTBAT%blurFill fh_ph3.png
fh_ph3_bf.pngjpg
call %PICTBAT%blurFill ^
  toes_holed.png . fh_toes_bf.png
fh_toes_bf.pngjpg

The script is used in Follow line and Straightening horizons in the construction of displacement maps.

Fill by resize

The script resizeFill.bat works like Fill by blur above, but resizes instead of blurring, and operates in a single convert command, with ever-decreasing images. Thus, it is fast.

call %PICTBAT%resizeFill fh_src1.png
fh_src1_rf.png
call %PICTBAT%resizeFill fh_src2.png
fh_src2_rf.png
call %PICTBAT%resizeFill fh_ph0.png
fh_ph0_rf.pngjpg
call %PICTBAT%resizeFill fh_ph3.png
fh_ph3_rf.pngjpg
call %PICTBAT%resizeFill ^
  toes_holed.png . fh_toes_rf.png
fh_toes_rf.pngjpg

By default, the resizing is by a factor of 2 (50%) at each step. This is coarse, and better quality is obtained when the resizing is less extreme, eg 1.1 (about 90%). This spreads colours from hole edges further towards hole centres.

call %PICTBAT%resizeFill ^
  fh_src1.png 1.1 fh_src1_rf2.png
fh_src1_rf2.png
call %PICTBAT%resizeFill ^
  fh_src2.png 1.1 fh_src2_rf2.png
fh_src2_rf2.png
call %PICTBAT%resizeFill ^
  fh_ph0.png 1.1 fh_ph0_rf2.png
fh_ph0_rf2.pngjpg
call %PICTBAT%resizeFill ^
  fh_ph3.png 1.1 fh_ph3_rf2.png
fh_ph3_rf2.pngjpg
call %PICTBAT%resizeFill ^
  toes_holed.png 1.1 fh_toes_rf2.png
fh_toes_rf2.pngjpg

Fill by shift

Another script, shiftFill.bat fills by compositing the image over a shifted clone. This is done in all four directions.

Like Fill by blur above, the process is successful for small holes, such as the last of the following examples. It is also useful when the "image" is really data, for example Curving in 3D.

It is also successful for images that contain virtually no detail around the hole, such as cartoon images. See Cartoon and texture.

call %PICTBAT%shiftFill fh_src1.png
fh_src1_sf.png
call %PICTBAT%shiftFill fh_src2.png
fh_src2_sf.png
call %PICTBAT%shiftFill fh_ph0.png
fh_ph0_sf.pngjpg
call %PICTBAT%shiftFill fh_ph3.png
fh_ph3_sf.pngjpg
call %PICTBAT%shiftFill ^
  toes_holed.png . fh_toes_sf.png
fh_toes_sf.pngjpg

Fill with mean colour

The script meanFill.bat flattens an image against the alpha-weighted colour of the entire image. Thus, pixels that are entirely transparent do not contribute towards the calculation of the average colour.

call %PICTBAT%meanFill ^
  fh_src1.png fh_src1_mf.png
fh_src1_mf.png
call %PICTBAT%meanFill ^
  fh_src2.png fh_src2_mf.png
fh_src2_mf.png
call %PICTBAT%meanFill ^
  fh_ph0.png fh_ph0_mf.png
fh_ph0_mf.pngjpg
call %PICTBAT%meanFill ^
  fh_ph3.png fh_ph3_mf.png
fh_ph3_mf.pngjpg
call %PICTBAT%meanFill ^
  fh_ph4.png fh_ph4_mf.png
fh_ph4_mf.pngjpg
call %PICTBAT%meanFill ^
  toes_holed.png fh_toes_mf.png
fh_toes_mf.pngjpg

Fill by Voronoi

The script voronoiFill.bat fills by setting each transparent pixel to the colour of the closest opaque pixel.

I couldn't see a simple solution for this. In a forum post, Anthony Thyssen gave this solution. -sparse-color Voronoi sets each pixel in an image to the colour of its nearest coordinate from a text list. We could make this list from all the opaque pixels. Instead, we make it from just the pixels at the edge of holes, which is sufficient provided transparency is binary, and provided we finish by compositing the original over the filled result.

-sparse-color takes a text list of {x,y,colour} tuples, so the script creates this list from opaque pixels. The text format is the same as the output from sparse-color:.

In this usage, creating the text list isn't logically necessary, and simply wastes resources. IM could use an operator that acted like -sparse-color but read its values from the top-most image in the list (and removed that image from the list).

 

call %PICTBAT%voronoiFill ^
  fh_src1.png fh_src1_vf.png
fh_src1_vf.png
call %PICTBAT%voronoiFill ^
  fh_src2.png fh_src2_vf.png
fh_src2_vf.png
call %PICTBAT%voronoiFill ^
  fh_ph0.png fh_ph0_vf.png
fh_ph0_vf.pngjpg
call %PICTBAT%voronoiFill ^
  fh_ph3.png fh_ph3_vf.png
fh_ph3_vf.pngjpg
call %PICTBAT%voronoiFill ^
  fh_ph4.png fh_ph4_vf.png
fh_ph4_vf.pngjpg
call %PICTBAT%voronoiFill ^
  toes_holed.png fh_toes_vf.png
fh_toes_vf.pngjpg

This is also generalized in the script sparseColFill.bat.

Fill by relaxation

The relaxation method is not particularly useful for filling end-use images. However, it is an important tool for certain image-processing operations, such as relaxing membranes, seamless photomontage, and completing sparse displacement maps.

This is a graphical method for solving the Poisson equation with Dirichlet conditions, and corresponds to Jacobi's iterative method. See for example:

(As used here, we solve merely a Laplacian equation, which is a particular form of the more general Poisson equation. In other applications, we use the same scripts to solve a Poisson equation.)

How is this solving an equation? Every channel of every pixel in a hole is an unknown value. If there are N unknowns, where N could be many million, there are also N simultaneous equations because each pixel is defined by its neighbours. This method finds the N values that satisfy (more or less) each of the N equations.

The script relaxFill.bat fills transparent pixels such that the solution is fully relaxed. Here, "relaxed" means that when we have a potential solution, if we then apply an operation to the solution and composite the original opaque pixels over it, this does not change the solution. The operation is typically a small-radius blur or convolve.

The process iterates towards a solution, which may not be an exact solution. The process stops when the improvement (in %5 steps) is less than a threshold (%4). The metric for improvement might be RMSE or PAE. Of those, PAE is the most sensitive.

To get the most perfect solution, set rfCOMP_METRIC=PAE, use 1e-5 for the threshold (assuming Q16 non-HDRI), and -1 for the number of steps.

For a "good enough" solution, I suggest metric PAE, threshold 1e-3, 1000 steps.

The process starts from an approximate solution and iterates to a stable solution. It assumes that repeatedly applying the operation takes us closer to the solution, i.e. that it converges. However, it usually converges very slowly. To get decent performance:

For increased performance, instead of Jacobi's method, relaxFill.bat could use Gauss-Seidel (probably as a process module) and/or Successive Over-Relaxation.

Use the blur-fill from above as first approximation.

call %PICTBAT%relaxFill ^
  fh_src1.png fh_src1_bf.png
fh_src1_rf.png
call %PICTBAT%relaxFill ^
  fh_src2.png fh_src2_bf.png
fh_src2_rf.png
call %PICTBAT%relaxFill ^
  fh_ph0.png fh_ph0_bf.png
fh_ph0_rf.pngjpg
call %PICTBAT%relaxFill ^
  fh_ph3.png fh_ph3_bf.png
fh_ph3_rf.pngjpg
call %PICTBAT%relaxFill ^
  toes_holed.png fh_toes_bf.png
fh_toes_rf.pngjpg

fillRelaxMS script

The script above, relaxFill.bat, works fine, but converging on an accurate solution for a large image is very slow. (The time for each iteration is proportional to the number of pixels, and the number of iterations required for a given gain in accuracy is also roughly proportional to the number of pixels.) We massively increase performance by processing at multiple scales. This starts at a coarse scale (small image), where relaxing the image is fast, then resizes the result up to the next scale, and uses it as the first approximation, and so on up to the full size. As the approximation at all levels except the first is already quite good, each convergence is quick.

This multi-scale approach reduces the error of low-frequency (long wavelength) detail first, then reduces the error in successively higher frequencies. By contrast, the simple relaxFill.bat works only with high-frequency error, very gradually correcting for low-frequency error by adding DC offsets that are slowly propagated.

See also Wikipedia: Multigrid method.

The script relaxFillMS.bat implements this. It recurses down to the coarsest level. At the lowest level (smallest image), it resizes the given INFILE and the given first approximation, and calls relaxFill.bat. At the next level up, the result from the previous call is resized and used as the first approximation for that level. Scaling in each direction is with an exact "-resize WxH!", without specifying a filter. Experimentation might show that a different method is quicker.

A high precision at the initial coarser levels takes time but doesn't significantly improve the time at the later levels. Hence we reduce the required precision by multiplying by the scale factor.

call %PICTBAT%relaxFillMS ^
  fh_src1.png fh_src1_bf.png ^
  fh_src1_rfms.png
fh_src1_rfms.png
call %PICTBAT%relaxFillMS ^
  fh_src2.png fh_src2_bf.png ^
  fh_src12_rfms.png
fh_src2_rfms.png
call %PICTBAT%relaxFillMS ^
  fh_ph0.png fh_ph0_bf.png ^
  fh_ph0_rfms.png
fh_ph0_rfms.pngjpg
call %PICTBAT%relaxFillMS ^
  fh_ph3.png fh_ph3_bf.png ^
  fh_ph3_rfms.png
fh_ph3_rfms.pngjpg
call %PICTBAT%relaxFillMS ^
  toes_holed.png fh_toes_bf.png ^
  fh_toes_holed_rfms.png
fh_toes_holed_rfms.pngjpg

relaxFillMS.bat optionally takes a guidance vector field. If so, it calls relaxFillScr.bat; see the next section.

relaxFillScr script

The script relaxFillScr.bat is a generalisation of relaxFill.bat. It optionally takes an extra input image, a guidance vector field. At each iteration, after the blur, it subtracts quarter of the guidance field. It repeats this alternation of blur, subtract, blur, subtract, until the result is stable.

This means we can't "convolve:n", and must subtract after each individual convolution. To get decent performance, we run a long command with many iterations. To avoid OS limitations on command length, we run an IM script with v6 "@name.scr" or v7 "-script name.scr".

For more details about guidance vector fields, see Seamless photomontage.

The relaxation method is not particularly useful for filling end-use images, unless we are filling between edges; see Cartoon and texture: relaxation. However, it is an important tool for certain image-processing operations, such as relaxing membranes, seamless photomontage, and completing sparse displacement maps (eg Pin or push). (I think it might also be useful for de-Bayering photographs.)

How much precision?

How much precision do we need in the relaxation process? Each iteration make a small difference to the slope, and this difference can be too small to register with integer Q16. For example, we make a 256x1 image that is transparent except for pixels at the ends and the middle. Then we fill the transparent pixels by relaxation, and show the result as a graph.

As the image is only one pixel high, we use a kernel that averages only the pixels to the left and right. This makes the morphology faster, and needs fewer iterations to reach stability. We also compare the PAE, which is more sensitive than RMSE.

set rfRLX_CONV="3x1:0.5,0,0.5"

set rfCOMP_METRIC=PAE
%IMG7%magick -size 256x1 xc:None ^
  -fill Black      -draw "point 0,0" ^
  -fill gray(30%%) -draw "point 128,0" ^
  -fill White      -draw "point 255,0" ^
  fh_rf_prec.png

call %PICTBAT%relaxFill ^
  fh_rf_prec.png . ^
  fh_rp_rf.png ^
  1e-4 1000

echo rfITER=%rfITER% rfDIST=%rfDIST% 
rfITER=24000 rfDIST=9.203097581445e-05 
call %PICTBAT%graphLineCol fh_rp_rf.png
fh_rp_rf_glc.png

We did 12,000 iterations. The last batch of one thousand caused no change, so the solution was exactly stable. But the solution is clearly wrong; both legs should be straight. Setting a lower threshold, or doing more iterations, won't cure the problem. Repeat the above, using Q32 HDRI for the relaxation:

call %PICTBAT%relaxFill ^
  fh_rf_prec.png . ^
  fh_rp_rf2.png ^
  1e-4 1000

echo rfITER=%rfITER% rfDIST=%rfDIST% 
rfITER=24000 rfDIST=9.203097581445e-05 
call %PICTBAT%graphLineCol fh_rp_rf2.png
fh_rp_rf2_glc.png

Using Q32 HDRI cured the obvious problem, athough the non-zero value of rfDIST tells us the solution is approximate. It has stabilised merely within the limit we set.

Extend by duplication

Above, we showed how to extend an image by regarding the extension as a "hole", and filling it. A related technique simply duplicates rows and columns.

:skip

%IMG7%magick ^
  %FH_PHSRC% ^
  -virtual-pixel Edge -filter box ^
  -define distort:viewport=307x440-20-20 ^
  -distort SRT 1,0 +repage ^
  fh_extdump.png
fh_extdump.pngjpg

This can be useful when the "image" is really data, such as a displacement map. For an example, see Coffee mug.

Fractal noise displacement map

The script fnDispFill.bat does the following:

  1. Creates a fractal noise displacement map.
  2. Displaces the image by this.
  3. Composes the input image over the result of (2).
  4. Repeats from (1) until the result of (3) is entirely opaque.
call %PICTBAT%fnDispFill ^
  toes_holed.png fh_toes_fndf.png
fh_toes_fndf.pngjpg
call %PICTBAT%fnDispFill ^
  fh_ph0.png fh_ph0_fndf.png
fh_ph0_fndf.pngjpg

The technique is not highly successful.

Fill by membrane

From pixels that define the edge of a hole, we can fill that hole by creating a membrane based on pixels at the hole edges. This requires individual processing per hole, so is not convenient for the examples on this page.

Using an example from my Membrane page:

Input:

mem_rim_c.png
call %PICTBAT%membrane ^
  mem_rim_c.png fh_mem_out_c.png
fh_mem_out_c.png

Laplacian pyramids

Any of the above techniques may be incorporated within the following:

  1. From the holed image, build a Laplacian pyramid.
  2. Fill the holes in each grid of the pyramid with one of the techniques.
  3. Collapse the pyramid.

See the page Laplacian pyramids with transparency.

Scripts

For convenience, this .c code and .bat are also available in a zip file. See Zipped BAT files.

fillholes.c

/*
    Reference: http://im.snibgo.com/fillholes.htm

    Update 21-Nov-2015
      Added copy_radius option.
      Normalise MSE result from CompareWindow to 0.0-1.0, for threshold option.
      Added threshold option.
      Added "unfilled" warning.
      Moved most code to fillholescommon.inc.

    Updated
      2-Feb-2016 for v7.
      3-April-2018 for v7.0.7-28
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include "vsn_defines.h"


#include "fillholescommon.inc"

#define DEBUG 0


// The next function corresponds in style to functions in transform.c
// It takes one image, and returns an image with filled holes.
//
static Image *fillholes (
  Image *image,
  FillHoleT * pfh,
  ExceptionInfo *exception)
{
  Image
    *holed_image,
    *new_image;

  CacheView
    *copy_inp_view,
    *copy_new_view2,
    *transp_view,
    *new_view;

  ssize_t
    y,
    holedXmult;

  int
    cols_plus_3,
    nIter = 0,
    frameNum = 0;

  MagickBooleanType
    unfilled,
    changedAny,
    status = MagickTrue;

  if (image->debug != MagickFalse)
    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);

  if (pfh->do_verbose) {
    fprintf (stderr, "fillholes: Input image [%s] %ix%i depth is %i\n",
             image->filename,
             (int)image->columns, (int)image->rows,
             (int)image->depth);
  }

  ResolveImageParams (image, pfh);

  InitAutoLimit (pfh);

  // Clone the image, same size, copied pixels:
  //
  holed_image=CloneImage(image, 0, 0, MagickTrue, exception);
  if (holed_image == (Image *) NULL)
    return(holed_image);

  if (SetNoPalette (holed_image, exception) == MagickFalse)
    return (Image *)NULL;

  holedXmult = Inc_ViewPixPtr (holed_image);

  // Clone holed_image into new_image.
  new_image=CloneImage(holed_image, 0, 0, MagickTrue, exception);
  if (new_image == (Image *) NULL)
    return(new_image);

  if (pfh->write_frames > 0) {
    WriteFrame (pfh, new_image, frameNum++, exception);
  }

  do {  // one onion-ring

#if DEBUG==1
    printf ("Do one onion-ring\n");
#endif

    changedAny = MagickFalse;
    unfilled = MagickFalse;

    // FIXME: performance: we can acquire/release some of these outside do loop?
//    inp_view    = AcquireVirtualCacheView (image, exception);
//    srch_holed_view = AcquireVirtualCacheView (holed_image, exception);

    pfh->CompWind.ref_image = image;
    pfh->CompWind.sub_image = holed_image;
    pfh->CompWind.ref_view = AcquireVirtualCacheView (image, exception);
    pfh->CompWind.sub_view = AcquireVirtualCacheView (holed_image, exception);

    transp_view = AcquireVirtualCacheView (holed_image, exception);
    new_view    = AcquireAuthenticCacheView (new_image, exception);

    copy_inp_view   = CloneCacheView (pfh->CompWind.ref_view);
    copy_new_view2  = AcquireVirtualCacheView (new_image, exception);

    cols_plus_3 = holed_image->columns + 3;
#if defined(MAGICKCORE_OPENMP_SUPPORT)
    #pragma omp parallel for schedule(static,4) shared(status,changedAny,frameNum) \
      MAGICK_THREADS(image,new_image,image->rows,1)
#endif
    for (y=0; y < (ssize_t) new_image->rows; y++)
    {
      ssize_t
        x;

      const VIEW_PIX_PTR
        *pp_this_pix,
        *pp_transp3,
        *pp_line0,
        *pp_line1,
        *pp_line2;

      CopyWhereT
        cw;

      if (pfh->copyWhat == copyWindow) {
        cw.wi = pfh->sqCopyDim;
        cw.ht = pfh->sqCopyDim;
      } else {
        cw.wi = 1;
        cw.ht = 1;
      }

      if (status == MagickFalse) continue;

      pp_this_pix = GetCacheViewVirtualPixels (
        copy_new_view2,0,y,holed_image->columns,1,exception); // FIXME: need diff view?
      if (pp_this_pix == (const VIEW_PIX_PTR *) NULL)
      {
        status=MagickFalse;
        continue;
      }

      pp_transp3 = GetCacheViewVirtualPixels (
        transp_view,-1,y-1,holed_image->columns+2,3,exception);
      if (pp_transp3 == (const VIEW_PIX_PTR *) NULL)
      {
        status=MagickFalse;
        continue;
      }

      // FIXME: offsets are wrong for v7.
//      pp_line0 = pp_transp3 + 1;
//      pp_line1 = pp_transp3 + cols_plus_3;
//      pp_line2 = pp_transp3 + 2 * cols_plus_3 - 1;

      pp_line0 = pp_transp3 + holedXmult;
      pp_line1 = pp_transp3 + holedXmult * cols_plus_3;
      pp_line2 = pp_transp3 + holedXmult * (2 * cols_plus_3 - 1);

#if DEBUG==1
      printf ("y=%li  ", (long int)y);
#endif

      for (x=0; x < (ssize_t) new_image->columns; x++)
      {
        // FIXME: get the 9 alphas from new_image, so we can copy a window-full each time.
        // But then we do too many.

        // FIXME: offsets are wrong for v7.

        //if (GetPixelAlpha (pp_line1 + x) <= 0) {
        if (GET_PIXEL_ALPHA (new_image, pp_this_pix + holedXmult * x) <= 0) {
          // The pixel is fully transparent.
          MagickBooleanType HasAdjOpaq = 
               (GET_PIXEL_ALPHA (holed_image, pp_line0 + holedXmult*(x-1)) > 0)
            || (GET_PIXEL_ALPHA (holed_image, pp_line0 + holedXmult*x    ) > 0)
            || (GET_PIXEL_ALPHA (holed_image, pp_line0 + holedXmult*(x+1)) > 0)

            || (GET_PIXEL_ALPHA (holed_image, pp_line1 + holedXmult*(x-1)) > 0)
            || (GET_PIXEL_ALPHA (holed_image, pp_line1 + holedXmult*(x+1)) > 0)

            || (GET_PIXEL_ALPHA (holed_image, pp_line2 + holedXmult*(x-1)) > 0)
            || (GET_PIXEL_ALPHA (holed_image, pp_line2 + holedXmult*x    ) > 0)
            || (GET_PIXEL_ALPHA (holed_image, pp_line2 + holedXmult*(x+1)) > 0);

          if (HasAdjOpaq == MagickTrue) {

            // The pixel is fully transparent,
            // with at least one opaque 8-neighbour.

            status = MatchAndCopy (
              pfh,
              new_image,
              new_view,
              copy_inp_view,
              x,
              y,
              &cw,
              &frameNum,
              &unfilled,
              &changedAny,
              exception);

/*==
            ssize_t
              i0, i1,
              j0, j1,
              hx, hy,
              i, j,
              besti=0, bestj=0;

            hx = x - pfh->WindowRad;
            hy = y - pfh->WindowRad;
            if (pfh->SearchToEdges) {
              i0 = -pfh->WindowRad;
              i1 = (ssize_t) holed_image->rows - pfh->WindowRad;
              j0 = -pfh->WindowRad;
              j1 = (ssize_t) holed_image->columns - pfh->WindowRad;
            } else {
              i0 = 0;
              i1 = (ssize_t) holed_image->rows - pfh->sqDim + 1;
              j0 = 0;
              j1 = (ssize_t) holed_image->columns - pfh->sqDim + 1;
            }
            if (pfh->LimSrchRad) {
              ssize_t limj0 = x - pfh->LimSrchRad - pfh->WindowRad;
              ssize_t limj1 = x + pfh->LimSrchRad - pfh->WindowRad;
              ssize_t limi0 = y - pfh->LimSrchRad - pfh->WindowRad;
              ssize_t limi1 = y + pfh->LimSrchRad - pfh->WindowRad;
              if (i0 < limi0) i0 = limi0;
              if (i1 > limi1) i1 = limi1;
              if (j0 < limj0) j0 = limj0;
              if (j1 > limj1) j1 = limj1;
            }
            double BestScore = pfh->WorstCompare;
            for (i = i0; i < i1; i++) {
              for (j = j0; j < j1; j++) {
                double v = CompareWindow (
                  pfh, inp_view, srch_holed_view, exception,
                  hx, hy, j, i);
                if (BestScore > v) {
                  BestScore = v;
                  besti = i;
                  bestj = j;
                  if (BestScore <= pfh->thresholdSq) break;
                }
              }
              if (BestScore <= pfh->thresholdSq) break;
            }
            //if (x==30)
            //  printf ("Best ji=%i,%i %g => hxy %i,%i\n",
            //    (int)bestj, (int)besti, BestScore, (int)hx, (int)hy);
            if (BestScore < pfh->WorstCompare) {

              if (pfh->copyWhat == copyOnePixel)
                CopyOnePix (pfh, new_view, copy_inp_view, hx, hy, bestj, besti, exception);
              else
                CopyWindow (pfh, new_view, copy_inp_view, hx, hy, bestj, besti, exception);

              if (SyncCacheViewAuthenticPixels (new_view,exception) == MagickFalse)
                status=MagickFalse;

              changedAny = MagickTrue;
              if (pfh->write_frames == 2) {
                WriteFrame (pfh, new_image, frameNum++, exception);
              }
            } else {
              unfilled = MagickTrue;
            }
==*/

          }
        }
      } // loop x
    } // loop y

#if DEBUG==1
    printf ("Done x,y\n");
#endif

    if (pfh->Match.SetAutoLimit && pfh->Match.nLimitCnt <= 0) UseAutoLimit (pfh);

    copy_inp_view   = DestroyCacheView (copy_inp_view);
    new_view        = DestroyCacheView (new_view);
    transp_view     = DestroyCacheView (transp_view);
    copy_new_view2  = DestroyCacheView (copy_new_view2);

    pfh->CompWind.sub_view = DestroyCacheView (pfh->CompWind.sub_view);
    pfh->CompWind.ref_view = DestroyCacheView (pfh->CompWind.ref_view);

    if (changedAny) {
      DestroyImage (holed_image);
      holed_image = CloneImage(new_image, 0, 0, MagickTrue, exception);
      if (pfh->write_frames == 1) {
        WriteFrame (pfh, new_image, frameNum++, exception);
      }
    }
    nIter++;

#if DEBUG==1
    printf ("Done one onion-ring\n");
#endif

  } while (changedAny);


  if (pfh->do_verbose) {
    fprintf (stderr, "Finished fillholes nIter=%i\n", nIter);
    if (unfilled) {
      fprintf (stderr, "Warning: some pixels were not filled\n");
    }
    if (pfh->write_frames > 0) {
      fprintf (stderr, "  numFrames=%i\n", frameNum);
    }
  }

  DestroyImage (new_image);

  return (holed_image);
}


ModuleExport size_t fillholesImage (
  Image **images,
  const int argc,
  const char **argv,
  ExceptionInfo *exception)
{
  Image
    *image,
    *new_image;

  MagickBooleanType
    status;

  FillHoleT
    fh;

  assert(images != (Image **) NULL);
  assert(*images != (Image *) NULL);
  assert((*images)->signature == MAGICK_CORE_SIG);

  status = menu (argc, argv, &fh);
  if (status == MagickFalse)
    return (-1);

  InitRand (&fh);

  for (image=(*images); image != (Image *) NULL; image=GetNextImageInList(image))
  {
    new_image = fillholes (image, &fh, exception);

    ReplaceImageInList(&image,new_image);
    *images=GetFirstImageInList(image);
  }

  DeInitRand (&fh);

  return(MagickImageFilterSignature);
}

fillholescommon.inc

This code is common to both fillholes.c and fillholespri.c.

/*
    Reference: http://im.snibgo.com/fillholes.htm

    Data structures and functions common to fillholes process modules.

    Created 21-Nov-2015
*/

#define CH_R 1
#define CH_G 2
#define CH_B 4

#include "compwind.h"
#include "match.h"


typedef enum {
  copyOnePixel,
  copyWindow
} CopyWhatT;

typedef struct {
  CompWindT
    CompWind;

  MatchT
    Match;

  /* Set by user. */
  MagickBooleanType
    do_verbose,
    AutoRepeat,
    CopyRad_IsPc,
    WindowRad_IsPc,
    debug;

  double
    WindowRad,
    CopyRad,
    ImgDiagSq;

  int
    WindowRadPix,
    CopyRadPix,
    write_frames;

  char
    write_filename[MaxTextExtent];

  CopyWhatT
    copyWhat;

  /* Calculated. */
  int
    sqDim,
    sqCopyDim;

  ssize_t
    Width,
    Height;

  RandomInfo
    *restrict random_info;

} FillHoleT;

typedef struct {
  ssize_t
    srcX,
    srcY,
    dstX,
    dstY,
    wi,
    ht;
} CopyWhereT;

#include "compwind.inc"
#include "match.inc"


static void usage (void)
{
  printf ("Usage: -process 'fillhole [OPTION]...'\n");
  printf ("Populates transparent pixels, InFill.\n");
  printf ("\n");
  printf ("  wr,  window_radius N        radius of search window, >= 1\n");
  printf ("  lsr, limit_search_radius N  limit radius from transparent pixel to search\n");
  printf ("                                for source, >= 0\n");
  printf ("                                default = 0 = no limit\n");
  printf ("  als,  auto_limit_search X   automatic limit search, on or off\n");
  printf ("                                default on\n");
  printf ("  hc,  hom_chk X              homogeneity check, X is off or a small number\n");
  printf ("                                default 0.1\n");
  printf ("  e,   search_to_edges X      search for matches to image edges, on or off\n");
  printf ("                                default on\n");
  printf ("  s,   search X               X=entire or random or skip\n");
  printf ("                                default entire\n");
  printf ("  rs,  rand_searches N        number of random searches (eg 100)\n");
  printf ("                                default 0\n");
  printf ("  sn,  skip_num N             number of searches to skip in each direction\n");
  printf ("                                (eg 10)\n");
  printf ("                                default 0\n");
  printf ("  ref, refine X               whether to refine random and skip searches,\n");
  printf ("                                on or off\n");
  printf ("                                default on\n");
  printf ("  st,  similarity_threshold N\n");
  printf ("                              stop searching when RMSE <= N (eg 0.01)\n");
  printf ("                                default 0\n");
  printf ("  dt,  dissimilarity_threshold N\n");
  printf ("                              don't copy if best RMSE >= N (eg 0.05)\n");
  printf ("                                default: no threshold\n");
  printf ("  cp,  copy X                 X=onepixel or window\n");
  printf ("  cr,  copy_radius N          radius of pixels to copy, >= 1, <= wr\n");
  printf ("                                default: 0 or wr\n");
  printf ("  a,   auto_repeat            if pixels changed but any unfilled, repeat\n");
  printf ("  w,   write filename         write frames to files\n");
  printf ("  w2,  write2 filename        write frames to files\n");
  printf ("  v,   verbose                 write text information to stdout\n");
  printf ("\n"); 
}


static MagickBooleanType IsArg (char * pa, char * ShtOpt, char * LongOpt)
{
  if ((LocaleCompare(pa, ShtOpt)==0) || (LocaleCompare(pa, LongOpt)==0))
    return MagickTrue;

  return MagickFalse;
}


static inline MagickBooleanType EndsPc (const char *s)
{
  char c = *(s+strlen(s)-1);
  if (c == '%' || c == 'c')
    return MagickTrue;
  return MagickFalse;
}


static MagickBooleanType menu(
  const int argc,
  const char **argv,
  FillHoleT * pfh
)
// Returns MagickTrue if okay.
{
  int
    i;

  MagickBooleanType
    status;

  status = MagickTrue;

  pfh->do_verbose = MagickFalse;
  pfh->WindowRad = 1.0;
  pfh->CopyRad = 0.0;
  pfh->WindowRad_IsPc = pfh->CopyRad_IsPc = MagickFalse;
  pfh->WindowRadPix = 1;
  pfh->CopyRadPix = 0;
  pfh->write_frames = 0;
  pfh->write_filename[0] = '\0';
  pfh->copyWhat = copyOnePixel;
  pfh->debug = MagickFalse;
  pfh->AutoRepeat = MagickFalse;

  pfh->Match.LimSrchRad = 0.0;
  SetDefaultMatch (&pfh->Match);

  pfh->CompWind.HomChkOn = MagickTrue;
  pfh->CompWind.HomChk = 0.1;
  pfh->CompWind.nCompares = 0;


  for (i=0; i < argc; i++) {
    char * pa = (char *)argv[i];

    if (IsArg (pa, "wr", "window_radius")==MagickTrue) {
      i++;
      if (!isdigit ((int)*argv[i]))
      {
        fprintf (stderr, "Non-numeric argument to 'window_radius'.\n");
        status = MagickFalse;
      }
      else {
        pfh->WindowRad = atof(argv[i]);
        pfh->WindowRad_IsPc = EndsPc (argv[i]);
        if (!pfh->WindowRad_IsPc)
          pfh->WindowRadPix = pfh->WindowRad + 0.5;
      }
      if (pfh->WindowRad <= 0) {
        fprintf (stderr, "Bad 'window_radius' value.\n");
        status = MagickFalse;
      }
    } else if (IsArg (pa, "cr", "copy_radius")==MagickTrue) {
      i++;
      if (!isdigit ((int)*argv[i]))
      {
        fprintf (stderr, "Non-numeric argument to 'copy_radius'.\n");
        status = MagickFalse;
      }
      else {
        pfh->CopyRad = atof(argv[i]);
        pfh->CopyRad_IsPc = EndsPc (argv[i]);
        if (!pfh->CopyRad_IsPc)
          pfh->CopyRadPix = pfh->CopyRad + 0.5;
      }
      if (pfh->CopyRad <= 0) {
        fprintf (stderr, "Bad 'copy_radius' value.\n");
        status = MagickFalse;
      }
    } else if (IsArg (pa, "lsr", "limit_search_radius")==MagickTrue) {
      // FIXME: also allow percentage of smaller image dimension.
      i++;
      if (!isdigit ((int)*argv[i])) {
        fprintf (stderr, "Non-numeric argument to 'limit_search_radius'.\n");
        status = MagickFalse;
      } else {
        pfh->Match.LimSrchRad = atof(argv[i]);
        pfh->Match.LimSrchRad_IsPc = EndsPc (argv[i]);
        if (!pfh->Match.LimSrchRad_IsPc)
          pfh->Match.limSrchRadX = pfh->Match.limSrchRadY = pfh->Match.LimSrchRad + 0.5;
      }
      if (pfh->Match.LimSrchRad < 0) {
        fprintf (stderr, "Bad 'limit_search_radius' value.\n");
        status = MagickFalse;
      }

/*---
    } else if (IsArg (pa, "c", "channel")==MagickTrue) {
      i++;
      pfh->channels = 0;
      const char * p = argv[i];
      while (*p) {
        switch (toupper ((int)*p)) {
          case 'R':
            pfh->channels |= CH_R;
            break;
          case 'G':
            pfh->channels |= CH_G;
            break;
          case 'B':
            pfh->channels |= CH_B;
            break;
          case 'L':
            pfh->channels |= CH_R;
            break;
          case 'A':
            pfh->channels |= CH_G;
            break;
          default:
            fprintf (stderr, "Invalid 'channels' [%s]\n", argv[i]);
            status = MagickFalse;
        }
        p++;
      }
---*/
    } else if (IsArg (pa, "als", "auto_limit_search")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "on")==0)
        pfh->Match.AutoLs = MagickTrue;
      else if (LocaleCompare(argv[i], "off")==0)
        pfh->Match.AutoLs = MagickFalse;
      else {
        fprintf (stderr, "Invalid 'auto_limit_search' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "ref", "refine")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "on")==0)
        pfh->Match.Refine = MagickTrue;
      else if (LocaleCompare(argv[i], "off")==0)
        pfh->Match.Refine = MagickFalse;
      else {
        fprintf (stderr, "Invalid 'refine' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "s", "search")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "entire")==0)
        pfh->Match.searchWhat = swEntire;
      else if (LocaleCompare(argv[i], "random")==0)
        pfh->Match.searchWhat = swRandom;
      else if (LocaleCompare(argv[i], "skip")==0)
        pfh->Match.searchWhat = swSkip;
      else {
        fprintf (stderr, "Invalid 'search' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "rs", "rand_searches")==MagickTrue) {
      i++;
      if (!isdigit ((int)*argv[i])) {
        fprintf (stderr, "Non-numeric argument to 'rand_searches'.\n");
        status = MagickFalse;
      } else {
        pfh->Match.RandSearchesF = atof (argv[i]);
        pfh->Match.RandSearches_IsPc = EndsPc (argv[i]);
        if (!pfh->Match.RandSearches_IsPc)
          pfh->Match.RandSearchesI = pfh->Match.RandSearchesF + 0.5;
      }
      if (pfh->Match.RandSearchesF <= 0) {
        fprintf (stderr, "Bad 'rand_searches' value.\n");
        status = MagickFalse;
      }
    } else if (IsArg (pa, "sn", "skip_num")==MagickTrue) {
      i++;
      if (!isdigit ((int)*argv[i])) {
        fprintf (stderr, "Non-numeric argument to 'skip_num'.\n");
        status = MagickFalse;
      } else {
        pfh->Match.SkipNumF = atof (argv[i]);
        pfh->Match.SkipNum_IsPc = EndsPc (argv[i]);
        if (!pfh->Match.SkipNum_IsPc)
          pfh->Match.SkipNumI = pfh->Match.SkipNumF + 0.5;
      }
      if (pfh->Match.SkipNumF <= 0) {
        fprintf (stderr, "Bad 'skip_num' value.\n");
        status = MagickFalse;
      }
    } else if (IsArg (pa, "hc", "hom_chk")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "off")==0) {
        pfh->CompWind.HomChkOn = MagickFalse;
      } else {
        pfh->CompWind.HomChkOn = MagickTrue;
        if (!isdigit ((int)*argv[i])) {
          fprintf (stderr, "'hom_chk' argument must be number or 'off'.\n");
          status = MagickFalse;
        } else {
          pfh->CompWind.HomChk = atof (argv[i]);
        }
      }
    } else if (IsArg (pa, "e", "search_to_edges")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "on")==0)
        pfh->Match.SearchToEdges = MagickTrue;
      else if (LocaleCompare(argv[i], "off")==0)
        pfh->Match.SearchToEdges = MagickFalse;
      else {
        fprintf (stderr, "Invalid 'search_to_edges' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "a", "auto_repeat")==MagickTrue) {
      pfh->AutoRepeat = MagickTrue;
    } else if (IsArg (pa, "cp", "copy")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "onepixel")==0)
        pfh->copyWhat = copyOnePixel;
      else if (LocaleCompare(argv[i], "window")==0)
        pfh->copyWhat = copyWindow;
      else {
        fprintf (stderr, "Invalid 'copy' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "st", "similarity_threshold")==MagickTrue) {
      i++;
      pfh->Match.simThreshold = atof (argv[i]);
    } else if (IsArg (pa, "dt", "dissimilarity_threshold")==MagickTrue) {
      i++;
      pfh->Match.DissimOn = MagickTrue;
      pfh->Match.dissimThreshold = atof (argv[i]);
    } else if (IsArg (pa, "w", "write")==MagickTrue) {
      pfh->write_frames = 1;
      i++;
      CopyMagickString (pfh->write_filename, argv[i], MaxTextExtent);
      if (!*pfh->write_filename) {
        fprintf (stderr, "Invalid 'write' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "w2", "write2")==MagickTrue) {
      pfh->write_frames = 2;
      i++;
      CopyMagickString (pfh->write_filename, argv[i], MaxTextExtent);
      if (!*pfh->write_filename) {
        fprintf (stderr, "Invalid 'write' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "v", "verbose")==MagickTrue) {
      pfh->do_verbose = MagickTrue;
    } else if (IsArg (pa, "d", "debug")==MagickTrue) {
      pfh->debug = MagickTrue;
    } else {
      fprintf (stderr, "fillhole: ERROR: unknown option [%s]\n", pa);
      status = MagickFalse;
    }
  }

/*
  pfh->num_chan = 0;
  if (pfh->channels & CH_R) pfh->num_chan++;
  if (pfh->channels & CH_G) pfh->num_chan++;
  if (pfh->channels & CH_B) pfh->num_chan++;
  if (pfh->channels == 0) {
    fprintf (stderr, "No channels\n");
    status = MagickFalse;
  }
*/


  if (pfh->CopyRadPix==0 || pfh->CopyRadPix > pfh->WindowRadPix) {
    pfh->CopyRadPix = pfh->WindowRadPix;
  }
  if (pfh->copyWhat==copyOnePixel) pfh->CopyRadPix=0;

  pfh->Match.simThresholdSq = pfh->Match.simThreshold * pfh->Match.simThreshold;
  pfh->Match.dissimThresholdSq = pfh->Match.dissimThreshold * pfh->Match.dissimThreshold;

  pfh->CompWind.win_wi = pfh->sqDim;
  pfh->CompWind.win_ht = pfh->sqDim;

  if (pfh->do_verbose) {
    fprintf (stderr, "fillhole options: ");

    fprintf (stderr, "  window_radius %g", pfh->WindowRad);
    if (pfh->WindowRad_IsPc) fprintf (stderr, "%c", '%');

    fprintf (stderr, "  limit_search_radius %g", pfh->Match.LimSrchRad);
    if (pfh->Match.LimSrchRad_IsPc) fprintf (stderr, "%c", '%');

    fprintf (stderr, "  auto_lsr %s", pfh->Match.AutoLs ? "on" : "off");

    fprintf (stderr, "  search");
    switch (pfh->Match.searchWhat) {
      case swEntire: fprintf (stderr, " entire"); break;
      case swSkip:
        fprintf (stderr, " skip  skip_num %g", pfh->Match.SkipNumF);
        if (pfh->Match.SkipNum_IsPc) fprintf (stderr, "%c", '%');
        break;
      case swRandom:
        fprintf (stderr, " random  rand_searches %g", pfh->Match.RandSearchesF);
        if (pfh->Match.RandSearches_IsPc) fprintf (stderr, "%c", '%');
        break;
      default: fprintf (stderr, " ??");
    }

    fprintf (stderr, "  hom_chk ");
    if (pfh->CompWind.HomChkOn) {
      fprintf (stderr, "%g", pfh->CompWind.HomChk);
    } else {
      fprintf (stderr, "off");
    }

/*-
    if (pfh->max_iter) fprintf (stderr, " max_iterations %i", pfh->max_iter);

    if (pfh->channels != (CH_R | CH_G | CH_B)) {
      fprintf (stderr, " channels ");
      if (pfh->channels & CH_R) fprintf (stderr, "R");
      if (pfh->channels & CH_G) fprintf (stderr, "G");
      if (pfh->channels & CH_B) fprintf (stderr, "B");
    }
-*/

    fprintf (stderr, "  refine %s", pfh->Match.Refine ? "on" : "off");

    if (!pfh->Match.SearchToEdges) fprintf (stderr, "  search_to_edges off");

    fprintf (stderr, "  similarity_threshold %g", pfh->Match.simThreshold);

    if (pfh->Match.DissimOn == MagickTrue)
      fprintf (stderr, "  dissimilarity_threshold %g", pfh->Match.dissimThreshold);

    if (pfh->AutoRepeat) fprintf (stderr, "  auto_repeat");

    fprintf (stderr, "  copy ");
    if (pfh->copyWhat==copyOnePixel) fprintf (stderr, "onepixel");
    else if (pfh->copyWhat==copyWindow) fprintf (stderr, "window");
    else fprintf (stderr, "??");

    if (pfh->CopyRadPix != pfh->WindowRadPix) {
      fprintf (stderr, "  copy_radius %g", pfh->CopyRad);
      if (pfh->CopyRad_IsPc) fprintf (stderr, "%c", '%');
    }

    if (pfh->debug) fprintf (stderr, "  debug");

    if (pfh->do_verbose) fprintf (stderr, "  verbose");

    fprintf (stderr, "\n");
  }

  if (status == MagickFalse)
    usage ();

  return (status);
}



static MagickStatusType WriteFrame (
  FillHoleT *pfh,
  Image * img,
  int frame_num,
  ExceptionInfo *exception)
{
  ImageInfo
    *ii;

  MagickStatusType
    status;

  Image
    *copy_img;

  ii = AcquireImageInfo ();

  copy_img = CloneImage(img, 0, 0, MagickTrue, exception);
  if (copy_img == (Image *) NULL)
    return(MagickFalse); // FIXME: raise error

  copy_img->scene = frame_num;

  CopyMagickString (copy_img->filename, pfh->write_filename, MaxTextExtent);

#if IMV6OR7==6
  status = WriteImage (ii, copy_img);
#else
  status = WriteImage (ii, copy_img, exception);
#endif

  DestroyImageList(copy_img);

  ii = DestroyImageInfo (ii);

  return status;
}


/*===
static double CompareWindow (
  FillHoleT * pfh,
  CacheView * inp_view,
  CacheView * subimage_view,
  ExceptionInfo *exception,
  ssize_t hx, ssize_t hy,
  ssize_t sx, ssize_t sy)
// Compares subimage window with hole starting at top-left=hx,hy
// with window in inp_view (which may contain hole) starting at top-left=sx, sy.
// Returns positive number MSE; closer to zero is closer match.
// If sx,sy has same number or more transparent pixels as hx,hy,
//   or either window is entirely transparent,
//   returns very large number.
{
  ssize_t
    xy;

  const VIEW_PIX_PTR
    *sxy, *sxySave,
    *hxy, *hxySave;

  VIEW_PIX_PTR
    minSub,
    maxSub;

  double
    score = 0;

  int
    cntNonTrans = 0;

  //if (pfh->debug==MagickTrue) printf ("CompareWindow %i %i  %i %i  ", (int)hx, (int)hy, (int)sx, (int)sy);

  if (sx==hx && sy==hy)
    return pfh->CompWind.WorstCompare;

  sxy = GetCacheViewVirtualPixels(inp_view,sx,sy,pfh->sqDim,pfh->sqDim,exception);
  if (sxy == (const VIEW_PIX_PTR *) NULL) {
    printf ("bad sxy");
    return pfh->CompWind.WorstCompare;
  }
  // If central pixel in sxy is transparent, forget about it.
  if (GetPixelAlpha (sxy + pfh->WindowRadPix*(pfh->sqDim+1)) <= 0)
    return pfh->CompWind.WorstCompare;

  hxy = GetCacheViewVirtualPixels(subimage_view,hx,hy,pfh->sqDim,pfh->sqDim,exception);
  if (hxy == (const VIEW_PIX_PTR *) NULL) {
    printf ("bad hxy");
    return pfh->CompWind.WorstCompare;
  }

  sxySave = sxy;
  hxySave = hxy;
  instantPp (&minSub);
  instantPp (&maxSub);

  MagickBooleanType InitMinMax = MagickFalse;
  for (xy=0; xy < pfh->sqDim * pfh->sqDim; xy++) {
    double sa = GetPixelAlpha (sxy);
    double ha = GetPixelAlpha (hxy);

    if (sa <= 0 && ha > 0) return pfh->CompWind.WorstCompare;

    if (sa > 0 && ha > 0) {
      double d = (GetPixelRed(sxy)-GetPixelRed(hxy)) / QuantumRange;
      score += d*d;
      d = (GetPixelGreen(sxy)-GetPixelGreen(hxy)) / QuantumRange;
      score += d*d;
      d = (GetPixelBlue(sxy)-GetPixelBlue(hxy)) / QuantumRange;
      score += d*d;
      cntNonTrans++;
    }

    if (pfh->CompWind.HomChkOn && ha > 0) {
      if (!InitMinMax) {
        minSub = *hxy;
        maxSub = *hxy;
        InitMinMax = MagickTrue;
      } else {
        // FIXME: or could record limits of sxy instead of hxy?
        if (GetPixelRed(&minSub) > GetPixelRed(hxy)) SetPixelRed(&minSub, GetPixelRed(hxy));
        if (GetPixelGreen(&minSub) > GetPixelGreen(hxy)) SetPixelGreen(&minSub, GetPixelGreen(hxy));
        if (GetPixelBlue(&minSub) > GetPixelBlue(hxy)) SetPixelBlue(&minSub, GetPixelBlue(hxy));
        if (GetPixelRed(&maxSub) < GetPixelRed(hxy)) SetPixelRed(&maxSub, GetPixelRed(hxy));
        if (GetPixelGreen(&maxSub) < GetPixelGreen(hxy)) SetPixelGreen(&maxSub, GetPixelGreen(hxy));
        if (GetPixelBlue(&maxSub) < GetPixelBlue(hxy)) SetPixelBlue(&maxSub, GetPixelBlue(hxy));
      }
    }

    sxy++;
    hxy++;
  }

  if (cntNonTrans == 0) return pfh->CompWind.WorstCompare;

  if (pfh->CompWind.HomChkOn) {
    sxy = sxySave;
    hxy = hxySave;
    int outside = 0;

    VIEW_PIX_PTR tppMin, tppMax;

    double hcp1 = pfh->CompWind.HomChk + 1;

    SetPixelRed   (&tppMin, hcp1*GetPixelRed(&minSub)   - pfh->CompWind.HomChk*GetPixelRed(&maxSub));
    SetPixelGreen (&tppMin, hcp1*GetPixelGreen(&minSub) - pfh->CompWind.HomChk*GetPixelGreen(&maxSub));
    SetPixelBlue  (&tppMin, hcp1*GetPixelBlue(&minSub)  - pfh->CompWind.HomChk*GetPixelBlue(&maxSub));

    SetPixelRed   (&tppMax, hcp1*GetPixelRed(&maxSub)   - pfh->CompWind.HomChk*GetPixelRed(&minSub));
    SetPixelGreen (&tppMax, hcp1*GetPixelGreen(&maxSub) - pfh->CompWind.HomChk*GetPixelGreen(&minSub));
    SetPixelBlue  (&tppMax, hcp1*GetPixelBlue(&maxSub)  - pfh->CompWind.HomChk*GetPixelBlue(&minSub));

    for (xy=0; xy < pfh->sqDim * pfh->sqDim; xy++) {
      double sa = GetPixelAlpha (sxy);
      double ha = GetPixelAlpha (hxy);

      if (sa > 0 && ha <= 0) {
        if      (GetPixelRed (sxy) < GetPixelRed (&tppMin)) outside++;
        else if (GetPixelRed (sxy) > GetPixelRed (&tppMax)) outside++;

        if      (GetPixelGreen (sxy) < GetPixelGreen (&tppMin)) outside++;
        else if (GetPixelGreen (sxy) > GetPixelGreen (&tppMax)) outside++;

        if      (GetPixelBlue (sxy) < GetPixelBlue (&tppMin)) outside++;
        else if (GetPixelBlue (sxy) > GetPixelBlue (&tppMax)) outside++;
      }
    }

    if (outside > 0) return pfh->CompWind.WorstCompare;
    // FIXME? Seems a bit harsh. Maybe make score just a bit worse.
  }

  score /= (cntNonTrans);

  return score;
}
===*/

static void inline CopyOnePix (
  FillHoleT * pfh,
  Image * image,
  CacheView * new_view,
  CacheView * inp_view,
  CopyWhereT *cw,
  ExceptionInfo *exception)
// Copies one pixel from inp_view to new_view.
// Adjusts cw coords.
{
  const VIEW_PIX_PTR
    *src;

  VIEW_PIX_PTR
    *dst;

  cw->srcX += pfh->WindowRadPix;
  cw->srcY += pfh->WindowRadPix;
  cw->dstX += pfh->WindowRadPix;
  cw->dstY += pfh->WindowRadPix;

  if (pfh->debug==MagickTrue) printf ("CopyOnePix %li,%li => %li,%li\n",
    cw->srcX, cw->srcY,
    cw->dstX, cw->dstY);

  src = GetCacheViewVirtualPixels(
    inp_view,cw->srcX,cw->srcY,1,1,exception);
  if (src == (const VIEW_PIX_PTR *) NULL) printf ("bad src");

  dst = GetCacheViewAuthenticPixels(
    new_view,cw->dstX,cw->dstY,1,1,exception);
  if (dst == (const VIEW_PIX_PTR *) NULL) printf ("bad dst");

  SET_PIXEL_RED   (image, GET_PIXEL_RED   (image, src), dst);
  SET_PIXEL_GREEN (image, GET_PIXEL_GREEN (image, src), dst);
  SET_PIXEL_BLUE  (image, GET_PIXEL_BLUE  (image, src), dst);
  SET_PIXEL_ALPHA (image, QuantumRange, dst);
}


static void inline CopyWindow (
  FillHoleT * pfh,
  Image * image,
  CacheView * new_view,
  CacheView * inp_view,
  CopyWhereT *cw,
  ExceptionInfo *exception)
// Copies non-transparent pixels from inp_view to replace transparent pixels in new_view.
// If CopyRad < WindowRad, or if out of bounds, the cw coords will be adjusted.
{
  const VIEW_PIX_PTR
    *src;

  VIEW_PIX_PTR
    *dst;

  int
    ij;

  // dst pixels could be outside authentic
  // We need to adjust GCV parameters so we are not outside authentic.

  // FIXME: Check somewhere for case of sqDim <= image dimensions.

  if (pfh->CopyRadPix < pfh->WindowRadPix) {
    int dRad = pfh->WindowRadPix - pfh->CopyRadPix;
    cw->srcX += dRad;
    cw->srcY += dRad;
    cw->dstX += dRad;
    cw->dstY += dRad;
  }

  int offsX = 0;
  int offsY = 0;

  if (cw->dstX < 0) offsX = -cw->dstX;
  if (cw->dstY < 0) offsY = -cw->dstY;

  cw->wi = pfh->sqCopyDim - offsX;
  cw->ht = pfh->sqCopyDim - offsY;

  cw->srcX += offsX;
  cw->srcY += offsY;

  cw->dstX += offsX;
  cw->dstY += offsY;

  // FIXME: chk next 22-Nov-2015:
  if (cw->wi + cw->dstX > pfh->Width)  cw->wi = pfh->Width - cw->dstX;
  if (cw->ht + cw->dstY > pfh->Height) cw->ht = pfh->Height - cw->dstY;

  if (pfh->debug==MagickTrue) printf ("CW offsXY=%i,%i dstXY=%li,%li wi=%li ht=%li\n", offsX, offsY, cw->dstX, cw->dstY, cw->wi, cw->ht);

  assert (cw->wi > 0 && cw->wi <= pfh->sqCopyDim);
  assert (cw->ht > 0 && cw->ht <= pfh->sqCopyDim);

  src = GetCacheViewVirtualPixels(inp_view,cw->srcX,cw->srcY,cw->wi,cw->ht,exception);
  if (src == (const VIEW_PIX_PTR *) NULL) { printf ("bad src"); return; }

  dst = GetCacheViewAuthenticPixels(new_view,cw->dstX,cw->dstY,cw->wi,cw->ht,exception);
  if (dst == (const VIEW_PIX_PTR *) NULL) { printf ("bad dst"); return; }

  // FIXME: "image" in following may be wrong.
  for (ij=0; ij < cw->wi*cw->ht; ij++) {
    if (GET_PIXEL_ALPHA (image, src) > 0 && GET_PIXEL_ALPHA (image, dst) == 0) {
      SET_PIXEL_RED   (image, GET_PIXEL_RED (image, src), dst);
      SET_PIXEL_GREEN (image, GET_PIXEL_GREEN (image, src), dst);
      SET_PIXEL_BLUE  (image, GET_PIXEL_BLUE (image, src), dst);
      SET_PIXEL_ALPHA (image, QuantumRange, dst);
    }
    src += Inc_ViewPixPtr (image);
    dst += Inc_ViewPixPtr (image);
  }

  if (pfh->debug==MagickTrue) printf ("doneCW\n");
}


static void ResolveImageParams (
  const Image *image,
  FillHoleT * pfh
)
{
  pfh->Width = image->columns;
  pfh->Height = image->rows;

  pfh->Match.ref_columns = image->columns;
  pfh->Match.ref_rows = image->rows;

  pfh->ImgDiagSq = image->rows * image->rows + image->columns * image->columns;

  if (pfh->WindowRad_IsPc)
    pfh->WindowRadPix = (0.5 + pfh->WindowRad/100.0
                         * (pfh->Width<pfh->Height ? pfh->Width:pfh->Height));

  if (pfh->WindowRadPix < 1) pfh->WindowRadPix = 1;

  pfh->Match.matchRadX = pfh->Match.matchRadY = pfh->WindowRadPix;

  if (pfh->CopyRad_IsPc)
    pfh->CopyRadPix = (0.5 + pfh->CopyRad/100.0
                         * (pfh->Width<pfh->Height ? pfh->Width:pfh->Height));

  if (pfh->CopyRadPix < 0) pfh->CopyRadPix = 0;

  if (pfh->Match.LimSrchRad_IsPc)
    pfh->Match.limSrchRadX = pfh->Match.limSrchRadY = (0.5 + pfh->Match.LimSrchRad/100.0
                         * (pfh->Width<pfh->Height ? pfh->Width:pfh->Height));

  pfh->sqDim = 2 * pfh->WindowRadPix + 1;
  pfh->sqCopyDim = 2 * pfh->CopyRadPix + 1;

  pfh->CompWind.win_wi = pfh->sqDim;
  pfh->CompWind.win_ht = pfh->sqDim;

//  pfh->WorstCompare = (4*QuantumRange*QuantumRange * pfh->sqDim * pfh->sqDim);
//  pfh->WorstCompare *= (pfh->ImgDiagSq * 2);

  pfh->CompWind.WorstCompare = (4*QuantumRange*QuantumRange * pfh->sqDim * pfh->sqDim);
  pfh->CompWind.WorstCompare *= (pfh->ImgDiagSq * 2);

  if (pfh->Match.DissimOn == MagickFalse) {
    pfh->Match.dissimThreshold = pfh->CompWind.WorstCompare;
    pfh->Match.dissimThresholdSq = pfh->CompWind.WorstCompare;
  }

  pfh->CompWind.AllowEquCoord = MagickFalse;

  if (pfh->do_verbose) {
    fprintf (stderr, "pfh->WindowRadPix = %i\n", pfh->WindowRadPix);
    fprintf (stderr, "pfh->Match.limSrchRad = %lix%li\n",
      pfh->Match.limSrchRadX, pfh->Match.limSrchRadY);
    fprintf (stderr, "pfh->CopyRadPix = %i\n", pfh->CopyRadPix);
    fprintf (stderr, "pfh->CompWind.WorstCompare = %g\n", pfh->CompWind.WorstCompare);
    fprintf (stderr, "pfh->sqDim = %i\n", pfh->sqDim);
    fprintf (stderr, "pfh->sqCopyDim = %i\n", pfh->sqCopyDim);
    fprintf (stderr, "pfh->Match.RandSearchesI = %i\n", pfh->Match.RandSearchesI);
    fprintf (stderr, "pfh->Match.SkipNumI = %i\n", pfh->Match.SkipNumI);
    fprintf (stderr, "pfh->Match.simThresholdSq = %g\n", pfh->Match.simThresholdSq);
    fprintf (stderr, "pfh->Match.dissimThresholdSq = %g\n", pfh->Match.dissimThresholdSq);
  }
}


static MagickBooleanType MatchAndCopy (
  FillHoleT * pfh,
  Image * new_image,
  CacheView *new_view,        // destination for changes
  CacheView *copy_inp_view,   // source for changes
  ssize_t x,
  ssize_t y,
  CopyWhereT *cpwh,
  int *frameNum,
  MagickBooleanType *unfilled,
  MagickBooleanType *changedAny,
  ExceptionInfo *exception)
// x,y is central pixel of window to be matched.
// Returns true if okay, or false if serious problem.
{
  MagickBooleanType
    status = MagickTrue;

  if (pfh->debug==MagickTrue) printf ("MaC %li,%li \n", x, y);

  MatchT * pm = &pfh->Match;
  CompWindT * cmpwin = &pfh->CompWind;

  Match (x, y, x, y, pm, cmpwin, pfh->random_info, exception);

  if (pfh->debug==MagickTrue) printf ("Mac Best ref xy=%li,%li %g  from sub xy %li,%li\n",
      pm->bestX, pm->bestY, pm->bestScore, cmpwin->subX, cmpwin->subY);

  if (pm->bestScore < pm->dissimThresholdSq) {

    // Copy functions expect these are the top-left of the search windows.
    cpwh->srcX = pm->bestX - pm->matchRadX;
    cpwh->srcY = pm->bestY - pm->matchRadY;
    cpwh->dstX = cmpwin->subX;
    cpwh->dstY = cmpwin->subY;

    // Note: cpwh->wi and cpwh->ht is the window for copy, not search.

    if (pfh->debug==MagickTrue) printf (" MaC cpwh: %li,%li %lix%li+%li+%li\n",
      cpwh->srcX, cpwh->srcY, cpwh->wi, cpwh->ht, cpwh->dstX, cpwh->dstY);

    if (pfh->copyWhat == copyOnePixel)
      CopyOnePix (pfh, new_image, new_view, copy_inp_view, cpwh, exception);
    else
      CopyWindow (pfh, new_image, new_view, copy_inp_view, cpwh, exception);

    if (SyncCacheViewAuthenticPixels (new_view,exception) == MagickFalse)
      status=MagickFalse;

    *changedAny = MagickTrue;
    if (pfh->write_frames == 2) {
      WriteFrame (pfh, new_image, *frameNum, exception);
      (*frameNum)++;
    }
  } else {
    *unfilled = MagickTrue;
  }
  if (pfh->debug==MagickTrue) printf ("doneMac\n");
  return (status);
}

static void InitRand (FillHoleT * pfh)
{
  pfh->random_info=AcquireRandomInfo();

  // There seems to be a problem: the first few values show coherency,
  // so skip over them.

  int i;
  for (i=0; i < 20; i++) {
    GetPseudoRandomValue(pfh->random_info);
  }
}

static void DeInitRand (FillHoleT * pfh)
{
  pfh->random_info=DestroyRandomInfo(pfh->random_info);
}

static void InitAutoLimit (FillHoleT * pfh)
{
  pfh->Match.SetAutoLimit = MagickTrue;
  pfh->Match.UseAutoLimit = MagickFalse;

  pfh->Match.limLeft = pfh->Width;
  pfh->Match.limTop = pfh->Height;
  pfh->Match.limRight = 0;
  pfh->Match.limBot = 0;

  pfh->Match.nLimitCnt = 0;
}

static void UseAutoLimit (FillHoleT * pfh)
{
  pfh->Match.SetAutoLimit = MagickFalse;
  pfh->Match.UseAutoLimit = pfh->Match.AutoLs;

  if (pfh->debug==MagickTrue)
    printf ("UseAutoLimit: LTRB %li,%li %li,%li\n",
      pfh->Match.limLeft, pfh->Match.limTop, pfh->Match.limRight, pfh->Match.limBot);
}

fillholescommon.inc

This code is common to both fillholes.c and fillholespri.c.

/*
    Reference: http://im.snibgo.com/fillholes.htm

    Data structures and functions common to fillholes process modules.

    Created 21-Nov-2015
*/

#define CH_R 1
#define CH_G 2
#define CH_B 4

#include "compwind.h"
#include "match.h"


typedef enum {
  copyOnePixel,
  copyWindow
} CopyWhatT;

typedef struct {
  CompWindT
    CompWind;

  MatchT
    Match;

  /* Set by user. */
  MagickBooleanType
    do_verbose,
    AutoRepeat,
    CopyRad_IsPc,
    WindowRad_IsPc,
    debug;

  double
    WindowRad,
    CopyRad,
    ImgDiagSq;

  int
    WindowRadPix,
    CopyRadPix,
    write_frames;

  char
    write_filename[MaxTextExtent];

  CopyWhatT
    copyWhat;

  /* Calculated. */
  int
    sqDim,
    sqCopyDim;

  ssize_t
    Width,
    Height;

  RandomInfo
    *restrict random_info;

} FillHoleT;

typedef struct {
  ssize_t
    srcX,
    srcY,
    dstX,
    dstY,
    wi,
    ht;
} CopyWhereT;

#include "compwind.inc"
#include "match.inc"


static void usage (void)
{
  printf ("Usage: -process 'fillhole [OPTION]...'\n");
  printf ("Populates transparent pixels, InFill.\n");
  printf ("\n");
  printf ("  wr,  window_radius N        radius of search window, >= 1\n");
  printf ("  lsr, limit_search_radius N  limit radius from transparent pixel to search\n");
  printf ("                                for source, >= 0\n");
  printf ("                                default = 0 = no limit\n");
  printf ("  als,  auto_limit_search X   automatic limit search, on or off\n");
  printf ("                                default on\n");
  printf ("  hc,  hom_chk X              homogeneity check, X is off or a small number\n");
  printf ("                                default 0.1\n");
  printf ("  e,   search_to_edges X      search for matches to image edges, on or off\n");
  printf ("                                default on\n");
  printf ("  s,   search X               X=entire or random or skip\n");
  printf ("                                default entire\n");
  printf ("  rs,  rand_searches N        number of random searches (eg 100)\n");
  printf ("                                default 0\n");
  printf ("  sn,  skip_num N             number of searches to skip in each direction\n");
  printf ("                                (eg 10)\n");
  printf ("                                default 0\n");
  printf ("  ref, refine X               whether to refine random and skip searches,\n");
  printf ("                                on or off\n");
  printf ("                                default on\n");
  printf ("  st,  similarity_threshold N\n");
  printf ("                              stop searching when RMSE <= N (eg 0.01)\n");
  printf ("                                default 0\n");
  printf ("  dt,  dissimilarity_threshold N\n");
  printf ("                              don't copy if best RMSE >= N (eg 0.05)\n");
  printf ("                                default: no threshold\n");
  printf ("  cp,  copy X                 X=onepixel or window\n");
  printf ("  cr,  copy_radius N          radius of pixels to copy, >= 1, <= wr\n");
  printf ("                                default: 0 or wr\n");
  printf ("  a,   auto_repeat            if pixels changed but any unfilled, repeat\n");
  printf ("  w,   write filename         write frames to files\n");
  printf ("  w2,  write2 filename        write frames to files\n");
  printf ("  v,   verbose                 write text information to stdout\n");
  printf ("\n"); 
}


static MagickBooleanType IsArg (char * pa, char * ShtOpt, char * LongOpt)
{
  if ((LocaleCompare(pa, ShtOpt)==0) || (LocaleCompare(pa, LongOpt)==0))
    return MagickTrue;

  return MagickFalse;
}


static inline MagickBooleanType EndsPc (const char *s)
{
  char c = *(s+strlen(s)-1);
  if (c == '%' || c == 'c')
    return MagickTrue;
  return MagickFalse;
}


static MagickBooleanType menu(
  const int argc,
  const char **argv,
  FillHoleT * pfh
)
// Returns MagickTrue if okay.
{
  int
    i;

  MagickBooleanType
    status;

  status = MagickTrue;

  pfh->do_verbose = MagickFalse;
  pfh->WindowRad = 1.0;
  pfh->CopyRad = 0.0;
  pfh->WindowRad_IsPc = pfh->CopyRad_IsPc = MagickFalse;
  pfh->WindowRadPix = 1;
  pfh->CopyRadPix = 0;
  pfh->write_frames = 0;
  pfh->write_filename[0] = '\0';
  pfh->copyWhat = copyOnePixel;
  pfh->debug = MagickFalse;
  pfh->AutoRepeat = MagickFalse;

  pfh->Match.LimSrchRad = 0.0;
  SetDefaultMatch (&pfh->Match);

  pfh->CompWind.HomChkOn = MagickTrue;
  pfh->CompWind.HomChk = 0.1;
  pfh->CompWind.nCompares = 0;


  for (i=0; i < argc; i++) {
    char * pa = (char *)argv[i];

    if (IsArg (pa, "wr", "window_radius")==MagickTrue) {
      i++;
      if (!isdigit ((int)*argv[i]))
      {
        fprintf (stderr, "Non-numeric argument to 'window_radius'.\n");
        status = MagickFalse;
      }
      else {
        pfh->WindowRad = atof(argv[i]);
        pfh->WindowRad_IsPc = EndsPc (argv[i]);
        if (!pfh->WindowRad_IsPc)
          pfh->WindowRadPix = pfh->WindowRad + 0.5;
      }
      if (pfh->WindowRad <= 0) {
        fprintf (stderr, "Bad 'window_radius' value.\n");
        status = MagickFalse;
      }
    } else if (IsArg (pa, "cr", "copy_radius")==MagickTrue) {
      i++;
      if (!isdigit ((int)*argv[i]))
      {
        fprintf (stderr, "Non-numeric argument to 'copy_radius'.\n");
        status = MagickFalse;
      }
      else {
        pfh->CopyRad = atof(argv[i]);
        pfh->CopyRad_IsPc = EndsPc (argv[i]);
        if (!pfh->CopyRad_IsPc)
          pfh->CopyRadPix = pfh->CopyRad + 0.5;
      }
      if (pfh->CopyRad <= 0) {
        fprintf (stderr, "Bad 'copy_radius' value.\n");
        status = MagickFalse;
      }
    } else if (IsArg (pa, "lsr", "limit_search_radius")==MagickTrue) {
      // FIXME: also allow percentage of smaller image dimension.
      i++;
      if (!isdigit ((int)*argv[i])) {
        fprintf (stderr, "Non-numeric argument to 'limit_search_radius'.\n");
        status = MagickFalse;
      } else {
        pfh->Match.LimSrchRad = atof(argv[i]);
        pfh->Match.LimSrchRad_IsPc = EndsPc (argv[i]);
        if (!pfh->Match.LimSrchRad_IsPc)
          pfh->Match.limSrchRadX = pfh->Match.limSrchRadY = pfh->Match.LimSrchRad + 0.5;
      }
      if (pfh->Match.LimSrchRad < 0) {
        fprintf (stderr, "Bad 'limit_search_radius' value.\n");
        status = MagickFalse;
      }

/*---
    } else if (IsArg (pa, "c", "channel")==MagickTrue) {
      i++;
      pfh->channels = 0;
      const char * p = argv[i];
      while (*p) {
        switch (toupper ((int)*p)) {
          case 'R':
            pfh->channels |= CH_R;
            break;
          case 'G':
            pfh->channels |= CH_G;
            break;
          case 'B':
            pfh->channels |= CH_B;
            break;
          case 'L':
            pfh->channels |= CH_R;
            break;
          case 'A':
            pfh->channels |= CH_G;
            break;
          default:
            fprintf (stderr, "Invalid 'channels' [%s]\n", argv[i]);
            status = MagickFalse;
        }
        p++;
      }
---*/
    } else if (IsArg (pa, "als", "auto_limit_search")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "on")==0)
        pfh->Match.AutoLs = MagickTrue;
      else if (LocaleCompare(argv[i], "off")==0)
        pfh->Match.AutoLs = MagickFalse;
      else {
        fprintf (stderr, "Invalid 'auto_limit_search' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "ref", "refine")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "on")==0)
        pfh->Match.Refine = MagickTrue;
      else if (LocaleCompare(argv[i], "off")==0)
        pfh->Match.Refine = MagickFalse;
      else {
        fprintf (stderr, "Invalid 'refine' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "s", "search")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "entire")==0)
        pfh->Match.searchWhat = swEntire;
      else if (LocaleCompare(argv[i], "random")==0)
        pfh->Match.searchWhat = swRandom;
      else if (LocaleCompare(argv[i], "skip")==0)
        pfh->Match.searchWhat = swSkip;
      else {
        fprintf (stderr, "Invalid 'search' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "rs", "rand_searches")==MagickTrue) {
      i++;
      if (!isdigit ((int)*argv[i])) {
        fprintf (stderr, "Non-numeric argument to 'rand_searches'.\n");
        status = MagickFalse;
      } else {
        pfh->Match.RandSearchesF = atof (argv[i]);
        pfh->Match.RandSearches_IsPc = EndsPc (argv[i]);
        if (!pfh->Match.RandSearches_IsPc)
          pfh->Match.RandSearchesI = pfh->Match.RandSearchesF + 0.5;
      }
      if (pfh->Match.RandSearchesF <= 0) {
        fprintf (stderr, "Bad 'rand_searches' value.\n");
        status = MagickFalse;
      }
    } else if (IsArg (pa, "sn", "skip_num")==MagickTrue) {
      i++;
      if (!isdigit ((int)*argv[i])) {
        fprintf (stderr, "Non-numeric argument to 'skip_num'.\n");
        status = MagickFalse;
      } else {
        pfh->Match.SkipNumF = atof (argv[i]);
        pfh->Match.SkipNum_IsPc = EndsPc (argv[i]);
        if (!pfh->Match.SkipNum_IsPc)
          pfh->Match.SkipNumI = pfh->Match.SkipNumF + 0.5;
      }
      if (pfh->Match.SkipNumF <= 0) {
        fprintf (stderr, "Bad 'skip_num' value.\n");
        status = MagickFalse;
      }
    } else if (IsArg (pa, "hc", "hom_chk")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "off")==0) {
        pfh->CompWind.HomChkOn = MagickFalse;
      } else {
        pfh->CompWind.HomChkOn = MagickTrue;
        if (!isdigit ((int)*argv[i])) {
          fprintf (stderr, "'hom_chk' argument must be number or 'off'.\n");
          status = MagickFalse;
        } else {
          pfh->CompWind.HomChk = atof (argv[i]);
        }
      }
    } else if (IsArg (pa, "e", "search_to_edges")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "on")==0)
        pfh->Match.SearchToEdges = MagickTrue;
      else if (LocaleCompare(argv[i], "off")==0)
        pfh->Match.SearchToEdges = MagickFalse;
      else {
        fprintf (stderr, "Invalid 'search_to_edges' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "a", "auto_repeat")==MagickTrue) {
      pfh->AutoRepeat = MagickTrue;
    } else if (IsArg (pa, "cp", "copy")==MagickTrue) {
      i++;
      if (LocaleCompare(argv[i], "onepixel")==0)
        pfh->copyWhat = copyOnePixel;
      else if (LocaleCompare(argv[i], "window")==0)
        pfh->copyWhat = copyWindow;
      else {
        fprintf (stderr, "Invalid 'copy' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "st", "similarity_threshold")==MagickTrue) {
      i++;
      pfh->Match.simThreshold = atof (argv[i]);
    } else if (IsArg (pa, "dt", "dissimilarity_threshold")==MagickTrue) {
      i++;
      pfh->Match.DissimOn = MagickTrue;
      pfh->Match.dissimThreshold = atof (argv[i]);
    } else if (IsArg (pa, "w", "write")==MagickTrue) {
      pfh->write_frames = 1;
      i++;
      CopyMagickString (pfh->write_filename, argv[i], MaxTextExtent);
      if (!*pfh->write_filename) {
        fprintf (stderr, "Invalid 'write' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "w2", "write2")==MagickTrue) {
      pfh->write_frames = 2;
      i++;
      CopyMagickString (pfh->write_filename, argv[i], MaxTextExtent);
      if (!*pfh->write_filename) {
        fprintf (stderr, "Invalid 'write' [%s]\n", argv[i]);
        status = MagickFalse;
      }
    } else if (IsArg (pa, "v", "verbose")==MagickTrue) {
      pfh->do_verbose = MagickTrue;
    } else if (IsArg (pa, "d", "debug")==MagickTrue) {
      pfh->debug = MagickTrue;
    } else {
      fprintf (stderr, "fillhole: ERROR: unknown option [%s]\n", pa);
      status = MagickFalse;
    }
  }

/*
  pfh->num_chan = 0;
  if (pfh->channels & CH_R) pfh->num_chan++;
  if (pfh->channels & CH_G) pfh->num_chan++;
  if (pfh->channels & CH_B) pfh->num_chan++;
  if (pfh->channels == 0) {
    fprintf (stderr, "No channels\n");
    status = MagickFalse;
  }
*/


  if (pfh->CopyRadPix==0 || pfh->CopyRadPix > pfh->WindowRadPix) {
    pfh->CopyRadPix = pfh->WindowRadPix;
  }
  if (pfh->copyWhat==copyOnePixel) pfh->CopyRadPix=0;

  pfh->Match.simThresholdSq = pfh->Match.simThreshold * pfh->Match.simThreshold;
  pfh->Match.dissimThresholdSq = pfh->Match.dissimThreshold * pfh->Match.dissimThreshold;

  pfh->CompWind.win_wi = pfh->sqDim;
  pfh->CompWind.win_ht = pfh->sqDim;

  if (pfh->do_verbose) {
    fprintf (stderr, "fillhole options: ");

    fprintf (stderr, "  window_radius %g", pfh->WindowRad);
    if (pfh->WindowRad_IsPc) fprintf (stderr, "%c", '%');

    fprintf (stderr, "  limit_search_radius %g", pfh->Match.LimSrchRad);
    if (pfh->Match.LimSrchRad_IsPc) fprintf (stderr, "%c", '%');

    fprintf (stderr, "  auto_lsr %s", pfh->Match.AutoLs ? "on" : "off");

    fprintf (stderr, "  search");
    switch (pfh->Match.searchWhat) {
      case swEntire: fprintf (stderr, " entire"); break;
      case swSkip:
        fprintf (stderr, " skip  skip_num %g", pfh->Match.SkipNumF);
        if (pfh->Match.SkipNum_IsPc) fprintf (stderr, "%c", '%');
        break;
      case swRandom:
        fprintf (stderr, " random  rand_searches %g", pfh->Match.RandSearchesF);
        if (pfh->Match.RandSearches_IsPc) fprintf (stderr, "%c", '%');
        break;
      default: fprintf (stderr, " ??");
    }

    fprintf (stderr, "  hom_chk ");
    if (pfh->CompWind.HomChkOn) {
      fprintf (stderr, "%g", pfh->CompWind.HomChk);
    } else {
      fprintf (stderr, "off");
    }

/*-
    if (pfh->max_iter) fprintf (stderr, " max_iterations %i", pfh->max_iter);

    if (pfh->channels != (CH_R | CH_G | CH_B)) {
      fprintf (stderr, " channels ");
      if (pfh->channels & CH_R) fprintf (stderr, "R");
      if (pfh->channels & CH_G) fprintf (stderr, "G");
      if (pfh->channels & CH_B) fprintf (stderr, "B");
    }
-*/

    fprintf (stderr, "  refine %s", pfh->Match.Refine ? "on" : "off");

    if (!pfh->Match.SearchToEdges) fprintf (stderr, "  search_to_edges off");

    fprintf (stderr, "  similarity_threshold %g", pfh->Match.simThreshold);

    if (pfh->Match.DissimOn == MagickTrue)
      fprintf (stderr, "  dissimilarity_threshold %g", pfh->Match.dissimThreshold);

    if (pfh->AutoRepeat) fprintf (stderr, "  auto_repeat");

    fprintf (stderr, "  copy ");
    if (pfh->copyWhat==copyOnePixel) fprintf (stderr, "onepixel");
    else if (pfh->copyWhat==copyWindow) fprintf (stderr, "window");
    else fprintf (stderr, "??");

    if (pfh->CopyRadPix != pfh->WindowRadPix) {
      fprintf (stderr, "  copy_radius %g", pfh->CopyRad);
      if (pfh->CopyRad_IsPc) fprintf (stderr, "%c", '%');
    }

    if (pfh->debug) fprintf (stderr, "  debug");

    if (pfh->do_verbose) fprintf (stderr, "  verbose");

    fprintf (stderr, "\n");
  }

  if (status == MagickFalse)
    usage ();

  return (status);
}



static MagickStatusType WriteFrame (
  FillHoleT *pfh,
  Image * img,
  int frame_num,
  ExceptionInfo *exception)
{
  ImageInfo
    *ii;

  MagickStatusType
    status;

  Image
    *copy_img;

  ii = AcquireImageInfo ();

  copy_img = CloneImage(img, 0, 0, MagickTrue, exception);
  if (copy_img == (Image *) NULL)
    return(MagickFalse); // FIXME: raise error

  copy_img->scene = frame_num;

  CopyMagickString (copy_img->filename, pfh->write_filename, MaxTextExtent);

#if IMV6OR7==6
  status = WriteImage (ii, copy_img);
#else
  status = WriteImage (ii, copy_img, exception);
#endif

  DestroyImageList(copy_img);

  ii = DestroyImageInfo (ii);

  return status;
}


/*===
static double CompareWindow (
  FillHoleT * pfh,
  CacheView * inp_view,
  CacheView * subimage_view,
  ExceptionInfo *exception,
  ssize_t hx, ssize_t hy,
  ssize_t sx, ssize_t sy)
// Compares subimage window with hole starting at top-left=hx,hy
// with window in inp_view (which may contain hole) starting at top-left=sx, sy.
// Returns positive number MSE; closer to zero is closer match.
// If sx,sy has same number or more transparent pixels as hx,hy,
//   or either window is entirely transparent,
//   returns very large number.
{
  ssize_t
    xy;

  const VIEW_PIX_PTR
    *sxy, *sxySave,
    *hxy, *hxySave;

  VIEW_PIX_PTR
    minSub,
    maxSub;

  double
    score = 0;

  int
    cntNonTrans = 0;

  //if (pfh->debug==MagickTrue) printf ("CompareWindow %i %i  %i %i  ", (int)hx, (int)hy, (int)sx, (int)sy);

  if (sx==hx && sy==hy)
    return pfh->CompWind.WorstCompare;

  sxy = GetCacheViewVirtualPixels(inp_view,sx,sy,pfh->sqDim,pfh->sqDim,exception);
  if (sxy == (const VIEW_PIX_PTR *) NULL) {
    printf ("bad sxy");
    return pfh->CompWind.WorstCompare;
  }
  // If central pixel in sxy is transparent, forget about it.
  if (GetPixelAlpha (sxy + pfh->WindowRadPix*(pfh->sqDim+1)) <= 0)
    return pfh->CompWind.WorstCompare;

  hxy = GetCacheViewVirtualPixels(subimage_view,hx,hy,pfh->sqDim,pfh->sqDim,exception);
  if (hxy == (const VIEW_PIX_PTR *) NULL) {
    printf ("bad hxy");
    return pfh->CompWind.WorstCompare;
  }

  sxySave = sxy;
  hxySave = hxy;
  instantPp (&minSub);
  instantPp (&maxSub);

  MagickBooleanType InitMinMax = MagickFalse;
  for (xy=0; xy < pfh->sqDim * pfh->sqDim; xy++) {
    double sa = GetPixelAlpha (sxy);
    double ha = GetPixelAlpha (hxy);

    if (sa <= 0 && ha > 0) return pfh->CompWind.WorstCompare;

    if (sa > 0 && ha > 0) {
      double d = (GetPixelRed(sxy)-GetPixelRed(hxy)) / QuantumRange;
      score += d*d;
      d = (GetPixelGreen(sxy)-GetPixelGreen(hxy)) / QuantumRange;
      score += d*d;
      d = (GetPixelBlue(sxy)-GetPixelBlue(hxy)) / QuantumRange;
      score += d*d;
      cntNonTrans++;
    }

    if (pfh->CompWind.HomChkOn && ha > 0) {
      if (!InitMinMax) {
        minSub = *hxy;
        maxSub = *hxy;
        InitMinMax = MagickTrue;
      } else {
        // FIXME: or could record limits of sxy instead of hxy?
        if (GetPixelRed(&minSub) > GetPixelRed(hxy)) SetPixelRed(&minSub, GetPixelRed(hxy));
        if (GetPixelGreen(&minSub) > GetPixelGreen(hxy)) SetPixelGreen(&minSub, GetPixelGreen(hxy));
        if (GetPixelBlue(&minSub) > GetPixelBlue(hxy)) SetPixelBlue(&minSub, GetPixelBlue(hxy));
        if (GetPixelRed(&maxSub) < GetPixelRed(hxy)) SetPixelRed(&maxSub, GetPixelRed(hxy));
        if (GetPixelGreen(&maxSub) < GetPixelGreen(hxy)) SetPixelGreen(&maxSub, GetPixelGreen(hxy));
        if (GetPixelBlue(&maxSub) < GetPixelBlue(hxy)) SetPixelBlue(&maxSub, GetPixelBlue(hxy));
      }
    }

    sxy++;
    hxy++;
  }

  if (cntNonTrans == 0) return pfh->CompWind.WorstCompare;

  if (pfh->CompWind.HomChkOn) {
    sxy = sxySave;
    hxy = hxySave;
    int outside = 0;

    VIEW_PIX_PTR tppMin, tppMax;

    double hcp1 = pfh->CompWind.HomChk + 1;

    SetPixelRed   (&tppMin, hcp1*GetPixelRed(&minSub)   - pfh->CompWind.HomChk*GetPixelRed(&maxSub));
    SetPixelGreen (&tppMin, hcp1*GetPixelGreen(&minSub) - pfh->CompWind.HomChk*GetPixelGreen(&maxSub));
    SetPixelBlue  (&tppMin, hcp1*GetPixelBlue(&minSub)  - pfh->CompWind.HomChk*GetPixelBlue(&maxSub));

    SetPixelRed   (&tppMax, hcp1*GetPixelRed(&maxSub)   - pfh->CompWind.HomChk*GetPixelRed(&minSub));
    SetPixelGreen (&tppMax, hcp1*GetPixelGreen(&maxSub) - pfh->CompWind.HomChk*GetPixelGreen(&minSub));
    SetPixelBlue  (&tppMax, hcp1*GetPixelBlue(&maxSub)  - pfh->CompWind.HomChk*GetPixelBlue(&minSub));

    for (xy=0; xy < pfh->sqDim * pfh->sqDim; xy++) {
      double sa = GetPixelAlpha (sxy);
      double ha = GetPixelAlpha (hxy);

      if (sa > 0 && ha <= 0) {
        if      (GetPixelRed (sxy) < GetPixelRed (&tppMin)) outside++;
        else if (GetPixelRed (sxy) > GetPixelRed (&tppMax)) outside++;

        if      (GetPixelGreen (sxy) < GetPixelGreen (&tppMin)) outside++;
        else if (GetPixelGreen (sxy) > GetPixelGreen (&tppMax)) outside++;

        if      (GetPixelBlue (sxy) < GetPixelBlue (&tppMin)) outside++;
        else if (GetPixelBlue (sxy) > GetPixelBlue (&tppMax)) outside++;
      }
    }

    if (outside > 0) return pfh->CompWind.WorstCompare;
    // FIXME? Seems a bit harsh. Maybe make score just a bit worse.
  }

  score /= (cntNonTrans);

  return score;
}
===*/

static void inline CopyOnePix (
  FillHoleT * pfh,
  Image * image,
  CacheView * new_view,
  CacheView * inp_view,
  CopyWhereT *cw,
  ExceptionInfo *exception)
// Copies one pixel from inp_view to new_view.
// Adjusts cw coords.
{
  const VIEW_PIX_PTR
    *src;

  VIEW_PIX_PTR
    *dst;

  cw->srcX += pfh->WindowRadPix;
  cw->srcY += pfh->WindowRadPix;
  cw->dstX += pfh->WindowRadPix;
  cw->dstY += pfh->WindowRadPix;

  if (pfh->debug==MagickTrue) printf ("CopyOnePix %li,%li => %li,%li\n",
    cw->srcX, cw->srcY,
    cw->dstX, cw->dstY);

  src = GetCacheViewVirtualPixels(
    inp_view,cw->srcX,cw->srcY,1,1,exception);
  if (src == (const VIEW_PIX_PTR *) NULL) printf ("bad src");

  dst = GetCacheViewAuthenticPixels(
    new_view,cw->dstX,cw->dstY,1,1,exception);
  if (dst == (const VIEW_PIX_PTR *) NULL) printf ("bad dst");

  SET_PIXEL_RED   (image, GET_PIXEL_RED   (image, src), dst);
  SET_PIXEL_GREEN (image, GET_PIXEL_GREEN (image, src), dst);
  SET_PIXEL_BLUE  (image, GET_PIXEL_BLUE  (image, src), dst);
  SET_PIXEL_ALPHA (image, QuantumRange, dst);
}


static void inline CopyWindow (
  FillHoleT * pfh,
  Image * image,
  CacheView * new_view,
  CacheView * inp_view,
  CopyWhereT *cw,
  ExceptionInfo *exception)
// Copies non-transparent pixels from inp_view to replace transparent pixels in new_view.
// If CopyRad < WindowRad, or if out of bounds, the cw coords will be adjusted.
{
  const VIEW_PIX_PTR
    *src;

  VIEW_PIX_PTR
    *dst;

  int
    ij;

  // dst pixels could be outside authentic
  // We need to adjust GCV parameters so we are not outside authentic.

  // FIXME: Check somewhere for case of sqDim <= image dimensions.

  if (pfh->CopyRadPix < pfh->WindowRadPix) {
    int dRad = pfh->WindowRadPix - pfh->CopyRadPix;
    cw->srcX += dRad;
    cw->srcY += dRad;
    cw->dstX += dRad;
    cw->dstY += dRad;
  }

  int offsX = 0;
  int offsY = 0;

  if (cw->dstX < 0) offsX = -cw->dstX;
  if (cw->dstY < 0) offsY = -cw->dstY;

  cw->wi = pfh->sqCopyDim - offsX;
  cw->ht = pfh->sqCopyDim - offsY;

  cw->srcX += offsX;
  cw->srcY += offsY;

  cw->dstX += offsX;
  cw->dstY += offsY;

  // FIXME: chk next 22-Nov-2015:
  if (cw->wi + cw->dstX > pfh->Width)  cw->wi = pfh->Width - cw->dstX;
  if (cw->ht + cw->dstY > pfh->Height) cw->ht = pfh->Height - cw->dstY;

  if (pfh->debug==MagickTrue) printf ("CW offsXY=%i,%i dstXY=%li,%li wi=%li ht=%li\n", offsX, offsY, cw->dstX, cw->dstY, cw->wi, cw->ht);

  assert (cw->wi > 0 && cw->wi <= pfh->sqCopyDim);
  assert (cw->ht > 0 && cw->ht <= pfh->sqCopyDim);

  src = GetCacheViewVirtualPixels(inp_view,cw->srcX,cw->srcY,cw->wi,cw->ht,exception);
  if (src == (const VIEW_PIX_PTR *) NULL) { printf ("bad src"); return; }

  dst = GetCacheViewAuthenticPixels(new_view,cw->dstX,cw->dstY,cw->wi,cw->ht,exception);
  if (dst == (const VIEW_PIX_PTR *) NULL) { printf ("bad dst"); return; }

  // FIXME: "image" in following may be wrong.
  for (ij=0; ij < cw->wi*cw->ht; ij++) {
    if (GET_PIXEL_ALPHA (image, src) > 0 && GET_PIXEL_ALPHA (image, dst) == 0) {
      SET_PIXEL_RED   (image, GET_PIXEL_RED (image, src), dst);
      SET_PIXEL_GREEN (image, GET_PIXEL_GREEN (image, src), dst);
      SET_PIXEL_BLUE  (image, GET_PIXEL_BLUE (image, src), dst);
      SET_PIXEL_ALPHA (image, QuantumRange, dst);
    }
    src += Inc_ViewPixPtr (image);
    dst += Inc_ViewPixPtr (image);
  }

  if (pfh->debug==MagickTrue) printf ("doneCW\n");
}


static void ResolveImageParams (
  const Image *image,
  FillHoleT * pfh
)
{
  pfh->Width = image->columns;
  pfh->Height = image->rows;

  pfh->Match.ref_columns = image->columns;
  pfh->Match.ref_rows = image->rows;

  pfh->ImgDiagSq = image->rows * image->rows + image->columns * image->columns;

  if (pfh->WindowRad_IsPc)
    pfh->WindowRadPix = (0.5 + pfh->WindowRad/100.0
                         * (pfh->Width<pfh->Height ? pfh->Width:pfh->Height));

  if (pfh->WindowRadPix < 1) pfh->WindowRadPix = 1;

  pfh->Match.matchRadX = pfh->Match.matchRadY = pfh->WindowRadPix;

  if (pfh->CopyRad_IsPc)
    pfh->CopyRadPix = (0.5 + pfh->CopyRad/100.0
                         * (pfh->Width<pfh->Height ? pfh->Width:pfh->Height));

  if (pfh->CopyRadPix < 0) pfh->CopyRadPix = 0;

  if (pfh->Match.LimSrchRad_IsPc)
    pfh->Match.limSrchRadX = pfh->Match.limSrchRadY = (0.5 + pfh->Match.LimSrchRad/100.0
                         * (pfh->Width<pfh->Height ? pfh->Width:pfh->Height));

  pfh->sqDim = 2 * pfh->WindowRadPix + 1;
  pfh->sqCopyDim = 2 * pfh->CopyRadPix + 1;

  pfh->CompWind.win_wi = pfh->sqDim;
  pfh->CompWind.win_ht = pfh->sqDim;

//  pfh->WorstCompare = (4*QuantumRange*QuantumRange * pfh->sqDim * pfh->sqDim);
//  pfh->WorstCompare *= (pfh->ImgDiagSq * 2);

  pfh->CompWind.WorstCompare = (4*QuantumRange*QuantumRange * pfh->sqDim * pfh->sqDim);
  pfh->CompWind.WorstCompare *= (pfh->ImgDiagSq * 2);

  if (pfh->Match.DissimOn == MagickFalse) {
    pfh->Match.dissimThreshold = pfh->CompWind.WorstCompare;
    pfh->Match.dissimThresholdSq = pfh->CompWind.WorstCompare;
  }

  pfh->CompWind.AllowEquCoord = MagickFalse;

  if (pfh->do_verbose) {
    fprintf (stderr, "pfh->WindowRadPix = %i\n", pfh->WindowRadPix);
    fprintf (stderr, "pfh->Match.limSrchRad = %lix%li\n",
      pfh->Match.limSrchRadX, pfh->Match.limSrchRadY);
    fprintf (stderr, "pfh->CopyRadPix = %i\n", pfh->CopyRadPix);
    fprintf (stderr, "pfh->CompWind.WorstCompare = %g\n", pfh->CompWind.WorstCompare);
    fprintf (stderr, "pfh->sqDim = %i\n", pfh->sqDim);
    fprintf (stderr, "pfh->sqCopyDim = %i\n", pfh->sqCopyDim);
    fprintf (stderr, "pfh->Match.RandSearchesI = %i\n", pfh->Match.RandSearchesI);
    fprintf (stderr, "pfh->Match.SkipNumI = %i\n", pfh->Match.SkipNumI);
    fprintf (stderr, "pfh->Match.simThresholdSq = %g\n", pfh->Match.simThresholdSq);
    fprintf (stderr, "pfh->Match.dissimThresholdSq = %g\n", pfh->Match.dissimThresholdSq);
  }
}


static MagickBooleanType MatchAndCopy (
  FillHoleT * pfh,
  Image * new_image,
  CacheView *new_view,        // destination for changes
  CacheView *copy_inp_view,   // source for changes
  ssize_t x,
  ssize_t y,
  CopyWhereT *cpwh,
  int *frameNum,
  MagickBooleanType *unfilled,
  MagickBooleanType *changedAny,
  ExceptionInfo *exception)
// x,y is central pixel of window to be matched.
// Returns true if okay, or false if serious problem.
{
  MagickBooleanType
    status = MagickTrue;

  if (pfh->debug==MagickTrue) printf ("MaC %li,%li \n", x, y);

  MatchT * pm = &pfh->Match;
  CompWindT * cmpwin = &pfh->CompWind;

  Match (x, y, x, y, pm, cmpwin, pfh->random_info, exception);

  if (pfh->debug==MagickTrue) printf ("Mac Best ref xy=%li,%li %g  from sub xy %li,%li\n",
      pm->bestX, pm->bestY, pm->bestScore, cmpwin->subX, cmpwin->subY);

  if (pm->bestScore < pm->dissimThresholdSq) {

    // Copy functions expect these are the top-left of the search windows.
    cpwh->srcX = pm->bestX - pm->matchRadX;
    cpwh->srcY = pm->bestY - pm->matchRadY;
    cpwh->dstX = cmpwin->subX;
    cpwh->dstY = cmpwin->subY;

    // Note: cpwh->wi and cpwh->ht is the window for copy, not search.

    if (pfh->debug==MagickTrue) printf (" MaC cpwh: %li,%li %lix%li+%li+%li\n",
      cpwh->srcX, cpwh->srcY, cpwh->wi, cpwh->ht, cpwh->dstX, cpwh->dstY);

    if (pfh->copyWhat == copyOnePixel)
      CopyOnePix (pfh, new_image, new_view, copy_inp_view, cpwh, exception);
    else
      CopyWindow (pfh, new_image, new_view, copy_inp_view, cpwh, exception);

    if (SyncCacheViewAuthenticPixels (new_view,exception) == MagickFalse)
      status=MagickFalse;

    *changedAny = MagickTrue;
    if (pfh->write_frames == 2) {
      WriteFrame (pfh, new_image, *frameNum, exception);
      (*frameNum)++;
    }
  } else {
    *unfilled = MagickTrue;
  }
  if (pfh->debug==MagickTrue) printf ("doneMac\n");
  return (status);
}

static void InitRand (FillHoleT * pfh)
{
  pfh->random_info=AcquireRandomInfo();

  // There seems to be a problem: the first few values show coherency,
  // so skip over them.

  int i;
  for (i=0; i < 20; i++) {
    GetPseudoRandomValue(pfh->random_info);
  }
}

static void DeInitRand (FillHoleT * pfh)
{
  pfh->random_info=DestroyRandomInfo(pfh->random_info);
}

static void InitAutoLimit (FillHoleT * pfh)
{
  pfh->Match.SetAutoLimit = MagickTrue;
  pfh->Match.UseAutoLimit = MagickFalse;

  pfh->Match.limLeft = pfh->Width;
  pfh->Match.limTop = pfh->Height;
  pfh->Match.limRight = 0;
  pfh->Match.limBot = 0;

  pfh->Match.nLimitCnt = 0;
}

static void UseAutoLimit (FillHoleT * pfh)
{
  pfh->Match.SetAutoLimit = MagickFalse;
  pfh->Match.UseAutoLimit = pfh->Match.AutoLs;

  if (pfh->debug==MagickTrue)
    printf ("UseAutoLimit: LTRB %li,%li %li,%li\n",
      pfh->Match.limLeft, pfh->Match.limTop, pfh->Match.limRight, pfh->Match.limBot);
}

blurFill.bat

This script has a similar effect to the fillholes.c process module. It works quite well for small holes (radius around 1 or 2), but not for large holes.

@rem From image %1 with transparency,
@rem blurs until fully opaque.
@rem
@rem Optional:
@rem   %2 blur sigma (default 1)
@rem     %2 can have a "+" suffix.
@rem     If it has, the sigma will increment on each pass.
@rem   %3 output filename
@rem   %4 blur type
@rem     0 -blur 0x{sigma}
@rem     1 -motion-blur 0x{sigma}+{angle}
@rem     2 -rotational-blur {angle}
@rem   %5 angle
@rem
@rem  bfTHRESH_PC percentage of alpha for thresholding.
@rem    For thin lines, use a low value eg 3.
@rem  bfMAX_ITER maximum number of iterations. [Default max(w,h).]
@rem  bfMAX_SIG Stop when + results in sigma greater than this.
@rem   [Default no check.]
@rem
@rem  An equivalent compiled program wouldn't need to save images
@rem    between iterations, so would be faster.
@rem
@rem  Updated:
@rem    28-August-2018 Added bfMAX_SIG
@rem    13-July-2022 Upgraded for IM v7.

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 bf


set IN_BLR_SIG=%2
if "%IN_BLR_SIG%"=="." set IN_BLR_SIG=
if "%IN_BLR_SIG%"=="" set IN_BLR_SIG=1

rem echo IN_BLR_SIG=%IN_BLR_SIG%

set BLR_SUFFIX=%IN_BLR_SIG:~-1%
if "%BLR_SUFFIX%"=="+" (
  set BLR_SIG=%IN_BLR_SIG:~0,-1%
) else (
  set BLR_SUFFIX=
  set BLR_SIG=%IN_BLR_SIG%
)

if not "%3"=="" if not "%3"=="." set OUTFILE=%3

echo %~n0: INFILE=%INFILE% OUTFILE=%OUTFILE% BLR_SIG=%BLR_SIG% BLR_SUFFIX=%BLR_SUFFIX%  OUT_NUM=%OUT_NUM%

set BLR_TYPE=%4
if "%BLR_TYPE%"=="." set BLR_TYPE=
if "%BLR_TYPE%"=="" set BLR_TYPE=0

set BLR_ANG=%5
if "%BLR_ANG%"=="." set BLR_ANG=
if "%BLR_ANG%"=="" set BLR_ANG=0

if %BLR_TYPE%==0 (
  set sBLUR=-blur 0x%BLR_SIG%
) else if %BLR_TYPE%==1 (
  set sBLUR=-channel RGBA -motion-blur 0x%BLR_SIG%+%BLR_ANG% +channel
) else if %BLR_TYPE%==2 (
  set sBLUR=-channel RGBA -rotational-blur %BLR_ANG% +channel
) else (
  echo %0: Unknown BLR_TYPE=%BLR_TYPE%
  exit /B 1
)

if "%bfMAX_SIG%"=="" set bfMAX_SIG=1e9
set SIG_OK=1

echo %0: sBLUR=%sBLUR%

call %PICTBAT%quantFp %OUTFILE%

if "%bfTHRESH_PC%"=="" set bfTHRESH_PC=25

if "%bfMAX_ITER%"=="" for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "bfMAX_ITER=%%[fx:max(w,h)]" ^
  %INFILE%`) do set %%L
if "%bfMAX_ITER%"=="" exit /B 1

rem echo %0: bfMAX_ITER=%bfMAX_ITER%

set TMPDIR=\temp
set TMP_INFILE=%TMPDIR%\bf_tmpin.miff
set TMP_FILE=%TMPDIR%\bf1.miff

%IMG7%magick ^
  %INFILE% ^
  +write %TMP_INFILE% ^
  -channel A ^
  -threshold %bfTHRESH_PC%%% ^
  +channel ^
  -define quantum:format=floating-point -depth 32 ^
  %TMP_FILE%
if ERRORLEVEL 1 exit /B 1

set nITER=0
set MEAN=-1

:loop
set PREV_MEAN=%MEAN%

set MEAN=

for /F "usebackq" %%L in (`%IMG7%magick ^
  %TMP_FILE% ^
  ^( +clone ^
     -alpha extract ^
     +write mpr:ALP ^
     +delete ^
  ^) ^
  %sBLUR% ^
  ^( +clone ^
     -alpha extract ^
     mpr:ALP ^
     -compose Lighten -composite ^
     -threshold %bfTHRESH_PC%%% ^
     -precision 15 ^
     -format "MIN=%%[fx:minima]\nMAX=%%[fx:maxima]\nMEAN=%%[fx:mean]\n" +write info: ^
  ^) ^
  -alpha off -compose CopyOpacity -composite ^
  %TMP_INFILE% ^
  -compose Over -composite ^
  -channel RGBA -clamp +channel ^
  +depth ^
  -define "quantum:format=floating-point" -depth 32 ^
  %TMP_FILE%`) do set %%L

if "%MEAN%"=="" (
  echo %0: magick failed, no mean
  exit /B 1
)

if "%BLR_SUFFIX%"=="+" (
  for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "BLR_SIG=%%[fx:%BLR_SIG%*1.01]\nSIG_OK=%%[fx:%BLR_SIG%>%bfMAX_SIG%?0:1]" ^
  xc:`) do set %%L

  if %BLR_TYPE%==0 (
    set sBLUR=-blur 0x%BLR_SIG%
  ) else if %BLR_TYPE%==1 (
    set sBLUR=-motion-blur 0x%BLR_SIG%+%BLR_ANG%
  )
)

set /A nITER+=1

echo %~n0: nITER=%nITER% MIN=%MIN% MAX=%MAX% MEAN=%MEAN% BLR_SIG=%BLR_SIG% SIG_OK=%SIG_OK%

if %nITER% LSS %bfMAX_ITER% if "%PREV_MEAN%" NEQ "%MEAN%" if %SIG_OK%==1 goto loop

echo %~n0: bfMAX_ITER=%bfMAX_ITER% nITER=%nITER%

%IMG7%magick ^
  %TMP_FILE% ^
  %QUANT_FP% ^
  %OUTFILE%

if not "%MEAN%"=="1" (
  set BUST=1
) else (
  set BUST=0
)

call echoRestore

@endlocal & set bfOUTFILE=%OUTFILE%& set bfBUST=%BUST%

resizeFill.bat

rem From image %1 with transparency,
rem fill holes by resizing.
@rem
@rem Optional:
@rem   %2 resize factor > 1 eg 1.5 (default 2)
@rem   %3 output filename
@rem
@rem Updated:
@rem   15 May 2016 use %IML% for @script.
@rem   8-September-2022 for IM v7.
@rem
@rem  An equivalent compiled program wouldn't need to save images
@rem    between iterations, so would be faster.
@rem

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 rf


set SCR_FILE=rf_%~n1_scr.scr

set RES_FACT=%2
if "%RES_FACT%"=="." set RES_FACT=
if "%RES_FACT%"=="" set RES_FACT=2

if not "%3"=="" set OUTFILE=%3

set WW=
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -precision 19 ^
  -format "WW=%%w\nHH=%%h\nMAXDIM=%%[fx:max(w,h)]\nRES_PC=%%[fx:100/%RES_FACT%]" ^
  %INFILE%`) do set %%L
if "%WW%"=="" exit /B 1

set SIZ=1
set CNT=0
:loop
set /A CNT+=1
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "SIZ=%%[fx:!SIZ!*%RES_FACT%]\nDONE=%%[fx:!SIZ!>%MAXDIM%?1:0]" ^
  xc:`) do set %%L

rem echo SIZ=!SIZ! CNT=%CNT% DONE=!DONE!

if %DONE%==0 goto loop

echo CNT=%CNT%


rem ( for /L %%I in (0,1,%CNT%) do @echo ^( +clone -resize %RES_PC%%% +write x_%%I.png ^) ) >%SCR_FILE%

set RESIZES=
for /L %%I in (0,1,%CNT%) do set RESIZES=!RESIZES! "(" +clone -resize %RES_PC%%% +write x_%%I.png ")"

set CONV_IN=%INFILE%

set CNT=0
:loop2
for /F "usebackq" %%L in (`%IMG7%magick ^
  %CONV_IN% ^
-alpha set ^
-background red -alpha background -virtual-pixel background ^
  %RESIZES% ^
  -layers RemoveDups ^
  -filter Gaussian -resize "%WW%x%HH%^!" ^
  -background None ^
  -compose DstOver -layers merge ^
  -format "OPAQ=%%[opaque]" +write info: ^
  %OUTFILE%`) do set %%L
if "%OPAQ%"=="" exit /B 1

set CONV_IN=%OUTFILE%

set /A CNT+=1
if "%OPAQ%"=="false" if %CNT% LSS 10 goto loop2


call echoRestore

endlocal & set rfOUTFILE=%OUTFILE%

shiftFill.bat

Like blurFill.bat, this works quite well for small holes (radius around 1 or 2), but not for large holes.

rem From image %1 with transparency,
rem shifts to fill in transparent holes.
@rem
@rem Optional:
@rem   %2 maximum shift in form XxY (default: W=width and Y=height)
@rem      If X or Y is zero, it won't shift in that direction.
@rem   %3 output filename
@rem   sfSTART initial shift. Default 1.
@rem   sfFLAT_COL
@rem   sfPOST_PROC post-processing applied to just the filler eg -blur 0x1
@rem   sfFAST if 1, multiplies by 2 instead of adding 1 at each iteration.
@rem   sfLTRB any of l, t, r, b.
@rem
@rem Updated:
@rem   11-July-2016 Added sfPOST_PROC
@rem   25-July-2016 Default is slower but better. Added sfFAST for old behaviour.
@rem   29-July-2020 for IM v7.

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 sf


set MAX_SHFT=%2
if "%MAX_SHFT%"=="." set MAX_SHFT=

if not "%3"=="" set OUTFILE=%3

set WW=
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "WW=%%w\nHH=%%h\nmaxWH=%%[fx:max(w,h)]" ^
  %INFILE%`) do set %%L

if "%WW%"=="" exit /B 1

call parseXxY2 %WW% %HH% sfXY %MAX_SHFT%

set MAX_SHFT_W=%sfXY_X%
set MAX_SHFT_H=%sfXY_Y%

if "%sfFLAT_COL%"=="." set sfFLAT_COL=
if "%sfFLAT_COL%"=="" set sfFLAT_COL=White

if "%sfLTRB%"=="" set sfLTRB=LTRB
call %PICTBAT%getLtrb %sfLTRB%


echo %0: MAX_SHFT_W=%MAX_SHFT_W% MAX_SHFT_H=%MAX_SHFT_H%

set TMP_SCR=sf_tmp.scr

if "%sfSTART%"=="" set sfSTART=1

echo %INFILE% +write mpr:ORIG -compose DstOver >%TMP_SCR%


if "%sfFAST%"=="1" (
  set IMG=^^^( -clone 0 ^^^)
) else (
  set IMG=mpr:ORIG
)

set MAX_ITER=%maxWH%
set nITER=0
set N_SHIFT=%sfSTART%
:loop

set DONEONE=0

if %N_SHIFT% LEQ %MAX_SHFT_W% (
  if %ltrbR%==1 echo %IMG% -geometry +%N_SHIFT%+0 -composite >>%TMP_SCR%
  if %ltrbL%==1 echo %IMG% -geometry -%N_SHIFT%+0 -composite >>%TMP_SCR%
  set DONEONE=1
)

if %N_SHIFT% LEQ %MAX_SHFT_H% (
  if %ltrbB%==1 echo %IMG% -geometry +0+%N_SHIFT% -composite >>%TMP_SCR%
  if %ltrbT%==1 echo %IMG% -geometry +0-%N_SHIFT% -composite >>%TMP_SCR%
  set DONEONE=1
)

set /A nITER+=1

if "%sfFAST%"=="1" (
  set /A N_SHIFT*=2
) else (
  set /A N_SHIFT+=1
)

if %nITER% LSS %MAX_ITER% if %DONEONE%==1 goto loop

echo %0: nITER=%nITER%

if "%IM_VSN_6or7%"=="" call %PICTBAT%isIm6or7

echo %sfPOST_PROC% mpr:ORIG -background %sfFLAT_COL% -compose Over -composite >>%TMP_SCR%


if "%IM_VSN_6or7%"=="6" (
  echo +write %OUTFILE% >>%TMP_SCR%
  %IM%convert @%TMP_SCR% NULL:
) else (
  set OUTFILE2=!OUTFILE:\=\\!
  echo +write !OUTFILE2! >>%TMP_SCR%
  echo -exit >>%TMP_SCR%
  %IMG7%magick -script %TMP_SCR%
)

if ERRORLEVEL 1 exit /B 1

call echoRestore

endlocal & set sfOUTFILE=%OUTFILE%

getLtrb.bat

@setlocal enabledelayedexpansion

@set INSTR=%1

@set L=0
@set T=0
@set R=0
@set B=0
@set ERR=0

@if "%INSTR%"=="" (
  @set ERR=1
  @goto end
)

@set n=0

:loop
@set c=!INSTR:~%n%,1!

@if /I "%c%"=="L" (
  @set L=1
) else if /I "%c%"=="T" (
  @set T=1
) else if /I "%c%"=="R" (
  @set R=1
) else if /I "%c%"=="B" (
  @set B=1
) else if not "%c%"=="" (
  @set ERR=1
)

@set /A n+=1
@if not "%c%"=="" goto loop

:end

endlocal & set ltrbL=%L%& set ltrbT=%T%& set ltrbR=%R%& set ltrbB=%B%& set ltrbERR=%ERR%

meanFill.bat

rem From image %1 with transparency,
rem flattens with colour that is weighted-mean of image pixels.
@rem
@rem Updated:
@rem   12-July-2022 Upgraded to IM v7.

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 mf

if not "%2"=="" set OUTFILE=%2

for /F "usebackq" %%L in (`%IMG7%magick ^
  -precision 15 ^
  %INFILE% ^
  -scale "1x1^!" ^
  -format "R_PC=%%[fx:100*u.r]\nG_PC=%%[fx:100*u.g]\nB_PC=%%[fx:100*u.b]" ^
  info:`) do set %%L

%IMG7%magick ^
  %INFILE% ^
  -background rgb(%R_PC%%%,%G_PC%%%,%B_PC%%%) ^
  -layers flatten ^
  -alpha off ^
  +depth ^
  %OUTFILE%

call echoRestore

endlocal & set mfOUTFILE=%OUTFILE%

voronoiFill.bat

rem From image %1 with transparency,
rem write %2 output name,
rem filling holes with Voronoi method
@rem
@rem See http://www.imagemagick.org/discourse-server/viewtopic.php?f=1&t=30680&p=138975#p138975
@rem
@rem Updated:
@rem   30-July-2020 for IM v7.

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 vf

if not "%2"=="" if not "%2"=="." set OUTFILE=%2

set TMP_DIR=%TEMP%

%IMG7%magick ^
  %INFILE% ^
  ( +clone ^
    -channel A -morphology EdgeIn Diamond +channel ^
    +write sparse-color:%TMP_DIR%\vf.txt ^
    -sparse-color Voronoi "@%TMP_DIR%\vf.txt" -alpha off ^
  ) ^
  -compose DstOver -composite ^
  %OUTFILE%

if ERRORLEVEL 1 exit /B 1

call echoRestore

@endlocal & set vfOUTFILE=%OUTFILE%

sparseColFill.bat

rem From image %1 with transparency,
rem write %2 output name,
rem filling holes with method %3, default Voronoi.
@rem
@rem See http://www.imagemagick.org/discourse-server/viewtopic.php?f=1&t=30680&p=138975#p138975
@rem
@rem Updated:
@rem   25-September-2022 for IM v7.
@rem

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 vf

if not "%2"=="" if not "%2"=="." set OUTFILE=%2

set METH=%3
if "%METH%"=="." set METH=
if "%METH%"=="" set METH=Voronoi

set TMP_DIR=%TEMP%

%IMG7%magick ^
  %INFILE% ^
  ( +clone ^
    -channel A -morphology EdgeIn Diamond +channel ^
    +write sparse-color:%TMP_DIR%\vf.txt ^
    -sparse-color %METH% "@%TMP_DIR%\vf.txt" -alpha off ^
  ) ^
  -compose DstOver -composite ^
  %OUTFILE%

if ERRORLEVEL 1 exit /B 1

call echoRestore

@endlocal & set vfOUTFILE=%OUTFILE%

relaxFill.bat

rem Given %1 is image, probably some pixels have transparency,
rem %2 is input opaque image, first approximation to solution,
rem writes relaxed output to %3. (Can be same as %2.)
rem
rem %4 is target RMSE or PAE score. [default 0.001]
rem %5 is number of iteration steps between tests. [default 100]
rem %6 is optional mask, black where pixels are to be changed.
rem   (eg from -alpha extract)
@rem
@rem Also uses:
@rem   rfRLX_CONV is a relaxation convolution. [default average of 4 neighbours]
@rem   rfCOMP_METRIC default RMSE. PAE is more sensitive.
@rem   IMRF the location of IM to be used. Default: %IM%.
@rem
@rem  An equivalent compiled program wouldn't need to save images
@rem    between iterations, so would be faster.
@rem
@rem
@rem Updated:
@rem   12-July-2022 Upgraded to IM v7. Assumes magick is HDRI.

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

echo %0: %1 %2 %3 %4 %5 %6

@if not "%7"=="" (
  echo %0: Can't have 7 arguments
  exit /B 1
)

rem set IMRF=%IM%
rem if "%IMRF%"=="" set IMRF=%IM%

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 rf

set APPROX=%2
if "%APPROX%"=="." set APPROX=

if not "%3"=="" if not "%3"=="." set OUTFILE=%3

set LIM_DIST=%4
if "%LIM_DIST%"=="." set LIM_DIST=
if "%LIM_DIST%"=="" set LIM_DIST=0.001

set NUM_STEPS=%5
if "%NUM_STEPS%"=="." set NUM_STEPS=
if "%NUM_STEPS%"=="" set NUM_STEPS=100

set MASK=%6
if "%MASK%"=="." set MASK=

echo %0: INFILE=%INFILE% APPROX=%APPROX% OUTFILE=%OUTFILE%

set rfTMP_IN=rf_tmpin.miff

%IMG7%magick %INFILE% -set colorspace sRGB %rfTMP_IN%

if "%MASK%"=="" (
  set MASK=rf_mask_tmp.miff
  %IMG7%magick %rfTMP_IN% -alpha extract -threshold 50%% !MASK!
)

if "%MASK%"=="" (
  set sMSK1=
  set sMSK2=
) else (
  set sMSK1=-write-mask %MASK%
  set sMSK2=+write-mask
)


set TMP_OUT=rf_tmp_out.miff

if "%rfCOMP_METRIC%"=="" set rfCOMP_METRIC=RMSE

if [%rfRLX_CONV%]==[] set rfRLX_CONV="3x3:0,0.25,0,0.25,0,0.25,0,0.25,0"

:: Other possibilities:
::   set rfRLX_CONV="3x3:0.05,0.2,0.05,0.2,0,0.2,0.05,0.2,0.05"
:: Or use -blur 1x10000"


if "%APPROX%"=="" (
  echo %0: meanFill
  call %PICTBAT%meanFill %INFILE% %TMP_OUT%
)

if not "%APPROX%"=="" %IMG7%magick ^
  %APPROX% %INFILE% -compose Over -composite -strip +depth %TMP_OUT%

set sSTEPS=-morphology convolve:%NUM_STEPS% %rfRLX_CONV% mpr:SRC -composite

%IMG7%magick identify %rfTMP_IN%
if ERRORLEVEL 1 exit /B 1
if not "%APPROX%"=="" %IMG7%magick identify %APPROX%
%IMG7%magick identify %MASK%
if ERRORLEVEL 1 exit /B 1
%IMG7%magick identify %TMP_OUT%
if ERRORLEVEL 1 exit /B 1

set DONE=0
set nITER=0
set PREV_DIST=99999999
set DIST=

for /L %%I in (0,1,99) do if !DONE!==0 (
  for /F "usebackq" %%L in (`%IMG7%magick ^
    -precision 14 ^
    +depth ^
    %rfTMP_IN% +write mpr:SRC +delete ^
    +depth ^
    %TMP_OUT% ^
    ^( +clone ^
       -compose Over ^
       %sMSK1% ^
       -morphology convolve:%NUM_STEPS% %rfRLX_CONV% mpr:SRC -composite ^
       %sMSK2% ^
      -depth 32 ^
      -define "quantum:format=floating-point" ^
       +write %TMP_OUT% ^
    ^) ^
    -metric %rfCOMP_METRIC% -format "DIST=%%[distortion]\n" -compare ^
    info:`) do set %%L

  if "!DIST!"=="" (
    echo %0: nITER=!nITER! DIST is blank
    exit /B 1
  )

  if "!DIST!"=="nan" (
    echo %0: nITER=!nITER! DIST is nan
    exit /B 1
  )

  echo %0: %rfCOMP_METRIC% DIST=!DIST!

  for /F "usebackq" %%L in (`%IMG7%magick identify ^
    -format "DONE=%%[fx:!DIST!<=%LIM_DIST%||!PREV_DIST!<!DIST!?1:0]"
    xc:`) do set %%L

  set PREV_DIST=!DIST!
  set /A nITER+=1
)

echo %0: DONE=%DONE%

:: Note: RMSE should strictly improve at each pass. PAE might not.

%IMG7%magick %TMP_OUT% -strip %OUTFILE%

if %nITER%==0 (
  echo %0: nITER is zero
  exit /B 1
)

set /A nITER*=%NUM_STEPS%

echo %0: nITER=%nITER% DIST=%DIST%

call echoRestore

@endlocal & set rfOUTFILE=%OUTFILE%& set rfITER=%nITER%& set rfDIST=%DIST%

relaxFillMS.bat

rem Given %1 is image with pixels either opaque or transparent.
rem %2 is input opaque image, first approximation to solution,
rem   (used only for first, coarsest, pass)
rem writes relaxed output to %3. (Mandatory, can be same as %2.)
rem
rem %4 is target RMSE or PAE score. [default 0.001].
rem %5 is number of iteration steps between tests. [default 100]
rem %6 is optional mask, black where pixels are to be changed.
rem   (eg from -alpha extract)
rem %7 optional guidance image
@rem
@rem Also uses:
@rem   rfRLX_FUNC is a relaxation function. [default average of 4 neighbours]
@rem   rfCOMP_METRIC default RMSE. PAE is more sensitive.
@rem
@rem  An equivalent compiled program wouldn't need to save images
@rem    between iterations, so would be faster.
@rem
@rem
@rem Updated:
@rem   25-June-2017 for guidance 0% means no guidance.
@rem   12-July-2022 Upgraded to IM v7. Assumes magick is HDRI.
@rem

@if "%3"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 rfms

set IMOUTFMT=-depth 32 -define "quantum:format=floating-point"

echo %0: IMOUTFMT = %IMOUTFMT%

set APPROX=%2
if "%APPROX%"=="" set APPROX=.

set IMG_RLX=%2
if "%IMG_RLX%"=="" set IMG_RLX=.

if not "%3"=="" if not "%3"=="." set OUTFILE=%3

set TGT_SCORE=%4
if "%TGT_SCORE%"=="." set TGT_SCORE=
if "%TGT_SCORE%"=="" set TGT_SCORE=0.001

set NSTEPS=%5
if "%NSTEPS%"=="" set NSTEPS=.

set MASK=%6
if "%MASK%"=="." set MASK=

set GUIDANCE=%7
if "%GUIDANCE%"=="." set GUIDANCE=

if "%MASK%"=="" (
  set MASK=rfms_mask_tmp.miff
  %IMG7%magick %INFILE% -alpha extract %IMOUTFMT% !MASK!
)

set MASK_SM=rfms_mask_sm_tmp.miff

set GUID_SM=.
if not "%GUIDANCE%"=="" set GUID_SM=rfms_guid_sm_tmp.miff

echo %0: MASK=%MASK% GUIDANCE=%GUIDANCE%

set WW=
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "WW=%%w\nHH=%%h" ^
  %INFILE%`) do set %%L
if "%WW%"=="" exit /B 1

set TMP_IMG_HOLES=rfms_tih.miff
set TMP_IMG_RLXD=rfms_rlxd.miff

if not "%APPROX%"=="." (
  %IMG7%magick %APPROX% -resize "%W%x%H%^!" %IMOUTFMT% %TMP_IMG_RLXD%

  set IMG_RLX=%TMP_IMG_RLXD%
)

set nITER=0

call :frms %WW% %HH%
if ERRORLEVEL 1 (
  echo %0: call to frms failed
  exit /B 1
)

echo %0: Final %IMG7%magick %TMP_IMG_RLXD% %OUTFILE%
%IMG7%magick %TMP_IMG_RLXD% %IMOUTFMT% %OUTFILE%
if ERRORLEVEL 1 exit /B 1

rem echo %0: nITER=%nITER%

call echoRestore

@endlocal & set rfmsOUTFILE=%OUTFILE%& set rfmsITER=%nITER%

@exit /B 0

::----------------------------------
:: Subroutine (recursive)

:frms
echo %0: %1 %2
setlocal
set W=%1
set H=%2
set /A W_2=%1/2
set /A H_2=%2/2

set minDim=%W_2%
if %minDim% GTR %H_2% set minDim=%H_2%

rem echo W=%W% H=%H%

if %minDim% GTR 50 call :frms %W_2% %H_2%
if ERRORLEVEL 1 exit /B 1

echo %0: W=%W% H=%H%

set sRESIZE=-resize "%W%x%H%^^^!"
if %W%==%WW% if %H%==%HH% set sRESIZE=

echo %0: sRESIZE=%sRESIZE%

%IMG7%magick ^
  %INFILE% ^
  -strip ^
  %sRESIZE% ^
  +depth ^
  -depth 32 ^
  -define "quantum:format=floating-point" ^
  %TMP_IMG_HOLES%

if ERRORLEVEL 1 exit /B 1

%IMG7%magick ^
  %MASK% ^
  %sRESIZE% ^
  -threshold 50%% ^
  %IMOUTFMT% ^
  %MASK_SM%

if not "%GUIDANCE%"=="" %IMG7%magick ^
  %GUIDANCE% ^
  %sRESIZE% ^
  %IMOUTFMT% ^
  %GUID_SM%


rem %IMG7%magick %MASK_SM% rfm_dbg_%W%x%H%.png

if not "%IMG_RLX%"=="." (
:: Scale previous result up.
  %IMG7%magick ^
    %IMG_RLX% ^
    -scale "%W%x%H%^!" ^
    %TMP_IMG_HOLES% ^
    -compose Over -composite ^
    -depth 32 ^
    -define "quantum:format=floating-point" ^
    %IMG_RLX%

  if ERRORLEVEL 1 exit /B 1

  %IMG7%magick identify %IMG_RLX%
)

for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "smTGT_SCORE=%%[fx:%TGT_SCORE%*%WW%/%W%*%HH%/%H%]" ^
  %INFILE%`) do set %%L

echo %0: TGT_SCORE=%TGT_SCORE% smTGT_SCORE=%smTGT_SCORE%

if "%GUIDANCE%"=="" (
  call %PICTBAT%relaxFill ^
    %TMP_IMG_HOLES% ^
    %IMG_RLX% ^
    %TMP_IMG_RLXD% ^
    %smTGT_SCORE% ^
    %NSTEPS% ^
    %MASK_SM%
) else (
  call %PICTBAT%relaxFillScr ^
    %TMP_IMG_HOLES% ^
    %IMG_RLX% ^
    %TMP_IMG_RLXD% ^
    %smTGT_SCORE% ^
    %NSTEPS% ^
    %MASK_SM% ^
    %GUID_SM%
)

if ERRORLEVEL 1 exit /B 1

endlocal& set /A nITER=%nITER%+%rfITER%& set rfDIST=%rfDIST%

echo %0: nITER=%nITER% rfDIST=%rfDIST%

set IMG_RLX=%TMP_IMG_RLXD%

exit /B 0

relaxFillScr.bat

rem Given %1 is image, probably some pixels have transparency,
rem %2 is input opaque image, first approximation to solution,
rem writes relaxed output to %3. (Mandatory, can be same as %2.)
rem
rem %4 is target RMSE or PAE score. [default 0.001]
rem %5 is number of iteration steps between tests. [default 100]
rem %6 is optional mask, black where pixels are to be changed.
rem   (eg from -alpha extract)
rem %7 optional guidance image
@rem
@rem Also uses:
@rem   rfRLX_FUNC is a relaxation function. [default average of 4 neighbours]
@rem   rfCOMP_METRIC default RMSE. PAE is more sensitive.
@rem
@rem Exactly like relaxFill.bat, but writes a script for IM to run.
@rem
@rem  An equivalent compiled program wouldn't need to save images
@rem    between iterations, so would be faster.
@rem
@rem FIXME: Currently, guidance value 50% means "zero guidance".
@rem        This will change to be 0% means "zero guidance".
@rem was:
@rem   set sGUIDE=mpr:GUIDE -compose Mathematics -define compose:args=0,-0.25,1,0.125 -composite -compose Over
@rem change 0.125 to 0.
@rem
@rem Assumes magick is HDRI.
@rem
@rem Updated:
@rem   22-July-2022 Upgraded for IM v7.
@rem


@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

echo %0: %1 %2 %3 %4 %5 %6 %7

@call echoOffSave

call %PICTBAT%setInOut %1 rfs

set TMP_SCR=rfs.scr

set APPROX=%2
if "%APPROX%"=="." set APPROX=

if not "%3"=="" if not "%3"=="." set OUTFILE=%3

set LIM_DIST=%4
if "%LIM_DIST%"=="." set LIM_DIST=
if "%LIM_DIST%"=="" set LIM_DIST=0.001

set NUM_STEPS=%5
if "%NUM_STEPS%"=="." set NUM_STEPS=
if "%NUM_STEPS%"=="" set NUM_STEPS=100

set MASK=%6
if "%MASK%"=="." set MASK=

set GUIDANCE=%7
if "%GUIDANCE%"=="." set GUIDANCE=

if "%GUIDANCE%"=="" (
  set sSAVE_GUIDE=
  set sGUIDE=
) else (
  set sSAVE_GUIDE=%GUIDANCE% +write mpr:GUIDE +delete
  set sGUIDE=mpr:GUIDE -compose Mathematics -define compose:args=0,-0.25,1,0 -composite -compose Over
)

set rfsTMP_IN=rf_tmpin.miff

%IMG7%magick %INFILE% -set colorspace sRGB %rfsTMP_IN%

if "%MASK%"=="" (
  set MASK=rfs_mask_tmp.miff
  %IMG7%magick %rfsTMP_IN% -alpha extract -alpha off -threshold 50%% !MASK!
)

if "%MASK%"=="" (
  set sMSK1=
  set sMSK2=
) else (
  set sMSK1=-mask %MASK%
  set sMSK2=+mask
)

if "%rfCOMP_METRIC%"=="" set rfCOMP_METRIC=RMSE

if "%rfRLX_CONV%"=="" set rfRLX_CONV="3x3:0,0.25,0,0.25,0,0.25,0,0.25,0"

:: Other possibilities:
::   set rfRLX_CONV="3x3:0.05,0.2,0.05,0.2,0,0.2,0.05,0.2,0.05"
:: Or use -blur 1x10000"


if "%APPROX%"=="" call %PICTBAT%meanFill %rfsTMP_IN% %OUTFILE%

if not "%APPROX%"=="" if not "%APPROX%"=="%OUTFILE%" %IMG7%magick ^
  %APPROX% %rfsTMP_IN% -compose Over -composite +depth %OUTFILE%

%IMG7%magick identify %rfsTMP_IN%
if not "%APPROX%"=="" %IMG7%magick identify %APPROX%
%IMG7%magick identify %OUTFILE%
if not "%MASK%"=="" %IMG7%magick identify %MASK%
if not "%GUIDANCE%"=="" %IMG7%magick identify %GUIDANCE%

:: FIXME: In following, also use mask, after testing.
(
  echo "%rfsTMP_IN%" +write mpr:SRC +delete %sSAVE_GUIDE%
  echo "%OUTFILE%"
  echo +depth
  echo ^( +clone -compose Over -channel RGB

  for /L %%I in (1,1,%NUM_STEPS%) do (
    echo -morphology convolve %rfRLX_CONV% %sGUIDE% mpr:SRC -composite
  )

  echo   -depth 32 -define quantum:format^=floating-point
  echo   +write %OUTFILE%
  echo ^)
  echo +channel
  echo -metric %rfCOMP_METRIC% -format "DIST=%%[distortion]\n" -compare -write info:

) >%TMP_SCR%

if "%IM_VSN_6or7%"=="" call %PICTBAT%isIm6or7

set DONE=0
set nITER=0
set PREV_DIST=99999999
set DIST=

if "%IM_VSN_6or7%"=="7" (
  echo -exit >>%TMP_SCR%
)

for /L %%I in (0,1,99) do if !DONE!==0 (

  if "%IM_VSN_6or7%"=="6" (
    for /F "usebackq" %%L in (`%IMG7%magick ^
      -precision 14 -define compose:clamp^=off +depth @%TMP_SCR% ^
      NULL:`) do set %%L
  ) else (
    for /F "usebackq" %%L in (`%IMG7%magick ^
      -precision 14 -define compose:clamp^=off +depth ^
      -script %TMP_SCR%`) do set %%L
  )

  if "!DIST!"=="" exit /B 1

  echo %0: %rfCOMP_METRIC% DIST=!DIST!

  for /F "usebackq" %%L in (`%IMG7%magick identify ^
    -format "DONE=%%[fx:!DIST!<=%LIM_DIST%||!PREV_DIST!<!DIST!?1:0]"
    xc:`) do set %%L

  set PREV_DIST=!DIST!
  set /A nITER+=1
)

:: Note: RMSE should strictly improve at each pass. PAE might not.

if %nITER%==0 exit /B 1

set /A nITER*=%NUM_STEPS%

echo %0: nITER=%nITER% DIST=%DIST%

call echoRestore

@endlocal & set rfsOUTFILE=%OUTFILE%& set rfITER=%nITER%& set rfDIST=%DIST%

fnDispFill.bat

rem From image %1,
rem fill holes by using fractal noise displacement map.
@rem
@rem For pyramids, we can specify
@rem displacements as percentage of width and height, plus a small number of pixels eg 2.
@rem FIXME: Should this be percentage of _minimum_ (width,height) ?
@rem
@rem
@rem Updated:
@rem   30-September-2022 for IM v7.
@rem


@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 fndf

if not "%2"=="" set OUTFILE=%2

if "%fndfDISP_FACT%"=="" set fndfDISP_FACT=0.5

if "%fndfDISP_PC%"=="" set fndfDISP_PC=5
if "%fndfDISP_ADD%"=="" set fndfDISP_ADD=5

if "%fndfMAX_ITER%"=="" set fndfMAX_ITER=100

set WW=
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "WW=%%w\nHH=%%h\nWD=%%[fx:w*%fndfDISP_PC%/100+%fndfDISP_ADD%]\nHD=%%[fx:h*%fndfDISP_PC%/100+%fndfDISP_ADD%]" ^
  %INFILE%`) do set %%L
if "%WW%"=="" exit /B 1

echo %0: INFILE=%INFILE% WW=%WW% HH=%HH% WD=%WD% HD=%HD%


set nIter=0

rem The long convert gives white spots with %IM% v6.9.1-6.

:loop

rem Correction: fractNoise can't take "10c" etc.
rem call %PICTBAT%fractNoise ^
rem   %WW% %HH% 10c 100c . 1.5 fndf_dm.png

call %PICTBAT%fractNoise ^
  %WW% %HH% . . . 1.5 fndf_dm.png

if ERRORLEVEL 1 (
  echo %0: fractNoise failed
  exit /B 1
)

for /F "usebackq" %%L in (`%IMG7%magick ^
  %INFILE% ^
  ^( +clone ^
    %fnOUTFILE% ^
    -virtual-pixel Tile ^
    -compose Displace ^
      -set option:compose:args %WD%x%HD% ^
      -composite ^
    +write x.png ^
  ^) ^
  -compose DstOver -composite ^
  -write %OUTFILE% ^
  -format "OPAQ=%%[opaque]" info:`) do set %%L

set /A nIter+=1

set INFILE=%OUTFILE%

if %nIter% LSS %fndfMAX_ITER% if "%OPAQ%"=="false" goto loop

if "%OPAQ%"=="false" (
  set BUST=1
) else (
  set BUST=0
)

call echoRestore

endlocal & set fndfOUTFILE=%OUTFILE%& set fndfBUST=%BUST%

All images on this page were created by the commands shown.

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG or TIFF or MIFF to JPG.

My usual version of IM is:

%IMG7%magick -version
Version: ImageMagick 7.1.0-49 Q16-HDRI x64 7a3f3f1:20220924 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL 
Delegates (built-in): bzlib cairo freetype gslib heic jng jp2 jpeg jxl lcms lqr lzma openexr pangocairo png ps raqm raw rsvg tiff webp xml zip zlib
Compiler: Visual Studio 2022 (193331630)

This customised development version is:

%IM7DEV%magick -version
Version: ImageMagick 7.1.0-20 Q32-HDRI x86_64 2021-12-29 https://imagemagick.org
Copyright: (C) 1999-2021 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP(4.5) 
Delegates (built-in): bzlib cairo fontconfig fpx freetype jbig jng jpeg lcms ltdl lzma pangocairo png raqm rsvg tiff webp wmf x xml zip zlib
Compiler: gcc (11.2)

Source file for this web page is fillholes.h1. To re-create this web page, execute "procH1 fillholes".


This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 27-Feb-2015.

Page created 17-Mar-2023 02:54:20.

Copyright © 2023 Alan Gibson.