Create a histogram, auto-generate corrected levels (max, min. And gamma), apply levels to the image. Assuming you somehow collected 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 is 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:
