693 lines
25 KiB
C++
693 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";
|
|
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") ? 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") { h_lo = 40; h_hi = 85; }
|
|
|
|
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 == "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
|
|
if (!cfg.fast_mode)
|
|
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 pinyin tags
|
|
string stem = fp.stem().string();
|
|
// Convert Chinese plate markers to ASCII tags
|
|
string tag = plate_number;
|
|
// Manual pinyin mapping for common plate characters (UTF-8 byte sequences)
|
|
// 港=E6B8AF 澳=E6BEB3 警=E8ADA6 使=E4BDBF 领=E9A286 学=E5ADA6
|
|
struct { const char* utf8; const char* py; } map[] = {
|
|
{"\xE6\xB8\xAF", "GANG"}, {"\xE6\xBE\xB3", "AO"},
|
|
{"\xE8\xAD\xA6", "JING"}, {"\xE4\xBD\xBF", "SHI"},
|
|
{"\xE9\xA2\x86", "LING"}, {"\xE5\xAD\xA6", "XUE"},
|
|
};
|
|
for (auto& m : map) {
|
|
size_t pos = 0;
|
|
while ((pos = tag.find(m.utf8, pos)) != string::npos) {
|
|
tag.replace(pos, 3, m.py);
|
|
pos += strlen(m.py);
|
|
}
|
|
}
|
|
// Remove any remaining non-ASCII
|
|
string clean;
|
|
for (char c : tag) {
|
|
if ((unsigned char)c < 128) clean += c;
|
|
}
|
|
string out_name = stem + "__" + cfg.plate_type + "__" + clean + ".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
|