Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Differences in Results Between Bit Shifting and Scaling for 8-Bit to 10-Bit Image Conversion

I am working on a project that involves converting images from 8-bit to 10-bit depth using OpenCV in C++. I’ve implemented two methods for this conversion: bit shifting and scaling. However, I’m observing differences in the image quality between these two methods, and I’m unsure which one is the correct approach for accurate conversion.

Code Snippet:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

// Constants for bit depth conversion
const int BIT_SHIFT = 2;
const float MAX_10BIT = pow(2,10) - 1;
const float MAX_8BIT = pow(2, 8) - 1;
const float SCALE_FACTOR_10Bit = MAX_10BIT / MAX_8BIT;
const float SCALE_FACTOR_8Bit = MAX_8BIT / MAX_10BIT;

// Function to display and save an image
static void displayAndSaveImage(const Mat& image, const string& windowName) {
    imshow(windowName, image);

    // Construct the filename using the window name and ".png" extension
    string filename = windowName + ".png";
    imwrite(filename, image);
}

// Function to display and save an image
static void displayAndSave10BitImage(const Mat& image_10Bit, const string& windowName) {
    cv::Mat image_8Bit;
    convertScaleAbs(image_10Bit, image_8Bit, 1.0 / 4.0);

    imshow(windowName, image_8Bit);

    // Construct the filename using the window name and ".png" extension
    string filename = windowName + ".png";
    imwrite(filename, image_8Bit);
}

Mat convert10BitTo8Bit(const Mat& image_10Bit) {
    Mat image_8Bit;
    convertScaleAbs(image_10Bit, image_8Bit, 1.0 / 4.0);
    return image_8Bit;
}

Mat convert10BitTo8Bit(const Mat& image_10Bit, bool useScaling) {
    Mat image_8Bit(image_10Bit.size(), CV_8UC1);

    for (int y = 0; y < image_10Bit.rows; ++y) {
        ushort* rowPtr_10bit = (ushort*)image_10Bit.ptr<ushort>(y);
        uchar* rowPtr_8bit = image_8Bit.ptr<uchar>(y);
        for (int x = 0; x < image_10Bit.cols; ++x) {
            rowPtr_8bit[x] = useScaling ? saturate_cast<uchar>(rowPtr_10bit[x] * SCALE_FACTOR_8Bit) // Use scaling for conversion
                                        : saturate_cast<uchar>(rowPtr_10bit[x] >> BIT_SHIFT);       // Use bit-shifting for conversion
        }
    }
    return image_8Bit;
}


Mat convert8BitTo10Bit(const Mat& image_8Bit, bool useScaling) {
    Mat image_10Bit(image_8Bit.size(), CV_16UC1);
    for (int y = 0; y < image_8Bit.rows; ++y) {
        uchar* rowPtr_8bit = (uchar*)image_8Bit.ptr<uchar>(y);
        ushort* rowPtr_10bit = image_10Bit.ptr<ushort>(y);
        for (int x = 0; x < image_8Bit.cols; ++x) {
            rowPtr_10bit[x] = useScaling ? saturate_cast<ushort>(rowPtr_8bit[x] * SCALE_FACTOR_10Bit) : // Use scaling for conversion
                                           rowPtr_8bit[x] << BIT_SHIFT;                                 // Use bit-shifting for conversion
        }
    }
    return image_10Bit;
}

void calculateErrorMetrics(const Mat& original, const Mat& converted, double& rmse, double& psnr) {
    Mat diff;
    absdiff(original, converted, diff);
    diff.convertTo(diff, CV_32F);
    diff = diff.mul(diff);

    Scalar mse = mean(diff);
    rmse = sqrt(mse[0]);
    psnr = 20 * log10(MAX_10BIT) - 10 * log10(mse[0]);
}

void Test10bitTo8BitConversion(cv::Mat img_10bit) {
    // Convert to 8-bit using both methods
    Mat img_8bit_shift = convert10BitTo8Bit(img_10bit, false);
    Mat img_8bit_scale = convert10BitTo8Bit(img_10bit, true);

    // Display and save both images
    displayAndSaveImage(img_8bit_shift, "img_8bit_shift");
    displayAndSaveImage(img_8bit_scale, "img_8bit_scale");

    // Compare the two images
    double maxDiff = cv::norm(img_8bit_shift, img_8bit_scale, NORM_INF);
    if (maxDiff == 0) {
        cout << "The images are exactly the same." << endl;
    }
    else {
        cout << "The images are not the same." << endl;

        Mat img_8bit_diff;
        absdiff(img_8bit_shift, img_8bit_scale, img_8bit_diff);

        cv::threshold(img_8bit_diff, img_8bit_diff, 0, 255, THRESH_BINARY);

        displayAndSaveImage(img_8bit_diff, "img_8bit_diff");
    }
}

