Chris
Chris

Reputation: 319

Removing color cast from image

I have a digital camera that takes photos 24x7 and it often depending on the weather returns images with a nasty color cast. Generally blue.

enter image description here

I have been trying to find some source code or library that I can call from c# to reduce the color cast of the images.

Photoshop has a feature that works quite well on the images I tested which is:

  1. Open the image
  2. Select Image -> Adjustments -> Match Color
  3. Check the Neutralize check box

This works well, but I do not know what it is doing.

enter image description here

I am not good at the maths so was looking for ideas on existing code or libraries that I could just use.

I have been searching the web but not found anything useful - would love some help.

Upvotes: 6

Views: 3345

Answers (5)

dynamichael
dynamichael

Reputation: 852

Create a histogram, auto-generate corrected levels (max, min and gamma), apply the levels to the image. Assuming you have somehow gathered your pixel data into an array of type Color...

public static Color[] AutoLevel(Color[] input) {
    var histogram = new Histogram();
    foreach(var _ in input) histogram.Add(_);
    var levels = histogram.GetAutoLevels();
    var ret = new Color[input.Length];
    for(int _ = 0; _ < input.Length; _++) {
        ret[_] = levels.Apply(input[_]).ToColor();
    }
    return ret;
}

... and here's the class...

public class Histogram {
    private long[,] _values = new long[3, 256];

    public void AddColor(Color color) {
        AddColor(color.R, color.G, color.B);
    }

    public void AddColor(RGB color) {
        AddColor(color.R, color.G, color.B);
    }

    public void AddColor(byte r, byte g, byte b) {
        _values[0, b]++;
        _values[1, g]++;
        _values[2, b]++;
    }

    public long this[int channel, int index] {
        get { return _values[channel, index]; }
    }

    public long GetMaxValue() {
        var ret = long.MinValue;
        foreach(var _ in _values) if(_ > ret) ret = _;
        return ret;
    }

    public RGB GetMeanColor() {
        var total = new long[3];
        var count = new long[3];
        var value = new byte[3];
        for(var _ = 0; _ < 3; _++) {
            for(var __ = 0; __ < 256; __++) {
                total[_] += (_values[_, __] * __);
                count[_] += _values[_, __];
            }
            value[_] = (byte)Math.Round((double)total[_] / count[_]);
        }
        return new RGB(value[2], value[1], value[0]);
    }

    public RGB GetPercentileColor(double percentile) {
        var ret = new RGB();
        for(var _ = 0; _ < 3; _++) {
            var total = 0L;
            for(var __ = 0; __ < 256; __++) total += _values[_, __];
            var cutoff = (total * percentile);
            var count = 0L;
            for(var __ = 0; __ < 256; __++) {
                count += _values[_, __];
                if(count > cutoff) {
                    ret[_] = (byte)__;
                    break;
                }
            }
        }
        return ret;
    }

    public Levels GetAutoLevels() {
        var low = GetPercentileColor(0.005);
        var middle = GetMeanColor();
        var high = GetPercentileColor(0.995);
        return Levels.GetAdjusted(low, middle, high);
    }


    public class Levels {
        private RGB _inputLow = new RGB(0, 0, 0);
        private RGB _inputHigh = new RGB(255, 255, 255);
        private RGB _outputLow = new RGB(0, 0, 0);
        private RGB _outputHigh = new RGB(255, 255, 255);
        private double[] _gamma = { 1, 1, 1 };

        public RGB InputLow {
            get { return _inputLow; }
            set {
                for(var _ = 0; _ < 3; _++) {
                    if(value[_] == 255) value[_] = 254;
                    if(_inputHigh[_] <= value[_]) _inputHigh[_] = (byte)(value[_] + 1);
                }
                _inputLow = value;
            }
        }

        public RGB InputHigh {
            get { return _inputHigh; }
            set {
                for(var _ = 0; _ < 3; _++) {
                    if(value[_] == 0) value[_] = 1;
                    if(_inputLow[_] >= value[_]) _inputLow[_] = (byte)(value[_] - 1);
                }
                _inputHigh = value;
            }
        }

        public RGB OutputLow {
            get { return _outputLow; }
            set {
                for(var _ = 0; _ < 3; _++) {
                    if(value[_] == 255) value[_] = 254;
                    if(_outputHigh[_] <= value[_]) _outputHigh[_] = (byte)(value[_] + 1);
                }
                _outputLow = value;
            }
        }

        public RGB OutputHigh {
            get { return _outputHigh; }
            set {
                for(var _ = 0; _ < 3; _++) {
                    if(value[_] == 0) value[_] = 1;
                    if(_outputLow[_] >= value[_]) _outputLow[_] = (byte)(value[_] - 1);
                }
                _outputHigh = value;
            }
        }

        public double GetGamma(int channel) {
            return _gamma[channel];
        }

        public void SetGamma(int channel, double value) {
            _gamma[channel] = SetRange(value, 0.1, 10);
        }

        public RGB Apply(int r, int g, int b) {
            var ret = new RGB();
            var input = new double[] { b, g, r };
            for(var _ = 0; _ < 3; _++) {
                var value_ = (input[_] - _inputLow[_]);
                if(value_ < 0) {
                    ret[_] = _outputLow[_];
                } else if((_inputLow[_] + value_) >= _inputHigh[_]) {
                    ret[_] = _outputHigh[_];
                } else {
                    ret[_] = (byte)SetRange((_outputLow[_] + ((_outputHigh[_] - _outputLow[_]) * Math.Pow((value_ / (_inputHigh[_] - _inputLow[_])), _gamma[_]))), 0, 255);
                }
            }
            return ret;
        }

