OpenCV4X学习-轮廓检测、水印、凸缺陷检测、数字水印

数药片

通过图像二值化、轮廓检测等方法来识别药片个数。假设药片在图像中与背景有明显的颜色或灰度差异,且相互之间有一定的间隔。

bash 复制代码
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char** argv) {
    // 读取图像
    Mat image = imread("1.png");
    if (image.empty()) {
        cout << "Could not open or find the image" << endl;
        return -1;
    }

    // 转换为灰度图像
    Mat gray;
    cvtColor(image, gray, COLOR_BGR2GRAY);

    // 图像平滑处理,减少噪声影响
    GaussianBlur(gray, gray, Size(5, 5), 0);

    // 二值化图像
    Mat binary;
    threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);

    // 寻找轮廓
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);

    // 过滤掉小的轮廓,只保留较大的(假设药片轮廓相对较大)
    vector<vector<Point>> validContours;
    for (size_t i = 0; i < contours.size(); i++) {
        if (contourArea(contours[i]) > 100) {
            validContours.push_back(contours[i]);
        }
    }

    // 绘制轮廓
    Mat drawing = Mat::zeros(binary.size(), CV_8UC3);
    for (size_t i = 0; i < validContours.size(); i++) {
        Scalar color = Scalar(0, 255, 0);
        drawContours(drawing, validContours, (int)i, color, 2, LINE_8, hierarchy, 0);
    }

    // 输出药片个数
    cout << "药片个数: " << validContours.size() << endl;

    // 显示结果
    imshow("Original Image", image);
    imshow("Binary Image", binary);
    imshow("Contours", drawing);
    waitKey(0);

    return 0;
}

读取图像:使用imread函数读取包含药片的图像。如果图像读取失败,输出错误信息并退出程序。

灰度转换:将彩色图像转换为灰度图像,简化后续处理。

图像平滑:通过GaussianBlur函数对灰度图像进行高斯平滑处理,减少噪声对后续处理的影响。

二值化:利用threshold函数结合THRESH_BINARY_INV + THRESH_OTSU方法自动确定阈值并进行二值化操作,使药片在图像中呈现为白色,背景为黑色。

轮廓检测:使用findContours函数查找图像中的轮廓。RETR_TREE表示轮廓检索模式为树形结构,CHAIN_APPROX_SIMPLE表示轮廓近似方法为保留端点。

轮廓过滤:遍历所有轮廓,通过contourArea函数计算轮廓面积,只保留面积大于 100 的轮廓,以过滤掉小的噪声轮廓。

绘制轮廓:创建一个黑色背景的图像drawing,使用drawContours函数在该图像上绘制经过过滤的有效轮廓。

输出与显示:输出检测到的药片个数,并使用imshow函数显示原始图像、二值化图像和绘制轮廓后的图像,waitKey(0)等待按键输入,保持图像窗口显示。

添加水印

bash 复制代码
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char** argv) {
    // 读取图像
    Mat image = imread("1.png");
    if (image.empty()) {
        cout << "Could not open or find the image" << endl;
        return -1;
    }

    // 转换为灰度图像
    Mat gray;
    cvtColor(image, gray, COLOR_BGR2GRAY);

    // 图像平滑处理,减少噪声影响
    GaussianBlur(gray, gray, Size(5, 5), 0);

    // 二值化图像
    Mat binary;
    threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);

    // 寻找轮廓
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);

    // 过滤掉小的轮廓,只保留较大的(假设药片轮廓相对较大)
    vector<vector<Point>> validContours;
    for (size_t i = 0; i < contours.size(); i++) {
        if (contourArea(contours[i]) > 100) {
            validContours.push_back(contours[i]);
        }
    }

    // 在原图上绘制轮廓并添加编号
    for (size_t i = 0; i < validContours.size(); i++) {
        Scalar color = Scalar(0, 255, 0);
        drawContours(image, validContours, (int)i, color, 2, LINE_8, hierarchy, 0);

        // 计算轮廓中心
        Moments m = moments(validContours[i]);
        if (m.m00 != 0) {
            int cX = static_cast<int>(m.m10 / m.m00);
            int cY = static_cast<int>(m.m01 / m.m00);

            // 添加编号
            string label = to_string(i + 1);
            putText(image, label, Point(cX, cY), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 255, 255), 2);
        }
    }

    // 输出药片个数
    cout << "药片个数: " << validContours.size() << endl;

    // 显示结果
    imshow("Original Image with Labels", image);
    imshow("Binary Image", binary);
    waitKey(0);

    return 0;
}

