Files
nanocode/web-opencv/cpp/PaperRecognizer.cpp
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

1094 lines
45 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "PaperRecognizer.h"
#include "GlobalParams.h"
#include <cmath>
#include <algorithm>
#include <iostream>
// 鸿蒙日志输出
#include "hilog/log.h"
#define LOG_TAG "PaperRecognizer"
#define LOG_DOMAIN 0xFF00
// 日志宏封装
#define PR_LOG_INFO(fmt, ...) OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, fmt, ##__VA_ARGS__)
#define PR_LOG_WARN(fmt, ...) OH_LOG_Print(LOG_APP, LOG_WARN, LOG_DOMAIN, LOG_TAG, fmt, ##__VA_ARGS__)
#define PR_LOG_ERROR(fmt, ...) OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, fmt, ##__VA_ARGS__)
#define PR_LOG_DEBUG(fmt, ...) OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, fmt, ##__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("图像尺寸: %{public}d x %{public}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=%{public}d", result_.errorCode);
return result_;
}
// Step 3: 验证定位块配对
if (!validateLocatorPair()) {
PR_LOG_ERROR("[失败] 定位块配对验证失败, errorCode=%{public}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<int>(bigBlur) > static_cast<int>(smallBlur) ? bigBlur : smallBlur;
result_.blurScore = std::min(bigScore, smallScore); // 取较低的分数
PR_LOG_INFO("[清晰度评估] 大定位块方差=%.2f, 小定位块方差=%.2f, 等级=%{public}d",
bigScore, smallScore, static_cast<int>(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("[杂物检测] 候选定位块数量(%{public}zu)超过最大阈值(%{public}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=%{public}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<int>(centers_.size());
result_.colorBlockCenters = centers_;
PR_LOG_INFO("[成功] 识别完成! 联数=%{public}d, 规格=%{public}d",
result_.itemCount, static_cast<int>(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<cv::Point2f> 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<std::vector<cv::Point>> contours;
findCandidateContours(contours);
PR_LOG_INFO("检测到轮廓数量: %{public}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<std::vector<cv::Point>>& 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<cv::Vec4i> 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<std::vector<cv::Point>>& contours) {
std::vector<LocatorInfo> 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("[面积过滤] 大定位块面积 %{public}.0f 超出范围 [%{public}.0f, %{public}.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("[面积过滤] 小定位块面积 %{public}.0f 超出范围 [%{public}.0f, %{public}.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("[定位块检测] 候选数量不足: %{public}zu < 2", candidates.size());
return false;
}
candidateCount_ = candidates.size(); // 记录候选数量
PR_LOG_INFO("候选定位块数量: %{public}zu", candidates.size());
// ========== 新增:处理多条试纸的情况 ==========
// 按Y坐标聚类,将定位块分组到不同的"行"(不同的试纸)
std::vector<std::vector<LocatorInfo*>> yGroups = groupCandidatesByY(candidates);
// 对每一组,尝试找到有效的定位块配对
std::vector<LocatorPair> validPairs;
for (size_t i = 0; i < yGroups.size(); i++) {
PR_LOG_INFO("[分组配对] 尝试第 %{public}zu 组,共 %{public}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<LocatorInfo*> 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("[定位块检测] 检测到 %{public}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("[定位块检测成功] 大定位块颜色=%{public}d, 小定位块颜色=%{public}d",
static_cast<int>(bigLocator_->color), static_cast<int>(smallLocator_->color));
return true;
}
std::vector<std::vector<LocatorInfo*>> PaperRecognizer::groupCandidatesByY(std::vector<LocatorInfo>& candidates) {
std::vector<std::vector<LocatorInfo*>> groups;
if (candidates.empty()) {
return groups;
}
// 先按Y坐标排序
std::vector<LocatorInfo*> 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<LocatorInfo*> 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<LocatorInfo*>& group) {
LocatorPair result;
result.isValid = false;
result.big = nullptr;
result.small = nullptr;
if (group.size() < 2) {
PR_LOG_WARN("[配对检测] 组内候选数量不足: %{public}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("[配对检测] 开始遍历 %{public}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("[配对检测] 跳过: 两个都是%{public}s定位块",
left->isBig ? "大" : "小");
continue;
}
LocatorInfo* big = left->isBig ? left : right;
LocatorInfo* small = left->isBig ? right : left;
PR_LOG_INFO("[配对检测] 尝试配对: 大定位块(面积=%{public}.0f) + 小定位块(面积=%{public}.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("[配对检测] 颜色判断: 大定位块=%{public}d, 小定位块=%{public}d",
static_cast<int>(big->color), static_cast<int>(small->color));
// 验证颜色类型一致性(必须相同且非UNKNOWN)
// 这是第一道防线:防止键盘按键等杂物被误识别为定位块
if (big->color == LocatorColor::UNKNOWN || small->color == LocatorColor::UNKNOWN) {
PR_LOG_WARN("[配对检测] 失败: 定位块颜色未知 (大=%{public}d, 小=%{public}d)",
static_cast<int>(big->color), static_cast<int>(small->color));
continue;
}
if (big->color != small->color) {
PR_LOG_WARN("[配对检测] 失败: 颜色类型不一致 (大=%{public}d, 小=%{public}d)",
static_cast<int>(big->color), static_cast<int>(small->color));
continue;
}
// 验证颜色一致性(RGB距离)
double colorDist = calculateColorDistance(big->avgColor, small->avgColor);
PR_LOG_INFO("[配对检测] 颜色距离: %{public}.2f (阈值: %{public}.1f)",
colorDist, GlobalParams::COLOR_DISTANCE_THRESHOLD);
if (colorDist > GlobalParams::COLOR_DISTANCE_THRESHOLD) {
PR_LOG_WARN("[配对检测] 失败: 颜色距离过大 %{public}.2f > %{public}.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("[配对检测] 面积比: %{public}.3f, 误差: %{public}.2f%% (阈值: %{public}.2f%%)",
areaRatio, areaError * 100, GlobalParams::AREA_ERROR_RATIO * 100);
if (areaError > GlobalParams::AREA_ERROR_RATIO) {
PR_LOG_WARN("[配对检测] 失败: 面积比误差过大 %{public}.2f%% > %{public}.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坐标差距: %{public}.2fpx, 平均高度: %{public}.2fpx",
yDiff, avgHeight);
// 放宽Y坐标容差到4倍平均高度(处理倾斜试纸)
if (yDiff > avgHeight * 4.0) { // Y差距超过4倍平均高度,可能不在同一试纸
PR_LOG_WARN("[配对检测] 失败: Y坐标差距过大 %{public}.2f > %{public}.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("[配对验证] 颜色类型不一致: %{public}d vs %{public}d",
static_cast<int>(locator1_.color), static_cast<int>(locator2_.color));
return false;
}
// 验证颜色一致性(RGB距离)
double colorDist = calculateColorDistance(locator1_.avgColor, locator2_.avgColor);
PR_LOG_INFO("定位块颜色距离: %{public}.2f (阈值: %{public}.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("[配对验证] 颜色不一致: 距离=%{public}.2f > 阈值=%{public}.1f",
colorDist, GlobalParams::COLOR_DISTANCE_THRESHOLD);
return false;
}
// 设置结果颜色
result_.locatorColor = locator1_.color;
PR_LOG_INFO("[配对验证成功] 定位块颜色: %{public}d", static_cast<int>(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("[规格匹配] 试纸高度无效: %{public}.2f", paperHeight);
return false;
}
// 计算距离比
double distRatio = dist / paperHeight;
// 调试输出 (hilog)
PR_LOG_INFO("--- 规格匹配参数 ---");
PR_LOG_INFO("定位块中心距离: %{public}.2f px", dist);
PR_LOG_INFO("试纸高度(大定位块短边): %{public}.2f px", paperHeight);
PR_LOG_INFO("距离比(distRatio): %{public}.3f", distRatio);
PR_LOG_INFO("大定位块: 宽=%{public}.1f, 高=%{public}.1f, 面积=%{public}.0f",
bigLocator_->width, bigLocator_->height, bigLocator_->area);
PR_LOG_INFO("小定位块: 宽=%{public}.1f, 高=%{public}.1f, 面积=%{public}.0f",
smallLocator_->width, smallLocator_->height, smallLocator_->area);
// 计算面积比
double areaRatio = bigLocator_->area / smallLocator_->area;
PR_LOG_INFO("面积比(areaRatio): %{public}.3f", areaRatio);
// 确定大定位块类型
BigLocatorType locatorType = determineBigLocatorType(areaRatio);
PR_LOG_INFO("大定位块类型: %{public}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=%{public}.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("[规格匹配成功] 类型=%{public}d, 联数=%{public}d",
static_cast<int>(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("[色块计算] 小定位块在%{public}s, 方向向量: (%.2f, %.2f)",
result_.smallLocatorOnLeft ? "左边" : "右边", unitX, unitY);
// 小定位块的边缘中心 = 靠近大定位块那边的边缘
// 无论小定位块在左还是右,都是:小定位块中心 + (半宽度 * 方向向量)
smallEdgeCenter.x = smallLocator_->center.x + static_cast<float>(smallLocatorWidth * 0.5 * unitX);
smallEdgeCenter.y = smallLocator_->center.y + static_cast<float>(smallLocatorWidth * 0.5 * unitY);
// 大定位块的边缘中心 = 靠近小定位块那边的边缘
// 无论大定位块在左还是右,都是:大定位块中心 - (半宽度 * 方向向量)
bigEdgeCenter.x = bigLocator_->center.x - static_cast<float>(bigLocatorWidth * 0.5 * unitX);
bigEdgeCenter.y = bigLocator_->center.y - static_cast<float>(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<float>(offset * unitX);
center.y = smallEdgeCenter.y + static_cast<float>(offset * unitY);
centers_.push_back(center);
PR_LOG_INFO("[色块计算] centers_[%{public}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<int>(centers_[i].x),
static_cast<int>(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<int>(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; // 严重模糊
}
}