int main() {
    // Create a 10-bit gradient image (1024x100)
    Size ImageSize(MAX_10BIT, 300);
    Mat img_10bit(ImageSize, CV_16UC1);
    for (int y = 0; y < img_10bit.rows; ++y) {
        ushort* rowPtr = img_10bit.ptr<ushort>(y);
        for (int x = 0; x < img_10bit.cols; ++x) {
            rowPtr[x] = x;
        }
    }

    Test10bitTo8BitConversion(img_10bit);

    Mat img_8bit = convert10BitTo8Bit(img_10bit);
    Mat img_10bit_shift = convert8BitTo10Bit(img_8bit, false);
    Mat img_10bit_scale = convert8BitTo10Bit(img_8bit, true);

    displayAndSave10BitImage(img_10bit_shift, "img_10bit_shift");
    displayAndSave10BitImage(img_10bit_scale, "img_10bit_scale");

    double shift_rmse = 0.0;
    double shift_psnr = 0.0;
    double scale_rmse = 0.0;
    double scale_psnr = 0.0;

    calculateErrorMetrics(img_10bit, img_10bit_shift, shift_rmse, shift_psnr);
    calculateErrorMetrics(img_10bit, img_10bit_scale, scale_rmse, scale_psnr);

    // Print Results
    cout << "Bit Shifting:\n RMSE: " << shift_rmse << "\n PSNR: " << shift_psnr << endl;
    cout << "Scaling Factor:\n RMSE: " << scale_rmse << "\n PSNR: " << scale_psnr << endl;

    // First, determine the better method based on PSNR and RMSE separately
    string betterMethodPSNR = (shift_psnr > scale_psnr) ? "Bit Shifting" : "Scaling Factor";
    string betterMethodRMSE = (shift_rmse < scale_rmse) ? "Bit Shifting" : "Scaling Factor";

    // Then, calculate the percentage improvement for PSNR and RMSE accurately
    double psnrImprovement = 0.0;
    double rmseImprovement = 0.0;

    if (shift_psnr != scale_psnr) { // Ensure we do not divide by zero
        // For PSNR, higher is better
        psnrImprovement = (betterMethodPSNR == "Bit Shifting") ?
            (shift_psnr - scale_psnr) / scale_psnr * 100 :
            (scale_psnr - shift_psnr) / shift_psnr * 100;
    }

    if (shift_rmse != scale_rmse) { // Ensure we do not divide by zero
        // For RMSE, lower is better
        rmseImprovement = (betterMethodRMSE == "Bit Shifting") ?
            (scale_rmse - shift_rmse) / scale_rmse * 100 :
            (shift_rmse - scale_rmse) / shift_rmse * 100;
    }

    // Print the results
    cout << "Better method is: " << betterMethodPSNR << endl;
    if (psnrImprovement != 0) {
        cout << "PSNR Improvement: " << abs(psnrImprovement) << "%" << endl;
    }
    else {
        cout << "No PSNR Improvement, both methods are equal." << endl;
    }

    if (rmseImprovement != 0) {
        cout << "RMSE Improvement: " << abs(rmseImprovement) << "%" << " (in favor of the method with better RMSE)" << endl;
    }
    else {
        cout << "No RMSE Improvement, both methods are equal." << endl;
    }

    waitKey(0);
    return 0;
}

Output:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Bit Shifting:
 RMSE: 1.22494
 PSNR: 58.4352
Scaling Factor:
 RMSE: 2.16025
 PSNR: 53.5074

Better method is: Bit Shifting
PSNR Improvement: 9.20945%
RMSE Improvement: 43.2961% (in favor of the method with better RMSE)

Input Gradient Image:
enter image description here
8-Bit Difference Image:
enter image description here

Expected Behavior:

I was expecting both the bit shifting and scaling methods to produce similar results when converting images between 8-bit and 10-bit representations, assuming that the conversion back and forth would yield negligible differences in the image quality metrics (RMSE and PSNR).

Observed Behavior:

However, I observed that the images converted using the bit shifting method significantly differ from those converted using the scaling method. This difference is quantifiable through RMSE and PSNR metrics, suggesting a discrepancy in the accuracy or quality of the conversions.

Question:

  1. What is the correct way to perform the conversion from 8-Bit to
    10-bit to ensure accuracy and preserve image quality?
  2. Why there is a significant difference in the image quality between the
    bit-shifting and scaling methods for depth conversion?

Any insights or suggestions on how to approach this problem would be greatly appreciated. Thank you in advance!

Corrected Code:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

// Constants for bit depth conversion
const int   BIT_SHIFT = 2;
const float MAX_10BIT = pow(2,10);
const float MAX_8BIT = pow(2, 8) ;
const float SCALE_FACTOR_10Bit = MAX_10BIT / MAX_8BIT;

// Function to display and save an image
static void displayAndSaveImage(const Mat& image, const string& windowName) {
    imshow(windowName, image);

    // Construct the filename using the window name and ".png" extension
    string filename = windowName + ".png";
    imwrite(filename, image);
}

// Function to display and save an image
static void displayAndSave10BitImage(const Mat& image_10Bit, const string& windowName) {
    cv::Mat image_8Bit;
    convertScaleAbs(image_10Bit, image_8Bit, 1.0 / 4.0);

    imshow(windowName, image_8Bit);

    // Construct the filename using the window name and ".png" extension
    string filename = windowName + ".png";
    imwrite(filename, image_8Bit);
}

Mat convert10BitTo8Bit(const Mat& image_10Bit) {
    Mat image_8Bit;
    convertScaleAbs(image_10Bit, image_8Bit, 1.0 / 4.0);
    return image_8Bit;
}

Mat convert10BitTo8Bit(const Mat& image_10Bit, bool useScaling) {
    Mat image_8Bit(image_10Bit.size(), CV_8UC1);

    for (int y = 0; y < image_10Bit.rows; ++y) {
        ushort* rowPtr_10bit = (ushort*)image_10Bit.ptr<ushort>(y);
        uchar* rowPtr_8bit = image_8Bit.ptr<uchar>(y);
        for (int x = 0; x < image_10Bit.cols; ++x) {
            rowPtr_8bit[x] = useScaling ? saturate_cast<uchar>(rowPtr_10bit[x] / 4 )                 // Use scaling for conversion
                                        : saturate_cast<uchar>(rowPtr_10bit[x] >> BIT_SHIFT);       // Use bit-shifting for conversion
        }
    }
    return image_8Bit;
}


Mat convert8BitTo10Bit(const Mat& image_8Bit, bool useScaling) {
    Mat image_10Bit(image_8Bit.size(), CV_16UC1);
    for (int y = 0; y < image_8Bit.rows; ++y) {
        uchar* rowPtr_8bit = (uchar*)image_8Bit.ptr<uchar>(y);
        ushort* rowPtr_10bit = image_10Bit.ptr<ushort>(y);
        for (int x = 0; x < image_8Bit.cols; ++x) {
            rowPtr_10bit[x] = useScaling ? saturate_cast<ushort>(rowPtr_8bit[x] * SCALE_FACTOR_10Bit) : // Use scaling for conversion
                                           rowPtr_8bit[x] << BIT_SHIFT;                                 // Use bit-shifting for conversion
        }
    }
    return image_10Bit;
}

void calculateErrorMetrics(const Mat& original, const Mat& converted, double& rmse, double& psnr) {
    Mat diff;
    absdiff(original, converted, diff);
    diff.convertTo(diff, CV_32F);
    diff = diff.mul(diff);

    Scalar mse = mean(diff);
    rmse = sqrt(mse[0]);
    psnr = 20 * log10(MAX_10BIT-1) - 10 * log10(mse[0]);
}

void Test10bitTo8BitConversion(cv::Mat img_10bit) {
    // Convert to 8-bit using both methods
    Mat img_8bit_shift = convert10BitTo8Bit(img_10bit, false);
    Mat img_8bit_scale = convert10BitTo8Bit(img_10bit, true);

    // Display and save both images
    displayAndSaveImage(img_8bit_shift, "img_8bit_shift");
    displayAndSaveImage(img_8bit_scale, "img_8bit_scale");

    // Compare the two images
    double maxDiff = cv::norm(img_8bit_shift, img_8bit_scale, NORM_INF);
    if (maxDiff == 0) {
        cout << "The images are exactly the same." << endl;
    }
    else {
        cout << "The images are not the same." << endl;

        Mat img_8bit_diff;
        absdiff(img_8bit_shift, img_8bit_scale, img_8bit_diff);

        cv::threshold(img_8bit_diff, img_8bit_diff, 0, 255, THRESH_BINARY);

        displayAndSaveImage(img_8bit_diff, "img_8bit_diff");
    }
}

