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.
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.
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.
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 |
|
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. |
|
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. |
|
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 |
|
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 |
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 |
|
%IM7DEV%magick ^ fh_src2.png ^ -virtual-pixel None ^ -process ^ 'fillholes' ^ fh_src2a_fh.png The window radius is too small. |
|
The smallest radius to get the desired result is 6.
%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. |
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
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% |
|
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. |
|
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. |
|
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. |
|
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. |
|
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.
|
|
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. |
|
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. |
|
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.
|
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.
%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. |
|
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. |
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 |
An example with a more "natural" image:
toes.png. |
|
With Gimp, erase (make transparent) all non-grass pixels. 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 |
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. |
|
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. |
|
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. |
|
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. |
|
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. |
|
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. |
|
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. |
|
As previous, but search random.
%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. |
|
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. |
|
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. |
|
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. |
|
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. |
|
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. |
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 |
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.
We don't need to keep the frame files, so delete them.
del fh_x_fr_*.png del fh_x_fr2_*.png
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.
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.
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 |
|
call %PICTBAT%blurFill fh_src2.png |
|
call %PICTBAT%blurFill fh_ph0.png |
|
call %PICTBAT%blurFill fh_ph3.png |
|
call %PICTBAT%blurFill ^ toes_holed.png . fh_toes_bf.png |
The script is used in Follow line and Straightening horizons in the construction of displacement maps.
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 |
|
call %PICTBAT%resizeFill fh_src2.png |
|
call %PICTBAT%resizeFill fh_ph0.png |
|
call %PICTBAT%resizeFill fh_ph3.png |
|
call %PICTBAT%resizeFill ^ toes_holed.png . fh_toes_rf.png |
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 |
|
call %PICTBAT%resizeFill ^ fh_src2.png 1.1 fh_src2_rf2.png |
|
call %PICTBAT%resizeFill ^ fh_ph0.png 1.1 fh_ph0_rf2.png |
|
call %PICTBAT%resizeFill ^ fh_ph3.png 1.1 fh_ph3_rf2.png |
|
call %PICTBAT%resizeFill ^ toes_holed.png 1.1 fh_toes_rf2.png |
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 |
|
call %PICTBAT%shiftFill fh_src2.png |
|
call %PICTBAT%shiftFill fh_ph0.png |
|
call %PICTBAT%shiftFill fh_ph3.png |
|
call %PICTBAT%shiftFill ^ toes_holed.png . fh_toes_sf.png |
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 |
|
call %PICTBAT%meanFill ^ fh_src2.png fh_src2_mf.png |
|
call %PICTBAT%meanFill ^ fh_ph0.png fh_ph0_mf.png |
|
call %PICTBAT%meanFill ^ fh_ph3.png fh_ph3_mf.png |
|
call %PICTBAT%meanFill ^ fh_ph4.png fh_ph4_mf.png |
|
call %PICTBAT%meanFill ^ toes_holed.png fh_toes_mf.png |
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 |
|
call %PICTBAT%voronoiFill ^ fh_src2.png fh_src2_vf.png |
|
call %PICTBAT%voronoiFill ^ fh_ph0.png fh_ph0_vf.png |
|
call %PICTBAT%voronoiFill ^ fh_ph3.png fh_ph3_vf.png |
|
call %PICTBAT%voronoiFill ^ fh_ph4.png fh_ph4_vf.png |
|
call %PICTBAT%voronoiFill ^ toes_holed.png fh_toes_vf.png |
This is also generalized in the script sparseColFill.bat.
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 |
|
call %PICTBAT%relaxFill ^ fh_src2.png fh_src2_bf.png |
|
call %PICTBAT%relaxFill ^ fh_ph0.png fh_ph0_bf.png |
|
call %PICTBAT%relaxFill ^ fh_ph3.png fh_ph3_bf.png |
|
call %PICTBAT%relaxFill ^ toes_holed.png fh_toes_bf.png |
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 |
|
call %PICTBAT%relaxFillMS ^ fh_src2.png fh_src2_bf.png ^ fh_src12_rfms.png |
|
call %PICTBAT%relaxFillMS ^ fh_ph0.png fh_ph0_bf.png ^ fh_ph0_rfms.png |
|
call %PICTBAT%relaxFillMS ^ fh_ph3.png fh_ph3_bf.png ^ fh_ph3_rfms.png |
|
call %PICTBAT%relaxFillMS ^ toes_holed.png fh_toes_bf.png ^ fh_toes_holed_rfms.png |
relaxFillMS.bat optionally takes a guidance vector field. If so, it calls relaxFillScr.bat; see the next section.
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 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 |
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 |
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.
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 |
This can be useful when the "image" is really data, such as a displacement map. For an example, see Coffee mug.
The script fnDispFill.bat does the following:
call %PICTBAT%fnDispFill ^ toes_holed.png fh_toes_fndf.png |
|
call %PICTBAT%fnDispFill ^ fh_ph0.png fh_ph0_fndf.png |
The technique is not highly successful.
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: |
|
call %PICTBAT%membrane ^ mem_rim_c.png fh_mem_out_c.png |
Any of the above techniques may be incorporated within the following:
See the page Laplacian pyramids with transparency.
For convenience, this .c code and .bat are also available in a zip file. See Zipped BAT files.
/* 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); }
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); }
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); }
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%
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%
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%
@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%
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%
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%
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%
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%
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
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%
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.