diff --git a/VisualRobot/DIP.cpp b/VisualRobot/DIP.cpp index bd9c445f2d7daa7046811ee38c5a85f5024985cc..eea937767567782d136d65d22dfdb14a1e54b79c 100644 --- a/VisualRobot/DIP.cpp +++ b/VisualRobot/DIP.cpp @@ -446,6 +446,67 @@ int DetectRectangleOpenCV(const string& imgPath, vector& Row, vector& pts) +{ + // 基本保护 + if (pts.size() < 10) + { + // 点太少时退化为 minAreaRect,避免崩 + return minAreaRect(pts); + } + + // 1) PCA 输入矩阵:N x 2 + Mat data((int)pts.size(), 2, CV_64F); + for (int i = 0; i < (int)pts.size(); ++i) + { + data.at(i, 0) = pts[i].x; + data.at(i, 1) = pts[i].y; + } + + // 2) PCA:取第一主轴 + PCA pca(data, Mat(), PCA::DATA_AS_ROW); + Point2d mean(pca.mean.at(0, 0), pca.mean.at(0, 1)); + + Vec2d v(pca.eigenvectors.at(0, 0), pca.eigenvectors.at(0, 1)); + double theta = std::atan2(v[1], v[0]); // rad + + // 3) 旋转到主轴坐标系并求包围盒 min/max + double ca = std::cos(-theta), sa = std::sin(-theta); + double minx = 1e18, maxx = -1e18, miny = 1e18, maxy = -1e18; + + for (const auto& p : pts) + { + double x = p.x - mean.x; + double y = p.y - mean.y; + double xr = ca * x - sa * y; + double yr = sa * x + ca * y; + + minx = std::min(minx, xr); + maxx = std::max(maxx, xr); + miny = std::min(miny, yr); + maxy = std::max(maxy, yr); + } + + // 4) 主轴坐标系下中心和尺寸 + Point2d cR((minx + maxx) * 0.5, (miny + maxy) * 0.5); + Size2d sz(maxx - minx, maxy - miny); + + // 5) 中心旋回原坐标系 + double ca2 = std::cos(theta), sa2 = std::sin(theta); + Point2d cW( + ca2 * cR.x - sa2 * cR.y + mean.x, + sa2 * cR.x + ca2 * cR.y + mean.y + ); + + // 6) 输出 RotatedRect(角度用度) + float angleDeg = (float)(theta * 180.0 / CV_PI); + return RotatedRect(Point2f((float)cW.x, (float)cW.y), Size2f((float)sz.width, (float)sz.height), angleDeg); +} /** * @brief 处理图像并计算单个物体的尺寸 @@ -462,130 +523,159 @@ int DetectRectangleOpenCV(const string& imgPath, vector& Row, vector> contours; // 轮廓列表 - vector hierarchy; // 轮廓层级 - vector> preservedContours; // 保留的轮廓(面积大于阈值) - vector> filteredContours; // 过滤的轮廓(面积小于阈值) - int thickness; // 绘制线宽 - RotatedRect rotatedRect; // 旋转矩形 - Size2f rotatedSize; // 旋转矩形尺寸 - float spring_length, spring_width, angle; // 物体尺寸和角度 - Point2f vertices[4]; // 矩形顶点 - int thickBorder; // 边框厚度 - - // 检查输入图像是否为空 + Result result; if (input.empty()) { - cerr << "图像读取失败" << endl; - return result; + return; } - // 检查通道数,如果需要则转换为灰度图 - if (input.channels() > 1) - { - cvtColor(input, source, COLOR_BGR2GRAY); - } - else + // 0) 灰度 + cv::Mat gray; + if (input.channels() == 3) { - source = input; + cv::cvtColor(input, gray, cv::COLOR_BGR2GRAY); } - - // 多阶段滤波:双边滤波 + 高斯模糊 - if (params.blurK >= 3) + else { - // 确保滤波核大小为奇数 - k = (params.blurK % 2 == 0) ? params.blurK - 1 : params.blurK; - if (k >= 3) - { - Mat dst; - bilateralFilter(source, dst, 5, 30, 2); // 双边滤波,保留边缘同时去除噪声 - GaussianBlur(dst, source, Size(k, k), 2.0, 2.0); // 高斯模糊,进一步平滑图像 - } + gray = input.clone(); } - // 预计算形态学操作核 - kernel = getStructuringElement(MORPH_RECT, Size(3, 3)); + // 1) 轻微降噪 + cv::GaussianBlur(gray, gray, cv::Size(5,5), 0); - // 阈值处理:将图像转换为二值图 - binary = source > params.thresh; - binary = 255 - binary; // 反转二值图,使目标变为白色 + // 2) OTSU 阈值 + cv::Mat binary; + cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU); - // 形态学操作:膨胀,连接断裂的边缘 - morphologyEx(binary, binary, MORPH_DILATE, kernel); + int imgW = gray.cols; + int imgH = gray.rows; - // 查找轮廓 - findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); + cv::Mat binaryMerged; + // 半径 r 可以根据图像分辨率来设,也可以放到 Params 里配置 + int mergeRadius = (params.mergeRadius > 0) ? params.mergeRadius : std::max(imgW, imgH) / 150; // 大概 5~15 像素 - // 按面积过滤轮廓 - for (const auto& contour : contours) - { - if (contourArea(contour) < params.areaMin) + mergeRadius = std::max(1, mergeRadius); + cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(2*mergeRadius+1, 2*mergeRadius+1)); + + // 闭运算 = 膨胀 + 腐蚀,更能填补物体内部的小空洞 + cv::morphologyEx(binary, binaryMerged, cv::MORPH_CLOSE, kernel, cv::Point(-1,-1), 1); + + // 3) 在 binaryMerged 上做连通域 + cv::Mat labels, stats, centroids; + int n = cv::connectedComponentsWithStats(binaryMerged, labels, stats, centroids, 8, CV_32S); + + // 4) 过滤太小的连通域 + struct Obj { + cv::RotatedRect rr; + int area; + }; + std::vector objects; + + int objAreaMin = (params.areaMin > 0) ? params.areaMin : 5000; + + for (int i = 1; i < n; ++i) + { // 0 是背景 + int area = stats.at(i, cv::CC_STAT_AREA); + if (area < objAreaMin) { - filteredContours.push_back(contour); // 面积小于阈值,过滤掉 - } - else + continue; + } + + int x = stats.at(i, cv::CC_STAT_LEFT); + int y = stats.at(i, cv::CC_STAT_TOP); + int w = stats.at(i, cv::CC_STAT_WIDTH); + int h = stats.at(i, cv::CC_STAT_HEIGHT); + + std::vector pts; + + // ⚠ 这里用的是“binaryMerged 的 label + 原始 binary 作为掩码” + for (int yy = y; yy < y + h; ++yy) { - preservedContours.push_back(contour); // 面积大于阈值,保留 + const int* rowL = labels.ptr(yy); + const uchar* rowB = binary.ptr(yy); // 原始二值 + for (int xx = x; xx < x + w; ++xx) + { + if (rowL[xx] == i && rowB[xx] > 0) + { + pts.emplace_back(xx, yy); + } + } } - } - // 查找最大面积的轮廓 - auto max_it = max_element(preservedContours.begin(), preservedContours.end(), - [](const vector& a, const vector& b) { - return contourArea(a) < contourArea(b); - }); + if (pts.size() < 80) + { + continue; + } - // 基于图像宽度计算线宽 - thickness = round(source.cols * 0.002); + cv::RotatedRect rr = GetOBBByPCA(pts); + objects.push_back({ rr, area }); + } - // 转换为BGR用于彩色绘图 - cvtColor(source, colorImage, COLOR_GRAY2BGR); + // ========= 后面画框、算长宽角度========= + cv::Mat color; + cv::cvtColor(gray, color, cv::COLOR_GRAY2BGR); + int thickness = std::max(2, (int)std::round(imgW * 0.002)); - if (max_it != preservedContours.end()) + if (objects.empty()) { - // 获取最小外接矩形 - rotatedRect = minAreaRect(*max_it); - rotatedSize = rotatedRect.size; + std::cerr << "没有满足面积要求的物体\n"; + result.widths.push_back(0); + result.heights.push_back(0); + result.angles.push_back(0); + result.image = color; + return result; + } - // 计算物体尺寸:长度为矩形的长边,宽度为短边 - spring_length = max(rotatedSize.width, rotatedSize.height); - spring_width = min(rotatedSize.width, rotatedSize.height); - result.widths.push_back(spring_width*bias); - result.heights.push_back(spring_length*bias); + // 按 x 排序 + std::sort(objects.begin(), objects.end(), + [](const Obj& a, const Obj& b){ + return a.rr.center.x < b.rr.center.x; + }); - // 用绿色绘制边界框 - rotatedRect.points(vertices); - thickBorder = static_cast(thickness * 1); + int idx = 1; + for (const auto& obj : objects) + { + cv::RotatedRect rr = obj.rr; - for (int i = 0; i < 4; i++) + cv::Point2f v[4]; + rr.points(v); + for (int k = 0; k < 4; ++k) { - line(colorImage, vertices[i], vertices[(i+1)%4], Scalar(0, 255, 0), thickBorder); + cv::line(color, v[k], v[(k+1)%4], cv::Scalar(0,255,0), thickness); } - // 输出测量结果 - cout << "物件长度: " << spring_length*bias << ", 物件宽度: " << spring_width*bias << endl; - angle = rotatedRect.angle; - cout << "检测角度: " << angle << "°" << endl; + float L = std::max(rr.size.width, rr.size.height); + float S = std::min(rr.size.width, rr.size.height); + float angle = rr.angle; + if (rr.size.width < rr.size.height) + { + angle += 90.0f; + } + + result.heights.push_back(L * bias); + result.widths.push_back(S * bias); result.angles.push_back(angle); - } - else - { - // 未找到有效轮廓,返回默认值 - result.widths.push_back(0); - result.heights.push_back(0); - result.angles.push_back(0); - cerr << "未找到有效轮廓" << endl; + + // 编号 + std::string label = std::to_string(idx++); + double fontScale = std::max(1.6, imgW / 700.0); + int fontFace = cv::FONT_HERSHEY_SIMPLEX; + int fontThick = std::max(2, thickness + 1); + int base = 0; + cv::Size ts = cv::getTextSize(label, fontFace, fontScale, fontThick, &base); + cv::Point c((int)rr.center.x, (int)rr.center.y); + cv::Point org(c.x - ts.width/2, c.y + ts.height/2); + + cv::Rect bg(org.x - 8, org.y - ts.height - 8, ts.width + 16, ts.height + 16); + bg &= cv::Rect(0,0,color.cols,color.rows); + cv::rectangle(color, bg, cv::Scalar(255,255,255), cv::FILLED); + cv::rectangle(color, bg, cv::Scalar(0,0,0), 1); + cv::putText(color, label, org, fontFace, fontScale, cv::Scalar(0,255,0), fontThick, cv::LINE_AA); } - // 设置结果图像 - result.image = colorImage; + result.image = color; return result; } diff --git a/VisualRobot/DIP.h b/VisualRobot/DIP.h index b5e56d83773f10eb7e8ecd8f79dcf44964d3285e..dc492eb4cc534a13659bcf4df6962d74adf1cf60 100644 --- a/VisualRobot/DIP.h +++ b/VisualRobot/DIP.h @@ -28,7 +28,8 @@ struct Params int thresh = 127; ///< 二值化阈值,默认127 int maxval = 255; ///< 二值化最大值,默认255 int blurK = 5; ///< 模糊核大小,默认5 - double areaMin = 100.0; ///< 最小轮廓面积,默认100.0 + double areaMin = 8000.0; ///< 最小轮廓面积,默认8000.0 + int mergeRadius = 8; ///< 轮廓合并半径,默认8 }; /** @@ -93,4 +94,11 @@ Result CalculateLength(const Mat& input, const Params& params, double bias); */ Result CalculateLengthMultiTarget(const Mat& input, const Params& params, double bias); +/** + * @brief 使用PCA主轴方向计算点集的有向包围盒(OBB),用于替代minAreaRect函数以获得更符合“主轴/最长方向”的旋转框 + * @param pts 输入点集 (通常是某个连通域/轮廓的像素点) + * @return PCA主轴对齐的旋转矩形 + */ +RotatedRect GetOBBByPCA(const vector& pts); + #endif // DIP_H diff --git a/VisualRobot/mainwindow.cpp b/VisualRobot/mainwindow.cpp index 98f13f0583a703e3fcf8cc0a2623b3552afc77b5..307f30ba9c7e2dd8e6177e29bf026088cf27438b 100644 --- a/VisualRobot/mainwindow.cpp +++ b/VisualRobot/mainwindow.cpp @@ -86,6 +86,14 @@ MainWindow::MainWindow(QWidget *parent) : // 启动定时器,每2秒自动枚举一次设备 m_deviceEnumTimer->start(2000); + // 初始化显示定时器,用于控制图像显示频率 + m_displayTimer = new QTimer(this); + connect(m_displayTimer, &QTimer::timeout, this, &MainWindow::updateDisplay); + connect(this, &MainWindow::newFrameAvailable, this, &MainWindow::updateDisplay, Qt::QueuedConnection); + + // 初始化实时检测线程指针 + m_realTimeDetectionThread = nullptr; + // 初始化系统监控 m_sysMonitor = new SystemMonitor(this); @@ -291,20 +299,7 @@ void __stdcall MainWindow::ImageCallBack(unsigned char * pData, MV_FRAME_OUT_INF MainWindow* pMainWindow = static_cast(pUser); - // 1. 显示图像(统一处理,避免重复显示) - MV_DISPLAY_FRAME_INFO disp{}; - disp.hWnd = pMainWindow->m_hWnd; - disp.pData = pData; - disp.nDataLen = pFrameInfo->nFrameLen; - disp.nWidth = pFrameInfo->nWidth; - disp.nHeight = pFrameInfo->nHeight; - disp.enPixelType = pFrameInfo->enPixelType; - if (pMainWindow->m_pcMyCamera) - { - pMainWindow->m_pcMyCamera->DisplayOneFrame(&disp); - } - - // 2. 缓存最新一帧(使用锁保护,避免竞争) + // 1. 缓存最新一帧(使用锁保护,避免竞争) { lock_guard lk(pMainWindow->m_frameMtx); pMainWindow->m_lastFrame.resize(pFrameInfo->nFrameLen); @@ -313,6 +308,9 @@ void __stdcall MainWindow::ImageCallBack(unsigned char * pData, MV_FRAME_OUT_INF pMainWindow->m_hasFrame = true; } + // 2. 发出信号通知主线程有新帧可用 + emit pMainWindow->newFrameAvailable(); + // 3. 异步计算清晰度(避免阻塞回调,使用临时引用而非拷贝) // 注意:为简化,这里仍使用同步计算,但移除了不必要的tempFrame拷贝 // 实际优化中应移到独立线程 @@ -556,22 +554,27 @@ void MainWindow::on_bnOpen_clicked() } } - // 优化SDK配置参数 - // 设置图像缓冲区数量(提高稳定性) - m_pcMyCamera->SetIntValue("GvspImageNodeCount", 10); // 图像缓冲区数量 - m_pcMyCamera->SetIntValue("GvspImageNodeBufferSize", 1024 * 1024 * 10); // 每个缓冲区10MB + // 优化SDK配置参数(针对OrangePi5平台优化) + // 设置更大的图像缓冲区数量和大小,提高稳定性(ARM平台可能需要更多缓冲) + m_pcMyCamera->SetIntValue("GvspImageNodeCount", 20); // 增加到20个图像缓冲区 + m_pcMyCamera->SetIntValue("GvspImageNodeBufferSize", 1024 * 1024 * 20); // 每个缓冲区20MB - // 设置传输缓冲区大小 - m_pcMyCamera->SetIntValue("GvspStreamBufferCount", 5); // 传输缓冲区数量 - m_pcMyCamera->SetIntValue("GvspStreamBufferSize", 1024 * 1024 * 5); // 每个缓冲区5MB + // 设置更大的传输缓冲区大小,减少丢帧 + m_pcMyCamera->SetIntValue("GvspStreamBufferCount", 10); // 增加到10个传输缓冲区 + m_pcMyCamera->SetIntValue("GvspStreamBufferSize", 1024 * 1024 * 10); // 每个缓冲区10MB - // 设置显示相关参数 - m_pcMyCamera->SetIntValue("GvspStreamDisplayMode", 1); // 1=自动显示,0=手动显示 + // 设置显示相关参数:手动显示模式,确保主线程控制 + m_pcMyCamera->SetIntValue("GvspStreamDisplayMode", 0); // 0=手动显示 // 设置采集模式和触发模式 m_pcMyCamera->SetEnumValue("AcquisitionMode", MV_ACQ_MODE_CONTINUOUS); m_pcMyCamera->SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF); + // 平台特定优化:设置更高的线程优先级或超时(如果SDK支持) + // 示例:设置采集超时为更长值,防止ARM平台超时崩溃 + m_pcMyCamera->SetIntValue("GevSCPD", 9000); // 数据包延迟9000ms + m_pcMyCamera->SetIntValue("GevSCDA", 1); // 丢包重传启用 + // 获取当前参数 on_bnGetParam_clicked(); // ch:获取参数 | en:Get Parameter @@ -672,6 +675,11 @@ void MainWindow::on_bnStart_clicked() AppendLog("开始抓图成功", INFO); m_bGrabbing = true; + // 启动显示定时器,控制显示频率(可选,30FPS) + if (m_displayTimer) { + m_displayTimer->start(33); // 约30FPS + } + ui->bnStart->setEnabled(false); ui->bnStop->setEnabled(true); ui->pushButton->setEnabled(true); @@ -726,6 +734,15 @@ void MainWindow::on_bnStop_clicked() m_bGrabbing = false; m_hasFrame = false; // 清空帧缓存标志 + // 停止显示定时器 + if (m_displayTimer) { + m_displayTimer->stop(); + } + + // 停止所有实时检测线程和定时器 + StopYoloRealTimeDetection(); + StopRealTimeDetection(); + ui->bnStart->setEnabled(true); ui->bnStop->setEnabled(false); ui->bnTriggerExec->setEnabled(false); @@ -1771,6 +1788,26 @@ double MainWindow::CalculateTenengradSharpness(const Mat& image) return meanValue; } +void MainWindow::updateDisplay() +{ + if (!m_hasFrame || m_lastFrame.empty()) return; + + MV_DISPLAY_FRAME_INFO disp{}; + { + lock_guard lk(m_frameMtx); + disp.hWnd = m_hWnd; + disp.pData = m_lastFrame.data(); + disp.nDataLen = m_lastInfo.nFrameLen; + disp.nWidth = m_lastInfo.nWidth; + disp.nHeight = m_lastInfo.nHeight; + disp.enPixelType = m_lastInfo.enPixelType; + } + + if (m_pcMyCamera) { + m_pcMyCamera->DisplayOneFrame(&disp); + } +} + // 更新清晰度显示 void MainWindow::updateSharpnessDisplay(double sharpness) { diff --git a/VisualRobot/mainwindow.h b/VisualRobot/mainwindow.h index 2fe53bf78ac9476881d7c5265c3166209ab14bf8..dfab88bd0e223d78a3fa14419a9945732fe9742e 100644 --- a/VisualRobot/mainwindow.h +++ b/VisualRobot/mainwindow.h @@ -35,6 +35,7 @@ class MainWindow : public QMainWindow signals: void sharpnessValueUpdated(double sharpness); + void newFrameAvailable(); // 新帧可用信号,用于主线程显示 public: explicit MainWindow(QWidget *parent = 0); @@ -102,6 +103,7 @@ private slots: void updateSystemStats(float cpuUsage, float memUsage, float temperature); void on_btnOpenManual_clicked(); void updateSharpnessDisplay(double sharpness); + void updateDisplay(); // 主线程图像显示槽 void on_setTemplate_clicked(); // 可选:从当前帧设模板(或弹框选择文件) void on_detect_clicked(); // 你要的检测按钮 @@ -174,8 +176,10 @@ private: // 实时检测相关 bool m_realTimeDetectionRunning; QMutex m_realTimeDetectionMutex; + QThread* m_realTimeDetectionThread; // 实时检测线程指针 void RealTimeDetectionThread(); void StartRealTimeDetection(); + void StopRealTimeDetection(); // 新增停止函数 void HandleQKeyPress(); // 处理Q键, 退出实时检测模式 // 日志优化相关 @@ -216,6 +220,9 @@ private: QMutex m_bufferMutex; // 缓冲区互斥锁 QWaitCondition m_bufferReady; // 缓冲区就绪条件变量 + // 显示定时器 + QTimer* m_displayTimer; // 显示更新定时器 + // YOLO统计信息 int m_yoloFrameCount; // 帧计数 QTime m_yoloStatsStartTime; // 统计开始时间