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 函数中,读取载体图像和水印图像,调用嵌入和提取函数,并显示相关图像。

相关推荐
yang011110012 小时前
论文总结 HVI: A New Color Space for Low-light Image Enhancement
图像处理·人工智能·学习·计算机视觉
LYS_06182 小时前
寒假学习(2)(C语言2+模数电2)
c语言·学习·算法
listhi5202 小时前
压缩感知信号重构的块稀疏贝叶斯学习(BSBL)算法:原理、实现与应用
学习·算法·重构
漏刻有时2 小时前
微信小程序学习实录15:微信小程序基于百度云人脸识别的刷脸打卡开发方案
学习·微信小程序·百度云
盐焗西兰花3 小时前
鸿蒙学习实战之路-PDF转换指定页面或指定区域为图片
学习·pdf·harmonyos
鄭郑3 小时前
【Playwright学习笔记 06】用户视觉定位的方法
笔记·学习
楼田莉子3 小时前
Linux系统小项目——“主从设计模式”进程池
linux·服务器·开发语言·c++·vscode·学习
云边散步3 小时前
godot2D游戏教程系列一(9)-终结
学习·游戏·游戏开发
jrlong3 小时前
DataWhale大模型基础与量化微调task4学习笔记(第 1章:参数高效微调_LoRA 方法详解)
笔记·学习