c++algorithmimage-processing

(Photoshop-like) Algorithm for adjustment of image levels


I need to implement in C++ algorithm for adjusting image levels that works similar to Levels function in Photoshop or GIMP. I.e. inputs are: color RGB image to be adjusted adjust, while point, black point, midtone point, output from/to values. But I didn't find yet any info on how to perform this adjustment. Probably someone recommend me algorithm description or materials to study.

To the moment I've came up with following code myself, but it doesn't give expected result, similar to what I can see, for example in the GIMP, image becomes too lightened. Below is my current fragment of the code:

const int normalBlackPoint = 0;
const int normalMidtonePoint = 127;
const int normalWhitePoint = 255;
const double normalLowRange = normalMidtonePoint - normalBlackPoint + 1;
const double normalHighRange = normalWhitePoint - normalMidtonePoint;

int blackPoint = 53;
int midtonePoint = 110;
int whitePoint = 168;
int outputFrom = 0;
int outputTo = 255;

double outputRange = outputTo - outputFrom + 1;
double lowRange = midtonePoint - blackPoint + 1;
double highRange = whitePoint - midtonePoint;
double fullRange = whitePoint - blackPoint + 1;
double lowPart = lowRange / fullRange; 
double highPart = highRange / fullRange; 

int dim(256);
cv::Mat lut(1, &dim, CV_8U);
for(int i = 0; i < 256; ++i)
{
    double p = i > normalMidtonePoint
        ? (static_cast<double>(i - normalMidtonePoint) / normalHighRange) * highRange * highPart + lowPart
        : (static_cast<double>(i + 1) / normalLowRange) * lowRange * lowPart;
    int v = static_cast<int>(outputRange * p ) + outputFrom - 1;
    if(v < 0) v = 0;
    else if(v > 255) v = 255;
    lut.at<uchar>(i) = v;
}


....

    cv::Mat sourceImage = cv::imread(inputFileName, CV_LOAD_IMAGE_COLOR);
    if(!sourceImage.data)
    {
        std::cerr << "Error: couldn't load image " << inputFileName << "." << std::endl;
        continue;
    }


#if 0       
    const int forwardConversion = CV_BGR2YUV;
    const int reverseConversion = CV_YUV2BGR;
#else
    const int forwardConversion = CV_BGR2Lab;
    const int reverseConversion = CV_Lab2BGR;
#endif

    cv::Mat convertedImage;
    cv::cvtColor(sourceImage, convertedImage, forwardConversion);

    // Extract the L channel
    std::vector<cv::Mat> convertedPlanes(3);
    cv::split(convertedImage, convertedPlanes);

    cv::LUT(convertedPlanes[0], lut, convertedPlanes[0]);

    //dst.copyTo(convertedPlanes[0]);
    cv::merge(convertedPlanes, convertedImage);

    cv::Mat resImage;
    cv::cvtColor(convertedImage, resImage, reverseConversion);
    cv::imwrite(outputFileName, resImage);

Solution

  • Pseudocode for Photoshop's Levels Adjustment

    First, calculate the gamma correction value to use for the midtone adjustment (if desired). The following roughly simulates Photoshop's technique, which applies gamma 9.99-1.00 for midtone values 0-128, and 1.00-0.01 for 128-255.

    Apply gamma correction:

    Gamma = 1
    MidtoneNormal = Midtones / 255
    If Midtones < 128 Then
        MidtoneNormal = MidtoneNormal * 2
        Gamma = 1 + ( 9 * ( 1 - MidtoneNormal ) )
        Gamma = Min( Gamma, 9.99 )
    Else If Midtones > 128 Then
        MidtoneNormal = ( MidtoneNormal * 2 ) - 1
        Gamma = 1 - MidtoneNormal
        Gamma = Max( Gamma, 0.01 )
    End If
    GammaCorrection = 1 / Gamma
    

    Then, for each channel value R, G, B (0-255) for each pixel, do the following in order.

    Apply the input levels:

    ChannelValue = 255 * ( ( ChannelValue - ShadowValue ) / 
        ( HighlightValue - ShadowValue ) )
    

    Apply the midtones:

    If Midtones <> 128 Then
        ChannelValue = 255 * ( Pow( ( ChannelValue / 255 ), GammaCorrection ) )
    End If
    

    Apply the output levels:

    ChannelValue = ( ChannelValue / 255 ) *
        ( OutHighlightValue - OutShadowValue ) + OutShadowValue
    

    Where:

    For a more accurate simulation of Photoshop, you can use a non-linear interpolation curve if Midtones < 128. Photoshop also chops off the darkest and lightest 0.1% of the values by default.