1089 lines
43 KiB
C++
1089 lines
43 KiB
C++
#include "PaperRecognizer.h"
|
||
#include "GlobalParams.h"
|
||
#include <cmath>
|
||
#include <algorithm>
|
||
#include <iostream>
|
||
|
||
// 日志宏(移植到桌面/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<int>(bigBlur) > static_cast<int>(smallBlur) ? bigBlur : smallBlur;
|
||
result_.blurScore = std::min(bigScore, smallScore); // 取较低的分数
|
||
|
||
PR_LOG_INFO("[清晰度评估] 大定位块方差=%.2f, 小定位块方差=%.2f, 等级=%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("[杂物检测] 候选定位块数量(%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<int>(centers_.size());
|
||
result_.colorBlockCenters = centers_;
|
||
|
||
PR_LOG_INFO("[成功] 识别完成! 联数=%d, 规格=%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("检测到轮廓数量: %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("[面积过滤] 大定位块面积 %.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<std::vector<LocatorInfo*>> yGroups = groupCandidatesByY(candidates);
|
||
|
||
// 对每一组,尝试找到有效的定位块配对
|
||
std::vector<LocatorPair> 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<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("[定位块检测] 检测到 %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<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("[配对检测] 组内候选数量不足: %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<int>(big->color), static_cast<int>(small->color));
|
||
|
||
// 验证颜色类型一致性(必须相同且非UNKNOWN)
|
||
// 这是第一道防线:防止键盘按键等杂物被误识别为定位块
|
||
if (big->color == LocatorColor::UNKNOWN || small->color == LocatorColor::UNKNOWN) {
|
||
PR_LOG_WARN("[配对检测] 失败: 定位块颜色未知 (大=%d, 小=%d)",
|
||
static_cast<int>(big->color), static_cast<int>(small->color));
|
||
continue;
|
||
}
|
||
if (big->color != small->color) {
|
||
PR_LOG_WARN("[配对检测] 失败: 颜色类型不一致 (大=%d, 小=%d)",
|
||
static_cast<int>(big->color), static_cast<int>(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<int>(locator1_.color), static_cast<int>(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<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("[规格匹配] 试纸高度无效: %.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<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("[色块计算] 小定位块在%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_[%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; // 严重模糊
|
||
}
|
||
}
|