读取图像:数据结构)。如果图像读取失败,image.empty() 函数会返回 true,此时程序输出错误信息并返回 -1 表示程序异常结束。

转换为灰度图像:cvtColor 函数将彩色图像 image(通常是 BGR 格式)转换为灰度图像 gray。颜色转换代码 COLOR_BGR2GRAY 表示将 BGR 格式转换为灰度格式。灰度图像在后续处理中计算量较小,并且对于一些基于灰度信息的图像处理算法更为适用。

图像平滑处理:

GaussianBlur 函数对灰度图像 gray 进行高斯模糊处理。高斯模糊通过对图像中的每个像素点与高斯核进行卷积运算,来平滑图像,减少噪声影响。Size(5, 5) 表示高斯核的大小为 5x5,最后一个参数 0 表示根据高斯核大小自动计算标准差。

二值化图像:

threshold 函数对灰度图像 gray 进行二值化处理,将其转换为二值图像 binary。0 和 255 分别表示二值化的下限和上限。THRESH_BINARY_INV + THRESH_OTSU 是两个标志的组合:

THRESH_OTSU 表示使用 Otsu 算法自动确定阈值。Otsu 算法通过分析图像的灰度直方图,找到一个阈值,使得前景和背景之间的类间方差最大。

THRESH_BINARY_INV 表示二值化的方式为反转,即大于阈值的像素设置为 0(黑色),小于阈值的像素设置为 255(白色)。

寻找轮廓:

findContours 函数用于在二值图像 binary 中查找轮廓。contours 是一个 vector,用于存储找到的所有轮廓,每个轮廓是一个 Point 类型的 vector,表示轮廓上的点。hierarchy 是一个 Vec4i 类型的 vector,用于存储轮廓之间的层次关系。RETR_TREE 表示轮廓检索模式为树形结构,它会返回所有的轮廓,并建立轮廓之间的父子关系。CHAIN_APPROX_SIMPLE 表示轮廓近似方法,它会压缩水平、垂直和对角方向的轮廓,只保留端点,从而减少轮廓点的数量。

过滤掉小的轮廓:

这段代码遍历所有找到的轮廓 contours,通过 contourArea 函数计算每个轮廓的面积。如果轮廓面积大于 100,则认为它是一个有效的药片轮廓,将其添加到 validContours 中。这一步是为了过滤掉一些可能由噪声或小的干扰物体产生的小轮廓。

查找和绘制轮廓,并计算面积


bash 复制代码
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char** argv) {
    // 读取图像
    Mat image = imread("2.png");
    if (image.empty()) {
        cout << "Could not open or find the image" << endl;
        return -1;
    }

    // 转换为灰度图像
    Mat gray;
    cvtColor(image, gray, COLOR_BGR2GRAY);

    // 图像平滑处理,减少噪声影响
    GaussianBlur(gray, gray, Size(5, 5), 0);

    // 二值化图像
    Mat binary;
    threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);

    // 寻找轮廓
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);

    // 创建一个用于绘制轮廓的图像
    Mat drawing = Mat::zeros(image.size(), CV_8UC3);

    // 遍历所有轮廓,绘制轮廓并计算面积
    for (size_t i = 0; i < contours.size(); i++) {
        // 绘制轮廓
        Scalar color = Scalar(0, 255, 0);
        drawContours(drawing, contours, (int)i, color, 2, LINE_8, hierarchy, 0);

        // 计算轮廓面积
        double area = contourArea(contours[i]);
        cout << "Contour " << i << " area: " << area << endl;
    }

    // 显示结果
    imshow("Original Image", image);
    imshow("Binary Image", binary);
    imshow("Contours", drawing);
    waitKey(0);

    return 0;
}

