OpenCV 筆記(2):圖像的屬性以及像素相關(guān)的操作

1. 圖像的屬性

1.1 Mat 的主要屬性

在前文中,我們大致了解了 Mat 的基本結(jié)構(gòu)以及它的創(chuàng)建與賦值。接下來(lái)我們通過(guò)一個(gè)例子,來(lái)看看 Mat 所包含的常用屬性。

先創(chuàng)建一個(gè) 3*4 的四通道的矩陣,并打印出其相關(guān)的屬性,稍后會(huì)詳細(xì)解釋每個(gè)屬性的含義。

Mat srcImage(3, 4, CV_16UC4, Scalar_<uchar>(1, 2, 3, 4));

cout << srcImage << endl;

cout << "dims:" << srcImage.dims << endl;
cout << "rows:" << srcImage.rows << endl;
cout << "cols:" << srcImage.cols << endl;
cout << "channels:" << srcImage.channels() << endl;
cout << "type:" << srcImage.type() << endl;
cout << "depth:" << srcImage.depth() << endl;
cout << "elemSize:" << srcImage.elemSize() << endl;
cout << "elemSize1:" << srcImage.elemSize1() << endl;
cout << "step:" << srcImage.step << endl;
cout << "step[0]:" << srcImage.step[0] << endl;
cout << "step[1]:" << srcImage.step[1] << endl;
cout << "step1[0]:" << srcImage.step1(0) << endl;
cout << "step1[1]:" << srcImage.step1(1) << endl;

輸出結(jié)果:

[1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4;
 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4;
 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]
dims:2
rows:3
cols:4
channels:4
type:26
depth:2
elemSize:8
elemSize1:2
step:32
step[0]:32
step[1]:8
step1[0]:16
step1[1]:4

在上述例子中我們打印了 Mat 的很多屬性,它們主要包括:

  • rows: 表示圖像的高度。
  • cols:表示圖像的寬度。
  • dims:表示矩陣的維度。
  • data:表示 Mat 對(duì)象中的指針(uchar 類型的指針),指向內(nèi)存中存放矩陣數(shù)據(jù)的一塊內(nèi)存 (uchar* data)。
  • channels:表示通道數(shù)量;例如常見(jiàn)的 RGB、HSV 彩色圖像,則 channels=3;若為灰度圖,則 channels=1。
  • depth:表示圖像的深度,它用來(lái)度量每一個(gè)像素中每一個(gè)通道的精度,它本身與通道數(shù)無(wú)關(guān),它的數(shù)值越大表示精度越高。
數(shù)據(jù)類型 depth 的值 數(shù)據(jù)類型 取值范圍 對(duì)應(yīng) C++ 的類型
CV_8U 0 8 位無(wú)符號(hào)類型 0—255 uchar, unsigned char
CV_8S 1 8 位有符號(hào)類型 -128—127 char
CV_16U 2 16 位無(wú)符號(hào)類型 0—65535 ushort, unsigned short, unsigned short int
CV_16S 3 16 位有符號(hào)類型 -32768—32767 short, short int
CV_32S 4 32 位整數(shù)數(shù)據(jù)類型 -2147483648—2147483647 int, long
CV_32F 5 32 位浮點(diǎn)數(shù)類型 ±(1.18e-38……3.40e38) float
CV_64F 6 32 位雙精度類型 ±(2.23e-308……1.79e308) double
  • type:表示矩陣的數(shù)據(jù)類型,它包含矩陣中元素的類型以及通道數(shù)信息。
數(shù)據(jù)類型 1 2 3 4
CV_8U CV_8UC1 CV_8UC2 CV_8UC3 CV_8UC4
CV_8S CV_8SC1 CV_8SC2 CV_8SC3 CV_8SC4
CV_16U CV_16UC1 CV_16UC2 CV_16UC3 CV_16UC4
CV_16S CV_16SC1 CV_16SC2 CV_16SC3 CV_16SC4
CV_32S CV_32SC1 CV_32SC2 CV_32SC3 CV_32SC4
CV_32F CV_32FC1 CV_32FC2 CV_32FC3 CV_32FC4
CV_64F CV_64FC1 CV_64FC2 CV_64FC3 CV_64FC4
  • elemSize:表示矩陣中每一個(gè)元素的數(shù)據(jù)大小,它與通道數(shù)相關(guān),單位是字節(jié)。
    舉幾個(gè)例子:
    如果 Mat 中的數(shù)據(jù)類型是 CV_8UC1 或 CV_8SC1,那么 elemSize=1(1 * 8 / 8 = 1 bytes);
    如果 Mat 中的數(shù)據(jù)類型是 CV_8UC3 或 CV_8SC3,那么 elemSize=3(3 * 8 / 8 = 3 bytes);
    如果 Mat 中的數(shù)據(jù)類型是 CV_16UC3 或 CV_16SC3,那么 elemSize=6(3 * 16 / 8 = 6 bytes);
    如果 Mat 中的數(shù)據(jù)類型是 CV_32SC3 或 CV_32FC3,那么 elemSize=12(3 * 32 / 8 = 12 bytes);

  • elemSize1:表示矩陣中每一個(gè)元素單個(gè)通道的數(shù)據(jù)大小,單位是字節(jié)。滿足:
    elemSize1=elemSize/channels

  • step: 字面意思是“步長(zhǎng)”,實(shí)際上它描述了矩陣的形狀。 step[] 為一個(gè)數(shù)組,矩陣有幾維,step[] 數(shù)組就有幾個(gè)元素。以一個(gè)三維矩陣為例,step[0] 表示一個(gè)平面的字節(jié)總數(shù),step[1] 表示一行元素的字節(jié)總數(shù),step[2] 表示每一個(gè)元素的字節(jié)總數(shù)。

在 OpenCV 的官方文檔中,關(guān)于解釋 step 時(shí)曾提到矩陣數(shù)據(jù)元素({i_{0}, i_{1}, ... i_{m-1}})的地址
addr(M_{i_{0}, i_{1}, ... i_{m-1}}) = M.data + M.step[0] * i_{0} + M.step[1] * i_{1} + ... + M.step[M.dims - 1] * i_{M_{dims-1}}

對(duì)于我們常用的二維數(shù)組,上述公式可化簡(jiǎn)為:
addr(M_{i,j}) = M.data + M.step[0] * i + M.step[1] * j

這里的 step[0] 表示一行元素的字節(jié)總數(shù),step[1] 表示每一個(gè)元素的字節(jié)總數(shù)。

mat.png
  • step1: step1 也是一個(gè)數(shù)組。step1 不再以字節(jié)為單位,而是以 elemSize1 為單位,滿足:
    step1[i]=step[i]/elemSize1

2. 圖像的像素操作

2.1 像素的類型

我們最常用的圖像是二維數(shù)組,灰度圖像(CV_8UC1)會(huì)存放 C++ 的 uchar 類型,RGB 彩色圖像一般會(huì)存放 Vec3b 類型。

其中,單通道數(shù)據(jù)存放格式:


單通道.png

三通道數(shù)據(jù)存放格式:


三通道png.png

對(duì)于彩色圖像而言,在 OpenCV 中通道的順序是 B、G、R,這跟我們通常所說(shuō)的 RGB 三原色正好相反。

當(dāng)然,灰度圖像也不一定都是 CV_8UC1 類型,也可能是 CV_16SC1、CV_32FC1 等,它們會(huì)存放 C++ 的 short、float 等基本類型。類似地,彩色圖像也可能是 CV_16SC3、CV_32FC3 等,那它們是怎么存放的呢?

OpenCV 定義了一系列的 Vec 類,它是一個(gè)一維的向量,代表像素的類型。

typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;

typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;

typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;

typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;

typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;

typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;

其中 b、s、w、i、f、d 分別表示如下的含義:

數(shù)據(jù)類型
b unsigned char
s short int
w unsigned short
i int
f float
d double

Vec 類又被稱為固定向量類,在編譯時(shí)就知道向量的大小。類似 Vec 這樣的類還有:Matx、Point、Size、Rect

我們用一張表,總結(jié)一下矩陣中的數(shù)據(jù)類型和像素的類型的對(duì)應(yīng)關(guān)系:

數(shù)據(jù)類型 C1 C2 C3 C4 C6
CV_8U uchar Vec2b Vec3b Vec4b
CV_8S char Vec<char, 2> Vec<char, 3> Vec<char, 4>
CV_16U ushort Vec2w Vec3w Vec4w
CV_16S short Vec2s Vec3s Vec4s
CV_32S int Vec2i Vec3i Vec4i
CV_32F float Vec2f Vec3f Vec4f Vec6f
CV_64F double Vec2d Vec3d Vec4d Vec6d

基于上述表格我們可以回答剛才的問(wèn)題,CV_16SC3 類型的圖像存放的是 Vec3s 類型,CV_32FC3 類型的圖像存放的是 Vec3f 類型。

2.2 像素點(diǎn)的讀取

Mat 的 at() 函數(shù)實(shí)現(xiàn)了對(duì)矩陣中的某個(gè)像素的讀寫(xiě)操作。

下面的代碼展示了 at() 函數(shù)對(duì)灰度圖像像素的讀寫(xiě):

Scalar value = grayImage.at<uchar>(y, x);
Scalar.at<uchar>(y, x) = 128;

三通道彩色的圖像的讀取:

Vec3b value = image.at<Vec3b>(y, x);

uchar blue = value.val[0];
uchar green = value.val[1];
uchar red = value.val[2];

三通道彩色圖像的賦值:

image.at<Vec3b>(y,x)[0]=128;
image.at<Vec3b>(y,x)[1]=128;
image.at<Vec3b>(y,x)[2]=128;

下面的例子結(jié)合像素的類型,展示了將加載的圖像轉(zhuǎn)換成灰度圖像,以及對(duì)灰度圖像進(jìn)行取反的操作。

Mat srcImage = imread("/Users/tony/beautiful.jpg");
if (srcImage.empty())
{
    cout << "could not load image ..." << endl;
    return -1;
}
imshow("src", srcImage);

Mat grayImage;
cvtColor(srcImage, grayImage, COLOR_BGR2GRAY); // 灰度處理
imshow("gray",grayImage);

int height = grayImage.rows;
int width  = grayImage.cols;

for (int row=0; row<height; row++)
{
    for (int col=0; col<width; col++)
    {
        int gray = grayImage.at<uchar>(row, col);
        grayImage.at<uchar>(row, col) = 255- gray;
    }
}

imshow("invert", grayImage);
像素點(diǎn)操作.png

簡(jiǎn)單提一下,上述例子中 cvtColor() 函數(shù)的作用是將圖像從一個(gè)顏色空間轉(zhuǎn)換到另一個(gè)顏色空間。例如,可以將圖像從 BGR 色彩空間轉(zhuǎn)換成灰度色彩空間,或者從 BGR 色彩空間轉(zhuǎn)換成 HSV 色彩空間等等。

2.3 圖像的遍歷

2.3.1 基于數(shù)組遍歷

前面 2.2 介紹過(guò) at() 函數(shù)可以對(duì)某個(gè)像素進(jìn)行讀寫(xiě)操作,并用例子展示了對(duì)單通道進(jìn)行遍歷。

對(duì)于三通道的彩色圖像可以這樣遍歷。

for(int i=0;i<srcImage.rows;i++){
    for(int j=0;j<srcImage.cols;j++){
        srcImage.at<Vec3b>(i,j)[0]=...  //B通道
        srcImage.at<Vec3b>(i,j)[1]=...  //G通道
        srcImage.at<Vec3b>(i,j)[2]=...  //R通道
    }
}

2.3.2 基于指針遍歷

Mat 類提供了更高效的 ptr() 函數(shù),它可以得到圖像任意行首地址。

下面的代碼,它返回第 i+1 行的首地址,也就是指向第 i+1 行第一個(gè)元素的指針。

uchar* data = srcImage.ptr<uchar>(i);

at() 函數(shù)跟 ptr() 函數(shù)在使用上有一定的區(qū)別:

at<類型>(i,j)
ptr<類型>(i)

當(dāng)然,使用 ptr() 函數(shù)訪問(wèn)某個(gè)像素也是可以的,采用如下的方式:

mat.ptr<type>(row)[col]

它返回的是 <> 中的模板類型指針,指向的是第 row+1 行 col+1 列的元素。

對(duì)于單通道圖像的遍歷:

for(int i=0;i<srcImage.rows;i++){
    uchar* data=srcImage.ptr<uchar>(i);
    for(int j=0;j<srcImage.cols;j++){
        data[j]=...
    }
}

對(duì)于三通道圖像的遍歷:

for(int i=0;i<srcImage.rows;i++){
    Vec3b* data=srcImage.ptr<Vec3b>(i);
    for(int j=0;j<srcImage.cols;j++){
        data[j][0]=...  //B通道
        data[j][1]=...  //G通道
        data[j][2]=...  //R通道
    }
}

2.3.3 基于迭代器遍歷

C++ STL 對(duì)每個(gè)集合類都定義了對(duì)應(yīng)的迭代器類,OpenCV 也提供了 cv::Mat 的迭代器類,并且與 C++ STL 中的標(biāo)準(zhǔn)迭代器兼容。

對(duì)于單通道圖像的遍歷:

Mat_<uchar>::iterator begin = srcImage.begin<uchar>();
Mat_<uchar>::iterator end = srcImage.end<uchar>();

for (auto it = begin; it != end; it++)
{
    *it = ...
}

迭代器 Mat_ 是 Mat 的模版子類,它重載了 operator() 讓我們可以更方便的取圖像上的點(diǎn)。類似的迭代器還有 Matlterator_。

對(duì)于三通道圖像的遍歷:

Mat_<cv::Vec3b>::iterator begin = srcImage.begin<cv::Vec3b>();
Mat_<cv::Vec3b>::iterator end = srcImage.end<cv::Vec3b>();

for (auto it = begin; it != end; it++)
{
    (*it)[0] = ... //B通道
    (*it)[1] = ... //G通道
    (*it)[2] = ... //R通道
} 

使用迭代器遍歷圖像會(huì)便捷一些,但是效率沒(méi)有使用指針的效率高。

2.3.4 基于 LUT 遍歷

LUT (LOOK -UP-TABLE) 意為查找表。

在數(shù)據(jù)結(jié)構(gòu)中,查找表是由同一類型的 數(shù)據(jù)元素 構(gòu)成的集合,它是一種以查找為“核心”,同時(shí)包括其他運(yùn)算的非常靈活的數(shù)據(jù)結(jié)構(gòu)。

在圖像處理中,經(jīng)常會(huì)通過(guò)事先建立一張查找表對(duì)圖像進(jìn)行映射。

例如,將灰度圖由某個(gè)區(qū)間映射到另一個(gè)區(qū)間,或者將單通道映射到三通道。它們都是以像素灰度值作為索引,以灰度值映射后的數(shù)值作為表中的內(nèi)容,通過(guò)索引號(hào)與映射后的輸出值建立聯(lián)系。

一般灰度圖像會(huì)有 0-255 個(gè)灰度值,有時(shí)我們不需要這么精確的灰度級(jí),例如黑白圖像。下面我們來(lái)展示如何建立一個(gè) LUT,將 64 到 196 之間的灰度值變成 0,其余變成 1。

Mat lut(1, 256, CV_8U);
for (int i = 0; i < 256; i++)
{
    if (i > 64 and i < 196)
    {
        lut.at<uchar>(i) = 0;
    }
    else
    {
        lut.at<uchar>(i) = i;
    }
}

從上述代碼可以看出,通過(guò)改變圖像中像素的灰度值,LUT 可以降低灰度級(jí)提高運(yùn)算速度。

LUT 只適用于 CV_8U 類型的圖像。

當(dāng)然,查找表并不一定都是單通道的。

  • 如果輸入圖像為單通道,那么查找表為單通道。
  • 如果輸入圖像為三通道,那么查找表可以為單通道或者三通道。

使用 LUT 進(jìn)行遍歷,采用的是顏色空間縮減的方式:把 unsigned char 類型的值除以一個(gè) int 類型的值,得到仍然是一個(gè) char 類型的數(shù)值。

我們采用如下的公式:Inew = (Iold/Q)*Q

其中,Q 表示量化級(jí)別,當(dāng) Q= 10 時(shí)則灰度值 1-10 用灰度值 1 表示,灰度值 11-20 用灰度值 11 表示,以此類推。256 個(gè)灰度值的灰度圖像可以用 26 個(gè)數(shù)值表示,那么彩色的圖像就可以用 26 * 26 * 26 個(gè)數(shù)值表示,比原先小了很多。

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

using namespace std;
using namespace cv;

#define QUAN_VAL1          10
#define QUAN_VAL2          20
#define QUAN_VAL3          100

void createLookupTable(Mat& table, uchar quanVal)
{
    table.create(1,256,CV_8UC1);

    uchar *p = table.data;
    for(int i = 0; i < 256; ++i)
    {
        p[i] = quanVal*(i/quanVal); // 顏色縮減運(yùn)算
    }
}

int main()
{
    Mat srcImage = imread("/Users/tony/beautiful.jpg");
    if (srcImage.empty())
    {
        cout << "could not load image ..." << endl;
        return -1;
    }
    imshow("src", srcImage); // 原圖

    Mat table,dst1,dst2,dst3;
    createLookupTable(table, QUAN_VAL1);
    LUT(srcImage, table, dst1);

    createLookupTable(table, QUAN_VAL2);
    LUT(srcImage, table, dst2);

    createLookupTable(table, QUAN_VAL3);
    LUT(srcImage, table, dst3);

    imshow("dst1", dst1); // Q=10
    imshow("dst2", dst2); // Q=20
    imshow("dst3", dst3); // Q=100
    waitKey(0);

    return 0;
}
lut.png

上述例子在創(chuàng)建查找表時(shí),遍歷了矩陣的每一個(gè)像素以及運(yùn)用顏色空間縮減的運(yùn)算公式。并且分別展示了原圖、Q=10、Q=20、Q=100 的圖片??梢钥吹疆?dāng) Q = 100 時(shí),圖像壓縮得比較厲害丟失了很多信息。

3. 圖像像素值的統(tǒng)計(jì)

3.1 均值與標(biāo)準(zhǔn)差

均值和標(biāo)準(zhǔn)差是統(tǒng)計(jì)學(xué)的概念。

均值的公式:\mu = \frac{\sum_{i = 1}^N x_i}{N}
標(biāo)準(zhǔn)差公式:\delta = \sqrt{\delta^2} = \sqrt{\frac{\sum_{i = 1}^N (x_i-\mu)^2}{N}}

在圖像處理中,它們能幫助我們了解圖像通道中像素值的分布情況。均值表示圖像整體的亮暗程度,圖像的均值越大則表示圖像越亮。標(biāo)準(zhǔn)差表示圖像中明暗變化的對(duì)比程度,標(biāo)準(zhǔn)差越大表示圖像中明暗變化越明顯。

在圖像分析的時(shí)候,我們通過(guò)圖像像素值的統(tǒng)計(jì),可以對(duì)圖像的有效信息作出判斷。當(dāng)標(biāo)準(zhǔn)差很小時(shí),圖像所攜帶的有效信息會(huì)很少,便于我們判斷這是否是我們所需要的圖像。說(shuō)一個(gè)題外話,曾經(jīng)我看到過(guò)一段很震驚的代碼,某同事寫(xiě)的判斷傳送帶上手機(jī)是否亮屏。當(dāng)時(shí)的代碼可能是為了偷懶,只通過(guò)判斷圖像的均值,當(dāng)均值超過(guò)某個(gè)閾值時(shí)就認(rèn)為手機(jī)是亮屏的。后來(lái)我接手后,當(dāng)即做了大量的修改。

下面舉個(gè)例子,通過(guò) meanStdDev() 函數(shù)獲取圖像的均值和標(biāo)準(zhǔn)差,以及每個(gè)通道的均值和標(biāo)準(zhǔn)差。

Mat srcImage = imread("/Users/tony/beautiful.jpg");
if (srcImage.empty())
{
    cout << "could not load image ..." << endl;
    return -1;
}
imshow("src", srcImage);

Mat mean, stddev;
meanStdDev(srcImage, mean, stddev);
std::cout << "mean:" << std::endl << mean << std::endl;
std::cout << "stddev:" << std::endl<< stddev << std::endl;
printf("blue channel mean:%.2f, stddev: %.2f \n", mean.at<double>(0, 0), stddev.at<double>(0, 0));
printf("green channel mean:%.2f, stddev: %.2f \n", mean.at<double>(1, 0), stddev.at<double>(1, 0));
printf("red channel mean:%.2f, stddev: %.2f \n", mean.at<double>(2, 0), stddev.at<double>(2, 0));

輸出結(jié)果:

mean:
[91.28189117330051;
 104.7030620995939;
 118.9715339648672]
stddev:
[77.24017058254671;
 79.5424883584348;
 83.89088339080149]
blue channel mean:91.28, stddev: 77.24 
green channel mean:104.70, stddev: 79.54 
red channel mean:118.97, stddev: 83.89 

4. 總結(jié)

本文通過(guò)一個(gè)簡(jiǎn)單的例子,介紹了 Mat 經(jīng)常使用的屬性和方法。后續(xù)還介紹了像素的類型和多種圖像遍歷的方式、像素值的統(tǒng)計(jì)。

在幾種圖像遍歷方式中,除了 LUT 遍歷外,其他的幾種方式它們的效率從高到低依次為:指針 > 迭代器 > 數(shù)組。在實(shí)際生產(chǎn)環(huán)境中,我們經(jīng)常會(huì)用指針遍歷的方式。

本文介紹的內(nèi)容是對(duì)前面一篇文章內(nèi)容的補(bǔ)充,它們都是 OpenCV 最基礎(chǔ)的內(nèi)容,接下來(lái)的文章會(huì)經(jīng)常使用這些內(nèi)容。本文還引申出了 LUT 以及圖像像素值的統(tǒng)計(jì), 特別是均值和標(biāo)準(zhǔn)差它們?cè)趫D像預(yù)處理中經(jīng)常用到。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容