OpenCV计算机视觉学习(15)——浅谈图像处理的饱和运算和取模运算

发布时间 2024-01-13 14:31:38作者: 战争热诚

如果需要其他图像处理的文章及代码,请移步小编的GitHub地址

  传送门:请点击我

  如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice

  本来在前面博客 OpenCV计算机视觉学习(2)——图像算术运算 &图像阈值(数值计算,掩膜mask操作,边界填充,二值化)里面已经学习了图像的数值计算,即常量加减等。但是在C++中和python使用不同的方式进行常量计算还是有一点点的区别,比如说python的numpy类型的运算符操作是取模操作,但是opencv的运算符操作却是饱和运算。当然opencv的cv2.add函数在C++和python是一致的。于是我这里将自己认为重要的点梳理一下。

1,什么是饱和运算,什么是取模运算

  饱和运算(Saturating Arithmetic)和取模运算(Modulo Operation)是两种不同的数学运算。

1.1 饱和运算(Saturating Arithmetic)

  定义:在计算机图像处理和信号处理中,饱和运算是一种处理溢出的方法。当进行某些运算(例如加法或乘法)时,结果可能会超出数据类型的表示范围,导致溢出。饱和运算就是在发生溢出时,将结果限制在数据类型的最大和最小值之间(通常是通过截断或设置上下界),而不是简单地截断或取模。

  具体来说,对于无符号数据类型,饱和运算会将溢出的结果设置为该数据类型的最大值;对于有符号数据类型,饱和运算会将溢出的结果设置为该数据类型的最大正值或最小负值,以保持在有符号范围内。

  示例: 在图像处理中,对于8位无符号整数(uchar)的像素值,其范围是0到255。饱和加法将确保结果在0到255之间。如果相加的结果大于255,饱和加法会将结果截断为255,类似地,如果结果小于0,饱和运算将结果设置为0。

1.2 取模运算(Modulo Operation)

  定义: 取模运算是指对两个整数相除,返回余数的运算。通常使用符号“%”表示。对于整数a和正整数b,a % b 的结果是一个非负整数,其大小小于b。

  示例: 在图像处理中,取模运算常用于周期性的操作,如周期性的亮度变化。对于像素值的取模加法,可以将结果限制在一个范围内,例如对256取模,确保结果在0到255之间。

  总体而言,饱和运算用于控制结果的范围,防止溢出,而取模运算用于获取除法的余数,通常应用于周期性的操作。在图像处理中,这两种运算都有其应用场景,具体取决于需要实现的效果。具体下面来说。

2,在图像处理中,饱和运算和取模运算的区别,联系,应用场景分别是什么?

  在图像处理中,饱和运算和取模运算都可以用于对图像像素值的调整,但它们的应用场景和效果略有不同。
饱和运算(Saturating Arithmetic)
  特点
    饱和运算主要用于防止溢出,确保结果在一个合理的范围内,通常是0到255。
    对于8位无符号整数(uchar)的像素值,饱和加法会将结果限制在0到255之间,超过255的部分会被截断为255,保持在合法范围内。
  应用场景
    饱和运算常用于图像亮度调整、滤波等场景,确保处理后的像素值不超出可表示的范围。
取模运算(Modulo Operation)
  特点
    取模运算主要用于周期性的操作,将结果限制在一个周期内,通常是对256取模,确保结果在0到255之间。
    取模运算可以用于模拟周期性的光照变化、颜色循环等效果。
  应用场景
    取模运算常用于需要产生循环或周期性效果的图像处理,例如通过周期性调整图像的亮度、对比度或颜色,以实现动态的视觉效果。
联系:
    饱和运算和取模运算都是对结果进行限制的方式,确保结果在某个特定范围内。
    在某些情况下,可以结合使用这两种运算,根据具体需求综合考虑。
  总体来说:

    1,如果你希望避免结果溢出,使图像保持在一个可接受范围内,使用饱和运算。

    2,如果你希望实现周期性的效果,例如循环的光照变化或颜色变换,使用取模运算。

    3,实际应用中,饱和运算和取模运算的选择取决于具体的图像处理任务和期望的视觉效果

 