读取图像:

Mat image = imread("2.png"); 使用 OpenCV 的 imread 函数从指定路径读取图像,并将其存储为 Mat 对象(OpenCV 中用于表示图像的数据结构)。这里尝试读取路径为 2.png 的图像。

if (image.empty()) {... } 检查图像是否成功读取。如果 image.empty() 返回 true,表示图像读取失败,程序会在控制台输出 Could not open or find the image,然后返回 -1 以表示程序异常结束。

转换为灰度图像:

Mat gray; 声明一个 Mat 对象 gray,用于存储灰度图像。

cvtColor(image, gray, COLOR_BGR2GRAY); 使用 cvtColor 函数将彩色图像 image(通常是 BGR 格式)转换为灰度图像 gray。COLOR_BGR2GRAY 是颜色转换代码,指定将 BGR 格式转换为灰度格式。灰度图像在后续处理中计算量相对较小,并且许多基于灰度信息的图像处理算法更适用于灰度图像。

图像平滑处理:

GaussianBlur(gray, gray, Size(5, 5), 0); 使用 GaussianBlur 函数对灰度图像 gray 进行高斯模糊处理。高斯模糊通过对图像中的每个像素点与高斯核进行卷积运算,来平滑图像,减少噪声影响。Size(5, 5) 表示高斯核的大小为 5x5,最后一个参数 0 表示根据高斯核大小自动计算标准差。经过平滑处理后,图像中的噪声点会被模糊,使得后续的轮廓查找更加准确。

二值化图像:

Mat binary; 声明一个 Mat 对象 binary,用于存储二值化后的图像。

threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU); 使用 threshold 函数对灰度图像 gray 进行二值化处理,将其转换为二值图像 binary。0 和 255 分别表示二值化的下限和上限。THRESH_BINARY_INV + THRESH_OTSU 是两个标志的组合:

THRESH_OTSU 表示使用 Otsu 算法自动确定阈值。Otsu 算法通过分析图像的灰度直方图,找到一个阈值,使得前景和背景之间的类间方差最大。

THRESH_BINARY_INV 表示二值化的方式为反转,即大于阈值的像素设置为 0(黑色),小于阈值的像素设置为 255(白色)。通过二值化,图像中的物体(前景)和背景被清晰地分离,便于后续的轮廓查找。

寻找轮廓:

vector<vector> contours; 声明一个二维 vector contours,用于存储找到的所有轮廓。每个轮廓是一个 Point 类型的 vector,其中 Point 表示轮廓上的点。

vector hierarchy; 声明一个 Vec4i 类型的 vector hierarchy,用于存储轮廓之间的层次关系。Vec4i 是一个包含 4 个整数的向量,分别表示当前轮廓的后一个轮廓、前一个轮廓、子轮廓和父轮廓的索引。

findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE); 使用 findContours 函数在二值图像 binary 中查找轮廓。RETR_TREE 表示轮廓检索模式为树形结构,这种模式会返回所有的轮廓,并建立轮廓之间的父子关系,适用于需要了解轮廓层次结构的场景。CHAIN_APPROX_SIMPLE 表示轮廓近似方法,它会压缩水平、垂直和对角方向的轮廓,只保留端点,从而减少轮廓点的数量,提高处理效率。

创建绘制轮廓的图像:

Mat drawing = Mat::zeros(image.size(), CV_8UC3); 创建一个与原始图像 image 大小相同的黑色图像 drawing,其数据类型为 CV_8UC3,表示 8 位无符号整数,3 通道(即彩色图像,可用于绘制彩色轮廓)。这个图像将用于绘制找到的轮廓。

遍历轮廓并绘制和计算面积:

for (size_t i = 0; i < contours.size(); i++) {... } 遍历所有找到的轮廓。

Scalar color = Scalar(0, 255, 0); 定义一个颜色 color,这里设置为绿色(在 OpenCV 中,颜色通常以 BGR 顺序表示,Scalar(0, 255, 0) 表示绿色),用于绘制轮廓。

drawContours(drawing, contours, (int)i, color, 2, LINE_8, hierarchy, 0); 使用 drawContours 函数在 drawing 图像上绘制第 i 个轮廓。参数依次为:目标图像 drawing,轮廓数组 contours,要绘制的轮廓索引 (int)i,轮廓颜色 color,轮廓线的粗细 2,线型 LINE_8(表示 8 邻域连接的线条),轮廓层次结构 hierarchy,轮廓绘制的偏移量 0(这里未使用)。

double area = contourArea(contours[i]); 使用 contourArea 函数计算第 i 个轮廓的面积,并将结果存储在 area 变量中。

cout << "Contour " << i << " area: " << area << endl; 在控制台输出第 i 个轮廓的面积。

利用凸缺陷检测来识别手势

bash 复制代码
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>

using namespace cv;
using namespace std;

// 手势类型枚举
enum GestureType {
    UNKNOWN,
    FIST,
    ONE_FINGER,
    TWO_FINGER,
    THREE_FINGER,
    FOUR_FINGER,
    FIVE_FINGER,
    OK_SIGN,
    THUMB_UP,
    PEACE_SIGN
};

class HandGestureRecognizer {
private:
    Mat image;
    Mat gray;
    Mat binary;
    vector<vector<Point>> contours;
    vector<Point> handContour;
    vector<Point> hull;
    vector<Vec4i> defects;
    vector<int> hullIndices;

    // 参数
    int blurSize = 5;
    int minContourArea = 5000;
    double defectDepthThreshold = 20.0;
    double fingerAngleThreshold = 80.0; // 角度阈值,用于筛选手指

public:
    HandGestureRecognizer() {}

    // 设置参数
    void setParameters(int blur, int minArea, double depthThresh, double angleThresh) {
        blurSize = blur;
        minContourArea = minArea;
        defectDepthThreshold = depthThresh;
        fingerAngleThreshold = angleThresh;
    }

    // 预处理图像
    bool preprocess(const Mat& input) {
        image = input.clone();

        // 转换为灰度图
        cvtColor(image, gray, COLOR_BGR2GRAY);

        // 高斯模糊
        GaussianBlur(gray, gray, Size(blurSize, blurSize), 0);

        // 自适应阈值或Otsu阈值
        // adaptiveThreshold(gray, binary, 255, ADAPTIVE_THRESH_GAUSSIAN_C,
        //                  THRESH_BINARY_INV, 11, 2);

        // Otsu阈值
        threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);

