【openCV】鼠标操作,像素类型转换与归一化

目录

一.鼠标操作与响应

1.1.鼠标双击

1.2.滚轮轮动

1.3.flag参数

[1.4.void* userdata参数的作用](#1.4.void* userdata参数的作用)

1.5.小例子

二.图像像素类型转换与归一化

2.1.归一化------cv::normalize

[2.1.1. NORM_MINMAX ------ "按比例缩放"](#2.1.1. NORM_MINMAX —— “按比例缩放”)

[2.1.2. NORM_L1 ------ "总金额 1 元"](#2.1.2. NORM_L1 —— “总金额 1 元”)

[2.1.3. NORM_L2 ------ "方向不变,长度变 1"](#2.1.3. NORM_L2 —— “方向不变,长度变 1”)

[2.1.4. NORM_INF ------ "削平最高的山头"](#2.1.4. NORM_INF —— “削平最高的山头”)

2.2.类型转换------cv::convertTo


一.鼠标操作与响应

OpenCV 的鼠标操作,简单来说就是通过 setMouseCallback 函数,为特定的显示窗口"绑定"一个回调函数。之后,你在这个窗口上的每一次鼠标点击、移动或释放,系统都会自动调用这个函数,让你的程序能"看到"并响应鼠标的动作。

⚙️ 核心机制与函数:setMouseCallback

在 OpenCV 中,鼠标交互的核心函数是 cv::setMouseCallback。它的原型如下:

复制代码
void setMouseCallback(const String& winname, MouseCallback onMouse, void* userdata = 0);

参数说明:

  • winname: **目标窗口的名称。**必须是已经通过 cv::namedWindow() 创建的窗口。
  • onMouse: **用户定义的回调函数指针。**当指定窗口发生鼠标事件时,该函数会被自动调用。
  • userdata: 用户自定义数据的指针(默认为0),用于向回调函数传递额外的数据,例如要操作的图像指针等。

回调函数 MouseCallback 有固定的格式:

复制代码
void onMouse(int event, int x, int y, int flags, void* userdata);

参数说明:

  • event: 核心参数,表示发生的具体鼠标事件。
  • x, y: 鼠标事件发生时,鼠标在窗口坐标系中的坐标(左上角为原点 (0,0))。
  • flags: 事件的附加状态,常用于判断鼠标移动时是否有其他按键(Ctrl/Shift等)被同时按下。
  • userdata: 与 setMouseCallback 中传递的 userdata 对应。

📋 事件大全:event 参数

event 参数是回调函数的灵魂,OpenCV 定义了多种事件常量,涵盖了基本操作。

事件常量 (C++中为 EVENT_*) 描述 数值常量

  • EVENT_MOUSEMOVE 鼠标移动 0
  • EVENT_LBUTTONDOWN 鼠标左键按下 1
  • EVENT_RBUTTONDOWN 鼠标右键按下 2
  • EVENT_MBUTTONDOWN 鼠标中键按下 3
  • EVENT_LBUTTONUP 鼠标左键释放 4
  • EVENT_RBUTTONUP 鼠标右键释放 5
  • EVENT_MBUTTONUP 鼠标中键释放 6
  • EVENT_LBUTTONDBLCLK 鼠标左键双击 7
  • EVENT_RBUTTONDBLCLK 鼠标右键双击 8
  • EVENT_MBUTTONDBLCLK 鼠标中键双击 9
  • EVENT_MOUSEWHEEL 鼠标滚轮垂直滚动 10

⌨️ 按键信息:flags 参数

flags 参数用于获取 event 发生时,鼠标或键盘的"额外状态"。

鼠标按钮状态:

  • EVENT_FLAG_LBUTTON (1):按下左键
  • EVENT_FLAG_RBUTTON (2):按下右键
  • EVENT_FLAG_MBUTTON (4):按下中键

功能键状态:

  • EVENT_FLAG_CTRLKEY (8):按下 Ctrl 键
  • EVENT_FLAG_SHIFTKEY (16):按下 Shift 键
  • EVENT_FLAG_ALTKEY (32):按下 Alt 键

我们直接看例子

例子一

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

void onMouse(int event, int x, int y, int flags, void* userdata) {
    // 根据 event 类型打印信息
    switch (event) {
        case cv::EVENT_MOUSEMOVE:
            std::cout << "鼠标移动: (" << x << ", " << y << ")" << std::endl;
            break;
        case cv::EVENT_LBUTTONDOWN:
            std::cout << "左键按下: (" << x << ", " << y << ")" << std::endl;
            break;
        case cv::EVENT_LBUTTONUP:
            std::cout << "左键释放: (" << x << ", " << y << ")" << std::endl;
            break;
        case cv::EVENT_RBUTTONDOWN:
            std::cout << "右键按下: (" << x << ", " << y << ")" << std::endl;
            break;
        case cv::EVENT_RBUTTONUP:
            std::cout << "右键释放: (" << x << ", " << y << ")" << std::endl;
            break;
        case cv::EVENT_MBUTTONDOWN:
            std::cout << "中键按下: (" << x << ", " << y << ")" << std::endl;
            break;
        case cv::EVENT_MBUTTONUP:
            std::cout << "中键释放: (" << x << ", " << y << ")" << std::endl;
            break;
        // 可以继续添加其他事件(如双击、滚轮等)
        default:
            break;
    }
}

int main() {
    // 创建一个简单的窗口
    cv::namedWindow("Mouse Test");
    // 绑定鼠标回调
    cv::setMouseCallback("Mouse Test", onMouse);

    // 显示一张空白图像(或者任何图像,这里用纯黑图)
    cv::Mat img = cv::Mat::zeros(400, 600, CV_8UC3);
    cv::imshow("Mouse Test", img);

    std::cout << "请在窗口中移动鼠标或点击按键,按 ESC 退出" << std::endl;
    while (true) {
        int key = cv::waitKey(10);
        if (key == 27) // ESC 键
            break;
    }
    return 0;
}

非常厉害!!

1.1.鼠标双击

然后我们看看鼠标双击的情况

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

void onMouse(int event, int x, int y, int flags, void* userdata) {
    switch (event) {
    case cv::EVENT_LBUTTONDOWN:
        std::cout << "左键按下: (" << x << ", " << y << ")" << std::endl;
        break;
    case cv::EVENT_LBUTTONUP:
        std::cout << "左键释放: (" << x << ", " << y << ")" << std::endl;
        break;
    case cv::EVENT_RBUTTONDOWN:
        std::cout << "右键按下: (" << x << ", " << y << ")" << std::endl;
        break;
    case cv::EVENT_RBUTTONUP:
        std::cout << "右键释放: (" << x << ", " << y << ")" << std::endl;
        break;
    case cv::EVENT_MBUTTONDOWN:
        std::cout << "中键按下: (" << x << ", " << y << ")" << std::endl;
        break;
    case cv::EVENT_MBUTTONUP:
        std::cout << "中键释放: (" << x << ", " << y << ")" << std::endl;
        break;
    case cv::EVENT_LBUTTONDBLCLK:
        std::cout << "左键双击: (" << x << ", " << y << ")" << std::endl;
        break;
    case cv::EVENT_RBUTTONDBLCLK:
        std::cout << "右键双击: (" << x << ", " << y << ")" << std::endl;
        break;
    case cv::EVENT_MBUTTONDBLCLK:
        std::cout << "中键双击: (" << x << ", " << y << ")" << std::endl;
        break;
    default:
        break;
    }
}

int main() {
    cv::namedWindow("Mouse Test");
    cv::setMouseCallback("Mouse Test", onMouse);

    cv::Mat img = cv::Mat::zeros(400, 600, CV_8UC3);
    cv::imshow("Mouse Test", img);

    std::cout << "请在窗口中移动鼠标或点击按键(包括双击),按 ESC 退出" << std::endl;
    while (true) {
        int key = cv::waitKey(10);
        if (key == 27) // ESC
            break;
    }
    return 0;
}

我们发现,双击出现之前,必定会触发一次单击!!!

1.2.滚轮轮动

我们看看鼠标滚轮的情况

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

void onMouse(int event, int x, int y, int flags, void* userdata) {
    if (event == cv::EVENT_MOUSEWHEEL) 
    {
        // 获取滚轮滚动量(正:向上滚动,负:向下滚动)
        int wheel_delta = cv::getMouseWheelDelta(flags);
        std::cout << "鼠标滚轮: (" << x << ", " << y << ") 滚动量 = " << wheel_delta << std::endl;
    }
}

int main() {
    cv::namedWindow("Mouse Wheel Test");
    cv::setMouseCallback("Mouse Wheel Test", onMouse);

    cv::Mat img = cv::Mat::zeros(400, 600, CV_8UC3);
    cv::imshow("Mouse Wheel Test", img);

    std::cout << "请在窗口中滚动鼠标滚轮,按 ESC 退出" << std::endl;
    while (true) {
        int key = cv::waitKey(10);
        if (key == 27) // ESC
            break;
    }
    return 0;
}

我们往前滚动,就是120,往后滚动就是-120

1.3.flag参数

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

void onMouse(int event, int x, int y, int flags, void*) {
    // 只在鼠标移动时检查 flags 状态(你也可以在其他事件中检查)
    if (event == cv::EVENT_MOUSEMOVE) {
        std::cout << "鼠标移动到 (" << x << ", " << y << ")   flags = " << flags << " -> ";

        // 检查各个标志位
        if (flags & cv::EVENT_FLAG_LBUTTON)
            std::cout << "[左键按住] ";
        if (flags & cv::EVENT_FLAG_RBUTTON)
            std::cout << "[右键按住] ";
        if (flags & cv::EVENT_FLAG_MBUTTON)
            std::cout << "[中键按住] ";
        if (flags & cv::EVENT_FLAG_CTRLKEY)
            std::cout << "[Ctrl键] ";
        if (flags & cv::EVENT_FLAG_SHIFTKEY)
            std::cout << "[Shift键] ";
        if (flags & cv::EVENT_FLAG_ALTKEY)
            std::cout << "[Alt键] ";
        
        std::cout << std::endl;
    }
}

int main() {
    cv::namedWindow("Flags Test");
    cv::setMouseCallback("Flags Test", onMouse);

    cv::Mat img = cv::Mat::zeros(400, 600, CV_8UC3);
    cv::imshow("Flags Test", img);

    std::cout << "请移动鼠标,可以同时按住鼠标按钮或 Ctrl/Shift/Alt 键" << std::endl;
    std::cout << "按 ESC 退出" << std::endl;

    while (true) {
        if (cv::waitKey(10) == 27) break;
    }
    return 0;
}

非常的厉害!!

1.4.void* userdata参数的作用

void* userdata 是一个可以向回调函数传递任意自定义数据的指针。你可以把任何类型的数据(例如 Mat 图像指针、整数、结构体等)通过它传入,这样回调函数就不再依赖全局变量,更加灵活和模块化。

下面是一个最简单的例子:通过 userdata 把一张图像传给鼠标回调函数,在鼠标左键点击时,在图像上画一个红色圆点。

这个参数非常厉害的!!!

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

// 鼠标回调函数
void onMouse(int event, int x, int y, int flags, void* userdata) {
    // 将 userdata 转换回 cv::Mat 指针
    cv::Mat* img = (cv::Mat*)userdata;

    if (event == cv::EVENT_LBUTTONDOWN) {
        // 在传入的图像上画一个蓝色小圆点
        cv::circle(*img, cv::Point(x, y), 3, cv::Scalar(0, 0, 255), -1);
        cv::imshow("Window", *img);
        std::cout << "点击坐标 (" << x << ", " << y << "),已画圆" << std::endl;
    }
}

int main() {
    // 创建一张黑色图像
    cv::Mat img = cv::Mat::zeros(400, 600, CV_8UC3);

    cv::namedWindow("Window");
    // 把图像指针作为 userdata 传入
    cv::setMouseCallback("Window", onMouse, &img);

    cv::imshow("Window", img);
    std::cout << "在窗口内点击鼠标左键,会画圆。按 ESC 退出。" << std::endl;

    while (true) {
        int key = cv::waitKey(10);
        if (key == 27) break;
    }
    return 0;
}

1.5.小例子

下面是一个完整的例子:鼠标左键按下时确定圆心,拖动鼠标时实时显示一个半径不断变化的圆(圆心到鼠标当前点的距离),松开左键后这个圆就固定在图像上。

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

cv::Mat original;   // 原始图像(已固定的内容)
cv::Mat temp;       // 临时图像(用于绘制橡皮筋圆)
bool drawing = false;
cv::Point center;

void onMouse(int event, int x, int y, int flags, void*) {
    if (event == cv::EVENT_LBUTTONDOWN) {
        // 左键按下:开始绘制,记录圆心
        drawing = true;
        center = cv::Point(x, y);
    }
    else if (event == cv::EVENT_MOUSEMOVE && drawing) {
        // 鼠标移动且正在绘制中:实时显示圆
        int radius = (int)cv::norm(cv::Point(x, y) - center); // 圆心到当前点的距离

        // 复制原始图像到临时图像
        original.copyTo(temp);//原图是啥也没有的,这里就相当于将tmp图像归零

        // 在临时图像上绘制圆(绿色,线宽2)
        cv::circle(temp, center, radius, cv::Scalar(0, 255, 0), 2);

        // 显示临时图像
        cv::imshow("Draw Circle", temp);
    }
    else if (event == cv::EVENT_LBUTTONUP && drawing) {
        // 左键释放:固定圆到原始图像
        int radius = (int)cv::norm(cv::Point(x, y) - center);

        // 在原始图像上绘制圆(绿色,线宽2)
        cv::circle(original, center, radius, cv::Scalar(0, 255, 0), 2);

        // 更新显示
        cv::imshow("Draw Circle", original);

        // 绘制结束
        drawing = false;
    }
}

int main() {
    // 创建一张白色背景的图像(或读取一张图片)
    original = cv::Mat::zeros(480, 640, CV_8UC3);
    original.setTo(cv::Scalar(255, 255, 255)); // 白色背景

    cv::namedWindow("Draw Circle");
    cv::setMouseCallback("Draw Circle", onMouse);

    cv::imshow("Draw Circle", original);
    std::cout << "操作说明:按住鼠标左键并拖动,实时显示圆,释放鼠标后固定圆。按ESC退出。\n";

    while (true) {
        int key = cv::waitKey(10);
        if (key == 27) // ESC
            break;
    }
    return 0;
}

这个还是非常简单的

二.图像像素类型转换与归一化

2.1.归一化------cv::normalize

归一化的本质:给数据"统一标准"

想象一下:你手里有三把尺子,一把量厘米,一把量英寸,一把量市寸。你想比较它们测出的长度,但单位不同,数值没法直接比。怎么办呢?你把它们全部换算成同一个单位,比如"米"。这样,所有数值都变成了统一的参照系,可以直接比较了。这个"换算到统一标准"的过程,就是归一化

在计算机里,图像、矩阵或任意一组数字也经常遇到这个问题:

  • 有的数字很大(比如 0 到 10000)

  • 有的数字很小(比如 0 到 1)

  • 有的数字有正有负(比如 -100 到 +100)

你想把它们放在一起处理,或者想用同一个规则去衡量它们,就需要"归一化"。

最常见的归一化:把数据压到 0, 1 区间

比如你有一组考试成绩:60分,75分,90分。你想知道每个人的相对水平,而不要绝对分数。你可以这样做:

  1. 找出最低分 60,最高分 90。

  2. 把每个分数减去最低分,再除以(最高分-最低分)。

    • 60 变成 0

    • 75 变成 0.5

    • 90 变成 1

结果就是 0、0.5、1。这组新数字都在 0 到 1 之间,而且保留了原来的大小顺序。这就是 线性归一化NORM_MINMAX)。在 OpenCV 里,你可以把一张很暗的图像(像素值 0~50)拉伸到 0~255,让图像变亮,本质上就是这种归一化。

另一种归一化:让数据"长度"变成 1

想象你把一个向量(一组数字)看作是从原点指向某个点的箭头。这个箭头有长度。如果你不想改变箭头的方向,只是想比较不同箭头的方向,就需要把所有的箭头都"拉长或缩短"到单位长度(长度 = 1)。这样,不同长度的箭头就变成了只代表方向的"单位向量"。这就是 L2 归一化NORM_L2)。它常用在机器学习里:比较两个特征向量是不是相似,先把他们归一化成单位长度,然后计算夹角。

cv::normalize 函数

opencv是借助了cv::normalize 函数来实现图像的归一化

cpp 复制代码
void cv::normalize(
    InputArray src,          // 输入数组(原图像或矩阵)
    InputOutputArray dst,    // 输出数组(可与 src 相同)
    double alpha,            // 归一化后的最小值(或 L1/L2/INF 的目标范数值)
    double beta,             // 归一化后的最大值(仅对 NORM_MINMAX 有效)
    int norm_type,           // 归一化类型(NORM_MINMAX, NORM_L1, NORM_L2, NORM_INF)
    int dtype = -1,          // 输出数组的数据类型(-1 表示与 src 相同)
    InputArray mask = noArray() // 可选掩膜
);

OpenCV 主要提供了 4 种 像素归一化方法,均通过 cv::normalize 函数的 norm_type 参数指定:

  • NORM_MINMAX
  • NORM_L1
  • NORM_L2
  • NORM_INF

2.1.1. NORM_MINMAX ------ "按比例缩放"

将像素值线性缩放到指定的范围(例如 0, 10, 255)。这是最常用的方法,用于对比度拉伸或统一不同图像的数值区间。

  • 类比:班级里考试,最高分 90 分,最低分 10 分。你想把所有成绩换算成 0 到 1 之间的"相对分",方便看出每个人的水平差距。
  • 做法:10 分变成 0,90 分变成 1,中间的分按比例变成 0.x。
  • 结果:所有数都落在 0,1 或你指定的区间内,但原来谁大谁小、差距比例不变。
  • 在图像中的用处:一张很暗的照片(所有像素值 30~80),拉伸到 0~255 后,暗部变黑,亮部变白,对比度提升。
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 1. 创建一个简单的 3x3 矩阵(模拟图像像素值)
    cv::Mat src = (cv::Mat_<float>(3, 3) << 10, 20, 30,
                                            40, 50, 60,
                                            70, 80, 90);
    std::cout << "原始矩阵:\n" << src << std::endl;

    // 2. 使用 NORM_MINMAX 将数值线性缩放到 [0, 1] 区间
    cv::Mat dst_minmax;
    cv::normalize(src, dst_minmax, 0, 1, cv::NORM_MINMAX);
    std::cout << "\nNORM_MINMAX 归一化到 [0, 1]:\n" << dst_minmax << std::endl;

    // 3. 使用 NORM_MINMAX 缩放到 [0, 255](常用于图像显示)
    cv::Mat dst_255;
    cv::normalize(src, dst_255, 0, 255, cv::NORM_MINMAX);
    std::cout << "\nNORM_MINMAX 归一化到 [0, 255]:\n" << dst_255 << std::endl;

    return 0;
}

2.1.2. NORM_L1 ------ "总金额 1 元"

将所有像素的绝对值之和 (L1 范数)**归一化为 1。**即每个像素除以全图像素绝对值总和,使得新图像的像素绝对值总和等于 1。常用于概率分布或稀疏表示。

  • 类比:你手里有一堆零钱:1 角、2 角、7 角,总共 1 元。现在你只想保留"每种零钱占总金额的比例",就把所有金额除以总金额(1 元)。
  • 结果:0.1 元、0.2 元、0.7 元,相加刚好 1 元。
  • 在图像中的用处:像素值变成"概率分布"(比如每个像素占图像总亮度的比例),常用于需要累加和为 1 的场合(如直方图匹配)。
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 创建一个简单的 3x3 矩阵(包含正数和负数,演示绝对值求和)
    cv::Mat src = (cv::Mat_<float>(3, 3) << 1, 2, 3,
        -4, 5, -6,
        7, -8, 9);
    std::cout << "原始矩阵:\n" << src << std::endl;

    // 计算原始矩阵的 L1 范数(所有元素绝对值之和)
    double l1_original = cv::norm(src, cv::NORM_L1);
    std::cout << "原始矩阵的 L1 范数(绝对值之和): " << l1_original << std::endl;

    // 使用 NORM_L1 归一化,使新的 L1 范数等于 1.0
    cv::Mat dst_l1;
    cv::normalize(src, dst_l1, 1.0, 0, cv::NORM_L1);
    std::cout << "\nNORM_L1 归一化后(目标范数 = 1.0):\n" << dst_l1 << std::endl;

    // 验证归一化后的 L1 范数是否为 1
    double l1_new = cv::norm(dst_l1, cv::NORM_L1);
    std::cout << "归一化后的 L1 范数: " << l1_new << std::endl;

    return 0;
}

这些数值的代数和(正负数直接相加)确实不等于 1,实际上大约是 0.2。但是 NORM_L1 归一化要求的是所有元素绝对值之和等于 1,而不是代数和。

让我验证一下你给出的这个矩阵的绝对值之和:

矩阵元素及其绝对值:

  • 0.022222223 → 0.022222223
  • 0.044444446 → 0.044444446
  • 0.06666667 → 0.06666667
  • -0.088888891 → 0.088888891
  • 0.11111111 → 0.11111111
  • -0.13333334 → 0.13333334
  • 0.15555556 → 0.15555556
  • -0.17777778 → 0.17777778
  • 0.2 → 0.2

现在计算绝对值之和:

  • 0.022222223 + 0.044444446 = 0.066666669
  • +0.06666667 = 0.133333339
  • +0.088888891 = 0.22222223
  • +0.11111111 = 0.33333334
  • +0.13333334 = 0.46666668
  • +0.15555556 = 0.62222224
  • +0.17777778 = 0.80000002
  • +0.2 = 1.00000002 ≈ 1

所以绝对值之和确实等于 1,这就是 NORM_L1 归一化的含义。

此外,我们在代码里面还看到了一个东西

cv::norm函数

给定一个向量(一组数字,比如 (1,2,3) 或者一张图像里的所有像素值),你可以用不同的"尺子"去量它的大小:

  1. L1 范数把每个数字的绝对值加起来。

    比如 (3, -4) 的 L1 范数 = |3| + |-4| = 7。

    这就像曼哈顿街区距离:你只能沿着街道走,不能斜穿。

  2. L2 范数每个数字平方,加起来,再开方。

    比如 (3, -4) 的 L2 范数 =

    这就是我们最熟悉的直线距离。

  3. 无穷范数只看绝对值最大的那个数字。

    比如 (3, -4, 8) 的无穷范数 = max(3,4,8) = 8。

    这就像在问:这支队伍里最壮的那个人有多壮?

cv::norm 是 OpenCV 中用于计算矩阵或向量的范数的函数。

在这里,我们只需要了解三种范数

  1. NORM_L1 L1 范数
  2. NORM_L2 L2 范数
  3. NORM_INF 无穷范数

2.1.3. NORM_L2 ------ "方向不变,长度变 1"

将所有像素的平方和的开方 (L2 范数,欧几里得范数)归一化为 1。 即每个像素除以 L2 范数,使得新图像的像素平方和等于 1。常用于特征向量归一化、机器学习预处理等。

  • 类比:你画一个箭头,从原点指向 (3,4)。这个箭头的长度是 5(勾股定理)。你只想保留箭头的"指向",但把长度缩短为 1。
  • 做法:所有坐标除以 5,得到 (0.6, 0.8),这就是一个单位向量。
  • 在图像中的用处:把整张图像看作一个高维向量,归一化后,不同的图像只看"方向"(即像素值的相对比例),忽略整体亮度的影响。常用于机器学习特征比较(比如人脸识别中,比较两个特征的余弦相似度)。
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 创建一个简单的 3x3 矩阵(可以看作一个9维向量)
    cv::Mat src = (cv::Mat_<float>(3, 3) << 3, 4, 0,
                                            0, 0, 0,
                                            0, 0, 0);
    std::cout << "原始矩阵:\n" << src << std::endl;

    // 计算原始矩阵的 L2 范数(平方和开方)
    double l2_original = cv::norm(src, cv::NORM_L2);
    std::cout << "原始矩阵的 L2 范数(欧几里得长度): " << l2_original << std::endl;

    // 使用 NORM_L2 归一化,使新的 L2 范数等于 1.0
    cv::Mat dst_l2;
    cv::normalize(src, dst_l2, 1.0, 0, cv::NORM_L2);
    std::cout << "\nNORM_L2 归一化后(目标范数 = 1.0):\n" << dst_l2 << std::endl;

    // 验证归一化后的 L2 范数是否为 1
    double l2_new = cv::norm(dst_l2, cv::NORM_L2);
    std::cout << "归一化后的 L2 范数: " << l2_new << std::endl;

    return 0;
}