        internal static Levels GetAdjusted(RGB low, RGB middle, RGB high) {
            var ret = new Levels();
            for(var _ = 0; _ < 3; _++) {
                if((low[_] < middle[_]) && (middle[_] < high[_])) {
                    ret._gamma[_] = SetRange(Math.Log(0.5, ((double)(middle[_] - low[_]) / (high[_] - low[_]))), 0.1, 10);
                } else {
                    ret._gamma[_] = 1;
                }
            }
            ret._inputLow = low;
            ret._inputHigh = high;
            return ret;
        }
    }

    private static double SetRange(double value, double min, double max) {
        if(value < min) value = min;
        if(value > max) value = max;
        return value;
    }



    public struct RGB {
        public byte B;
        public byte G;
        public byte R;

        public RGB(byte r, byte g, byte b) {
            B = b;
            G = g;
            R = r;
        }

        public byte this[int channel] {
            get {
                switch(channel) {
                    case 0: return B;
                    case 1: return G;
                    case 2: return R;
                    default: throw new ArgumentOutOfRangeException();
                }
            }
            set {
                switch(channel) {
                    case 0: B = value; break;
                    case 1: G = value; break;
                    case 2: R = value; break;
                    default: throw new ArgumentOutOfRangeException();
                }
            }
        }

        public Color ToColor() {
            return Color.FromArgb(R, G, B);
        }
    }
}

Results:
Results

Upvotes: 0

You can use OpenCV to develop an algorithm that will fit your needs. When researching to find a solution for your problem, I realized that the problem of "color balance could be solved with a lot of different way.

I choose to show you how to code a very simple algorithm that will not completely re-create the "perfect" picture that you get with photoshop but something better than the original. You can then search for these topic in openCV on google and try different approach. To code this, I have use the new OpenCV NuGet package, that you can get here. Just add the binary from openCV in your output directory (debug folder) and you are up and running!

Then here is the code:

public Form1()
{
    InitializeComponent();

    NamedWindow windowsOriginal = new NamedWindow("Original");
    NamedWindow windowsModified = new NamedWindow("Modified");

    IplImage img = OpenCV.Net.CV.LoadImage(@"D:\hZpWG.jpg", LoadImageFlags.Color);
    IplImage imgDest = equalizeIntensity(img);


    windowsOriginal.ShowImage(img);
    windowsModified.ShowImage(imgDest);
}

IplImage equalizeIntensity(IplImage inputImage)
{
    if(inputImage.Channels >= 3)
    {
        IplImage ycrcb = new IplImage(inputImage.Size, inputImage.Depth, inputImage.Channels);

        OpenCV.Net.CV.CvtColor(inputImage, ycrcb, ColorConversion.Bgr2YCrCb);

        IplImage Y = new IplImage(ycrcb.Size, IplDepth.U8, 1);
        IplImage Cr = new IplImage(ycrcb.Size, IplDepth.U8, 1);
        IplImage Cb = new IplImage(ycrcb.Size, IplDepth.U8, 1);
        OpenCV.Net.CV.Split(ycrcb, Y, Cr, Cb, null);

        OpenCV.Net.CV.EqualizeHist(Y, Y);

        IplImage result = new IplImage(inputImage.Size, IplDepth.U8, inputImage.Channels);
        OpenCV.Net.CV.Merge(Y, Cr, Cb, null, ycrcb);

        OpenCV.Net.CV.CvtColor(ycrcb, result, ColorConversion.YCrCb2Bgr);

        return result;
    }

    return null;
}

I put it in a form but you can use it in a console application too.

Here is the result Original enter image description here

Hope it helps!

Upvotes: 1

beroe
beroe

Reputation: 12316

This looks like the White Balance is set for indoors (expecting reddish light) but getting daylight (blue). GIMP has a color temperature slider that will change the cast of the pictures. Are you talking about preventing this in the future, or batch processing a bunch of existing images. Even simple cameras (but maybe not mobile phones) have controls on white balance, for future shots.

This looks like a web cam plugged into your computer? So it is probably a moving target, meaning that the WB is being re-evaluated each time it takes a pic, and you may not be able to apply the same correction to each image.

Here is an imagemagick script which can batch process color temperature on a bunch of images. I think an approach which uses the temperature would be better than one that is just normalizing levels, because what if you are shooting the sky or ocean, and it should be blue? You just want to make sure it is the right blue.

Edit: For specific C# code, you might check here. The lower-left example in the first set of color balanced images look very much like your source image. There is also white balance function in the source code of paint.net

Upvotes: 2

Display Name
Display Name

Reputation: 8128

Tuning White Balance automatically cannot always work nice, because your algorithm will have less information as input data (no real light metering, only pixel values recorded by matrix, some of which may be clipped). So this can help when camera's setting is ridiculously wrong (like on your picture), but it cannot make WB right. You'd better buy a decent camera (there are even cheap ones that still can do good pictures)

BTW, if you want to invent a wheel, the idea is to scale color channels to make their average level equal. You can try different definitions of "average" here, and you can also try to exclude pixels with clipped values from measurement. But there is no fun in doing it again, as there are good ways to do this mentioned in @mickro's answer.

Upvotes: 4

mickro
mickro

Reputation: 891

I guess best solution is playing with ImageMagick which gets .Net implementation.

To start there is this topic from stackoverflow

Also you should find the correct effect. Certainly good by trying around match and neutralize effects. Randomly this autocolour's script may help.

hope that help. Good luck.

Upvotes: 2

Related Questions