        // 形态学操作(可选)
        Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(5, 5));
        morphologyEx(binary, binary, MORPH_CLOSE, kernel);
        morphologyEx(binary, binary, MORPH_OPEN, kernel);

        return true;
    }

    // 检测手部轮廓
    bool detectHandContour() {
        contours.clear();
        vector<Vec4i> hierarchy;

        // 查找轮廓
        findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

        if (contours.empty()) {
            cout << "未检测到轮廓" << endl;
            return false;
        }

        // 找到最大轮廓(假设是手)
        int maxIdx = -1;
        double maxArea = 0;

        for (size_t i = 0; i < contours.size(); i++) {
            double area = contourArea(contours[i]);
            if (area > maxArea && area > minContourArea) {
                maxArea = area;
                maxIdx = i;
            }
        }

        if (maxIdx == -1) {
            cout << "未找到有效的手部轮廓" << endl;
            return false;
        }

        handContour = contours[maxIdx];

        // 简化轮廓(可选)
        vector<Point> approx;
        double epsilon = 0.002 * arcLength(handContour, true);
        approxPolyDP(handContour, approx, epsilon, true);
        handContour = approx;

        return true;
    }

    // 计算凸包和凸缺陷
    bool computeConvexFeatures() {
        if (handContour.size() < 10) {
            cout << "轮廓点太少,无法计算凸包" << endl;
            return false;
        }

        // 计算凸包
        hullIndices.clear();
        convexHull(handContour, hullIndices, false, false);

        // 获取凸包点
        hull.clear();
        for (int idx : hullIndices) {
            hull.push_back(handContour[idx]);
        }

        // 计算凸缺陷
        defects.clear();
        if (handContour.size() > 3 && hullIndices.size() > 3) {
            convexityDefects(handContour, hullIndices, defects);
        }

        return true;
    }

    // 计算两点间距离
    double distanceBetweenPoints(Point p1, Point p2) {
        return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
    }

    // 计算角度(余弦定理)
    double calculateAngle(Point a, Point b, Point c) {
        double ab = distanceBetweenPoints(a, b);
        double bc = distanceBetweenPoints(b, c);
        double ac = distanceBetweenPoints(a, c);

        if (ab == 0 || bc == 0) return 180.0;

        double angle = acos((ab * ab + bc * bc - ac * ac) / (2 * ab * bc));
        return angle * 180 / CV_PI;
    }

    // 识别手指
    vector<Point> detectFingers() {
        vector<Point> fingerTips;

        if (defects.empty() || hull.empty()) {
            return fingerTips;
        }

        // 找到手腕点(假设在底部)
        Point wristPoint = findWristPoint();

        // 筛选有效的凸缺陷(手指间凹陷)
        vector<Vec4i> validDefects;
        for (const Vec4i& defect : defects) {
            int startIdx = defect[0];
            int endIdx = defect[1];
            int farIdx = defect[2];
            int depth = defect[3] / 256;

            Point startPt = handContour[startIdx];
            Point endPt = handContour[endIdx];
            Point farPt = handContour[farIdx];

            // 计算角度,筛选手指
            double angle = calculateAngle(startPt, farPt, endPt);

            // 筛选条件:深度足够,角度合理
            if (depth > defectDepthThreshold && angle < fingerAngleThreshold) {
                validDefects.push_back(defect);

                // 手指尖通常是startPt或endPt中离手腕更远的点
                double dist1 = distanceBetweenPoints(startPt, wristPoint);
                double dist2 = distanceBetweenPoints(endPt, wristPoint);

                if (dist1 > dist2) {
                    fingerTips.push_back(startPt);
                } else {
                    fingerTips.push_back(endPt);
                }
            }
        }

        // 去重和排序
        // fingerTips = removeClosePoints(fingerTips, 30);
        // sortFingerTips(fingerTips);

        return fingerTips;
    }

    // 找到手腕点(简单实现)
    Point findWristPoint() {
        if (handContour.empty()) return Point(0, 0);

        // 找到轮廓的最低点作为手腕(假设手在图像底部)
        Point wrist(0, 0);
        for (const Point& p : handContour) {
            if (p.y > wrist.y) {
                wrist = p;
            }
        }

        // 或者使用凸包的最低点
        for (const Point& p : hull) {
            if (p.y > wrist.y) {
                wrist = p;
            }
        }

        return wrist;
    }

    // 移除过于接近的点
    vector<Point> removeClosePoints(const vector<Point>& points, double minDist) {
        vector<Point> result;
        vector<bool> keep(points.size(), true);

        for (size_t i = 0; i < points.size(); i++) {
            if (!keep[i]) continue;

            for (size_t j = i + 1; j < points.size(); j++) {
                if (distanceBetweenPoints(points[i], points[j]) < minDist) {
                    keep[j] = false;
                }
            }

            result.push_back(points[i]);
        }

        return result;
    }

    // 按顺时针方向排序手指尖
    void sortFingerTips(vector<Point>& fingerTips) {
        if (fingerTips.empty()) return;

        // 计算中心点
        Point center(0, 0);
        for (const Point& p : fingerTips) {
            center.x += p.x;
            center.y += p.y;
        }
        center.x /= fingerTips.size();
        center.y /= fingerTips.size();

        // 按角度排序
        sort(fingerTips.begin(), fingerTips.end(),
             [center](const Point& a, const Point& b) {
                 double angleA = atan2(a.y - center.y, a.x - center.x);
                 double angleB = atan2(b.y - center.y, b.x - center.x);
                 return angleA < angleB;
             });
    }

    // 识别手势
    GestureType recognizeGesture() {
        if (!detectHandContour()) {
            return UNKNOWN;
        }

        if (!computeConvexFeatures()) {
            return UNKNOWN;
        }

        vector<Point> fingerTips = detectFingers();
        int fingerCount = fingerTips.size();

        // 根据手指数量判断手势
        switch (fingerCount) {
        case 0:
            return FIST;
        case 1:
            return ONE_FINGER;
        case 2:
            // 检查是否是胜利手势(V字)
            if (isPeaceSign(fingerTips)) {
                return PEACE_SIGN;
            }
            return TWO_FINGER;
        case 3:
            return THREE_FINGER;
        case 4:
            return FOUR_FINGER;
        case 5:
            return FIVE_FINGER;
        default:
            return UNKNOWN;
        }
    }

    // 检查是否是胜利手势
    bool isPeaceSign(const vector<Point>& fingerTips) {
        if (fingerTips.size() != 2) return false;

        // 计算两个手指尖的角度
        Point wrist = findWristPoint();
        Point p1 = fingerTips[0];
        Point p2 = fingerTips[1];

        // 计算向量
        double angle = atan2(p2.y - p1.y, p2.x - p1.x) * 180 / CV_PI;
        angle = abs(angle);

        // 胜利手势通常手指分开较大角度
        return (angle > 30 && angle < 150);
    }

    // 获取结果图像
    Mat getResultImage() {
        Mat result = image.clone();

        if (handContour.empty()) {
            return result;
        }

        // 绘制轮廓
        drawContours(result, vector<vector<Point>>{handContour}, 0,
                     Scalar(0, 255, 0), 2);

        // 绘制凸包
        if (!hull.empty()) {
            vector<vector<Point>> hulls = {hull};
            drawContours(result, hulls, 0, Scalar(255, 0, 0), 2);
        }

        // 检测手指并绘制
        vector<Point> fingerTips = detectFingers();

        // 绘制手指尖
        for (const Point& tip : fingerTips) {
            circle(result, tip, 8, Scalar(0, 0, 255), -1);
            circle(result, tip, 10, Scalar(255, 255, 255), 2);
        }

        // 绘制凸缺陷点
        for (const Vec4i& defect : defects) {
            int startIdx = defect[0];
            int endIdx = defect[1];
            int farIdx = defect[2];
            int depth = defect[3] / 256;

            Point startPt = handContour[startIdx];
            Point endPt = handContour[endIdx];
            Point farPt = handContour[farIdx];

            if (depth > defectDepthThreshold) {
                // 绘制缺陷点
                circle(result, farPt, 6, Scalar(255, 0, 255), -1);
                line(result, startPt, farPt, Scalar(200, 200, 0), 2);
                line(result, endPt, farPt, Scalar(200, 200, 0), 2);
            }
        }

        // 绘制手腕点
        Point wrist = findWristPoint();
        circle(result, wrist, 10, Scalar(255, 255, 0), -1);

        // 添加文本信息
        GestureType gesture = recognizeGesture();
        string gestureText = getGestureText(gesture);

        putText(result, "Gesture: " + gestureText, Point(10, 30),
                FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255), 2);

        putText(result, "Fingers: " + to_string(fingerTips.size()),
                Point(10, 70), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 255, 0), 2);

        return result;
    }

    // 获取手势文本
    string getGestureText(GestureType gesture) {
        switch (gesture) {
        case FIST: return "Fist";
        case ONE_FINGER: return "One Finger";
        case TWO_FINGER: return "Two Fingers";
        case THREE_FINGER: return "Three Fingers";
        case FOUR_FINGER: return "Four Fingers";
        case FIVE_FINGER: return "Five Fingers";
        case PEACE_SIGN: return "Peace Sign";
        case OK_SIGN: return "OK Sign";
        case THUMB_UP: return "Thumb Up";
        default: return "Unknown";
        }
    }

    // 显示所有处理步骤
    void displayAllSteps() {
        if (!image.empty()) imshow("1. Original Image", image);
        if (!gray.empty()) imshow("2. Gray Image", gray);
        if (!binary.empty()) imshow("3. Binary Image", binary);

        Mat contourImg = Mat::zeros(image.size(), CV_8UC3);
        if (!handContour.empty()) {
            drawContours(contourImg, vector<vector<Point>>{handContour},
                         0, Scalar(0, 255, 0), 2);
            imshow("4. Hand Contour", contourImg);
        }

        Mat result = getResultImage();
        if (!result.empty()) {
            imshow("5. Final Result", result);
        }
    }
};

// 主函数
int main(int argc, char** argv) {
    // 读取图像
    string imagePath = "5.png"; 
    Mat image = imread(imagePath);

    if (image.empty()) {
        cout << "无法加载图像: " << imagePath << endl;
        cout << "请使用摄像头捕获..." << endl;

        // 尝试使用摄像头
        VideoCapture cap(0);
        if (!cap.isOpened()) {
            cout << "无法打开摄像头" << endl;
            return -1;
        }

        cout << "按空格键捕获图像,ESC键退出" << endl;

        HandGestureRecognizer recognizer;
        recognizer.setParameters(5, 5000, 20.0, 80.0);

        while (true) {
            Mat frame;
            cap >> frame;
            if (frame.empty()) break;

            // 预处理
            recognizer.preprocess(frame);

            // 识别手势
            GestureType gesture = recognizer.recognizeGesture();

            // 获取结果图像
            Mat result = recognizer.getResultImage();

            // 显示结果
            imshow("Hand Gesture Recognition", result);

            // 按键处理
            int key = waitKey(30);
            if (key == 32) { // 空格键
                imwrite("captured_hand.jpg", frame);
                cout << "图像已保存为 captured_hand.jpg" << endl;
            }
            if (key == 27) { // ESC键
                break;
            }
        }

        cap.release();
    } else {
        // 处理单张图像
        HandGestureRecognizer recognizer;
        recognizer.setParameters(5, 5000, 20.0, 80.0);

        // 预处理
        recognizer.preprocess(image);

        // 识别手势
        GestureType gesture = recognizer.recognizeGesture();

        // 显示所有处理步骤
        recognizer.displayAllSteps();

        // 等待按键
        waitKey(0);
    }

    destroyAllWindows();
    return 0;
}

