# Color curve/Tone curve for an image

17 views (last 30 days)
Andy on 27 Nov 2013
Edited: DGM on 3 Apr 2022
If you open up an image editor such as Gimp or photoshop, in addition to the image histogram you can also view the colour curve. I want to know how this tone curve is created and updated. I understand it matches the input pixel along the x axis to the output pixel along the y.
For the original image the curve is a straight diagonal line from bottom left to top right. When a value such as brightness or contrast is changed then this curve will change shape.
My initial thought was that the change in y would be the difference between the original histogram values 0-255 and the current values. So the curve would be displaced +ve or -ve from its original position depending on how much the edited image was changed.
However I then read that it is a mapping function between the values along the x and y so i am confused as to what the correct approach is and how you would go about creating this programatically.
Below is an example of the original tone curve for an image in Gimp.

Image Analyst on 27 Nov 2013
The line or curve is a plot of the output gray level as a function of the input gray level. If it's a 45 degree line, then the output exactly equals the input and there is no alteration. If the line changes, then the displayed output image is altered. For example if the curve is flat until the tail of the histogram, then ramps up from 96 to 230 linearly, then flattens out again after 230, it will basically have little effect on bright or dark pixels because there are so few of them that you don't notice or care about them. But the bulk of the pixels between 96 and 230 would have increased contrast. Instead of going from 96 to 230, they are now remapped to the full dynamic range of 0 to 255, giving a "better" looking image. In MATLAB, you can do this to affect the display only (no change to the input) with colormap(). If you actually want an output image with the mapped pixels, you can use intlut() to actually change pixel values.
##### 2 CommentsShowHide 1 older comment
Image Analyst on 27 Nov 2013
Any time you steepen the curve you increase contrast. Let's say that the curve was a step function at 200. So any input gray level less than 200 got mapped to 0 and any input gray level more than 200 got mapped to 255. This is an extreme example of contrast increase (in fact it's called thresholding). Look at another extreme: a flat curve. If the curve were flat at, say, 150 then no matter what the input gray level was, you'd get an image of 150 gray levels out - very very low contrast. Maybe it's slightly steeper but still fairly flat and went from 150 to 160. Then no matter what the input gray level is, your image will be between 150 and 160 gray levels, a very low contrast image. It's best if you can just play around with that curve, moving it around, until you get an intuitive feel for what it does.
You asked "If I had the original image and the same image with increased contrast, how would I know where to map the curve based on the original input value and the increased contrast output value?" Well first of all you can only get the curve if it were a strictly gray level-to-gray level mapping that was global over the whole image. if you had some locally adaptive process where a gray level of 100 might get mapped to 120 in some locations but to 140 in other locations, then you can't get the curve. To get the curve that was used to map an input image to an output image, you simply scan the output image and see what it srated out as by looking at the input image for that same pixel .
for k = 1 : numel(outputImage)
inputGrayLevel = inputImage(k);
outputGrayLevel = outputImage(k);
% Assign the look up table
conversionLUT(inputGrayLevel) = outputGrayLevel;
end

DGM on 3 Apr 2022
Edited: DGM on 3 Apr 2022
This is an example of how one might back-calculate the curve applied to an image. For the purposes of this example, I'm going to use MIMT imcurves(), since MATLAB/IPT do not have a similar image adjustment tool (though interp1() should suffice). A couple other MIMT tools are used as well. Throughout, I'm assuming that the images are uint8.
% say we have a grayscale image
if size(inpict,3)==3
inpict = rgb2gray(inpict);
end
imshow(inpict)
% and we have a modified version of the image
% wherein the intensity transformation is global
x = [0 0.25 0.5 0.75 1];
y = [0 0.10 0.5 0.90 1];
modifiedpict = imcurves(inpict,x,y); % adjust the image
modifiedpict = jpegger(modifiedpict,20); % severely degrade the image
imshow(modifiedpict)
% find unique values of the input image
% and the mean of corresponding pixels in the modified image
[g uin] = findgroups(inpict(:));
umod = splitapply(@mean,modifiedpict(:),g);
% use a basic curve fit to back-calculate the transformation
estx = linspace(0,255,9); % assume relatively few points were used
[pp s mu] = polyfit(double(uin),double(umod),5); % adjust order to suit
esty = imclamp(polyval(pp,estx,s,mu),[0 255]);
% plot the original and estimated curves
plot(x*255,y*255,'ko'); hold on; grid on
plot(uin,umod,'r')
plot(estx,esty,'b*')
legend('original','modified','estimate','location','northwest')
xlim([0 255])
ylim([0 255])
% use the estimated curve to recreate the value transformation
recreatedpict = imcurves(inpict,estx/255,esty/255);
imshow(recreatedpict)
By default, imcurves() will use cubic spline interpolation, so the use of a relatively short point list is typically sufficient to produce smooth curves.
This approach works well for this image, but bear in mind that the modified image only describes the curve over the range of the input image. For an input which spans only a small part of the nominal data range, you have less information about the shape of the curve.
Of particular interest is the behavior at the endpoints. Consider what happens if the above code is run on this image:
This is the curve estimate:
While this estimated curve would accurately reproduce the modification to the original image, the inversion near black would become apparent if it were applied to an image with a wider value range. If some endpoint values can be assumed, they can always be asserted prior to doing the fit.
% ... finding points, etc
% if the input image does not span the data range
% then it's hard to tell how the curve should be extrapolated
% it may suffice to assume endpoint values (e.g. [0 0] and [1 1])
% otherwise, you may try your luck with other fitting methods
if min(uin)>0
uin = [0; uin];
umod = [0; umod];
end
if max(uin)<255
uin = [uin; 255];
umod = [umod; 255];
end
% start doing curve fit ...
Now the fit looks fine and should work on other images.
MIMT is not necessary to do any of this, but it's a convenience that I don't feel like avoiding at the moment. imcurves() can be replaced with interp1(...,'pchip') after casting/scaling the image with im2double(). Afterwards, the image needs to be cast/scaled back with im2uint8(). imclamp() can be replaced with min() and max(). Although it's only used for creating the test image, jpegger() could be replaced by simply saving the image to disk as JPG with a specified 'quality' parameter and then reading it back. FWIW, it would also suffice to use imnoise() instead. I just prefer to use jpegger(), since compression artifacts are the most typical sort of image damage that I run into.