#include "PaperRecognizer.h" #include "GlobalParams.h" #include #include #include // 日志宏(移植到桌面/Web环境) #define PR_LOG_INFO(fmt, ...) fprintf(stderr, "[INFO] " fmt "\n", ##__VA_ARGS__) #define PR_LOG_ERROR(fmt, ...) fprintf(stderr, "[ERROR] " fmt "\n", ##__VA_ARGS__) #define PR_LOG_WARN(fmt, ...) fprintf(stderr, "[WARN] " fmt "\n", ##__VA_ARGS__) #define PR_LOG_DEBUG(fmt, ...) fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__) PaperRecognizer::PaperRecognizer() : hasLocator1_(false), hasLocator2_(false), bigLocator_(nullptr), smallLocator_(nullptr), config_(nullptr), candidateCount_(0) { } PaperRecognizer::~PaperRecognizer() { } RecognitionResult PaperRecognizer::recognize(const cv::Mat& image) { result_ = RecognitionResult(); PR_LOG_INFO("========== 开始识别 =========="); PR_LOG_INFO("图像尺寸: %d x %d", image.cols, image.rows); if (image.empty()) { result_.errorCode = 7; result_.errorMessage = "Input image is empty"; PR_LOG_ERROR("[错误] 输入图像为空"); return result_; } // Step 1: 预处理 preprocessImage(image); // Step 2: 检测定位块 if (!detectLocators()) { PR_LOG_ERROR("[失败] 定位块检测失败, errorCode=%d", result_.errorCode); return result_; } // Step 3: 验证定位块配对 if (!validateLocatorPair()) { PR_LOG_ERROR("[失败] 定位块配对验证失败, errorCode=%d", result_.errorCode); return result_; } // Step 3.5: 评估图像清晰度 double bigScore = 0, smallScore = 0; BlurLevel bigBlur = assessLocatorBlur(grayImage_, bigLocator_->rotatedRect, bigScore); BlurLevel smallBlur = assessLocatorBlur(grayImage_, smallLocator_->rotatedRect, smallScore); // 取两个定位块中较差的清晰度等级 result_.blurLevel = static_cast(bigBlur) > static_cast(smallBlur) ? bigBlur : smallBlur; result_.blurScore = std::min(bigScore, smallScore); // 取较低的分数 PR_LOG_INFO("[清晰度评估] 大定位块方差=%.2f, 小定位块方差=%.2f, 等级=%d", bigScore, smallScore, static_cast(result_.blurLevel)); // 如果严重模糊,可以选择返回警告(但仍然继续识别) if (result_.blurLevel == BlurLevel::BLUR_SEVERE) { PR_LOG_WARN("[警告] 图像严重模糊,颜色识别可能不准确"); // 继续识别,但标记模糊状态 } // 杂物检测:试纸尺寸和候选数量检查 double paperHeight = bigLocator_->height; // 大定位块短边(试纸高度) // 严格过滤:试纸高度太小或候选太多,认为是杂物 const double MIN_PAPER_HEIGHT = 30.0; // 最小试纸高度30像素 const size_t MAX_CANDIDATE_COUNT = 15; // 最大候选数量15个 if (paperHeight < MIN_PAPER_HEIGHT) { PR_LOG_ERROR("[杂物检测] 试纸高度(%.1fpx)小于最小阈值(%.1fpx),可能是杂物", paperHeight, MIN_PAPER_HEIGHT); result_.errorCode = 9; result_.errorMessage = "Paper too small, possible debris"; return result_; } if (candidateCount_ > MAX_CANDIDATE_COUNT) { PR_LOG_ERROR("[杂物检测] 候选定位块数量(%zu)超过最大阈值(%zu),场景杂乱", candidateCount_, MAX_CANDIDATE_COUNT); result_.errorCode = 10; result_.errorMessage = "Too many candidates, scene too cluttered"; return result_; } // Step 4: 匹配试纸规格 if (!matchPaperSpec()) { PR_LOG_ERROR("[失败] 试纸规格匹配失败, errorCode=%d", result_.errorCode); return result_; } // Step 5: 计算色块中心点 calculateColorBlockCenters(); // Step 6: 绘制标注 drawAnnotations(); // Step 7: 保存试纸图片(NAPI用) savePaperImg(); result_.success = true; result_.errorCode = 0; result_.itemCount = static_cast(centers_.size()); result_.colorBlockCenters = centers_; PR_LOG_INFO("[成功] 识别完成! 联数=%d, 规格=%d", result_.itemCount, static_cast(result_.paperType)); PR_LOG_INFO("========== 识别结束 ==========\n"); return result_; } cv::Mat PaperRecognizer::getPaperImg() const { return paperImg_.clone(); } void PaperRecognizer::savePaperImg() { if (!hasLocator1_ || !hasLocator2_) { return; } // 获取两个定位块的旋转矩形顶点 cv::Point2f pts1[4]; cv::Point2f pts2[4]; locator1_.rotatedRect.points(pts1); locator2_.rotatedRect.points(pts2); // 收集所有顶点 std::vector points; for (int i = 0; i < 4; i++) { points.push_back(pts1[i]); points.push_back(pts2[i]); } // 计算最小外包矩形 cv::Rect maxRect = cv::boundingRect(points); // 边界检查 if (maxRect.x < 1) maxRect.x = 1; if (maxRect.y < 1) maxRect.y = 1; if ((maxRect.width + maxRect.x) >= originalImage_.cols) { maxRect.width = originalImage_.cols - maxRect.x; } if ((maxRect.height + maxRect.y) >= originalImage_.rows) { maxRect.height = originalImage_.rows - maxRect.y; } // 裁剪试纸区域 paperImg_.release(); if ((maxRect.width + maxRect.x) <= originalImage_.cols && (maxRect.height + maxRect.y) <= originalImage_.rows) { cv::Mat(originalImage_, maxRect).copyTo(paperImg_); } else { originalImage_.copyTo(paperImg_); } // 转换为RGBA格式(NAPI需要) if (paperImg_.channels() == 3) { cv::cvtColor(paperImg_, paperImg_, cv::COLOR_BGR2RGBA); } else if (paperImg_.channels() == 4) { cv::cvtColor(paperImg_, paperImg_, cv::COLOR_BGRA2RGBA); } } void PaperRecognizer::preprocessImage(const cv::Mat& image) { originalImage_ = image.clone(); // 转换颜色空间 if (image.channels() == 4) { cv::cvtColor(image, processImage_, cv::COLOR_BGRA2BGR); } else if (image.channels() == 3) { processImage_ = image.clone(); } else { cv::cvtColor(image, processImage_, cv::COLOR_GRAY2BGR); } // 转换到HSV和灰度 cv::cvtColor(processImage_, hsvImage_, cv::COLOR_BGR2HSV); cv::cvtColor(processImage_, grayImage_, cv::COLOR_BGR2GRAY); // 高斯滤波降噪 cv::GaussianBlur(grayImage_, grayImage_, cv::Size(5, 5), 0); } bool PaperRecognizer::detectLocators() { std::vector> contours; findCandidateContours(contours); PR_LOG_INFO("检测到轮廓数量: %zu", contours.size()); if (contours.empty()) { result_.errorCode = 1; result_.errorMessage = "No contours found"; PR_LOG_ERROR("[定位块检测] 未找到任何轮廓"); return false; } return filterLocatorCandidates(contours); } void PaperRecognizer::findCandidateContours(std::vector>& contours) { // 使用HSV颜色分割检测红、绿、蓝定位块 cv::Mat redMask1, redMask2; cv::inRange(hsvImage_, cv::Scalar(0, 70, 50), cv::Scalar(10, 255, 255), redMask1); cv::inRange(hsvImage_, cv::Scalar(160, 70, 50), cv::Scalar(180, 255, 255), redMask2); cv::Mat redMask = redMask1 | redMask2; cv::Mat greenMask; cv::inRange(hsvImage_, cv::Scalar(35, 70, 50), cv::Scalar(85, 255, 255), greenMask); // 蓝色范围:检测深蓝色(低明度),排除亮蓝绿色反应色块 cv::Mat blueMask; cv::inRange(hsvImage_, cv::Scalar(90, 50, 10), cv::Scalar(150, 255, 120), blueMask); cv::Mat colorMask = redMask | greenMask | blueMask; cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(9, 9)); cv::morphologyEx(colorMask, colorMask, cv::MORPH_CLOSE, kernel); cv::morphologyEx(colorMask, colorMask, cv::MORPH_OPEN, kernel); cv::findContours(colorMask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // cv::imwrite("../test_output/debug_colormask.jpg", colorMask); // 备用:边缘检测 if (contours.size() < 2) { cv::Mat edges; cv::Canny(grayImage_, edges, 50, 150); cv::morphologyEx(edges, edges, cv::MORPH_CLOSE, kernel); std::vector hierarchy; cv::findContours(edges, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // cv::imwrite("../test_output/debug_edges.jpg", edges); } } bool PaperRecognizer::filterLocatorCandidates(const std::vector>& contours) { std::vector candidates; double imageArea = originalImage_.rows * originalImage_.cols; double minArea = GlobalParams::MIN_CONTOUR_AREA; // 最小面积100 double maxArea = imageArea * GlobalParams::MAX_AREA_RATIO; for (const auto& contour : contours) { double area = cv::contourArea(contour); // 早期过滤1:面积过滤(抛弃特别小的轮廓) if (area < minArea || area > maxArea) { continue; } // 获取最小外接矩形 cv::RotatedRect rotatedRect = cv::minAreaRect(contour); double w = std::max(rotatedRect.size.width, rotatedRect.size.height); double h = std::min(rotatedRect.size.width, rotatedRect.size.height); double aspectRatio = w / h; // 早期过滤2:检查是否可能是定位块(包括过滤正方形) double tempRatio; if (!isPotentialLocator(rotatedRect, area, tempRatio)) { continue; } // 早期过滤3:计算矩形度,抛弃形状不对的轮廓 double rectArea = rotatedRect.size.width * rotatedRect.size.height; double rectangularity = area / rectArea; if (rectangularity < GlobalParams::MIN_RECTANGULARITY) { continue; } // 创建候选定位块信息(暂不计算颜色,延迟到最后确定候选后计算) LocatorInfo info; info.rotatedRect = rotatedRect; info.center = rotatedRect.center; info.area = area; info.width = std::max(rotatedRect.size.width, rotatedRect.size.height); info.height = std::min(rotatedRect.size.width, rotatedRect.size.height); info.color = LocatorColor::UNKNOWN; // 延迟计算 // 判断大小定位块 double ratioTolerance = 0.5; double distToBigShort = std::abs(aspectRatio - GlobalParams::BIG_LOCATOR_RATIO_SHORT); double distToBigLong = std::abs(aspectRatio - GlobalParams::BIG_LOCATOR_RATIO_LONG); double distToSmall = std::abs(aspectRatio - GlobalParams::SMALL_LOCATOR_RATIO); if (distToBigShort < distToSmall && distToBigShort < ratioTolerance) { info.isBig = true; } else if (distToBigLong < distToSmall && distToBigLong < ratioTolerance) { info.isBig = true; } else { info.isBig = false; } // 方案1&2:面积范围过滤,排除异常轮廓 if (info.isBig) { // 大定位块面积范围检查 if (info.area < GlobalParams::MIN_BIG_LOCATOR_AREA || info.area > GlobalParams::MAX_BIG_LOCATOR_AREA) { PR_LOG_DEBUG("[面积过滤] 大定位块面积 %.0f 超出范围 [%.0f, %.0f],跳过", info.area, GlobalParams::MIN_BIG_LOCATOR_AREA, GlobalParams::MAX_BIG_LOCATOR_AREA); continue; } } else { // 小定位块面积范围检查 if (info.area < GlobalParams::MIN_SMALL_LOCATOR_AREA || info.area > GlobalParams::MAX_SMALL_LOCATOR_AREA) { PR_LOG_DEBUG("[面积过滤] 小定位块面积 %.0f 超出范围 [%.0f, %.0f],跳过", info.area, GlobalParams::MIN_SMALL_LOCATOR_AREA, GlobalParams::MAX_SMALL_LOCATOR_AREA); continue; } } candidates.push_back(info); } if (candidates.size() < 2) { result_.errorCode = 1; result_.errorMessage = "Not enough locator candidates found"; PR_LOG_ERROR("[定位块检测] 候选数量不足: %zu < 2", candidates.size()); return false; } candidateCount_ = candidates.size(); // 记录候选数量 PR_LOG_INFO("候选定位块数量: %zu", candidates.size()); // ========== 新增:处理多条试纸的情况 ========== // 按Y坐标聚类,将定位块分组到不同的"行"(不同的试纸) std::vector> yGroups = groupCandidatesByY(candidates); // 对每一组,尝试找到有效的定位块配对 std::vector validPairs; for (size_t i = 0; i < yGroups.size(); i++) { PR_LOG_INFO("[分组配对] 尝试第 %zu 组,共 %zu 个候选", i + 1, yGroups[i].size()); LocatorPair pair = findValidLocatorPairInGroup(yGroups[i]); if (pair.isValid) { validPairs.push_back(pair); } } // 如果分组配对失败,尝试跨组配对(处理倾斜试纸的情况) if (validPairs.empty() && yGroups.size() > 1) { PR_LOG_WARN("[分组配对] 所有组内配对失败,尝试跨组配对(试纸可能倾斜)"); // 将所有候选合并,放宽Y坐标限制进行配对 std::vector allCandidates; for (auto& group : yGroups) { for (auto* ptr : group) { allCandidates.push_back(ptr); } } LocatorPair pair = findValidLocatorPairInGroup(allCandidates); if (pair.isValid) { validPairs.push_back(pair); PR_LOG_INFO("[跨组配对] 成功找到配对!"); } } if (validPairs.empty()) { result_.errorCode = 2; result_.errorMessage = "No valid locator pair found"; PR_LOG_ERROR("[定位块检测] 未找到有效配对 (需要一大一小,颜色一致,面积比正确)"); return false; } // 如果有多个有效配对(检测到多条试纸),返回失败避免误判 if (validPairs.size() > 1) { result_.errorCode = 6; result_.errorMessage = "Multiple test strips detected (" + std::to_string(validPairs.size()) + "), please provide image with single strip"; result_.success = false; PR_LOG_WARN("[定位块检测] 检测到 %zu 条试纸, 请单独拍摄", validPairs.size()); return false; } // 只有一对有效定位块 locator1_ = *validPairs[0].big; locator2_ = *validPairs[0].small; bigLocator_ = &locator1_; smallLocator_ = &locator2_; hasLocator1_ = true; hasLocator2_ = true; // 按位置排序(确保locator1在左,locator2在右) sortLocatorsByPosition(); PR_LOG_INFO("[定位块检测成功] 大定位块颜色=%d, 小定位块颜色=%d", static_cast(bigLocator_->color), static_cast(smallLocator_->color)); return true; } std::vector> PaperRecognizer::groupCandidatesByY(std::vector& candidates) { std::vector> groups; if (candidates.empty()) { return groups; } // 先按Y坐标排序 std::vector sortedPtrs; for (auto& c : candidates) { sortedPtrs.push_back(&c); } std::sort(sortedPtrs.begin(), sortedPtrs.end(), [](const LocatorInfo* a, const LocatorInfo* b) { return a->center.y < b->center.y; }); // 聚类:Y坐标差距小于阈值认为在同一行 // 阈值设为定位块高度的2倍(同一条试纸的定位块应该在相近的Y位置) double avgHeight = 0; for (auto* ptr : sortedPtrs) { avgHeight += ptr->height; } avgHeight /= sortedPtrs.size(); double yThreshold = avgHeight * 3.0; // 3倍高度的容差 std::vector currentGroup; currentGroup.push_back(sortedPtrs[0]); for (size_t i = 1; i < sortedPtrs.size(); i++) { double yDiff = sortedPtrs[i]->center.y - sortedPtrs[i-1]->center.y; if (yDiff < yThreshold) { // 同一组 currentGroup.push_back(sortedPtrs[i]); } else { // 新组 groups.push_back(currentGroup); currentGroup.clear(); currentGroup.push_back(sortedPtrs[i]); } } if (!currentGroup.empty()) { groups.push_back(currentGroup); } return groups; } PaperRecognizer::LocatorPair PaperRecognizer::findValidLocatorPairInGroup(std::vector& group) { LocatorPair result; result.isValid = false; result.big = nullptr; result.small = nullptr; if (group.size() < 2) { PR_LOG_WARN("[配对检测] 组内候选数量不足: %zu < 2", group.size()); return result; } // 按X坐标排序 std::sort(group.begin(), group.end(), [](const LocatorInfo* a, const LocatorInfo* b) { return a->center.x < b->center.x; }); PR_LOG_INFO("[配对检测] 开始遍历 %zu 个候选的配对组合", group.size()); // 遍历所有可能的大-小定位块配对 for (size_t i = 0; i < group.size(); i++) { for (size_t j = i + 1; j < group.size(); j++) { LocatorInfo* left = group[i]; LocatorInfo* right = group[j]; // 必须是一个大一个小的定位块 if (left->isBig == right->isBig) { PR_LOG_DEBUG("[配对检测] 跳过: 两个都是%s定位块", left->isBig ? "大" : "小"); continue; } LocatorInfo* big = left->isBig ? left : right; LocatorInfo* small = left->isBig ? right : left; PR_LOG_INFO("[配对检测] 尝试配对: 大定位块(面积=%.0f) + 小定位块(面积=%.0f)", big->area, small->area); // 计算颜色(延迟计算) if (big->color == LocatorColor::UNKNOWN) { big->avgColor = getCenterRegionColor(big->rotatedRect); big->color = determineLocatorColorByCenter(big->rotatedRect); } if (small->color == LocatorColor::UNKNOWN) { small->avgColor = getCenterRegionColor(small->rotatedRect); small->color = determineLocatorColorByCenter(small->rotatedRect); } PR_LOG_INFO("[配对检测] 颜色判断: 大定位块=%d, 小定位块=%d", static_cast(big->color), static_cast(small->color)); // 验证颜色类型一致性(必须相同且非UNKNOWN) // 这是第一道防线:防止键盘按键等杂物被误识别为定位块 if (big->color == LocatorColor::UNKNOWN || small->color == LocatorColor::UNKNOWN) { PR_LOG_WARN("[配对检测] 失败: 定位块颜色未知 (大=%d, 小=%d)", static_cast(big->color), static_cast(small->color)); continue; } if (big->color != small->color) { PR_LOG_WARN("[配对检测] 失败: 颜色类型不一致 (大=%d, 小=%d)", static_cast(big->color), static_cast(small->color)); continue; } // 验证颜色一致性(RGB距离) double colorDist = calculateColorDistance(big->avgColor, small->avgColor); PR_LOG_INFO("[配对检测] 颜色距离: %.2f (阈值: %.1f)", colorDist, GlobalParams::COLOR_DISTANCE_THRESHOLD); if (colorDist > GlobalParams::COLOR_DISTANCE_THRESHOLD) { PR_LOG_WARN("[配对检测] 失败: 颜色距离过大 %.2f > %.1f", colorDist, GlobalParams::COLOR_DISTANCE_THRESHOLD); continue; } // 验证面积比 double areaRatio = big->area / small->area; double errorShort = std::abs(areaRatio - GlobalParams::AREA_RATIO_SHORT) / GlobalParams::AREA_RATIO_SHORT; double errorLong = std::abs(areaRatio - GlobalParams::AREA_RATIO_LONG) / GlobalParams::AREA_RATIO_LONG; double areaError = std::min(errorShort, errorLong); PR_LOG_INFO("[配对检测] 面积比: %.3f, 误差: %.2f%% (阈值: %.2f%%)", areaRatio, areaError * 100, GlobalParams::AREA_ERROR_RATIO * 100); if (areaError > GlobalParams::AREA_ERROR_RATIO) { PR_LOG_WARN("[配对检测] 失败: 面积比误差过大 %.2f%% > %.2f%%", areaError * 100, GlobalParams::AREA_ERROR_RATIO * 100); continue; } // 验证Y坐标差距(同一试纸的定位块应该Y相近) // 注意:这个检查在跨组配对时会放宽 double yDiff = std::abs(big->center.y - small->center.y); double avgHeight = (big->height + small->height) / 2.0; PR_LOG_INFO("[配对检测] Y坐标差距: %.2fpx, 平均高度: %.2fpx", yDiff, avgHeight); // 放宽Y坐标容差到4倍平均高度(处理倾斜试纸) if (yDiff > avgHeight * 4.0) { // Y差距超过4倍平均高度,可能不在同一试纸 PR_LOG_WARN("[配对检测] 失败: Y坐标差距过大 %.2f > %.2f", yDiff, avgHeight * 4.0); continue; } // 找到有效配对 PR_LOG_INFO("[配对检测] 成功: 找到有效配对!"); result.isValid = true; result.big = big; result.small = small; return result; } } PR_LOG_ERROR("[配对检测] 遍历所有组合后仍未找到有效配对"); return result; } bool PaperRecognizer::validateLocatorPair() { if (!hasLocator1_ || !hasLocator2_) { result_.errorCode = 2; result_.errorMessage = "Locator pair not found"; PR_LOG_ERROR("[配对验证] 定位块未初始化"); return false; } // 验证颜色类型一致性(必须相同且非UNKNOWN) if (locator1_.color == LocatorColor::UNKNOWN || locator2_.color == LocatorColor::UNKNOWN) { result_.errorCode = 3; result_.errorMessage = "Locator color is UNKNOWN"; PR_LOG_ERROR("[配对验证] 定位块颜色未知"); return false; } if (locator1_.color != locator2_.color) { result_.errorCode = 3; result_.errorMessage = "Locator colors do not match"; PR_LOG_ERROR("[配对验证] 颜色类型不一致: %d vs %d", static_cast(locator1_.color), static_cast(locator2_.color)); return false; } // 验证颜色一致性(RGB距离) double colorDist = calculateColorDistance(locator1_.avgColor, locator2_.avgColor); PR_LOG_INFO("定位块颜色距离: %.2f (阈值: %.1f)", colorDist, GlobalParams::COLOR_DISTANCE_THRESHOLD); if (colorDist > GlobalParams::COLOR_DISTANCE_THRESHOLD) { result_.errorCode = 3; result_.errorMessage = "Locator colors do not match"; PR_LOG_ERROR("[配对验证] 颜色不一致: 距离=%.2f > 阈值=%.1f", colorDist, GlobalParams::COLOR_DISTANCE_THRESHOLD); return false; } // 设置结果颜色 result_.locatorColor = locator1_.color; PR_LOG_INFO("[配对验证成功] 定位块颜色: %d", static_cast(result_.locatorColor)); return true; } LocatorColor PaperRecognizer::determineLocatorColor(const cv::Rect& roi) { // 边界检查 cv::Rect safeRoi = roi & cv::Rect(0, 0, processImage_.cols, processImage_.rows); if (safeRoi.area() <= 0) { return LocatorColor::UNKNOWN; } cv::Mat roiMat(processImage_, safeRoi); cv::Scalar avgColor = cv::mean(roiMat); double b = avgColor[0]; double g = avgColor[1]; double r = avgColor[2]; // 调试输出 std::cout << "[COLOR DEBUG] ROI: " << safeRoi << std::endl; std::cout << "[COLOR DEBUG] RGB: R=" << r << " G=" << g << " B=" << b << std::endl; // 策略1: RGB分量比例判断(主策略) // 蓝色判断:B必须明显大于R和G(深蓝色定位块特征) // 对于蓝绿色(cyan),G≈B,不满足B明显大于G的条件 double blueMargin = 20.0; // 蓝色需要更大的领先幅度 double otherMargin = 15.0; // 其他颜色的领先幅度 std::cout << "[COLOR DEBUG] B-G=" << (b-g) << " B-R=" << (b-r) << std::endl; // 先判断蓝色:B必须明显大于G和R(区分深蓝色定位块和蓝绿色反应色块) if (b > g + blueMargin && b > r + blueMargin) { std::cout << "[COLOR DEBUG] 判断为: BLUE (RGB策略)" << std::endl; return LocatorColor::BLUE; } // 绿色判断:G必须明显大于B和R if (g > b + otherMargin && g > r + otherMargin) { std::cout << "[COLOR DEBUG] 判断为: GREEN (RGB策略)" << std::endl; return LocatorColor::GREEN; } // 红色判断:R必须明显大于G和B if (r > g + otherMargin && r > b + otherMargin) { std::cout << "[COLOR DEBUG] 判断为: RED (RGB策略)" << std::endl; return LocatorColor::RED; } // 策略2: HSV的H分量判断(对于边界情况作为补充) cv::Mat roiHsv(hsvImage_, safeRoi); cv::Scalar avgHsv = cv::mean(roiHsv); double h = avgHsv[0]; double s = avgHsv[1]; double v = avgHsv[2]; std::cout << "[COLOR DEBUG] HSV: H=" << h << " S=" << s << " V=" << v << std::endl; // 红色:H在0-25或150-180范围内(考虑首尾环绕) if ((h < 25 || h > 150) && s > 50) { std::cout << "[COLOR DEBUG] 判断为: RED (HSV策略)" << std::endl; return LocatorColor::RED; } // 绿色:H在35-90范围内(纯绿色) // 蓝绿色(cyan)H在85-100之间,饱和度较低时可能是蓝绿色反应色块 if (h > 35 && h < 85 && s > 50) { std::cout << "[COLOR DEBUG] 判断为: GREEN (HSV策略)" << std::endl; return LocatorColor::GREEN; } // 蓝色:H在95-130范围内(深蓝色) // 或者H在85-100但B分量相对较高(蓝绿色偏蓝) if ((h >= 95 && h < 130) || (h >= 85 && h < 95 && b > g)) { std::cout << "[COLOR DEBUG] 判断为: BLUE (HSV策略)" << std::endl; return LocatorColor::BLUE; } // 蓝绿色区域(H=85-100)的进一步判断 // 如果B和G接近,但B略高,倾向于蓝色(可能是浅蓝色定位块) if (h >= 85 && h < 100) { if (b >= g) { std::cout << "[COLOR DEBUG] 判断为: BLUE (蓝绿色区域)" << std::endl; return LocatorColor::BLUE; } else { std::cout << "[COLOR DEBUG] 判断为: GREEN (蓝绿色区域)" << std::endl; return LocatorColor::GREEN; } } std::cout << "[COLOR DEBUG] 判断为: UNKNOWN" << std::endl; return LocatorColor::UNKNOWN; } cv::Scalar PaperRecognizer::getCenterRegionColor(const cv::RotatedRect& rotatedRect) { // 使用旋转矩形中心的小区域计算颜色,避免背景干扰 // 取中心区域的 30% 大小 double scale = 0.3; cv::Size newSize(rotatedRect.size.width * scale, rotatedRect.size.height * scale); cv::RotatedRect centerRect(rotatedRect.center, newSize, rotatedRect.angle); // 获取中心区域的 boundingRect cv::Rect roi = centerRect.boundingRect() & cv::Rect(0, 0, processImage_.cols, processImage_.rows); if (roi.area() <= 0) { return cv::Scalar(0, 0, 0); } cv::Mat roiMat(processImage_, roi); return cv::mean(roiMat); } LocatorColor PaperRecognizer::determineLocatorColorByCenter(const cv::RotatedRect& rotatedRect) { // 使用中心区域颜色判断 cv::Scalar avgColor = getCenterRegionColor(rotatedRect); double b = avgColor[0]; double g = avgColor[1]; double r = avgColor[2]; // 调试输出 std::cout << "[CENTER COLOR] RGB: R=" << r << " G=" << g << " B=" << b << std::endl; std::cout << "[CENTER COLOR] B-G=" << (b-g) << " B-R=" << (b-r) << std::endl; // 降低蓝色判断阈值,因为中心区域颜色更纯 double blueMargin = 15.0; // 从20降到15 double otherMargin = 15.0; // 蓝色判断 if (b > g + blueMargin && b > r + blueMargin) { std::cout << "[CENTER COLOR] 判断为: BLUE" << std::endl; return LocatorColor::BLUE; } // 绿色判断 if (g > b + otherMargin && g > r + otherMargin) { std::cout << "[CENTER COLOR] 判断为: GREEN" << std::endl; return LocatorColor::GREEN; } // 红色判断 if (r > g + otherMargin && r > b + otherMargin) { std::cout << "[CENTER COLOR] 判断为: RED" << std::endl; return LocatorColor::RED; } // HSV辅助判断 cv::Mat hsvRoi; cv::Rect roi = rotatedRect.boundingRect() & cv::Rect(0, 0, hsvImage_.cols, hsvImage_.rows); if (roi.area() > 0) { // 取中心小区域 cv::Point center(roi.x + roi.width/2, roi.y + roi.height/2); int w = roi.width * 0.3; int h = roi.height * 0.3; cv::Rect centerRoi(center.x - w/2, center.y - h/2, w, h); centerRoi = centerRoi & cv::Rect(0, 0, hsvImage_.cols, hsvImage_.rows); if (centerRoi.area() > 0) { cv::Mat roiHsv(hsvImage_, centerRoi); cv::Scalar avgHsv = cv::mean(roiHsv); double hue = avgHsv[0]; double sat = avgHsv[1]; std::cout << "[CENTER COLOR] HSV: H=" << hue << " S=" << sat << std::endl; // 蓝色:H在95-130范围内 if (hue >= 95 && hue < 130 && sat > 50) { std::cout << "[CENTER COLOR] HSV判断为: BLUE" << std::endl; return LocatorColor::BLUE; } // 绿色:H在35-90范围内 if (hue > 35 && hue < 85 && sat > 50) { std::cout << "[CENTER COLOR] HSV判断为: GREEN" << std::endl; return LocatorColor::GREEN; } // 红色:H在0-25或150-180范围内 if ((hue < 25 || hue > 150) && sat > 50) { std::cout << "[CENTER COLOR] HSV判断为: RED" << std::endl; return LocatorColor::RED; } } } std::cout << "[CENTER COLOR] 判断为: UNKNOWN" << std::endl; return LocatorColor::UNKNOWN; } double PaperRecognizer::calculateColorDistance(const cv::Scalar& c1, const cv::Scalar& c2) { double db = c1[0] - c2[0]; double dg = c1[1] - c2[1]; double dr = c1[2] - c2[2]; return std::sqrt(dr * dr + dg * dg + db * db); } cv::Scalar PaperRecognizer::getAverageColor(const cv::Rect& roi) { cv::Rect safeRoi = roi & cv::Rect(0, 0, processImage_.cols, processImage_.rows); if (safeRoi.area() <= 0) { return cv::Scalar(0, 0, 0); } cv::Mat roiMat(processImage_, safeRoi); return cv::mean(roiMat); } bool PaperRecognizer::matchPaperSpec() { // 计算定位块中心距离 double dx = locator2_.center.x - locator1_.center.x; double dy = locator2_.center.y - locator1_.center.y; double dist = std::sqrt(dx * dx + dy * dy); // 计算试纸高度 // 根据老代码 ImgLocateDryChemistry 的算法: // 试纸高度 = 大定位块的短边(min(width, height)) // 这是因为试纸高度=5mm,大定位块尺寸是10mm×5mm或15mm×5mm // 短边就是5mm方向 double paperHeight = bigLocator_->height; // 大定位块的短边 if (paperHeight < 1) { result_.errorCode = 4; result_.errorMessage = "Invalid paper height"; PR_LOG_ERROR("[规格匹配] 试纸高度无效: %.2f", paperHeight); return false; } // 计算距离比 double distRatio = dist / paperHeight; // 调试输出 (hilog) PR_LOG_INFO("--- 规格匹配参数 ---"); PR_LOG_INFO("定位块中心距离: %.2f px", dist); PR_LOG_INFO("试纸高度(大定位块短边): %.2f px", paperHeight); PR_LOG_INFO("距离比(distRatio): %.3f", distRatio); PR_LOG_INFO("大定位块: 宽=%.1f, 高=%.1f, 面积=%.0f", bigLocator_->width, bigLocator_->height, bigLocator_->area); PR_LOG_INFO("小定位块: 宽=%.1f, 高=%.1f, 面积=%.0f", smallLocator_->width, smallLocator_->height, smallLocator_->area); // 计算面积比 double areaRatio = bigLocator_->area / smallLocator_->area; PR_LOG_INFO("面积比(areaRatio): %.3f", areaRatio); // 确定大定位块类型 BigLocatorType locatorType = determineBigLocatorType(areaRatio); PR_LOG_INFO("大定位块类型: %s", locatorType == BigLocatorType::BLT_SHORT ? "S型(10mm)" : "L型(15mm)"); // 匹配配置 result_.paperType = PaperConfigManager::getInstance().matchConfig( distRatio, areaRatio, locatorType); if (result_.paperType == PaperSpatialType::PST_UNKNOWN) { result_.errorCode = 4; result_.errorMessage = "Paper specification not matched"; PR_LOG_ERROR("[规格匹配失败] distRatio=%.2f 不在任何规格范围内", distRatio); PR_LOG_INFO("规格范围参考: 14S[19.5-23.5], 11S[15.9-19.2], 8S[11.6-14.1]"); PR_LOG_INFO(" 14L[20.0-24.0], 11L[16.2-19.6], 8L[12.0-14.6]"); return false; } config_ = PaperConfigManager::getInstance().getConfig(result_.paperType); PR_LOG_INFO("[规格匹配成功] 类型=%d, 联数=%d", static_cast(result_.paperType), config_ ? config_->itemsNum : 0); return config_ != nullptr; } void PaperRecognizer::calculateColorBlockCenters() { centers_.clear(); if (!config_ || !bigLocator_ || !smallLocator_) { return; } // 判断小定位块的位置(locator1在左边,locator2在右边) result_.smallLocatorOnLeft = !locator1_.isBig; // 关键修正:参考老代码ImgLocateDryChemistry的算法 // 起点和终点应该是定位块的边缘中心,而不是定位块中心 // 这样可以确保色块位置不会跑到定位块内部 // 计算定位块的宽度(长边) double bigLocatorWidth = bigLocator_->width; // 大定位块的长边 double smallLocatorWidth = smallLocator_->width; // 小定位块的长边 // 计算大定位块和小定位块的边缘中心点 // 关键:始终计算"靠近试纸主体"的边缘 cv::Point2f bigEdgeCenter, smallEdgeCenter; // 计算方向向量(从小定位块指向大定位块) double dx = bigLocator_->center.x - smallLocator_->center.x; double dy = bigLocator_->center.y - smallLocator_->center.y; double dist = std::sqrt(dx * dx + dy * dy); if (dist < 1) return; double unitX = dx / dist; double unitY = dy / dist; PR_LOG_INFO("[色块计算] 小定位块在%s, 方向向量: (%.2f, %.2f)", result_.smallLocatorOnLeft ? "左边" : "右边", unitX, unitY); // 小定位块的边缘中心 = 靠近大定位块那边的边缘 // 无论小定位块在左还是右,都是:小定位块中心 + (半宽度 * 方向向量) smallEdgeCenter.x = smallLocator_->center.x + static_cast(smallLocatorWidth * 0.5 * unitX); smallEdgeCenter.y = smallLocator_->center.y + static_cast(smallLocatorWidth * 0.5 * unitY); // 大定位块的边缘中心 = 靠近小定位块那边的边缘 // 无论大定位块在左还是右,都是:大定位块中心 - (半宽度 * 方向向量) bigEdgeCenter.x = bigLocator_->center.x - static_cast(bigLocatorWidth * 0.5 * unitX); bigEdgeCenter.y = bigLocator_->center.y - static_cast(bigLocatorWidth * 0.5 * unitY); PR_LOG_INFO("[色块计算] 小定位块边缘: (%.1f, %.1f), 大定位块边缘: (%.1f, %.1f)", smallEdgeCenter.x, smallEdgeCenter.y, bigEdgeCenter.x, bigEdgeCenter.y); // 计算有效距离(两个边缘中心之间的距离) double effectiveDx = bigEdgeCenter.x - smallEdgeCenter.x; double effectiveDy = bigEdgeCenter.y - smallEdgeCenter.y; double effectiveDist = std::sqrt(effectiveDx * effectiveDx + effectiveDy * effectiveDy); if (effectiveDist < 1) { PR_LOG_ERROR("[色块计算] 有效距离太小: %.2f", effectiveDist); return; } PR_LOG_INFO("[色块计算] 有效距离: %.2f, slicesNum: %d", effectiveDist, config_->slicesNum); // 计算基准单位 double avgDist = effectiveDist / config_->slicesNum; // 计算第一个色块位置和步长 double firstOffset = avgDist * config_->firstPos; double stepOffset = avgDist * config_->step; PR_LOG_INFO("[色块计算] firstOffset: %.2f, stepOffset: %.2f, itemsNum: %d", firstOffset, stepOffset, config_->itemsNum); // 从小定位块的边缘中心开始,沿方向向量偏移 // 色块在试纸主体区域内(小定位块和大定位块之间) // centers_[0] 始终靠近小定位块 for (int i = 0; i < config_->itemsNum; ++i) { double offset = firstOffset + stepOffset * i; cv::Point2f center; center.x = smallEdgeCenter.x + static_cast(offset * unitX); center.y = smallEdgeCenter.y + static_cast(offset * unitY); centers_.push_back(center); PR_LOG_INFO("[色块计算] centers_[%d]: (%.1f, %.1f)", i, center.x, center.y); } } void PaperRecognizer::drawAnnotations() { result_.annotatedImage = originalImage_.clone(); // 确保图像是 BGR 格式(用于绘制彩色标注) if (result_.annotatedImage.channels() == 1) { // 灰度图转 BGR cv::cvtColor(result_.annotatedImage, result_.annotatedImage, cv::COLOR_GRAY2BGR); } else if (result_.annotatedImage.channels() == 4) { // BGRA 转 BGR(注意:传入的是 BGRA,不是 RGBA) cv::cvtColor(result_.annotatedImage, result_.annotatedImage, cv::COLOR_BGRA2BGR); } // 绘制定位块 // if (hasLocator1_) { // cv::Point2f points[4]; // locator1_.rotatedRect.points(points); // for (int i = 0; i < 4; ++i) { // cv::line(result_.annotatedImage, points[i], points[(i + 1) % 4], // cv::Scalar(0, 0, 255), 2); // } // } // if (hasLocator2_) { // cv::Point2f points[4]; // locator2_.rotatedRect.points(points); // for (int i = 0; i < 4; ++i) { // cv::line(result_.annotatedImage, points[i], points[(i + 1) % 4], // cv::Scalar(0, 0, 255), 2); // } // } // 绘制色块中心点 // 编号顺序:centers_[0] 始终靠近小定位块 for (size_t i = 0; i < centers_.size(); ++i) { cv::Point center(static_cast(centers_[i].x), static_cast(centers_[i].y)); cv::circle(result_.annotatedImage, center, 6, cv::Scalar(0, 0, 255), 2); // 标注编号(与NAPI返回的顺序一致) // centers_[0] 始终靠近小定位块,无需反转 // std::string text = std::to_string(static_cast(i)); // cv::putText(result_.annotatedImage, text, // cv::Point(center.x - 10, center.y - 10), // cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 255, 0), 1); } } double PaperRecognizer::calculateAspectRatio(const cv::RotatedRect& rect) { double w = rect.size.width; double h = rect.size.height; return std::max(w, h) / std::max(std::min(w, h), 1.0); } bool PaperRecognizer::isPotentialLocator(const cv::RotatedRect& rect, double area, double& aspectRatio) { aspectRatio = calculateAspectRatio(rect); // 定位块永远不会是正方形,早期过滤正方形轮廓 // 正方形定义:宽高比在0.9-1.1之间 if (aspectRatio >= 0.9 && aspectRatio <= 1.1) { return false; } // 试纸上的轮廓最大宽高比是3.0(15mm定位块) // 考虑透视变形,放宽到4.0,过滤掉过长的轮廓(如键盘按键边缘、线条等) if (aspectRatio > 4.0) { return false; } // 检查是否在定位块的宽高比范围内 // 大定位块:宽高比约2.0(10mm)或3.0(15mm) // 小定位块:宽高比约1.667(5mm/3mm) // 收紧容差从0.5到0.35,减少误识别 double tolerance = 0.35; // 宽高比容差 bool isBigRatio = (std::abs(aspectRatio - GlobalParams::BIG_LOCATOR_RATIO_SHORT) < tolerance || std::abs(aspectRatio - GlobalParams::BIG_LOCATOR_RATIO_LONG) < tolerance); bool isSmallRatio = std::abs(aspectRatio - GlobalParams::SMALL_LOCATOR_RATIO) < tolerance; return isBigRatio || isSmallRatio; } void PaperRecognizer::sortLocatorsByPosition() { // 按X坐标排序,确保locator1在左边,locator2在右边 if (locator1_.center.x > locator2_.center.x) { std::swap(locator1_, locator2_); if (bigLocator_ == &locator1_) { bigLocator_ = &locator2_; smallLocator_ = &locator1_; } else { bigLocator_ = &locator1_; smallLocator_ = &locator2_; } } } BigLocatorType PaperRecognizer::determineBigLocatorType(double areaRatio) { // 面积比约3.33对应10mm,约5.0对应15mm double midPoint = (GlobalParams::AREA_RATIO_SHORT + GlobalParams::AREA_RATIO_LONG) / 2.0; if (areaRatio < midPoint) { return BigLocatorType::BLT_SHORT; } else { return BigLocatorType::BLT_LONG; } } BlurLevel PaperRecognizer::assessLocatorBlur(const cv::Mat& gray, const cv::RotatedRect& locator, double& score) { // 提取定位块 ROI(外扩3像素用于计算边缘梯度) double expandRatio = 0.15; // 外扩15% double w = locator.size.width; double h = locator.size.height; double expandW = w * expandRatio; double expandH = h * expandRatio; cv::Rect roi( locator.center.x - w/2 - expandW, locator.center.y - h/2 - expandH, w + 2 * expandW, h + 2 * expandH ); // 边界检查 roi = roi & cv::Rect(0, 0, gray.cols, gray.rows); if (roi.area() <= 0) { score = 0; return BlurLevel::BLUR_SEVERE; } cv::Mat roiMat(gray, roi); // Laplacian 方差法 cv::Mat laplacian; cv::Laplacian(roiMat, laplacian, CV_64F); cv::Scalar mean, stddev; cv::meanStdDev(laplacian, mean, stddev); score = stddev.val[0] * stddev.val[0]; // 方差 // 根据方差判断清晰度 // 在局部定位块区域,阈值相对固定 if (score > 300) { return BlurLevel::BLUR_NONE; // 清晰 } else if (score > 100) { return BlurLevel::BLUR_SLIGHT; // 轻微模糊 } else { return BlurLevel::BLUR_SEVERE; // 严重模糊 } }