图像预处理:

输入彩色图像 → 灰度转换 → 高斯模糊 → 二值化 → 形态学处理

关键操作

THRESH_BINARY_INV + THRESH_OTSU:反相二值化+自动阈值

形态学闭操作:填充小孔洞

形态学开操作:去除小噪点

轮廓检测:

findContours() → 筛选最大轮廓 → 轮廓简化

筛选逻辑:

只保留面积最大的轮廓(假设是手)

使用approxPolyDP简化轮廓,减少计算量

过滤面积过小的轮廓(minContourArea参数)

凸包与凸缺陷计算:

convexHull() → 获取凸包点 → convexityDefects() → 获取凸缺陷

凸缺陷是什么?

轮廓与凸包之间的凹陷区域

每个缺陷包含3个关键点:起点、终点、最深点

手指间的凹陷就是凸缺陷

手指检测算法

关键判断条件:

深度条件:缺陷深度 > defectDepthThreshold

角度条件:起点-最深点-终点的角度 < fingerAngleThreshold

手指尖确定:

比较缺陷起点和终点到手腕的距离

距离手腕更远的点作为手指尖

手腕点:轮廓或凸包的最低点(假设手在图像底部)

核心算法示意图:

bash 复制代码
原始图像
    ↓
预处理(灰度化→模糊→二值化)
    ↓
轮廓提取 → 最大轮廓筛选
    ↓
凸包计算 → 凸缺陷检测
    ↓
    ├── 深度筛选
    ├── 角度筛选
    └── 手腕距离比较
    ↓
手指尖定位 → 计数
    ↓
手势类型映射
    ↓
可视化输出

数字水印基本原理:

数字水印是一种将特定信息(如版权信息、认证信息等)嵌入到数字媒体(如图像、音频、视频等)中的技术,旨在不影响原始媒体正常使用的前提下,提供诸如版权保护、内容认证、篡改提示等功能。其基本原理是利用数字媒体数据的冗余性和人类感知系统的局限性,通过特定算法将水印信息隐藏在载体数据中。例如,在图像中,可以利用图像像素值的微小变化来嵌入水印,由于人类视觉系统对微小变化不敏感,所以不会影响图像的视觉效果。

