前言
本篇是上一篇【QT常用技术讲解】opencv实现指定分辨率打开摄像头的延伸,增加了opencv常见的物体检测及裁剪功能。
效果图

源码请查看资源,opencv的window环境搭建请看【QT入门到晋级】window opencv安装及引入qtcreator(包含两种qt编译器:MSVC和MinGW)
功能讲解
本篇只讲增加的物体检测、(拍照)裁剪功能。
物体描边
增加了一个勾选项edgeDetectionCheckBox,方便结合【拍照】功能,截取出裁边/不裁边的图片,物体描边源码如下
void MainWindow::detectAndDrawObjects(cv::Mat &frame)
{
    cv::Mat gray, blurred, diff;
    cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
    cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
    // 背景初始化
    if (background.empty()) {
        blurred.copyTo(background);
        return;
    }
    // 背景减除
    cv::absdiff(blurred, background, diff);
    cv::threshold(diff, diff, 30, 255, cv::THRESH_BINARY);//可对255进行调整
    // 形态学操作,去除噪声
    cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5));
    cv::morphologyEx(diff, diff, cv::MORPH_CLOSE, kernel);
    cv::morphologyEx(diff, diff, cv::MORPH_OPEN, kernel);
    // 边缘检测
    cv::Mat edged;
    cv::Canny(blurred, edged, 50, 150);
    // 结合运动检测和边缘检测
    cv::bitwise_and(edged, edged, edged, diff);
    // 查找轮廓
    std::vector<std::vector<cv::Point>> contours;
    cv::findContours(edged, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
    // 计算轮廓的"圆度"作为置信度指标
    std::vector<std::pair<cv::Rect, double>> detectedObjects;
    for (size_t i = 0; i < contours.size(); i++) {
        double area = cv::contourArea(contours[i]);
        if (area > 500) { // 只处理足够大的轮廓
            cv::Rect rect = cv::boundingRect(contours[i]);
            // 计算轮廓的圆度(周长^2/面积)
            double perimeter = cv::arcLength(contours[i], true);
            double circularity = (perimeter * perimeter) / (4 * CV_PI * area);
            // 圆度接近1表示更接近圆形(更可能是真实物体)
            if (circularity < 2.0) { // 只保留形状较简单的物体
                detectedObjects.push_back(std::make_pair(rect, circularity));
            }
        }
    }
    // 按面积排序,选择最大的物体
    cv::Rect currentRect(0, 0, 0, 0);
    if (!detectedObjects.empty()) {
        std::sort(detectedObjects.begin(), detectedObjects.end(),
            [](const std::pair<cv::Rect, double>& a, const std::pair<cv::Rect, double>& b) {
                return (a.first.width * a.first.height) > (b.first.width * b.first.height);
            });
        currentRect = detectedObjects[0].first;
    }
    // 添加到缓冲区
    recentDetections.push_back(currentRect);
    if (recentDetections.size() > BUFFER_SIZE) {
        recentDetections.erase(recentDetections.begin());
    }
    // 计算平均位置和大小
    int avgX = 0, avgY = 0, avgWidth = 0, avgHeight = 0;
    int validCount = 0;
    for (const auto& rect : recentDetections) {
        if (rect.width > 0 && rect.height > 0) { // 只考虑有效检测
            avgX += rect.x;
            avgY += rect.y;
            avgWidth += rect.width;
            avgHeight += rect.height;
            validCount++;
        }
    }
    // 如果有有效检测,绘制平滑后的矩形
    if (validCount > 0) {
        avgX /= validCount;
        avgY /= validCount;
        avgWidth /= validCount;
        avgHeight /= validCount;
        cv::Rect smoothedRect(avgX, avgY, avgWidth, avgHeight);
        cv::rectangle(frame, smoothedRect, cv::Scalar(0, 255, 0), contourThickness);
        // 显示轮廓面积
        std::string label = cv::format("Area: %d", avgWidth * avgHeight);
        cv::putText(frame, label, cv::Point(avgX, avgY - 10),
                   cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 2);
    }
    // 缓慢更新背景(每5帧更新一次)
    static int frameCount = 0;
    frameCount++;
    if (frameCount % 5 == 0) {
        cv::addWeighted(background, 0.95, blurred, 0.05, 0, background);
    }
}| 函数名 | 功能描述 | 参数说明 | 返回值/作用 | 
|---|---|---|---|
| cv::cvtColor() | 颜色空间转换 | frame: 输入图像gray: 输出图像cv::COLOR_BGR2GRAY: 转换类型 | 将BGR图像转换为灰度图像 | 
| cv::GaussianBlur() | 高斯模糊滤波 | gray: 输入图像blurred: 输出图像cv::Size(5, 5): 核大小0: 标准差 | 减少图像噪声和细节 | 
| cv::absdiff() | 计算绝对差值 | blurred: 输入图像1background: 输入图像2diff: 输出图像 | 计算两幅图像的绝对差异 | 
| cv::threshold() | 图像阈值化 | diff: 输入图像diff: 输出图像30: 阈值255: 最大值cv::THRESH_BINARY: 阈值类型 | 将图像二值化 | 
| cv::getStructuringElement() | 创建结构元素 | cv::MORPH_ELLIPSE: 形状cv::Size(5, 5): 大小 | 返回椭圆形的结构元素 | 
| cv::morphologyEx() | 形态学操作 | diff: 输入图像diff: 输出图像cv::MORPH_CLOSE/OPEN: 操作类型kernel: 结构元素 | 执行闭运算和开运算 | 
| cv::Canny() | Canny边缘检测 | blurred: 输入图像edged: 输出图像50: 低阈值150: 高阈值 | 检测图像中的边缘 | 
| cv::bitwise_and() | 按位与操作 | edged: 输入图像1edged: 输入图像2edged: 输出图像diff: 掩码 | 结合边缘检测和运动检测结果 | 
| cv::findContours() | 查找轮廓 | edged: 输入图像contours: 输出轮廓cv::RETR_EXTERNAL: 检索模式cv::CHAIN_APPROX_SIMPLE: 近似方法 | 查找图像中的轮廓 | 
| cv::contourArea() | 计算轮廓面积 | contours[i]: 输入轮廓 | 返回轮廓的面积 | 
| cv::boundingRect() | 计算边界矩形 | contours[i]: 输入轮廓 | 返回包含轮廓的最小矩形 | 
| cv::arcLength() | 计算轮廓周长 | contours[i]: 输入轮廓true: 轮廓是否闭合 | 返回轮廓的周长 | 
| cv::rectangle() | 绘制矩形 | frame: 目标图像smoothedRect: 矩形位置和大小cv::Scalar(0, 255, 0): 颜色contourThickness: 线宽 | 在图像上绘制矩形框 | 
| cv::putText() | 添加文本 | frame: 目标图像label: 文本内容cv::Point(avgX, avgY - 10): 位置cv::FONT_HERSHEY_SIMPLEX: 字体0.5: 字体大小cv::Scalar(0, 255, 0): 颜色2: 线宽 | 在图像上添加文本标签 | 
| cv::addWeighted() | 图像加权融合 | background: 输入图像10.95: 权重1blurred: 输入图像20.05: 权重20: 伽马值background: 输出图像 | 更新背景图像 | 
以上代码做了优化:每5帧更新一次。另外,为了图像稳定,设置了10fps更新一次。(假设图像不是一直在变化,比如高拍仪拍照的场景)
拍照裁剪
void MainWindow::on_captureBtn_clicked()
{
    if(!capture || !capture->isOpened()) return;
    cv::Mat frame;
    *capture >> frame;
    if(!frame.empty()) {//检查图像矩阵是否为空
        if(enableObjectDetection) {
            // 找到主要物体并裁剪
            cv::Rect mainObject = findMainObject(frame);//查找图像中的主要物体
            cv::Mat cropped = segmentObject(frame, mainObject);//裁剪图像中的特定区域
            saveCapturedImage(cropped);//保存图像到文件
        } else {
            // 未开启检测时保存整图
            saveCapturedImage(frame);//保存图像到文件
        }
    }
}
void MainWindow::saveCapturedImage(const cv::Mat& image)
{
    // 获取桌面路径
    QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
    QString timestr = QDateTime::currentDateTime().toString("yyyy_MM_dd_hh_mm_ss");
    QString fileName = QString("%1/%2.png").arg(desktopPath).arg(timestr);
    cv::imwrite(fileName.toStdString(), image);
    QMessageBox::information(this, "提示", "图片保存成功: " + fileName);
}enableObjectDetection是勾选项edgeDetectionCheckBox的值,通过segmentObject裁剪图像中的特定区域。
篇尾
以上是在黑色背景版下试验效果比较好。