0

I have sRGB images with color casts. To remove it manually I usually use Photoshop Level Adjustments. Photoshop also have tools for that: Auto Contrast or even better Auto Tone which also takes shadows, midtones & highlights into account.

If I remove the cast manually I adjust each of the RGB channels individually so that the darkest pixels are set to pure black and the lightest to pure white and then redistribute all other values (spreading the histogram). This is a simple approach but shows good results for my images.

In my node.js app I'm using sharp for image processing which uses libvips as its processing engine. I tried to remove the cast with .normalize() but this command works on all channels together and not individual for each of the RGB channels. So it doesn't work for me. I also asked this question on the sharp project page. I tested the suggestion from lovell to try it with hist_local but the results are not useable for me.

Now I would like to find out how this could be done using the native libvips. I've played around with nip2 GUI and different commands but could not figure out how it could be achieved:

  1. Histogram > Equalise Histogram > Global => Picture looks over saturated
  2. Image > Levels > Scale to 0 - 255 => Channels ar not all spreading from 0 - 255 (I don't understand exactly what this command does?)

Thanks for every hint!

Addition Here is a example with pictures from Photoshop to show what I want.

The source image is a picture of a frame from a film negative. Image before processing

Step1 Invert image Image after inversion

Step2 using Auto tone in Photoshop (works the same way as my description above about manually remove the color cast) Image after Auto Tone

This last picture is ok for me.

groboter
  • 81
  • 2
  • 6

3 Answers3

1

nip2 has a menu item for this.

Load your image and mark a region on it containing the area you'd like to be neutral. It can be any lightness, it doesn't need to be white.

  • Use File / Open to get the file dialog and you should see the image loaded in your workspace as a thumbnail.
  • Doubleclick on the thumbnail to open an image view window.
  • In the view window, zoom and pan to the right spot. The user guide (press F1) has a section on image navigation.
  • Hold down CTRL and click and drag down and right to mark a rectangular region.

image view window with marked region

Back in the main window, click Toolkits / Tasks / Capture / White balance. You should see something like:

white balance widget

You can drag an resize your region to change the neutral point. Use the colour picker to set what white means. You can make other whites with (for example) Colour / New / Colour from CCT and link them together.

  • Click Colour / New / Colour from CCT to make a colour picker from CCT (correlated colour temperature) -- the temperature in Kelvin of that white.
  • Set it to something interesting, like 4800 for warm white.
  • Click on the formula for A5.white to edit it, and enter the cell of your CCT widget (A7 in this case).

Now you can drag the region to adjust the pixels to set the neutral from, and drag the CCT slider to set the temperature.

custom white

It can be annoying to find things in the toolkit menu. There's a thing for searching toolkits: in the main window, click View / Toolkit browser. You can enter something like "white" and it'll show related toolkit entries.

jcupitt
  • 10,213
  • 2
  • 23
  • 39
  • Hi jcupitt thank you very much for this detailed explanation! I tried it but I don't get the same results as with my Photoshop actions. I have added more details and also pictures showing my steps in PS in my first question. – groboter Jan 11 '20 at 13:23
  • Step 1 "Invert" works fine with sharp or libvips. But I don't know how to achieve Step2. I can pick the min & max values for each RGB channel (using stats) but I don't know how to use this values to spread the histogram of each channel over the whole range (0...255). – groboter Jan 11 '20 at 13:27
  • Your PS example looks like it's just stretching the three channels independently. To do that in nip2, split the bands, scale them, then rejoin them. I'll make another answer. – jcupitt Jan 11 '20 at 13:44
  • I've just tried your suggestions using: Image->Band->Extract for each channel and then Image->Levels->Scale 0...255 and finally Image->Join->Bandwise Join. This gives a good result with a light blue cast but the PS one is a better because the blue cast is lighter. I suspect PS cuts a few of the lowest & highest values in the histogram. Is it possible to set such limits in libvips? – groboter Jan 11 '20 at 15:34
  • Yes, I think PS is ignoring a few percent of pixels at each end when calculating the stretch. You can do the same thing in nip2 by finding the normalised cumulative histogram, thresholding, and finding the index. – jcupitt Jan 11 '20 at 17:31
  • Sorry, I've tried to implement your suggestions but I do not know how. How can I get normalised cumulative histogram (integrate?) and do the thresholding, finding the index and building the final image? Sorry for bother you again! – groboter Jan 12 '20 at 08:22
0

Here's another answer, but using pyvips and responding to the previous comments. I didn't want to delete the first answer as it still seemed useful.

This version finds the image histogram, searches for thresholds which will select 0.5% and 99.5% of pixels in each image band, then rescales the image so that those pixel values become 0 and 255.

import sys
import pyvips

# trim off this percentage of pixels from the top and bottom
trim_percent = 0.5

def percent(hist, percentage):
    """From a histogram, find the threshold above which lie 
    @percentage of pixels."""
    # normalised cumulative histogram
    norm = hist.hist_cum().hist_norm() 

    # column and row profile over percentage
    c, r = (norm > norm.width * percentage / 100).profile()

    return r.avg()

image = pyvips.Image.new_from_file(sys.argv[1])

# photographic negative
image = image.invert()

# find image histogram, split to set of separate bands
bands = image.hist_find().bandsplit()

# for each band, the low and high thresholds
low = [percent(band, trim_percent) for band in bands]
high = [percent(band, 100 - trim_percent) for band in bands]

# rescale image 
scale = [255.0 / (h - l) for h, l in zip(high, low)]
image = (image - low) * scale

image.write_to_file(sys.argv[2])

It seems to give roughly similar results to the PS button. If I run:

$ ./autolevel.py ~/pics/before.jpg x.jpg

I see:

result of script on sample image

jcupitt
  • 10,213
  • 2
  • 23
  • 39
  • Thanks for this! Now understand better. I've also found a possible solution but without libvips which I think is simpler to implement in node.js. Because as far as I understand sharp does not support some of the necessary commands for your solution. I post a new answer with my solution. – groboter Jan 17 '20 at 13:59
0

In the meantime I've found the Simplest Color Balance Algorithm which exactly describes the problem with color casts and there you can also find a C source code.

It is exactly the same solution as John describes in his second answer but as a small piece of c-code.

I'm now trying to use it as C/C++ addon with N-API under node.js.

groboter
  • 81
  • 2
  • 6