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:
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:

8-Bit Difference Image:

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:
- What is the correct way to perform the conversion from 8-Bit to
10-bit to ensure accuracy and preserve image quality? - 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.