I. Code Implementation Notes
- Key points of technical reconstruction:
- Simulate Epson’s “multi-media LUT adaptation”: generate dedicated color lookup tables for different media such as RC photo paper, plain paper, and backlit film;
- Metamerism suppression: use calibration in the CIE Lab color space to ensure color consistency under different light sources;
- Coordination with halftoning: output LUT-calibrated color data to provide an accurate color foundation for the halftone module.
- Dependencies:
- OpenCV 4.5+
- C++ 17
- OpenCV_contrib (optional, for CIE Lab color conversion)
- Applicable scenarios:
- Color image color calibration (such as advertising backlit film and photo printing), simulating the accurate color output characteristics of Epson SureColor series printers.

II. Complete C++ Code Implementation
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/types_c.h>
#include <iostream>
#include <vector>
#include <map>
#include <cmath>
using namespace cv;
using namespace std;
// Epson media type enumeration (based on publicly available product parameters)
enum class EpsonMedium {
RC_PHOTO_PAPER, // RC photo paper (high color saturation)
PLAIN_PAPER, // Plain paper (low ink penetration)
BACKLIT_FILM, // Backlit film (high light-blocking rate, deep black optimization)
CANVAS // Canvas (texture adaptation)
};
// Core Epson Color Lookup Table (LUT) class
class EpsonColorLUT {
public:
// Constructor: initialize LUT according to media type
EpsonColorLUT(EpsonMedium medium) {
this->medium = medium;
// 1. Initialize media color parameters (based on Epson public technical documents)
initMediumParams();
// 2. Generate a 17x17x17 3D LUT (17 levels for each RGB channel, covering the full 0-255 range)
generate3DLUT();
}
// Core method: apply LUT color calibration (input BGR image, output calibrated image)
Mat applyLUT(const Mat& inputImg) {
if (inputImg.empty() || inputImg.channels() != 3) {
throw invalid_argument(“Input must be a 3-channel BGR image!”);
}
Mat resultImg = inputImg.clone();
int rows = resultImg.rows;
int cols = resultImg.cols;
// Traverse each pixel and apply LUT calibration
for (int y = 0; y < rows; y++) {
Vec3b* rowPtr = resultImg.ptr<Vec3b>(y);
for (int x = 0; x < cols; x++) {
// Input is BGR (OpenCV default), convert to RGB for LUT query
uchar B = rowPtr[x][0];
uchar G = rowPtr[x][1];
uchar R = rowPtr[x][2];
// Query LUT to obtain calibrated RGB values
Vec3b calibratedRGB = queryLUT(R, G, B);
// Convert back to BGR and update the pixel
rowPtr[x][0] = calibratedRGB[2]; // B = calibrated B
rowPtr[x][1] = calibratedRGB[1]; // G = calibrated G
rowPtr[x][2] = calibratedRGB[0]; // R = calibrated R
}
}
return resultImg;
}
// Auxiliary method: export LUT file (for coordination with halftone module)
void exportLUT(const string& filePath) {
FileStorage fs(filePath, FileStorage::WRITE);
if (!fs.isOpened()) {
throw runtime_error(“Unable to write LUT file!”);
}
fs << “EpsonColorLUT” << “{“;
fs << “MediumType” << static_cast<int>(medium);
fs << “LUTSize” << LUT_SIZE;
fs << “3DLUT” << lutData;
fs << “}”;
fs.release();
cout << “LUT has been exported to: ” << filePath << endl;
}
private:
const int LUT_SIZE = 17; // LUT dimension (17 levels, step size 16: 0,16,…,255)
const int LUT_STEP = 255 / (LUT_SIZE – 1); // Step size between levels
EpsonMedium medium; // Current media type
vector<vector<vector<Vec3b>>> lutData; // 3D LUT data (R[LUT_SIZE][G][B])
// Media color parameters (gain, offset, gamma value)
struct MediumParams {
Vec3f rgbGain; // RGB channel gain (increase saturation)
Vec3f rgbOffset; // RGB channel offset (shadow correction)
float gamma; // Gamma value (brightness curve optimization)
float blackLevel; // Black level (deep black optimization, for backlit film only)
} mediumParams;
// Step 1: initialize color parameters for different media
void initMediumParams() {
switch (medium) {
case EpsonMedium::RC_PHOTO_PAPER:
// RC photo paper: high saturation, gamma 1.8 (recommended by Epson)
mediumParams = { {1.1f, 1.05f, 1.1f}, {0, 0, 0}, 1.8f, 0.0f };
break;
case EpsonMedium::PLAIN_PAPER:
// Plain paper: reduce red gain (reduce penetration color shift), gamma 2.2
mediumParams = { {0.9f, 1.0f, 0.95f}, {5, 3, 5}, 2.2f, 0.0f };
break;
case EpsonMedium::BACKLIT_FILM:
// Backlit film: increase blue gain (deep black optimization), black level 0.15
mediumParams = { {1.0f, 1.0f, 1.2f}, {0, 0, 0}, 1.6f, 0.15f };
break;
case EpsonMedium::CANVAS:
// Canvas: reduce green gain (adapt to texture), gamma 2.0
mediumParams = { {1.05f, 0.95f, 1.05f}, {3, 5, 3}, 2.0f, 0.0f };
break;
}
}
// Step 2: generate 3D LUT (core: color space conversion and media calibration)
void generate3DLUT() {
// Initialize LUT data structure
lutData.resize(LUT_SIZE, vector<vector<Vec3b>>(LUT_SIZE, vector<Vec3b>(LUT_SIZE)));
for (int r = 0; r < LUT_SIZE; r++) {
for (int g = 0; g < LUT_SIZE; g++) {
for (int b = 0; b < LUT_SIZE; b++) {
// 1. Get raw RGB value of current LUT node (0-255)
float rawR = r * LUT_STEP;
float rawG = g * LUT_STEP;
float rawB = b * LUT_STEP;
// 2. Media calibration: apply gain, offset, and gamma correction
float calibratedR = calibrateChannel(rawR, 0);
float calibratedG = calibrateChannel(rawG, 1);
float calibratedB = calibrateChannel(rawB, 2);
// 3. Metamerism suppression: fine-tune in CIE Lab space (based on D65 standard illuminant)
Vec3b labCalibrated = suppressMetamerism(calibratedR, calibratedG, calibratedB);
// 4. Store calibrated RGB value in the LUT
lutData[r][g][b] = labCalibrated;
}
}
}
}
// Auxiliary method: single-channel calibration (gain + offset + gamma)
float calibrateChannel(float rawVal, int channel) {
// 1. Normalize to the range 0-1
float normVal = rawVal / 255.0f;
// 2. Apply black level correction (for backlit film only)
if (medium == EpsonMedium::BACKLIT_FILM) {
normVal = max(normVal – mediumParams.blackLevel, 0.0f) / (1.0f – mediumParams.blackLevel);
}
// 3. Apply gamma correction (solve media luminance nonlinearity)
float gammaVal = pow(normVal, 1.0f / mediumParams.gamma);
// 4. Apply gain and offset
float gain = (channel == 0) ? mediumParams.rgbGain[0]
: (channel == 1) ? mediumParams.rgbGain[1]
: mediumParams.rgbGain[2];
float offset = (channel == 0) ? mediumParams.rgbOffset[0]
: (channel == 1) ? mediumParams.rgbOffset[1]
: mediumParams.rgbOffset[2];
float calibrated = gammaVal * gain * 255.0f + offset;
// 5. Clamp to 0-255
return clamp(calibrated, 0.0f, 255.0f);
}
// Auxiliary method: metamerism suppression (CIE Lab space calibration)
Vec3b suppressMetamerism(float R, float G, float B) {
// 1. Convert RGB to CIE Lab (based on D65 illuminant, Epson standard)
Mat rgbMat(1, 1, CV_32FC3);
rgbMat.at<Vec3f>(0, 0) = Vec3f(R / 255.0f, G / 255.0f, B / 255.0f);
Mat labMat;
cvtColor(rgbMat, labMat, COLOR_RGB2Lab);
Vec3f lab = labMat.at<Vec3f>(0, 0);
// 2. Fine-tune a/b channels in Lab space according to media type (suppress metamerism)
switch (medium) {
case EpsonMedium::BACKLIT_FILM:
// Backlit film: reduce a channel (reduce red color cast), increase b channel (enhance yellow reproduction)
lab[1] *= 0.9f; // a channel (red-green)
lab[2] *= 1.05f; // b channel (yellow-blue)
break;
case EpsonMedium::CANVAS:
// Canvas: increase a channel (compensate for red loss caused by texture)
lab[1] *= 1.05f;
break;
default:
// Other media: slight correction to b channel (general metamerism suppression)
lab[2] *= 0.98f;
break;
}
// 3. Convert Lab back to RGB
Mat calibratedRgbMat;
labMat.at<Vec3f>(0, 0) = lab;
cvtColor(labMat, calibratedRgbMat, COLOR_Lab2RGB);
Vec3f calibratedRgb = calibratedRgbMat.at<Vec3f>(0, 0);
// 4. Convert to uchar and return
return Vec3b(
static_cast<uchar>(clamp(calibratedRgb[0] * 255, 0.0f, 255.0f)),
static_cast<uchar>(clamp(calibratedRgb[1] * 255, 0.0f, 255.0f)),
static_cast<uchar>(clamp(calibratedRgb[2] * 255, 0.0f, 255.0f))
);
}
// Auxiliary method: query LUT (trilinear interpolation to improve accuracy)
Vec3b queryLUT(uchar R, uchar G, uchar B) {
// 1. Calculate current RGB position in LUT (fractional part used as interpolation weights)
float r = static_cast<float>(R) / LUT_STEP;
float g = static_cast<float>(G) / LUT_STEP;
float b = static_cast<float>(B) / LUT_STEP;
// 2. Get LUT node indices (integer part)
int r0 = static_cast<int>(floor(r));
int r1 = min(r0 + 1, LUT_SIZE – 1);
int g0 = static_cast<int>(floor(g));
int g1 = min(g0 + 1, LUT_SIZE – 1);
int b0 = static_cast<int>(floor(b));
int b1 = min(b0 + 1, LUT_SIZE – 1);
// 3. Calculate interpolation weights (fractional part)
float wr1 = r – r0; float wr0 = 1.0f – wr1;
float wg1 = g – g0; float wg0 = 1.0f – wg1;
float wb1 = b – b0; float wb0 = 1.0f – wb1;
auto interpolate = [](Vec3b a, Vec3b b, float wa, float wb) {
return Vec3b(
static_cast<uchar>(a[0] * wa + b[0] * wb),
static_cast<uchar>(a[1] * wa + b[1] * wb),
static_cast<uchar>(a[2] * wa + b[2] * wb)
);
};
// First layer: interpolate between b0 and b1
Vec3b c00 = interpolate(lutData[r0][g0][b0], lutData[r0][g0][b1], wb0, wb1);
Vec3b c01 = interpolate(lutData[r0][g1][b0], lutData[r0][g1][b1], wb0, wb1);
Vec3b c10 = interpolate(lutData[r1][g0][b0], lutData[r1][g0][b1], wb0, wb1);
Vec3b c11 = interpolate(lutData[r1][g1][b0], lutData[r1][g1][b1], wb0, wb1);
// Second layer: interpolate between g0 and g1
Vec3b c0 = interpolate(c00, c01, wg0, wg1);
Vec3b c1 = interpolate(c10, c11, wg0, wg1);
// Third layer: interpolate between r0 and r1
return interpolate(c0, c1, wr0, wr1);
}
// Numeric clamp
float clamp(float val, float minVal, float maxVal) {
return (val < minVal) ? minVal : (val > maxVal) ? maxVal : val;
}
};
// Test code: integrate with Epson halftone module (simulate complete color workflow)
int main(int argc, char** argv) {
// 1. Read input image (OpenCV default is BGR)
string inputPath = (argc > 1) ? argv[1] : “test_image.jpg”;
Mat inputImg = imread(inputPath);
if (inputImg.empty()) {
cerr << “Error: Unable to read image!” << endl;
return -1;
}
try {
// 2. Initialize Epson color lookup table (take backlit film as an example, common in advertising)
EpsonColorLUT epsonLUT(EpsonMedium::BACKLIT_FILM);
// 3. Apply LUT color calibration
Mat calibratedImg = epsonLUT.applyLUT(inputImg);
// 4. (Optional) Export LUT for coordination with halftone module
epsonLUT.exportLUT(“epson_backlit_lut.yml”);
// 5. Save calibration result
imwrite(“epson_lut_calibrated.png”, calibratedImg);
cout << “Color calibration completed! Result has been saved to epson_lut_calibrated.png” << endl;
// 6. Display comparison (original image vs calibrated image)
imshow(“Original Image (BGR)”, inputImg);
imshow(“LUT-Calibrated Image”, calibratedImg);
waitKey(0);
destroyAllWindows();
}
catch (const exception& e) {
cerr << “Processing exception: ” << e.what() << endl;
return -1;
}
return 0;
}