int main() {
    // Create a 10-bit gradient image (1024x100)
    Size ImageSize(MAX_10BIT, 300);
    Mat img_10bit(ImageSize, CV_16UC1);
    for (int y = 0; y < img_10bit.rows; ++y) {
        ushort* rowPtr = img_10bit.ptr<ushort>(y);
        for (int x = 0; x < img_10bit.cols; ++x) {
            rowPtr[x] = x;
        }
    }

    Test10bitTo8BitConversion(img_10bit);

    Mat img_8bit = convert10BitTo8Bit(img_10bit);
    Mat img_10bit_shift = convert8BitTo10Bit(img_8bit, false);
    Mat img_10bit_scale = convert8BitTo10Bit(img_8bit, true);

    displayAndSave10BitImage(img_10bit_shift, "img_10bit_shift");
    displayAndSave10BitImage(img_10bit_scale, "img_10bit_scale");

    double shift_rmse = 0.0;
    double shift_psnr = 0.0;
    double scale_rmse = 0.0;
    double scale_psnr = 0.0;

    calculateErrorMetrics(img_10bit, img_10bit_shift, shift_rmse, shift_psnr);
    calculateErrorMetrics(img_10bit, img_10bit_scale, scale_rmse, scale_psnr);

    Mat img_10bit_diff;
    absdiff(img_10bit_shift, img_10bit_scale, img_10bit_diff);

    cv::threshold(img_10bit_diff, img_10bit_diff, 0, 255, THRESH_BINARY);

    displayAndSaveImage(img_10bit_diff, "img_10bit_diff");

    // Print Results
    cout << "Bit Shifting:\n RMSE: " << shift_rmse << "\n PSNR: " << shift_psnr << endl;
    cout << "Scaling Factor:\n RMSE: " << scale_rmse << "\n PSNR: " << scale_psnr << endl;

    // First, determine the better method based on PSNR and RMSE separately
    string betterMethodPSNR = (shift_psnr > scale_psnr) ? "Bit Shifting" : "Scaling Factor";
    string betterMethodRMSE = (shift_rmse < scale_rmse) ? "Bit Shifting" : "Scaling Factor";

    // Then, calculate the percentage improvement for PSNR and RMSE accurately
    double psnrImprovement = 0.0;
    double rmseImprovement = 0.0;

    if (shift_psnr != scale_psnr) { // Ensure we do not divide by zero
        // For PSNR, higher is better
        psnrImprovement = (betterMethodPSNR == "Bit Shifting") ?
            (shift_psnr - scale_psnr) / scale_psnr * 100 :
            (scale_psnr - shift_psnr) / shift_psnr * 100;
    }

    if (shift_rmse != scale_rmse) { // Ensure we do not divide by zero
        // For RMSE, lower is better
        rmseImprovement = (betterMethodRMSE == "Bit Shifting") ?
            (scale_rmse - shift_rmse) / scale_rmse * 100 :
            (shift_rmse - scale_rmse) / shift_rmse * 100;
    }

    // Print the results
    if (psnrImprovement != 0) {
        cout << "Better method is: " << betterMethodPSNR << endl;
    }

    if (psnrImprovement != 0) {
        cout << "PSNR Improvement: " << abs(psnrImprovement) << "%" << endl;
    }
    else {
        cout << "No PSNR Improvement, both methods are equal." << endl;
    }

    if (rmseImprovement != 0) {
        cout << "RMSE Improvement: " << abs(rmseImprovement) << "%" << " (in favor of the method with better RMSE)" << endl;
    }
    else {
        cout << "No RMSE Improvement, both methods are equal." << endl;
    }

    waitKey(0);
    return 0;
}

>Solution :

Since you haven’t specified "image quality" well enough, there’s no unique answer to your question. Both bit-shifting (*4) and scaling (*1023/255) are lossless conversions from 8 bits to 10 bits, so with most image quality definitions there is no loss whatsoever.

Things can be slightly different if your gamma correction curve is known, but you don’t tell us about that. Also, for many input domains, there is a spatial correlation between adjacent pixels that can be exploited. Your scaling is purely local and does not take adjacent pixels into account.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading