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

戰爭熱誠發表於2024-01-13

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

  

 

相關文章