Files
kirl a64690477a Dataset: add READMEs, organize scripts, update .gitignore
- Add README.md to all 16 dataset directories with script references
- Centralize generation scripts under scripts/ with full README
- Update .gitignore to cover all data directories, keep only metadata
- Remove redundant CCPD/.gitignore (parent covers same rules)
- Remove stale scripts/test/ (254MB pipeline residue)
- Fix label parser reference: scripts are authoritative source of truth
2026-06-23 14:41:39 +08:00

674 lines
25 KiB
C++

#include "plate_swap.hpp"
#include <fstream>
#include <algorithm>
#include <numeric>
#include <cmath>
#include <cstring>
#include <filesystem>
namespace fs = std::filesystem;
using namespace cv;
using namespace std;
namespace ice {
// ═══════════════════════════════════════════════════════════
// CCPD 文件名解析
// ═══════════════════════════════════════════════════════════
CCPDMeta parse_ccpd_filename(const string& filename) {
CCPDMeta meta;
meta.valid = false;
// 提取不带扩展名的部分
fs::path p(filename);
string stem = p.stem().string();
// 按 '-' 分割
vector<string> parts;
size_t pos = 0;
while (pos < stem.size()) {
size_t dash = stem.find('-', pos);
if (dash == string::npos) {
parts.push_back(stem.substr(pos));
break;
}
parts.push_back(stem.substr(pos, dash - pos));
pos = dash + 1;
}
if (parts.size() < 5) return meta;
try {
// parts[3] = 4角点
string& coords = parts[3];
vector<Point2f> pts;
size_t start = 0;
while (start < coords.size()) {
size_t us = coords.find('_', start);
string pt = (us == string::npos) ? coords.substr(start) : coords.substr(start, us - start);
size_t amp = pt.find('&');
if (amp == string::npos) break;
float x = stof(pt.substr(0, amp));
float y = stof(pt.substr(amp + 1));
pts.push_back({x, y});
if (us == string::npos) break;
start = us + 1;
}
if (pts.size() != 4) return meta;
// 几何排序: TL=min(x+y), TR=max(x-y), BR=max(x+y), BL=min(x-y)
float sums[4], diffs[4];
for (int i = 0; i < 4; i++) {
sums[i] = pts[i].x + pts[i].y;
diffs[i] = pts[i].x - pts[i].y;
}
int tl = (int)(min_element(sums, sums+4) - sums);
int tr = (int)(max_element(diffs, diffs+4) - diffs);
int br = (int)(max_element(sums, sums+4) - sums);
int bl = (int)(min_element(diffs, diffs+4) - diffs);
meta.corners[0] = pts[tl];
meta.corners[1] = pts[tr];
meta.corners[2] = pts[br];
meta.corners[3] = pts[bl];
meta.valid = true;
} catch (...) {
meta.valid = false;
}
return meta;
}
// ═══════════════════════════════════════════════════════════
// 合成车牌池
// ═══════════════════════════════════════════════════════════
void PlatePool::load(const string& dir) {
// Fallback: load from directory
for (auto& entry : fs::directory_iterator(dir)) {
if (!entry.is_directory()) continue;
string bg_color = entry.path().filename().string();
vector<PlateEntry>& vec = pools_[bg_color];
for (auto& f : fs::directory_iterator(entry.path())) {
if (!f.is_regular_file()) continue;
string ext = f.path().extension().string();
if (ext != ".jpg" && ext != ".png" && ext != ".PNG") continue;
Mat img = imread_unicode(f.path().string());
if (img.empty()) continue;
PlateEntry pe;
pe.image = img;
vec.push_back(pe);
}
}
}
void PlatePool::load_bin(const string& bin_path) {
ifstream f(bin_path, ios::binary);
if (!f) { cerr << "[WARN] pool.bin not found" << endl; return; }
char magic[4];
f.read(magic, 4);
if (memcmp(magic, "IPPL", 4) != 0) { cerr << "[WARN] bad magic" << endl; return; }
uint32_t num_types;
f.read((char*)&num_types, 4);
for (uint32_t ti = 0; ti < num_types; ti++) {
uint32_t name_len;
f.read((char*)&name_len, 4);
string plate_type(name_len, 0);
f.read(&plate_type[0], name_len);
uint32_t count;
f.read((char*)&count, 4);
vector<PlateEntry>& vec = pools_[plate_type];
vec.reserve(count);
for (uint32_t pi = 0; pi < count; pi++) {
uint32_t w, h, data_len;
f.read((char*)&w, 4);
f.read((char*)&h, 4);
f.read((char*)&data_len, 4);
vector<uint8_t> buf(data_len);
f.read((char*)buf.data(), data_len);
PlateEntry pe;
pe.image = Mat(h, w, CV_8UC3, buf.data()).clone();
// Read plate number metadata
uint32_t num_len;
f.read((char*)&num_len, 4);
pe.plate_number.resize(num_len);
f.read(&pe.plate_number[0], num_len);
vec.push_back(pe);
}
}
}
const PlateEntry& PlatePool::random(const string& plate_type, mt19937& rng) {
auto& vec = pools_[plate_type];
if (vec.empty()) {
for (auto& [k, v] : pools_) {
if (!v.empty()) {
uniform_int_distribution<size_t> dist(0, v.size()-1);
return v[dist(rng)];
}
}
throw runtime_error("PlatePool is empty");
}
uniform_int_distribution<size_t> dist(0, vec.size()-1);
return vec[dist(rng)];
}
size_t PlatePool::count(const string& plate_type) const {
auto it = pools_.find(plate_type);
return (it != pools_.end()) ? it->second.size() : 0;
}
// Helper: plate_type → bg_color for color matching
static string bg_color_of(const string& plate_type) {
if (plate_type == "police") return "white";
if (plate_type == "army") return "white_army";
if (plate_type == "gangao") return "black";
if (plate_type == "shiguan_old" || plate_type == "shiguan_new") return "black_shi";
if (plate_type == "lingguan_old" || plate_type == "lingguan_new") return "black";
if (plate_type == "coach") return "yellow";
if (plate_type == "new_energy") return "green_truck";
if (plate_type == "blue") return "blue";
if (plate_type == "green") return "green_car";
return "white";
}
// ═══════════════════════════════════════════════════════════
// Unicode 文件读写
// ═══════════════════════════════════════════════════════════
Mat imread_unicode(const string& path) {
ifstream f(fs::path(path), ios::binary);
if (!f) return Mat();
vector<uint8_t> data((istreambuf_iterator<char>(f)), istreambuf_iterator<char>());
return imdecode(data, IMREAD_COLOR);
}
bool imwrite_unicode(const string& path, const Mat& img, const vector<int>& params) {
vector<uint8_t> buf;
fs::path p(path);
string ext = p.extension().string();
if (!imencode(ext, img, buf, params)) return false;
// Use fs::path directly for proper Unicode support on Windows
ofstream f(p, ios::binary);
if (!f) return false;
f.write(reinterpret_cast<const char*>(buf.data()), buf.size());
return f.good();
}
// ═══════════════════════════════════════════════════════════
// 透视提取车牌 ROI
// ═══════════════════════════════════════════════════════════
Mat extract_plate_roi(const Mat& img, const Point2f corners[4],
int plate_w, int plate_h) {
Point2f dst[4] = {
{0, 0},
{(float)(plate_w - 1), 0},
{(float)(plate_w - 1), (float)(plate_h - 1)},
{0, (float)(plate_h - 1)}
};
Mat M = getPerspectiveTransform(corners, dst);
Mat roi;
warpPerspective(img, roi, M, Size(plate_w, plate_h),
INTER_LINEAR, BORDER_REPLICATE);
return roi;
}
// ═══════════════════════════════════════════════════════════
// 色温匹配
// ═══════════════════════════════════════════════════════════
Mat match_plate_color(const Mat& synthetic, const Mat& orig_roi,
const string& bg_color) {
Mat syn, orig;
synthetic.convertTo(syn, CV_32F);
orig_roi.convertTo(orig, CV_32F);
// 尺寸对齐
if (syn.size() != orig.size())
resize(syn, syn, orig.size(), 0, 0, INTER_LINEAR);
if (bg_color == "black" || bg_color == "black_shi") {
Mat orig_gray;
cvtColor(orig_roi, orig_gray, COLOR_BGR2GRAY);
double thresh_val;
{
Mat sorted;
orig_gray.reshape(1, 1).copyTo(sorted);
cv::sort(sorted, sorted, SORT_EVERY_ROW + SORT_ASCENDING);
int idx = (int)(sorted.cols * 0.90);
thresh_val = sorted.at<uint8_t>(0, idx);
}
Mat white_mask = orig_gray > thresh_val;
if (countNonZero(white_mask) < 50) return synthetic;
Scalar white_ref = mean(orig, white_mask);
float ref_mean = (float)(white_ref[0] + white_ref[1] + white_ref[2]) / 3.0f;
if (ref_mean < 10) return synthetic;
Scalar color_cast(white_ref[0]/ref_mean, white_ref[1]/ref_mean, white_ref[2]/ref_mean);
Scalar amplified(1.0f + (color_cast[0]-1.0f), 1.0f+(color_cast[1]-1.0f), 1.0f+(color_cast[2]-1.0f));
amplified[0] = std::clamp((double)amplified[0], 0.85, 1.15);
amplified[1] = std::clamp((double)amplified[1], 0.85, 1.15);
amplified[2] = std::clamp((double)amplified[2], 0.85, 1.15);
Mat syn_gray;
cvtColor(synthetic, syn_gray, COLOR_BGR2GRAY);
Mat alpha;
syn_gray.convertTo(alpha, CV_32F, 1.0/128.0);
alpha = max(0.0f, min(1.0f, alpha));
Mat result = syn.clone();
for (int c = 0; c < 3; c++) {
Mat ch_syn, ch_res;
extractChannel(syn, ch_syn, c);
ch_res = ch_syn.mul(1.0f + ((float)amplified[c] - 1.0f) * alpha);
ch_res.setTo(0, ch_res < 0);
ch_res.setTo(255, ch_res > 255);
insertChannel(ch_res, result, c);
}
result.convertTo(result, CV_8U);
return result;
}
// 亮底牌:提取白参考
Mat orig_gray;
cvtColor(orig_roi, orig_gray, COLOR_BGR2GRAY);
double thresh_val;
{
Mat sorted;
orig_gray.reshape(1, 1).copyTo(sorted);
cv::sort(sorted, sorted, SORT_EVERY_ROW + SORT_ASCENDING);
int idx = (int)(sorted.cols * 0.90);
thresh_val = sorted.at<uint8_t>(0, idx);
}
Mat white_mask = orig_gray > thresh_val;
if (countNonZero(white_mask) < 50) return synthetic;
Scalar white_ref = mean(orig, white_mask);
float ref_mean = (float)(white_ref[0] + white_ref[1] + white_ref[2]) / 3.0f;
if (ref_mean < 10) return synthetic;
if (bg_color == "yellow") {
Scalar gain = white_ref / 255.0;
gain[0] = std::clamp((float)gain[0], 0.6f, 1.3f);
gain[1] = std::clamp((float)gain[1], 0.6f, 1.3f);
gain[2] = std::clamp((float)gain[2], 0.6f, 1.3f);
Mat res;
multiply(syn, gain, res);
res.setTo(0, res < 0);
res.setTo(255, res > 255);
res.convertTo(res, CV_8U);
return res;
}
// 白/绿牌
Scalar color_cast(white_ref[0]/ref_mean, white_ref[1]/ref_mean, white_ref[2]/ref_mean);
float strength = (bg_color == "green_truck" || bg_color == "green_car") ? 1.5f : 1.0f;
Scalar amplified(1.0f + (color_cast[0]-1.0f)*strength, 1.0f+(color_cast[1]-1.0f)*strength, 1.0f+(color_cast[2]-1.0f)*strength);
amplified[0] = std::clamp((float)amplified[0], 0.85f, 1.15f);
amplified[1] = std::clamp((float)amplified[1], 0.85f, 1.15f);
amplified[2] = std::clamp((float)amplified[2], 0.85f, 1.15f);
Mat syn_gray;
cvtColor(synthetic, syn_gray, COLOR_BGR2GRAY);
Mat alpha;
syn_gray.convertTo(alpha, CV_32F, 1.0/128.0);
alpha = max(0.0f, min(1.0f, alpha));
Mat result = syn.clone();
for (int c = 0; c < 3; c++) {
Mat ch_syn, ch_res;
extractChannel(syn, ch_syn, c);
ch_res = ch_syn.mul(1.0f + (amplified[c] - 1.0f) * alpha);
ch_res.setTo(0, ch_res < 0);
ch_res.setTo(255, ch_res > 255);
insertChannel(ch_res, result, c);
}
result.convertTo(result, CV_8U);
return result;
}
// ═══════════════════════════════════════════════════════════
// 底板色校正
// ═══════════════════════════════════════════════════════════
Mat fix_bg_color(const Mat& plate, const string& bg_color,
int h_shift, int s_scale, int v_scale) {
if (h_shift == 0 && s_scale == 100 && v_scale == 100)
return plate;
int h_lo = 0, h_hi = 180;
if (bg_color == "yellow") { h_lo = 15; h_hi = 50; }
else if (bg_color == "green_truck" || bg_color == "green_car") { h_lo = 40; h_hi = 85; }
else if (bg_color == "blue") { h_lo = 90; h_hi = 130; }
Mat hsv;
cvtColor(plate, hsv, COLOR_BGR2HSV);
hsv.convertTo(hsv, CV_32F);
vector<Mat> ch(3);
split(hsv, ch);
Mat &H = ch[0], &S = ch[1], &V = ch[2];
Mat bg_mask = (H > h_lo) & (H < h_hi) & (S > 80) & (V > 100);
if (countNonZero(bg_mask) < 200) return plate;
// 距离变换羽化
Mat bg_u8, dist;
bg_mask.convertTo(bg_u8, CV_8U, 255);
distanceTransform(bg_u8, dist, DIST_L2, DIST_MASK_PRECISE);
Mat soft;
dist.convertTo(soft, CV_32F, 1.0/4.0);
soft = max(0.0f, min(1.0f, soft));
H = H + h_shift * soft;
S = S.mul(1.0f + (s_scale / 100.0f - 1.0f) * soft);
V = V.mul(1.0f + (v_scale / 100.0f - 1.0f) * soft);
H.setTo(0, H < 0); H.setTo(180, H > 180);
S.setTo(0, S < 0); S.setTo(255, S > 255);
V.setTo(0, V < 0); V.setTo(255, V > 255);
Mat result;
merge(ch, hsv);
hsv.convertTo(hsv, CV_8U);
cvtColor(hsv, result, COLOR_HSV2BGR);
return result;
}
// ═══════════════════════════════════════════════════════════
// 透视回贴
// ═══════════════════════════════════════════════════════════
pair<Mat, Mat> warp_plate_back(const Mat& plate, const Point2f dst_corners[4],
Size output_size) {
int h = plate.rows, w = plate.cols;
Point2f src[4] = {
{0, 0}, {(float)(w-1), 0}, {(float)(w-1), (float)(h-1)}, {0, (float)(h-1)}
};
Mat M = getPerspectiveTransform(src, dst_corners);
Mat warped, mask_warped;
warpPerspective(plate, warped, M, output_size, INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0,0));
Mat mask(h, w, CV_8UC1, Scalar(255));
warpPerspective(mask, mask_warped, M, output_size, INTER_LINEAR, BORDER_CONSTANT, Scalar(0));
return {warped, mask_warped};
}
// ═══════════════════════════════════════════════════════════
// 羽化贴合
// ═══════════════════════════════════════════════════════════
Mat feather_blend(const Mat& original, const Mat& warped,
const Mat& mask, int edge_width) {
Mat mask_u8;
mask.convertTo(mask_u8, CV_8U);
if (countNonZero(mask_u8) < 100) {
Mat result = original.clone();
warped.copyTo(result, mask_u8);
return result;
}
// Fast blend: erode mask slightly for edge transition, no expensive distanceTransform
Mat eroded;
int ksize = max(3, edge_width) | 1; // ensure odd
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(ksize, ksize));
erode(mask_u8, eroded, kernel);
Mat alpha;
eroded.convertTo(alpha, CV_32F, 1.0/255.0);
// Soften edges with a small blur
GaussianBlur(alpha, alpha, Size(ksize, ksize), ksize/3.0);
Mat result(original.size(), CV_32FC3);
Mat warped_f, orig_f;
warped.convertTo(warped_f, CV_32F);
original.convertTo(orig_f, CV_32F);
for (int c = 0; c < 3; c++) {
Mat wc, oc;
extractChannel(warped_f, wc, c);
extractChannel(orig_f, oc, c);
Mat rc = wc.mul(alpha) + oc.mul(1.0f - alpha);
rc.setTo(0, rc < 0);
rc.setTo(255, rc > 255);
insertChannel(rc, result, c);
}
result.convertTo(result, CV_8U);
return result;
}
// ═══════════════════════════════════════════════════════════
// 噪声匹配
// ═══════════════════════════════════════════════════════════
Mat match_noise(const Mat& blended, const Mat& original, const Mat& mask) {
Mat mask_u8;
mask.convertTo(mask_u8, CV_8U);
if (countNonZero(mask_u8) < 100) return blended;
// Crop plate region for noise estimation (like Python's plate_crop)
vector<Point> pts;
findNonZero(mask_u8, pts);
if (pts.size() < 200) return blended;
int bw = blended.cols, bh = blended.rows;
int xmin = bw, xmax = 0, ymin = bh, ymax = 0;
for (auto& p : pts) {
if (p.x < xmin) xmin = p.x;
if (p.x > xmax) xmax = p.x;
if (p.y < ymin) ymin = p.y;
if (p.y > ymax) ymax = p.y;
}
int x1 = max(0, xmin - 5), x2 = min(bw, xmax + 5);
int y1 = max(0, ymin - 5), y2 = min(bh, ymax + 5);
Mat plate_crop = original(Rect(x1, y1, x2-x1, y2-y1));
// 1) Noise sigma from original plate crop
Mat crop_blurred, crop_residual;
GaussianBlur(plate_crop, crop_blurred, Size(5,5), 1.0);
Mat crop_f32, blur_f32;
plate_crop.convertTo(crop_f32, CV_32F);
crop_blurred.convertTo(blur_f32, CV_32F);
crop_residual = crop_f32 - blur_f32;
Scalar stddev;
meanStdDev(crop_residual, Scalar(), stddev);
float noise_sigma = (float)max(stddev[0], max(stddev[1], max(stddev[2], 1.5)));
// 2) Blur sigma from original plate crop
Mat crop_gray;
cvtColor(plate_crop, crop_gray, COLOR_BGR2GRAY);
Mat lap;
Laplacian(crop_gray, lap, CV_64F);
Scalar lap_m, lap_s;
meanStdDev(lap, lap_m, lap_s);
float lap_var = (float)(lap_s[0] * lap_s[0]);
float blur_sigma = 0;
if (lap_var < 50) blur_sigma = 2.0f;
else if (lap_var < 200) blur_sigma = 1.2f;
else if (lap_var < 500) blur_sigma = 0.5f;
// Apply blur to plate area
Mat result = blended.clone();
if (blur_sigma > 0.1f) {
Mat blurred_full;
GaussianBlur(result, blurred_full, Size(0,0), blur_sigma);
blurred_full.copyTo(result, mask_u8);
}
// Add noise to plate area only
Mat result_f;
result.convertTo(result_f, CV_32F);
Mat noise(result.size(), CV_32FC3);
randn(noise, Scalar(0,0,0), Scalar(noise_sigma, noise_sigma, noise_sigma));
Mat noisy = result_f + noise;
// Alpha blend: noise in plate area, clean outside
Mat alpha;
mask_u8.convertTo(alpha, CV_32F, 1.0/255.0);
Mat alpha_3;
merge(vector<Mat>{alpha, alpha, alpha}, alpha_3);
Mat one_minus_a;
subtract(Scalar::all(1.0), alpha_3, one_minus_a);
noisy = noisy.mul(alpha_3) + result_f.mul(one_minus_a);
// Clip AFTER blend (matching Python np.clip order)
noisy.setTo(0, noisy < 0);
noisy.setTo(255, noisy > 255);
noisy.convertTo(result, CV_8U);
return result;
}
Mat degrade_plate(const Mat& plate) {
Mat result = plate.clone();
// JPEG 极限压缩
vector<uint8_t> buf;
imencode(".jpg", result, buf, {IMWRITE_JPEG_QUALITY, 2});
result = imdecode(buf, IMREAD_COLOR);
// 降采样升采样
int w = result.cols, h = result.rows;
Mat small;
resize(result, small, Size(w/4, h/4), 0, 0, INTER_LINEAR);
resize(small, result, Size(w, h), 0, 0, INTER_NEAREST);
// 模糊
GaussianBlur(result, result, Size(5,5), 1.5);
return result;
}
// ═══════════════════════════════════════════════════════════
// 单张处理
// ═══════════════════════════════════════════════════════════
bool process_single(const string& img_path, const SwapConfig& cfg,
PlatePool& pool, mt19937& rng, const CCPDMeta* pre_meta) {
// 1. 解析文件名 (use pre-parsed if available)
fs::path fp(img_path);
CCPDMeta meta;
if (pre_meta && pre_meta->valid) {
meta = *pre_meta;
} else {
meta = parse_ccpd_filename(fp.filename().string());
if (!meta.valid) return false;
}
// 2. 读原图
Mat img_orig = imread_unicode(img_path);
if (img_orig.empty()) return false;
int h_img = img_orig.rows, w_img = img_orig.cols;
// 校验角点
for (int i = 0; i < 4; i++) {
auto& c = meta.corners[i];
if (c.x < 0 || c.y < 0 || c.x > w_img*1.5 || c.y > h_img*1.5)
return false;
}
// 3. Pick from pool
string bg_color = bg_color_of(cfg.plate_type);
const PlateEntry& entry = pool.random(cfg.plate_type, rng);
Mat synthetic = entry.image.clone();
int plate_w = synthetic.cols, plate_h = synthetic.rows;
string plate_number = entry.plate_number;
// 4. Extract original ROI
Mat orig_roi = extract_plate_roi(img_orig, meta.corners, plate_w, plate_h);
// 5. Fix bg color
synthetic = fix_bg_color(synthetic, bg_color,
cfg.h_shift, cfg.s_scale, cfg.v_scale);
// 6. Color match
Mat matched = match_plate_color(synthetic, orig_roi, bg_color);
// 7. Brightness match
Mat orig_gray, matched_gray;
cvtColor(orig_roi, orig_gray, COLOR_BGR2GRAY);
cvtColor(matched, matched_gray, COLOR_BGR2GRAY);
auto p95 = [](const Mat& m) -> float {
Mat sorted;
m.reshape(1,1).copyTo(sorted);
cv::sort(sorted, sorted, SORT_EVERY_ROW + SORT_ASCENDING);
return sorted.at<uint8_t>(0, (int)(sorted.cols * 0.95));
};
float target_w = p95(orig_gray);
float current_w = p95(matched_gray);
if (current_w > 10 && abs(target_w - current_w) > 10) {
float l_ratio = target_w / current_w;
float min_c = 0.5f, max_c = 1.1f;
if (bg_color == "green_truck" || bg_color == "green_car" || bg_color == "yellow") { min_c = 0.4f; max_c = 1.0f; }
else if (bg_color == "black" || bg_color == "black_shi") { min_c = 0.3f; max_c = 1.2f; }
l_ratio = std::clamp(l_ratio, min_c, max_c);
Mat alpha;
matched_gray.convertTo(alpha, CV_32F, 1.0/128.0);
alpha = max(0.3f, min(1.0f, alpha));
Mat matched_f;
matched.convertTo(matched_f, CV_32F);
for (int c = 0; c < 3; c++) {
Mat ch;
extractChannel(matched_f, ch, c);
ch = ch.mul(1.0f + (l_ratio - 1.0f) * alpha);
ch.setTo(0, ch < 0); ch.setTo(255, ch > 255);
insertChannel(ch, matched_f, c);
}
matched_f.convertTo(matched, CV_8U);
}
// 8. Degradation
matched = degrade_plate(matched);
// 9-11. Warp + blend + noise
auto [warped, mask] = warp_plate_back(matched, meta.corners, Size(w_img, h_img));
Mat blended = feather_blend(img_orig, warped, mask);
Mat final = match_noise(blended, img_orig, mask);
// 12. Save with original Chinese plate number (imwrite_unicode supports UTF-8)
string stem = fp.stem().string();
string out_name = stem + "__" + cfg.plate_type + "__" + plate_number + ".jpg";
string ccpd_subdir = fp.parent_path().filename().string();
fs::path out_dir = fs::path(cfg.output_dir) / ccpd_subdir / cfg.plate_type;
fs::create_directories(out_dir);
return imwrite_unicode((out_dir / out_name).string(), final, {IMWRITE_JPEG_QUALITY, 75});
}
} // namespace ice