3,以C++和Python 的具体实例测试

3.1 python实现饱和运算和取模运算

  python 示例如下(以加法为例,当然你也可以测试减法,乘法等):

import numpy as np

# 初始化两个像素点的值
pixel_a = np.uint8([150])
need_to_add_pixel = np.uint8([120])

# 饱和运算:将数值限制在一定范围内,通常是0~255之间
# 在图像处理中,这用于确保像素不会超出表示颜色的范围,例如某个像素的计算结果超出255,则被饱和到255
# 150+120 = 270 => 255
print(cv2.add(pixel_a, need_to_add_pixel))
# 打印结果为:[[255]]

# 取模运算:计算两个数相除的余数
# 在图像处理中,取模运算可以用于创建循环效果,例如在图像边缘处形成循环纹理
# 250+10 = 260 % 256 = 4
print(pixel_a + need_to_add_pixel)
# 打印结果为: [14]

  我将python的结果和过程解释都写在代码中了,实际上确实opencv实现的常量运算是饱和运算。而运算符实现的常量运算是取模运算。下面再看C++的。

3.2 C++实现饱和运算和取模运算

  C++示例如下:

    // 创建两个单像素的Mat,像素值分别为170和190
    cv::Mat pixel1(1, 1, CV_8UC1, cv::Scalar(170));
    cv::Mat pixel2(1, 1, CV_8UC1, cv::Scalar(190));

    // 创建两个单像素的uchar,像素值分别是200和210
    uchar pixel3 = 200;
    uchar pixel4 = 210;

    std::cout << "Pixel1 value: " << static_cast<int>(pixel1.at<uchar>(0, 0)) << std::endl;
    std::cout << "Pixel2 value: " << static_cast<int>(pixel2.at<uchar>(0, 0)) << std::endl;
    std::cout << "Pixel3 value: " << static_cast<int>(pixel3) << std::endl;
    std::cout << "Pixel4 value: " << static_cast<int>(pixel4) << std::endl;
    std::cout << "Data type of pixel1: " << typeid(pixel1).name() << std::endl;
    std::cout << "Data type of pixel2: " << typeid(pixel2).name() << std::endl;
    std::cout << "Data type of pixel3: " << typeid(pixel3).name() << std::endl;
    std::cout << "Data type of pixel4: " << typeid(pixel4).name() << std::endl;

    // 使用 cv::add 进行饱和运算
    cv::Mat result_add_saturate12;
    cv::add(pixel1, pixel2, result_add_saturate12);

    // 使用 + 运算符进行溢出运算
    cv::Mat result_add_overflow12 = pixel1 + pixel2;
    uchar result_add_overflow34 = pixel3 + pixel4;

    // 输出结果
    std::cout << "Result12 using cv::add: " << static_cast<int>(result_add_saturate12.at<uchar>(0, 0)) << std::endl;
    std::cout << "Result12 using + operator: " << static_cast<int>(result_add_overflow12.at<uchar>(0, 0)) << std::endl;
    std::cout << "Result34 using + operator: " << static_cast<int>(result_add_overflow34) << std::endl;

  结果如下:

Pixel1 value: 170
Pixel2 value: 190
Pixel3 value: 200
Pixel4 value: 210
Data type of pixel1: class cv::Mat
Data type of pixel2: class cv::Mat
Data type of pixel3: unsigned char
Data type of pixel4: unsigned char
Result12 using cv::add: 255
Result12 using + operator: 255
Result34 using + operator: 154

  但是C++中,我发现如果类型为cv::mat,无论是进行cv::add还是直接使用加法运算符,总是进行饱和操作。而不进行取模操作。但是如果对数据类型设置为uchar,然后使用加法运算符,则结果就是取模运算。

3.3 讨论:为什么opencv的add是饱和运算,而numpy的加法却写成取模

  OpenCV的cv::add和NumPy中的加法在设计时可能有不同的考虑,导致了它们在溢出处理上的差异。
OpenCV的 cv::add
  cv::add 函数在图像处理中默认采用饱和运算。这是由于在图像处理领域,特别是对于8位无符号整数(uchar)表示的像素值,饱和运算是一种常见的保护手段。饱和运算确保结果不会溢出范围(通常是0到255),防止图像亮度等调整操作导致不可预知的结果。

  OpenCV在处理图像时更注重保持图像的可视性,因此默认情况下选择了饱和运算。

NumPy的加法
  NumPy是一个通用的数学库,广泛用于科学计算和数组操作,不仅仅是图像处理。NumPy的加法操作默认采用取模运算,这是因为在通用的数学运算中,取模操作更为常见。

  在科学计算中,溢出通常表示一个错误,而取模操作则可以使结果在一定范围内循环,更适合一些数学和统计的应用。

  虽然OpenCV和NumPy在处理图像时采用了不同的默认溢出处理策略,但两者都提供了灵活的参数选项,允许用户指定其他的溢出处理方法。在OpenCV中,你可以使用cv::addWeighted来实现一定程度上的取模运算;而在NumPy中,你可以使用numpy.remainder函数来实现类似的效果。

  总体来说,这种差异主要是由于库设计时的偏好和目标应用的不同。在实际使用中,你可以根据具体需求选择适当的库和参数。

3.4 为什么Opencv要做饱和操作

  OpenCV选择使用饱和运算而不是取模运算,主要是因为饱和运算能够更好地处理图像处理任务中的边界情况和避免出现意外的结果。下面是一些理由:

  1. 物理解释: 在图像处理中,像素值通常被解释为光强度或颜色强度。对于灰度图像,典型的像素值范围是 [0, 255],代表黑到白的强度。超出这个范围的值在物理上没有明确的解释。

  2. 数学稳定性: 饱和运算确保在进行数学运算时,结果始终保持在合理的范围内,避免了溢出引起的不稳定性。在图像处理算法中,保持数学的稳定性对于正确的输出非常重要。

  3. 避免失真: 取模运算可能导致图像失真,因为它不会模拟实际图像处理中的物理行为。在处理图像时,饱和运算更符合图像处理任务的实际需求。

  4. 避免伪影: 取模运算可能导致伪影(artifacts),因为回绕到0可能导致图像中出现意外的亮度变化。饱和运算避免了这样的问题。

  总的来说,OpenCV选择饱和运算是为了确保在图像处理中获得可靠和直观的结果。取模运算通常更适用于某些特定的应用场景,例如密码学等,而不是图像处理领域。

 

4,C++中opencv的CV_8U类型,CV_8UC1类型,Uchar类型等笔记 

4.1 CV_8U类型

  在OpenCV中,CV_8U 是一种图像数据类型,表示图像中的每个像素值为8位无符号整数(8-bit Unsigned)。在这种数据类型下,每个像素的取值范围为0到255。

  具体来说,CV_8U 表示一个8位无符号整数的图像。这种图像类型通常用于表示灰度图像,其中每个像素的亮度值在0到255之间,0表示最暗,255表示最亮。

  以下是使用 CV_8U 数据类型创建一个简单的灰度图像的示例:

    // 创建一个单通道的8位无符号整数图像,大小为 100x100
    cv::Mat grayscaleImage(100, 100, CV_8U, cv::Scalar(128));

  

4.2 CV_8UC1类型

  CV_8UC1 是OpenCV中用于表示8位无符号整数单通道图像的数据类型标识。这个标识的含义如下:

  • CV_8U:表示8位无符号整数(uchar),像素值范围为 [0, 255]。
  • C1:表示单通道,即灰度图像。

  因此,CV_8UC1 表示单通道的8位无符号整数图像,通常用于表示灰度图像,其中每个像素的值是一个8位无符号整数。例如,以下是创建一个单通道的8位无符号整数图像的示例:

cv::Mat grayImage(100, 100, CV_8UC1, cv::Scalar(0));

  这将创建一个100x100的灰度图像,所有像素的初始值为0。

 

4.3 uchar 类型

  uchar类型不是C++标准库中的类型,相反,C++标准库使用了 unsigned char类型。

  定义:unsigned char 是一个整数数据类型,用于存储无符号(非负)的字符值,在C++中,unsigned char 通常用于表示字节,范围是0~255之间。

  取值范围:unsigned char类型是一个1字节的整数类型,其范围是从0~255之间(包括0和255)。因为它是无符号类型,所以它不能表示负数,但可以表示0~255之间的所有整数。

  如何打印:你可以使用 std::cout 来打印 unsigned char 的值:

unsigned char ucharValue = 200;
std::cout << static_cast<int>(ucharValue) << std::endl;;

  对于创建的一个uchar类型的 ucharVaule,我们通过将其转换为int并打印。

 

4.4 CV_8U类型和CV_8UC1类型的区别是什么

在OpenCV中,CV_8U 和 CV_8UC1 表示图像矩阵的数据类型,但它们之间存在一些区别:

  1.  CV_8U:

    1. CV_8U 表示8位无符号整数。这种数据类型通常用于表示图像中的像素值。
    2. 在 CV_8U 类型的矩阵中,每个像素值都是一个无符号字节(0 到 255),表示图像的亮度。
  2.  CV_8UC1:

    1. CV_8UC1 表示8位无符号整数,且矩阵只有一个通道(channel)。这是灰度图像的常见数据类型。
    2. 在 CV_8UC1 类型的矩阵中,每个元素表示一个像素的亮度值,而且图像只有一个通道。

  总的来说,CV_8U 表示一个通用的8位无符号整数类型,而 CV_8UC1 表示一个8位无符号整数类型的矩阵,且该矩阵只有一个通道。如果你处理的是灰度图像,通常会使用 CV_8UC1 类型的矩阵。如果处理的是彩色图像,可能会使用 CV_8UC3(表示三个通道的8位无符号整数类型)等。

  如果你使用 cv::Mat 的 at<uchar>(i, j) 打印出来的结果是全零,可能是因为 cv::getStructuringElement 返回的矩阵是 CV_8U 类型,而不是 CV_8UC1

  在 CV_8U 类型的图像中,元素的值被认为是无符号字节(unsigned byte),而不是灰度值。这可能导致 at<uchar> 访问失败。

  你可以尝试使用 at<int> 来访问元素,或者使用 static_cast<uchar> 进行转换。这里是一种可能的修改:

void printStructuringElement(const cv::Mat& kernel) {
    for (int i = 0; i < kernel.rows; ++i) {
        for (int j = 0; j < kernel.cols; ++j) {
            std::cout << static_cast<int>(kernel.at<uchar>(i, j)) << " ";
        }
        std::cout << std::endl;
    }
    std::cout << std::endl;
}

  这将确保uchar类型的元素被正确的转换并打印。

  在OpenCV中,CV_8U 和 CV_8UC1 都表示8位无符号整数类型。其实,它们的存储方式是相同的,都是使用 uchar(无符号字符,即 uint8_t)来存储每个像素的值。在内存中,它们都是占用一个字节。

  总体来说,实际上两者是相同的数据类型,都是以 uchar 存储的无符号8位整数。在实际应用中,你可以根据需要选择使用 CV_8U 或 CV_8UC1,并根据情况是否需要进行强制转换来正确打印。

// 创建一个3x3的CV_8U矩阵
cv::Mat img_8u = cv::Mat::zeros(3, 3, CV_8U);

// 设置矩阵中的像素值
img_8u.at<uchar>(0, 0) = 100;
img_8u.at<uchar>(1, 1) = 200;
img_8u.at<uchar>(2, 2) = 50;

// 打印矩阵中的像素值
std::cout << "CV_8U Matrix:" << std::endl;
for (int i = 0; i < img_8u.rows; ++i) {
    for (int j = 0; j < img_8u.cols; ++j) {
        std::cout << static_cast<int>(img_8u.at<uchar>(i, j)) << " ";
    }
    std::cout << std::endl;
}

// 创建一个3x3的CV_8UC1矩阵
cv::Mat img_8uc1 = cv::Mat::zeros(3, 3, CV_8UC1);

// 设置矩阵中的像素值
img_8uc1.at<uchar>(0, 0) = 150;
img_8uc1.at<uchar>(1, 1) = 50;
img_8uc1.at<uchar>(2, 2) = 255;

// 打印矩阵中的像素值
std::cout << "\nCV_8UC1 Matrix:" << std::endl;
for (int i = 0; i < img_8uc1.rows; ++i) {
    for (int j = 0; j < img_8uc1.cols; ++j) {
        // std::cout << img_8uc1.at<uchar>(i, j) << " ";
        std::cout << static_cast<int>(img_8uc1.at<uchar>(i, j)) << " ";
    }
    std::cout << std::endl;
}

  打印的结果:

CV_8U Matrix:
100 0 0
0 200 0
0 0 50

CV_8UC1 Matrix:
150 0 0
0 50 0
0 0 255

  

4.5  cv::Mat中的cv::Scalar是什么

  cv::Scalar 是OpenCV中用于表示多通道数据的数据类型,通常用于表示像素值或颜色信息。它是一个简单的容器,可以存储1到4个数值,分别对应图像中的通道。cv::Scalar 的构造函数有多个版本,最常用的版本接受1到4个数值,分别对应通道的值。

  以下是一些示例:

// 创建一个Scalar对象,表示灰度图像中的像素值
cv::Scalar gray_pixel(128);

// 创建一个Scalar对象,表示RGB图像中的颜色(蓝色)
cv::Scalar blue_color(255, 0, 0);

// 创建一个Scalar对象,表示RGBA图像中的颜色(半透明绿色)
cv::Scalar transparent_green(0, 255, 0, 128);

  在处理图像时,cv::Scalar 可以与 cv::Mat 结合使用,例如设置像素值或提取像素值。例如:

cv::Mat image(100, 100, CV_8UC3, cv::Scalar(0, 0, 255));  // 创建一个红色的图像

cv::Scalar pixel_value = image.at<cv::Vec3b>(50, 50);  // 提取像素值
std::cout << "Pixel value at (50, 50): " << pixel_value << std::endl;

  在这个例子中,cv::Vec3b 表示3通道的 cv::Matcv::Scalar 用于存储提取的像素值。cv::Scalar 的使用使得代码更加简洁,而且可以方便地处理不同通道的数值。

 

4.6  cv::Mat和unsigned char的区别是什么

cv::Matunsigned char 是两种不同的数据类型,它们分别用于不同的目的。

  1. cv::Mat

    • cv::Mat 是OpenCV库中用于表示图像和矩阵数据的数据类型。
    • 它是一个通用的多维数组类,可以表示单通道或多通道的图像,矩阵,甚至是其他类型的数据。
    • cv::Mat 有丰富的功能和方法,使得在图像处理和计算机视觉任务中更加方便。
  2. unsigned char

    • unsigned char 是C++语言中的基本数据类型之一,表示一个8位无符号整数。
    • 它的取值范围是 [0, 255]。
    • 通常用于表示像素值(灰度图像中的每个像素值),其中0表示最暗,255表示最亮。

区别:

  • cv::Mat 是一个复杂的数据结构,用于存储和处理图像和矩阵数据,提供了许多高级的操作和功能。
  • unsigned char 是一个基本的数据类型,主要用于表示8位无符号整数,特别适用于存储像素值。

  在图像处理中,你通常会使用 cv::Mat 来处理图像数据,而 unsigned char 可能是 cv::Mat 中像素值的底层数据类型。例如,对于灰度图像,cv::Mat 可能是单通道 CV_8UC1 类型,其中每个像素值为 unsigned char

 