2.1.4. NORM_INF ------ "削平最高的山头"

将所有像素绝对值的最大值 (无穷范数)**归一化为 1。**即每个像素除以全图像素绝对值的最大值,使得新图像的最大绝对值为 1。常用于将数值范围限制在 -1, 1 区间,防止溢出。

  • 类比:你有一组数:3, -5, 10, 2。绝对值最大的是 10。你希望所有数的绝对值不超过 1,同时保持符号和相对大小。
  • 做法:每个数都除以 10,得到 0.3, -0.5, 1, 0.2。现在最大的绝对值是 1。
  • 在图像中的用处:把像素值范围压缩到 -1, 1 之间,避免计算时溢出;或者只看"相对于最强响应的比例"。
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 创建一个包含正数和负数的矩阵
    cv::Mat src = (cv::Mat_<float>(3, 3) << 3, -5, 10,
                                            2, -8, 4,
                                            -12, 6, -1);
    std::cout << "原始矩阵:\n" << src << std::endl;

    // 计算原始矩阵的无穷范数(绝对值最大的元素)
    double inf_original = cv::norm(src, cv::NORM_INF);
    std::cout << "原始矩阵的无穷范数(最大绝对值): " << inf_original << std::endl;

    // 使用 NORM_INF 归一化,使新的无穷范数等于 1.0
    cv::Mat dst_inf;
    cv::normalize(src, dst_inf, 1.0, 0, cv::NORM_INF);
    std::cout << "\nNORM_INF 归一化后(目标范数 = 1.0):\n" << dst_inf << std::endl;

    // 验证归一化后的无穷范数是否为 1
    double inf_new = cv::norm(dst_inf, cv::NORM_INF);
    std::cout << "归一化后的无穷范数: " << inf_new << std::endl;

    return 0;
}

