OpenCV計算機視覺學習(15)——淺談圖像處理的飽和運算和取模運算

如果需要其他圖像處理的文章及代碼,請移步小編的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

  

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章