4.7 cv::Scalar和 cv::Mat的取值范围分别是多少

  1. cv::Scalar

    • cv::Scalar 是一个简单的数据结构,通常用于表示颜色或像素值。
    • 对于灰度图像,cv::Scalar 中的每个通道的取值范围是 [0, 255]。
    • 对于彩色图像,每个通道的取值范围同样是 [0, 255]。
    • cv::Scalar 最多可以存储4个数值,分别对应4个通道。
  2. cv::Mat

    • cv::Mat 是OpenCV中用于表示图像和矩阵的多通道数据结构。
    • 对于图像,通常使用8位无符号整数 (CV_8U) 类型,其取值范围是 [0, 255]。
    • 对于其他数据类型,例如 CV_32F(32位浮点数),取值范围可以是任意的,取决于具体的数据类型。

  在处理图像时,通常会使用 CV_8U 类型的 cv::Mat,其中像素值的取值范围是 [0, 255],与 cv::Scalar 中的灰度值或颜色值相匹配。在使用其他数据类型时,需要根据具体的情况来理解像素值的取值范围。

   在默认的情况下,对于灰度图像,cv::Scalarcv::Mat 的取值范围是相同的,都是 [0, 255]。这是因为 cv::Scalar 通常用于表示像素值,而像素值在灰度图像中是单通道的,每个通道的值都在 [0, 255] 范围内。

  例如,对于灰度图像,下面的 cv::Scalarcv::Mat 表示相同的像素值:

cv::Scalar scalar_value(128);
cv::Mat mat_value(1, 1, CV_8UC1, cv::Scalar(128));

  这两个表示都是灰度值为128的像素。然而,需要注意以下几点:

  1. cv::Scalar 可以用于表示多通道数据: 当处理彩色图像时,cv::Scalar 可以表示多通道的颜色信息,每个通道的值同样在 [0, 255] 范围内。

  2. cv::Mat 的数据类型可以不同: 对于 cv::Mat,具体的数据类型可能不仅仅是 CV_8UC1,还可以是其他类型,例如 CV_32F。在这种情况下,像素值的取值范围将根据具体的数据类型而有所不同。

  总体而言,当处理灰度图像时,cv::Scalarcv::Mat 的取值范围是相同的。在处理彩色图像或其他数据类型时,需要考虑具体的通道数和数据类型。

 

4.8 总结:如果超出取值范围,cv::Mat类型还是会进行饱和运算,而uchar只是进行取模运算

  当使用 cv::Mat 作为容器表示像素值时,确实会执行饱和运算。这是因为 OpenCV 在处理图像时通常使用 cv::Mat 类型,而这个类提供了丰富的图像处理功能。

  对于 unsigned char,它是C++的基本数据类型,如果超出了255,将执行取模运算。这是因为 unsigned char 是一个循环数据类型,其值会在达到最大值时回绕到0。

  让我们通过一个示例来说明这一点:

#include <iostream>

int main() {
    // 使用 cv::Mat 进行饱和运算
    cv::Mat mat_pixel(1, 1, CV_8UC1, cv::Scalar(400));
    std::cout << "cv::Mat pixel value: " << static_cast<int>(mat_pixel.at<uchar>(0, 0)) << std::endl;

    // 使用 unsigned char 进行取模运算
    unsigned char uchar_pixel = 400;
    std::cout << "unsigned char pixel value: " << static_cast<int>(uchar_pixel) << std::endl;

    return 0;
}

  在这个例子中,cv::Mat 类型的像素值为200,但输出将是255,因为它被饱和到了255。而 unsigned char 的像素值也为200,但输出将是200,因为它进行了取模运算,回绕到了0。

cv::Mat pixel value: 255
unsigned char pixel value: 144