1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-31 08:13:04 +04:00

Merge pull request #27051 from gursimarsingh:move_ccm_to_photo_module

Adding color correction module to photo module from opencv_contrib #27051

This PR moved color correction module from opencv_contrib to main repo inside photo module.

### Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

- [x] I agree to contribute to the project under Apache 2 License.
- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
- [x] The PR is proposed to the proper branch
- [x] There is a reference to the original bug report and related work
- [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
Gursimar Singh
2025-06-12 19:37:16 +05:30
committed by GitHub
parent dd87ffc340
commit 425d5cfcf0
34 changed files with 5387 additions and 35 deletions
+224
View File
@@ -0,0 +1,224 @@
//! [tutorial]
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/photo.hpp>
#include <opencv2/objdetect.hpp>
#include <opencv2/dnn.hpp>
#include <iostream>
#include "../dnn/common.hpp"
using namespace std;
using namespace cv;
using namespace cv::dnn;
using namespace cv::ccm;
using namespace mcc;
const string about =
"This sample detects Macbeth color checker using DNN or thresholding and applies color correction."
"To run default:\n"
"\t ./example_cpp_color_correction_model --input=path/to/your/input/image --query=path/to/your/query/image\n"
"With DNN model:\n"
"\t ./example_cpp_color_correction_model mcc --input=path/to/your/input/image --query=path/to/your/query/image\n\n"
"Using pre-computed CCM:\n"
"\t ./example_cpp_color_correction_model mcc --ccm_file=path/to/ccm_output.yaml --query=path/to/your/query/image\n\n"
"Model path can also be specified using --model argument. And config path can be specified using --config. Download it using python download_models.py mcc from dnn samples directory\n\n";
const string param_keys =
"{ help h | | Print help message. }"
"{ @alias | | An alias name of model to extract preprocessing parameters from models.yml file. }"
"{ zoo | ../dnn/models.yml | An optional path to file with preprocessing parameters }"
"{ input i | mcc_ccm_test.jpg | Path to input image for computing CCM.}"
"{ query q | baboon.jpg | Path to query image to apply color correction. If not provided, input image will be used. }"
"{ type | 0 | chartType: 0-Standard, 1-DigitalSG, 2-Vinyl }"
"{ num_charts | 1 | Maximum number of charts in the image }"
"{ ccm_file | | Path to YAML file containing pre-computed CCM parameters}";
const string backend_keys = format(
"{ backend | default | Choose one of computation backends: "
"default: automatically (by default), "
"openvino: Intel's Deep Learning Inference Engine (https://software.intel.com/openvino-toolkit), "
"opencv: OpenCV implementation, "
"vkcom: VKCOM, "
"cuda: CUDA, "
"webnn: WebNN }");
const string target_keys = format(
"{ target | cpu | Choose one of target computation devices: "
"cpu: CPU target (by default), "
"opencl: OpenCL, "
"opencl_fp16: OpenCL fp16 (half-float precision), "
"vpu: VPU, "
"vulkan: Vulkan, "
"cuda: CUDA, "
"cuda_fp16: CUDA fp16 (half-float preprocess) }");
string keys = param_keys + backend_keys + target_keys;
static bool processFrame(const Mat& frame, Ptr<CCheckerDetector> detector, Mat& src, int nc){
if (!detector->process(frame, nc))
{
return false;
}
vector<Ptr<CChecker>> checkers = detector->getListColorChecker();
src = checkers[0]->getChartsRGB(false);
return true;
}
int main(int argc, char* argv[]) {
CommandLineParser parser(argc, argv, keys);
parser.about(about);
if (parser.has("help")) {
cout << about << endl;
parser.printMessage();
return 0;
}
string modelName = parser.get<String>("@alias");
string zooFile = parser.get<String>("zoo");
const char* path = getenv("OPENCV_SAMPLES_DATA_PATH");
if ((path != NULL) || parser.has("@alias")) {
zooFile = findFile(zooFile);
}
else{
cout<<"[WARN] set the environment variables or pass the arguments --model, --config and models.yml file using --zoo for using dnn based detector. Continuing with default detector.\n\n";
}
keys += genPreprocArguments(modelName, zooFile);
parser = CommandLineParser(argc, argv, keys);
int t = parser.get<int>("type");
if (t < 0 || t > 2)
{
cout << "Error: --type must be 0, 1 or 2" << endl;
parser.printMessage(); // prints full usage
return -1;
}
ColorChart chartType = ColorChart(t);
const string sha1 = parser.get<String>("sha1");
const string modelPath = findModel(parser.get<string>("model"), sha1);
const string config_sha1 = parser.get<String>("config_sha1");
const string configPath = findModel(parser.get<string>("config"), config_sha1);
const string backend = parser.get<String>("backend");
const string target = parser.get<String>("target");
int nc = parser.get<int>("num_charts");
// Get input and target image paths
const string inputFile = parser.get<String>("input");
const string queryFile = parser.get<String>("query");
const string ccmFile = parser.get<String>("ccm_file");
if (!ccmFile.empty()) {
// When ccm_file is provided, only query is required
if (queryFile.empty()) {
cout << "Error: Query image path must be provided when using pre-computed CCM." << endl;
parser.printMessage();
return -1;
}
} else {
// Original validation for when computing new CCM
if (inputFile.empty()) {
cout << "Error: Input image path must be provided." << endl;
parser.printMessage();
return -1;
}
}
ColorCorrectionModel model;
Mat queryImage;
if (!ccmFile.empty()) {
// Load CCM from YAML file
FileStorage fs(ccmFile, FileStorage::READ);
if (!fs.isOpened()) {
cout << "Error: Unable to open CCM file: " << ccmFile << endl;
return -1;
}
model.read(fs["ColorCorrectionModel"]);
fs.release();
cout << "Loaded CCM from file: " << ccmFile << endl;
// Read query image when using pre-computed CCM
queryImage = imread(findFile(queryFile));
if (queryImage.empty()) {
cout << "Error: Unable to read query image." << endl;
return -1;
}
} else {
// Read input image for computing new CCM
Mat originalImage = imread(findFile(inputFile));
if (originalImage.empty()) {
cout << "Error: Unable to read input image." << endl;
return -1;
}
// Process first image to compute CCM
Mat image = originalImage.clone();
Mat src;
Ptr<CCheckerDetector> detector;
if (!modelPath.empty() && !configPath.empty()) {
Net net = readNetFromTensorflow(modelPath, configPath);
net.setPreferableBackend(getBackendID(backend));
net.setPreferableTarget(getTargetID(target));
detector = CCheckerDetector::create(net);
cout << "Using DNN-based checker detector." << endl;
} else {
detector = CCheckerDetector::create();
cout << "Using thresholding-based checker detector." << endl;
}
detector->setColorChartType(chartType);
if (!processFrame(image, detector, src, nc)) {
cout << "No chart detected in the input image!" << endl;
return -1;
}
// Convert to double and normalize
src.convertTo(src, CV_64F, 1.0/255.0);
// Color correction model
model = ColorCorrectionModel(src, COLORCHECKER_MACBETH);
model.setCcmType(CCM_LINEAR);
model.setDistance(DISTANCE_CIE2000);
model.setLinearization(LINEARIZATION_GAMMA);
model.setLinearizationGamma(2.2);
Mat ccm = model.compute();
cout << "Computed CCM Matrix:\n" << ccm << endl;
cout << "Loss: " << model.getLoss() << endl;
// Save model parameters to YAML file
FileStorage fs("ccm_output.yaml", FileStorage::WRITE);
model.write(fs);
fs.release();
cout << "Model parameters saved to ccm_output.yaml" << endl;
// Set query image for correction
if (queryFile.empty()) {
cout << "[WARN] No query image provided, applying color correction on input image" << endl;
queryImage = originalImage.clone();
} else {
queryImage = imread(findFile(queryFile));
if (queryImage.empty()) {
cout << "Error: Unable to read query image." << endl;
return -1;
}
}
}
Mat calibratedImage;
model.correctImage(queryImage, calibratedImage);
imshow("Original Image", queryImage);
imshow("Corrected Image", calibratedImage);
waitKey(0);
return 0;
}
//! [tutorial]
+24
View File
@@ -0,0 +1,24 @@
0.380463 0.31696 0.210053
0.649781 0.520561 0.452553
0.323114 0.37593 0.50123
0.314785 0.396522 0.258116
0.452971 0.418602 0.578767
0.34908 0.608649 0.652283
0.691127 0.517818 0.144984
0.208668 0.224391 0.485851
0.657849 0.378126 0.304115
0.285762 0.229671 0.31913
0.513422 0.685031 0.337381
0.786459 0.676133 0.246303
0.11751 0.135079 0.383441
0.190745 0.470513 0.296844
0.587832 0.299132 0.196117
0.783908 0.746261 0.294357
0.615481 0.359983 0.471403
0.107095 0.370516 0.573142
0.708598 0.718936 0.740915
0.593812 0.612474 0.63222
0.489774 0.510077 0.521757
0.380591 0.398499 0.393662
0.27461 0.293267 0.275244
0.180753 0.194968 0.145006
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+185
View File
@@ -0,0 +1,185 @@
import cv2 as cv
import numpy as np
import argparse
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from dnn.common import *
def get_args_parser(func_args):
backends = ("default", "openvino", "opencv", "vkcom", "cuda", "webnn")
targets = ("cpu", "opencl", "opencl_fp16", "vpu", "vulkan", "cuda", "cuda_fp16")
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--zoo', default=os.path.join(os.path.dirname(os.path.abspath(__file__)), '../dnn', 'models.yml'),
help='An optional path to file with preprocessing parameters.')
parser.add_argument('--input', default='mcc_ccm_test.jpg', help='Path to input image for computing CCM')
parser.add_argument('--query', default='baboon.jpg', help='Path to query image to apply color correction')
parser.add_argument('--ccm_file', help='Path to YAML file containing pre-computed CCM parameters')
parser.add_argument('--chart_type', type=int, default=0,
help='chartType: 0-Standard, 1-DigitalSG, 2-Vinyl, default:0')
parser.add_argument('--num_charts', type=int, default=1,
help='Maximum number of charts in the image')
parser.add_argument('--backend', default="default", type=str, choices=backends,
help="Choose one of computation backends: "
"default: automatically (by default), "
"openvino: Intel's Deep Learning Inference Engine (https://software.intel.com/openvino-toolkit), "
"opencv: OpenCV implementation, "
"vkcom: VKCOM, "
"cuda: CUDA, "
"webnn: WebNN")
parser.add_argument('--target', default="cpu", type=str, choices=targets,
help="Choose one of target computation devices: "
"cpu: CPU target (by default), "
"opencl: OpenCL, "
"opencl_fp16: OpenCL fp16 (half-float precision), "
"vpu: VPU, "
"vulkan: Vulkan, "
"cuda: CUDA, "
"cuda_fp16: CUDA fp16 (half-float preprocess)")
args, _ = parser.parse_known_args()
add_preproc_args(args.zoo, parser, 'mcc', 'mcc')
parser = argparse.ArgumentParser(parents=[parser],
description='''
To run:
Default (compute new CCM):
python color_correction_model.py --input=path/to/your/input/image --query=path/to/query/image
DNN model:
python color_correction_model.py mcc --input=path/to/your/input/image --query=path/to/query/image
Using pre-computed CCM:
python color_correction_model.py --ccm_file=path/to/ccm_output.yaml --query=path/to/query/image
Model path can also be specified using --model argument. And config path can be specified using --config.
''', formatter_class=argparse.RawTextHelpFormatter)
return parser.parse_args(func_args)
def process_frame(frame, detector, num_charts):
if not detector.process(frame, num_charts):
return None
checkers = detector.getListColorChecker()
src = checkers[0].getChartsRGB(False)
return src
def main(func_args=None):
args = get_args_parser(func_args)
if not (0 <= args.chart_type <= 2):
raise ValueError("chartType must be 0, 1, or 2")
# Validate arguments based on whether using pre-computed CCM
if args.ccm_file:
if not args.query:
print("[ERROR] Query image path must be provided when using pre-computed CCM.")
return -1
else:
if not args.input:
print("[ERROR] Input image path must be provided when computing new CCM.")
return -1
# Read query image
query_image = None
if args.query:
query_image = cv.imread(findFile(args.query))
if query_image is None:
print("[ERROR] Unable to read query image.")
return -1
if os.getenv('OPENCV_SAMPLES_DATA_PATH') is not None:
try:
args.model = findModel(args.model, args.sha1)
args.config = findModel(args.config, args.config_sha1)
except:
print("[WARN] Model file not provided, using default detector. Pass model using --model and config using --config to use dnn based detector.\n\n")
args.model = None
args.config = None
else:
args.model = None
args.config = None
print("[WARN] Model file not provided, using default detector. Pass model using --model and config using --config to use dnn based detector. Or, set OPENCV_SAMPLES_DATA_PATH environment variable.\n\n")
# Create color correction model
model = cv.ccm.ColorCorrectionModel()
if args.ccm_file:
# Load CCM from YAML file
fs = cv.FileStorage(args.ccm_file, cv.FileStorage_READ)
if not fs.isOpened():
print(f"[ERROR] Unable to open CCM file: {args.ccm_file}")
return -1
model.read(fs.getNode("ColorCorrectionModel"))
fs.release()
print(f"Loaded CCM from file: {args.ccm_file}")
else:
# Read input image for computing new CCM
image = cv.imread(findFile(args.input))
if image is None:
print("[ERROR] Unable to read input image.")
return -1
# Create color checker detector
if args.model and args.config:
# Load the DNN from TensorFlow model
engine = cv.dnn.ENGINE_AUTO
if args.backend != "default" or args.target != "cpu":
engine = cv.dnn.ENGINE_CLASSIC
net = cv.dnn.readNetFromTensorflow(args.model, args.config, engine)
net.setPreferableBackend(get_backend_id(args.backend))
net.setPreferableTarget(get_target_id(args.target))
detector = cv.mcc_CCheckerDetector.create(net)
print("Detecting checkers using neural network.")
else:
detector = cv.mcc_CCheckerDetector.create()
print("Detecting checkers using default method (no DNN).")
detector.setColorChartType(args.chart_type)
# Process image to detect color checker
src = process_frame(image, detector, args.num_charts)
if src is None:
print("No chart detected in the input image!")
return -1
print("Actual colors:", src)
# Convert to double and normalize
src = src.astype(np.float64) / 255.0
# Create and configure color correction model
model = cv.ccm.ColorCorrectionModel(src, cv.ccm.COLORCHECKER_MACBETH)
model.setCcmType(cv.ccm.CCM_LINEAR)
model.setDistance(cv.ccm.DISTANCE_CIE2000)
model.setLinearization(cv.ccm.LINEARIZATION_GAMMA)
model.setLinearizationGamma(2.2)
# Compute color correction matrix
ccm = model.compute()
print("Computed CCM Matrix:\n", ccm)
print("Loss:", model.getLoss())
# Save model parameters to YAML file
fs = cv.FileStorage("ccm_output.yaml", cv.FileStorage_WRITE)
model.write(fs)
fs.release()
print("Model parameters saved to ccm_output.yaml")
# Set query image for correction if not provided
if query_image is None:
print("[WARN] No query image provided, applying color correction on input image")
query_image = image.copy()
# Apply correction to query image
calibrated_image = np.empty_like(query_image)
model.correctImage(query_image, calibrated_image)
cv.imshow("Original Image", query_image)
cv.imshow("Corrected Image", calibrated_image)
cv.waitKey(0)
return 0
if __name__ == "__main__":
main()