Scanned image scratch removal with “ICE”

Thanks for the explanation, now I understand better. Is there a difference between outputting the RGB channels of the original image and outputting the shared buffer [-2], which is the RGB channels of the original image?

I experimented a bit with the image and these are my findings so far:

This is a crop of the original image, upper left corner (it’s a negative, so colours are inverted):

Its alpha channel (the infrared channel) clearly exhibits some of the scratches in the original, especially a big blop in the upper right, some sprinkles in the centre and a long horizontal line-like scratch in the lower part, the slight scratch in the upper part seems to be not visible in the infrared image:

The threshold operation leaves a lot of “noise”, maybe it is too high:

The dilation adds a lot of the noisy region to the mask:

This is handled very well by the inpainting algorithm, the only thing it has (little) problems with is the long horizontal scratch (only visible at 100% view):

However, I thought getting rid of the “noise” would be beneficial to speed up inpainting and to prevent the whole image to be reconstructed, so I inserted an erosion step before dilation, which gets rid of too little sprinkles. The following is mask and result with -erode. 2 -dilate. 4:


The same with -erode. 3 -dilate. 5:


And another one with reduced threshold of 82 %, rest as before:


Seems the key parameters for a good mask are threshold, erosion and dilation size (OK, looks obvious, but at least we have reasonable starting parameters).

5 Likes

Nicely done! Very helpful to show what’s happening in each step :slight_smile:

It depends which form of output - if you mean a file, nope no difference so you could indeed miss out the “-channels 0,2” at the end of the command line version and just output the shared buffer. Obviously the shared buffer can only exist while the image it’s shared with exists, meaning you can’t remove the original and leave gimp with just the shared version - if you try to do so you’ll most likely get an error.

If with larger images you end up using large values with -erode and -dilate the mask can end up “blocky”. Alternatives might be -dilate_circ, -distance (with a threshold) or -blur (also with a threshold) to give rounder/smoother edges.

1 Like

While the concept of shared buffers is clear, I still have problems to figure out how the GIMP plugin interacts with the GIMP layer(s). Is there some documentation available (I was not able to find docs regarding the plugin, only language docs and tutorials)?

This is the size I need, because it’s the maximum my scanner can handle (approx. 18 MP), and I wonder where even bigger values would be needed. The erosion gets rid of single pixel errors (or slightly larger) which are not visible anyway, inpainted or not, and noise, and the dilation adds back the removed edges from larger areas and adds a little margin. So I might end up using dilate_circ becaus the areas to be inpainted will be slightly smaller and smoother at the corners, but I guess I will hardly go beyond a size of 5 (or erode_parameter + 2). With 3/5, the inpainted areas appear way bigger than the scratch size, if you compare directly.

Now I have to figure out how I get G’MIC into darktable, Christmas is approaching very fast ;-). To become serious again, the biggest problem I see with doing this at the end of the processing is that the crop I do in $RawProcessor will cause an alignment problem afterwards. But that’s a different story (unless you accidentally have a solution :wink:).

By the way, my gmic command did not accept “.” as parameter for the inpainting, only “[-1]”. Maybe a quoting problem.

If there are docs specifically about G’MIC/gimp layer interactions I haven’t seen them. Perhaps when I have more time I can write some on the wiki. It’s mostly straightforward (each layer becomes an image on the stack) but there are quirks; the non-removable alpha channel being one, another related to opacity 0 areas not being copied back to gimp - needs to be a gimp bug report really.

The dot image parameter issue could be down to version, it was only introduced recently. If not as you say could be escape character related.

Not sure if this will help but rather than creating layers in the GIMP I sometimes use Fotoxx. It uses them too but hidden from the user. One of the noise removal tools goes by the name of a top hat filter. Playing with the rad killed the noise. I then reduced size by 2/3. It has another feature - brightness distribution. I flattened that by a lot and use the deband option. Then used it’s tone mapping heavily to bring the contract back up from the flattening. Sounds odd but is useful. I allowed it to work on high contrast. Then the combo tool, curves, saturation and colour balance plus some work on the blue curve, lowering the dark end. Also tried spot white balance but no idea what it should look like.

Then in the GIMP. Wavelet decompose to try and get rid of the black line. Might be possible but will takes some time. I’d wonder if it was possible in this case to simply cut it out. Something like 2 layers one part transparent no line overlapping the line and then scale to make the joins match,

Anyway result on noise removal and some of the line toned down. Few spots left but they would heal easily.

Fotoxx is a Linux package - :slight_smile: Aren’t we lucky.
John

During my vacation I had little time but at least I was able to do a comparison of the inpainting methods provided by G’MIC. I did not optimize the parameters but used the default configuration, except for -inpaint_diffusion, where I used the parameter given by @garagecoder, and one other case, where no default values are available. I compared at two areas of the image, a line scratch segment and a large dot-like scratch. These places can be clearly seen on the used mask:

The command line is the same for all, but the inpainting option (and output file name) was changed of course:

gmic -i raw0003.tif -crop 0%,0%,30%,30% -sh 0,2 -sh.. 3 -lt. 82% -erode. 3 -dilate. 5 -inpaint.. [-1] -k... -channels 0,2 -o test_.tif,ushort

The results, upscaled without interpolation, clearly show a “winner” algorithm :wink:. First the results:

Numbering the upper results 1 to 3 from left to right and the lower ones 4 to 6, I think in both cases 6 is the winner and 5 on the second place, but with quite some distance. The others are almost equally bad. The main problem is IMHO that the other algos tend to shift/stretch outer values towards the mask center, which leaves a visible artefact in form of “columns” for the line and a “cone” for the dot-like scratch. I guess you already know which class the winner algos are belonging to, the used command for the 6 cases are:

  1. -inpaint
  2. -inpaint_diffusion
  3. -inpaint_flow
  4. -inpaint_morpho
  5. -inpaint.. [-1],5,15,0.5,1,9,0
  6. -inpaint_patchmatch
4 Likes

I’m sure many will appreciate the examples you’ve shown here :smiley:

The results more or less follow the complexity of the algorithm and likely processing time, but where result is most important that’s no issue. Version 5 (a patch based inpaint) can be difficult to tweak given the number of parameters.

I think complexity is one side, but the big difference is between patch-based and “others”. Since 6 is better than 5 out-of-the-box, I would not tweak parameters of 5 but just use 6, since it is already that good. Maybe if speed is a concern, as you are saying, one may use one of the other methods, but in most cases that will not be necessary and for me definitely not.

More important and time consuming is to tweak the mask, because a good mask can hardly be judged by an algorithm. Luckily, computation time for the operations is very low :smile:.

I still wonder what algorithms are implemented in proprietary software such as VueScan, Nikon, Canon or SilverFast. I only have VueScan, and there scratch removal is really fast, but I did not check the quality of the results. It has only one Parameter “strength” with 3 different choices.

Hm, I tested the same with VueScans infrared cleaning, strongest setting, and the results are extremely different:

If I try to ignore the compression artefacts (I should have saved as tiff) the problematic area is still visible, I would say better than 1 to 4, but worse than 5.

The line was hardly removed at all. At that point, I am so happy about having the opportunity of using FLOSS software :smile:!

1 Like

I had the time to make a little filter for the GIMP G’MIC plugin from the findings above. Maybe this is useful for somebody else.

#@gimp ICE : ice, ice(0)
#@gimp : note = note("Scratch removal for scanned film images")
#@gimp : sep = separator()
#@gimp : Threshold = float(82,0,100)
#@gimp : Erosion = int(2,0,5)
#@gimp : Dilation = int(4,0,7)
#@gimp : Show preview after = choice("threshold","erosion","dilation","final image")
ice :
-sh 0,2
-sh.. 3
-lt. $1%

-if {$4>=1}
  -erode. $2
-endif

-if {$4>=2}
  -dilate. $3
-endif

-if {$4<3}
  -k...
  -channels 3
  -* 255
-else
  -inpaint_patchmatch.. [-1]
  -k...
  -channels 0,2
-endif

3 Likes

Hm, I was too fast, there’s a little problem with the preview, it seems to scale/normalize the input values depending on the image data that is actually shown. @garagecoder, any advice?

@David_Tschumperle, since you recently mentioned the preview possibilities in another thread, I try to address this directly to you. Given above G’MIC GIMP script (2 posts above), the preview of intermediate results is changing heavily depending on the area of the image shown. What is the reason? Is any parameter relative to the input data (I thought this is not the case)?

And another question, is there a simple possibility to make a standalone GUI program out of this? I don’t want to have to load all the images, one by one, into GIMP to find reasonable parameters. The processing is done on the command line anyway, but selecting the preview viewpoint works much better in the GIMP filter, and pulling sliders with direct feedback from the preview is much closer to the nature of the problem than iterating by changing numbers on the command line.

I’m not so surprised by the erratic behavior of the preview, as having an accurate preview for a filter is a very hard task to be honest. All depend on how your filter is coded actually, and what kind of information it uses to render its result.
If it requires only local image information, then the preview should be relatively accurate when the preview zoom factor is set to 1:1 by default. It it requires only a global view of the image, then the preview should be not so bad with a full view zoom factor. If it requires both, then it will be a mess.
The GimpPreview widget is able to give us only a small image for computing the preview either as a global view of the image, either as a local view of the image. But this is often not enough to get an accurate preview, for complex filters that requires local/global information, e.g. the min/max values of the whole image, as well as an estimate of the XY derivatives at the same time.

Concerning the stand-along GUI. Yes, this is planed.
We have started to work on an Universal plug-in done in Qt, which should be also working as a stand-alone application. This is planed for the next release 1.8.0, probably in a few months.

I can hardly wait :smiley:

It requires only the 1:1 view. E.g., if it shows the mask (only black and white values), it seems that the magnitude scaling (black/white point) when the threshold is computed depends on the area shown. It works correct with different areas on the command line.

I uploaded the test file again if you want to have a look: https://filebin.net/gqc36e26a63vmuin/raw0009.tif (150 MB).

Great news. I read about it here already, but did not know about standalone capabilities.

@chris
The problem is the combination of view area and “-lt $1%”. Using a percentage means base it on min/max values in the area supplied to the filter.

You could either base it on a fixed min/max or you could make the preview full image, but neither are likely to be perfect anyway. Perhaps there’s some other way I haven’t thought of though.

Hm, but on the command line the “-lt x%” seems independent of the actual input data range but in GIMP it seems not. What’s the difference and how can I achieve identical behaviour, such that I can reuse the parameters found in GIMP on the command line?

@garagecoder is right, you have this problem here :slight_smile:

  • The -lt. $1% is intended to depend on the min/max values of the whole image, which means that in the preview you’ll compute the threshold uniquely based on the min/max values of a cropped region. Not good or an accurate preview, if you zoom only on very dark or bright regions for instance. I suggest you use an absolute value instead of a percentage (the RGB input image has always values constrained to be in [0,255], so -lt. {$1*255%} should be great here.

Thanks for the suggestions, @garagecoder and @David_Tschumperle, it works well in GIMP G’MIC. Here’s the updated code for Version 1.8.0:

#@gui ICE : ice, ice(0)
#@gui : note = note("Scratch removal for scanned film images")
#@gui : sep = separator()
#@gui : Threshold = float(72,0,100)
#@gui : Erosion = int(2,0,5)
#@gui : Dilation = int(4,0,7)
#@gui : Show preview after = choice("threshold","erosion","dilation","final image")
ice :
-sh 0,2
-sh.. 3
-lt. {$1*255%}

-if {$4>=1}
  -erode. $2
-endif

-if {$4>=2}
  -dilate. $3
-endif

-if {$4<3}
  -k...
  -channels 3
  -* 255
-else
  -inpaint_patchmatch.. [-1]
  -k...
  -channels 0,2
-endif

Feel free to include this into G’MIC if you think it could be useful for other people. Maybe better change the name then, ICE may be trademarked in the context of scratch removal. Since in Germany ICE is a type of high speed train and G’MIC is a french product, I would suggest “TGV scratch removal” ;-).

Edit: If you decide to do so, I of course would contribute documentation on the whole process.

Hello Chris,

I’ve worked a bit on your code, to make it more clean :

  • I’ve added a Split preview option.
  • I’ve separated the preview process and the final process, so that you always get the final result when you actually apply the filter (even when the Preview as option is different).
  • I’ve added a loop to make your filter work with multiple input layers (and do not crash when input image is not in RGBA).
  • I’ve added a credit note at the end of the filter.

That’s how the code looks now, feel free to suggest any modification you would like to see:

#@gui Remove scratches : fx_remove_scratches, fx_remove_scratches_preview(0)
#@gui : note = note("<small><b>Note:</b> Scratch removal for scanned film images</small>")
#@gui : sep = separator()
#@gui : Threshold = float(72,0,100)
#@gui : Erosion = int(2,0,5)
#@gui : Dilation = int(4,0,7)
#@gui : Show preview after = choice(3,"Threshold","Erosion","Dilation","Final image")
#@gui : sep = separator(), Preview type = choice("Full","Forward horizontal","Forward vertical","Backward horizontal","Backward vertical","Duplicate top","Duplicate left","Duplicate bottom","Duplicate right")
#@gui : sep = separator(), note = note("<small>Author: <i>Chris/Pixls.us</i>.      Latest update: <i>01/04/2017</i>.</small>")
_fx_remove_scratches :
  -repeat $! -l[$>] -to_rgba
    -sh 0,2
    -sh.. 3
    -lt. {$1*255%}
    -if {$4>=1} -erode. $2 -endif
    -if {$4>=2} -dilate. $3 -endif
    -if {$4<3} -k... -channels 3 -* 255
    -else -inpaint_patchmatch.. [-1] -k... -channels 0,2
    -endif
  -endl -done

fx_remove_scratches :
  -_fx_remove_scratches ${1-3},3

fx_remove_scratches_preview :
  -gui_split_preview "-_fx_remove_scratches $*",$-1

I’ve put this filter in Testing / Chris - pixls.us. It should be really available after a filter refresh, with G’MIC plug-in 1.8.0.

2 Likes