#include "plate_swap.hpp" #include #include #include #include #include #include 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 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 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& 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& 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 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 dist(0, v.size()-1); return v[dist(rng)]; } } throw runtime_error("PlatePool is empty"); } uniform_int_distribution 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 data((istreambuf_iterator(f)), istreambuf_iterator()); return imdecode(data, IMREAD_COLOR); } bool imwrite_unicode(const string& path, const Mat& img, const vector& params) { vector 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(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(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(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 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 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 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{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 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(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