2.2.类型转换------cv::convertTo

它的主要作用是在不同的数值类型之间转换,比如把 CV_8U(0~255 的 uint8)转换为 CV_32F(浮点数),或者反过来。

cpp 复制代码
void cv::Mat::convertTo(
    OutputArray dst,      // 输出矩阵
    int rtype,            // 目标数据类型(如 CV_32F, CV_8U)
    double alpha = 1,     // 缩放因子(乘数)
    double beta = 0       // 偏移量(加数)
) const;

实际转换公式为:

cpp 复制代码
dst(x,y) = saturate_cast<目标类型>( src(x,y) * alpha + beta )

saturate_cast 会确保结果值落在目标数据类型的有效范围内(比如 uint8 会截断到 0~255)。

cv::convertTo 的第二个参数 rtype 指定目标矩阵的数据类型,可以是以下常见的 OpenCV 类型宏(以单通道为例,也可以扩展为多通道,如 CV_8UC3):

  • CV_8U:8 位无符号整数(0~255),最常用的图像类型。
  • CV_8S:8 位有符号整数(-128~127)。
  • CV_16U:16 位无符号整数(0~65535),常用于深度图。
  • CV_16S:16 位有符号整数(-32768~32767)。
  • CV_32S:32 位有符号整数(-2^31~2^31-1)。
  • CV_32F:32 位单精度浮点数。
  • CV_64F:64 位双精度浮点数。

我们看个简单的例子

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 创建一个 8 位无符号的矩阵(数值范围 0-255)
    cv::Mat src = (cv::Mat_<uchar>(2, 2) << 10, 20, 30, 40);
    std::cout << "原始矩阵 (uint8):\n" << src << std::endl;

    // 1. 转换为 float 类型,数值不变(alpha=1, beta=0)
    cv::Mat dst1;
    src.convertTo(dst1, CV_32F, 1.0, 0.0);
    std::cout << "转 float,数值不变:\n" << dst1 << std::endl;

    // 2. 转换为 float,同时缩放到 [0,1] 区间(除以 255)
    cv::Mat dst2;
    src.convertTo(dst2, CV_32F, 1.0/255.0, 0.0);
    std::cout << "转 float,缩放 [0,1]:\n" << dst2 << std::endl;

    // 3. 转换为 int16,同时平移(加 100)
    cv::Mat dst3;
    src.convertTo(dst3, CV_16S, 1.0, 100.0);
    std::cout << "转 int16,加 100:\n" << dst3 << std::endl;

    return 0;
}

很不错!!!

相关推荐
腾科IT教育13 小时前
Spring AI Alibaba 向量(VectorStore)
人工智能·spring·microsoft
IT_陈寒14 小时前
React中useEffect依赖项这个坑我居然踩了三天
前端·人工智能·后端
江畔柳前堤14 小时前
github实战指南02-仓库管理与 Issue
人工智能·深度学习·github·信号处理·caffe·wps·issue
邵宇然14 小时前
内存分配优化:基于 Unsafe 指针与内存对齐的 Rust 区域分配器
人工智能
海兰14 小时前
【游戏】迷雾镇(Mist Town)AI 沙箱游戏详细设计与部署指南(附源代码)
人工智能·游戏
小赖同学啊14 小时前
智能连接器集群化高可用生产方案
linux·运维·人工智能
ZStack开发者社区14 小时前
基于AI Agent的ZCF API文档全链路自动化
运维·人工智能·自动化
沈麽鬼14 小时前
别瞎用AI写代码!90%开发者都搞错了AI编程的底层逻辑
人工智能·ai编程·trae
小陈爱编程15 小时前
我终于把 Codex 的 API 配置理顺了:从踩坑到跑通
人工智能
不爱洗脚的小滕15 小时前
【Agent】如何为 AI Agent 设计高可用的 Tools
人工智能·aigc·ai编程·rag