浮现式和隐藏式数字水印:

浮现式数字水印:这种水印在正常情况下可见,通常以半透明的标志、图案或文字形式直接显示在数字媒体上。比如电视频道的台标,它始终出现在画面的某个角落,明确展示版权或频道信息。其优点是直观、易于识别,缺点是可能会对原始内容的视觉效果产生一定干扰。

隐藏式数字水印:隐藏式水印在正常情况下不可见,需要通过特定的提取算法才能检测到。它利用数字媒体数据的特性,将水印信息以不可察觉的方式嵌入。例如在图像的频域或空域中,通过微调像素值或系数来嵌入水印。优点是不影响原始内容的外观,对内容的完整性和视觉效果影响小;缺点是提取过程相对复杂,且可能受到信号处理、噪声等因素影响。

嵌入和提取过程:

嵌入过程:选择合适的嵌入位置(空域或频域等),根据水印信息和原始载体数据的特点,使用特定算法将水印信息嵌入到载体数据中。例如在空域嵌入时,可能直接修改像素值;在频域嵌入时,可能调整频率系数。

提取过程:根据嵌入算法和水印特性,对含有水印的载体数据进行分析和处理,提取出嵌入的水印信息。这可能涉及到逆运算、特征提取等操作。

bitwise_and、bitwise_or 和 bitwise_xor 函数:

bitwise_and函数:对两个数组(通常是图像的像素值)按位进行与运算。即对于两个对应位置的二进制位,只有当两者都为 1 时,结果位才为 1,否则为 0。

bitwise_or函数:对两个数组按位进行或运算。只要两个对应位置的二进制位中有一个为 1,结果位就为 1,只有当两者都为 0 时,结果位才为 0。

bitwise_xor函数:对两个数组按位进行异或运算。当两个对应位置的二进制位不同时,结果位为 1,相同时结果位为 0。

实现基于bitwise_xor的数字水印嵌入和提取的代码

bash 复制代码
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

// 嵌入水印
Mat embedWatermark(Mat coverImage, Mat watermarkImage) {
    // 确保水印图像和载体图像大小相同
    if (coverImage.size() != watermarkImage.size()) {
        resize(watermarkImage, watermarkImage, coverImage.size());
    }

    Mat watermarkedImage;
    bitwise_xor(coverImage, watermarkImage, watermarkedImage);
    return watermarkedImage;
}

// 提取水印
Mat extractWatermark(Mat watermarkedImage, Mat coverImage) {
    Mat extractedWatermark;
    bitwise_xor(watermarkedImage, coverImage, extractedWatermark);
    return extractedWatermark;
}

int main() {
    Mat coverImage = imread("D:/ljl/FPGA/QT_openCV_pro/pro_01/untitled_04/png.png", IMREAD_GRAYSCALE);
    Mat watermarkImage = imread("D:/ljl/FPGA/QT_openCV_pro/pro_01/untitled_04/1.png", IMREAD_GRAYSCALE);

    if (coverImage.empty() || watermarkImage.empty()) {
        cout << "Could not open or find the images" << endl;
        return -1;
    }

    Mat watermarkedImage = embedWatermark(coverImage, watermarkImage);
    Mat extractedWatermark = extractWatermark(watermarkedImage, coverImage);

    imshow("Cover Image", coverImage);
    imshow("Watermark Image", watermarkImage);
    imshow("Watermarked Image", watermarkedImage);
    imshow("Extracted Watermark", extractedWatermark);

    waitKey(0);
    return 0;
}

embedWatermark 函数将水印图像通过 bitwise_xor 运算嵌入到载体图像中。

extractWatermark 函数从含水印图像中提取水印,同样使用 bitwise_xor 运算。

在 main 函数中,读取载体图像和水印图像,调用嵌入和提取函数,并显示相关图像。

相关推荐
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意4 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码4 天前
嵌入式学习路线
学习
毛小茛4 天前
计算机系统概论——校验码
学习
babe小鑫4 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms4 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下4 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。4 天前
2026.2.25监控学习
学习
im_AMBER4 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J4 天前
从“Hello World“ 开始 C++
c语言·c++·学习