0、前言
這篇文章主要闡述瞭如何使用Qt在像素級別上對圖像進行操作,並實現了一些圖像效果,這些效果主要有:灰度,模糊,銳化,添加相框,金屬質感,改變圖像飽和度,亮度還有白平衡。
scanLine 返回某一行數據,轉換爲QRgb指針可進行直接有效的像素存取操作。
一、QImage、QPixmap、QPicture區別
簡介:
QBitmap:存儲單色的圖像,比如遮罩
QPicture:在存儲QPainter的一些操作指令
QPixmap:後臺顯示的圖像,在屏幕上繪製圖像最快的方法,不過壞處就是無法訪問和修改像素
QImage:提供了與硬件無關的圖像表示,在IO操作中有很快的速度,並且給出了訪問像素的接口
區別:
1. QPixmap主要是用於繪圖,針對屏幕顯示而最佳化設計,QImage主要是爲圖像I/O、圖片訪問和像素修改而設計的
2. QPixmap依賴於所在的平臺的繪圖引擎,故例如反鋸齒等一些效果在不同的平臺上可能會有不同的顯示效果,QImage使用Qt自身的繪圖引擎,可在不同平臺上具有相同的顯示效果
3. 目前的Qt會把QPixmap都存儲在graphics memory中,QImage是存儲在客戶端的,是獨立於硬件的。在 X11, Mac 以及 Symbian平臺上,QPixmap 是存儲在服務器端,而QImage則是存儲在客戶端,在Windows平臺上,QPixmap和QImage都是存儲在客戶端,並不使用任何的GDI資源
4. 由於QImage是獨立於硬件的,也是一種QPaintDevice,因此我們可以在另一個線程中對其進行繪製,而不需要在GUI線程中處理,使用這一方式可以很大幅度提高UI響應速度
5. QImage可通過setPixpel()和pixel()等方法直接存取指定的像素
6. QBitmap是QPixmap的一個子類,主要用於顯示單色位圖。用於製作遊標QCursor或筆刷QBrush等
二、圖形處理
1. 處理大的圖片
如果你是要處理大的圖片,比如攝像頭拍攝的照片,這種情況最好是將原圖縮小之後作爲預覽圖顯示在屏幕上,將圖像加載進QImage或者QPixmap,然後調整大小:
QImage image("sample.png");
image = image.scaled(width, height);
使用QImageReader來讀取和縮放圖片,然後再加載進QImage中。QImageReader無法將一張圖片加載進QPixmap中去,但是可以使用靜態方法 QPixmap::fromImage(QImage img)從QImage中加載進QPixmap。這個方法非常快,並且不需要加載大圖的內存開銷:
QImageReader imgReader("sample.png");
imgReader.setScaledSize(QSize(width, height));
QImage * image;
imgReader.read(image);
2. 彩色圖轉換成灰度圖
每一張圖片都是由像素點組成,每一個像素都有三個通道:紅,綠,藍,還有一個alpha通道來保存透明度(JPEG格式的圖片不支持透明)。每個通道的值是0-255,三個通道都是0的話,表示黑色,都是255表示白色。這篇文章中我們用RGB來表示一種顏色,也就是三個通道的值。
相比於一個像素一個像素地讀取,uchar * QImage::scanLine(int i)可以一次讀取整行的像素值,會更加高效,下面的例子就是按行讀取的例子,也是我們將要講的第一個例子,轉灰度圖
QImage * MainWindow::greyScale(QImage * origin)
{
QImage * newImage = new QImage(origin->width(), origin->height(), QImage::Format_ARGB32);
QRgb * line;
for(int y = 0; y<newImage->height(); y++)
{
QRgb * line = (QRgb *)origin->scanLine(y);
for(int x = 0; x<newImage->width(); x++)
{
// 灰度 - 取R,G,B值爲三者的算數平均數;
int average = (qRed(line[x]) + qGreen(line[x]) + qRed(line[x]))/3;
newImage->setPixel(x,y, qRgb(average, average, average));
}
}
return newImage;
}
灰度
我們要學習的第一個技術就是將彩色圖轉換成灰度圖,我們首先要明白的一點就是,其實標準的灰度圖就是每個像素點的三個通道的值一樣或者近似,我們的策略就是將每個像素的每個通道的值都調成一樣,取R,G,B值爲三者的算數平均數就可以了,比如原色是RGB(169,204,69), 那麼最終的RGB就是(169+204+69)/3 = 147.
QImage * MainWindow::greyScale(QImage * origin)
{
QImage * newImage = new QImage(origin->width(), origin->height(), QImage::Format_ARGB32);
QColor oldColor;
for(int x = 0; x<newImage->width(); x++)
{
for(int y = 0; y<newImage->height(); y++)
{
oldColor = QColor(origin->pixel(x,y));
int average = (oldColor.red()+oldColor.green()+oldColor.blue())/3;
newImage->setPixel(x,y,qRgb(average,average,average));
}
}
return newImage;
}
原始圖
灰度圖
3.亮度調節
就如之前我們提到的,白色用RGB(255,255,255)表示,黑色用RGB(0,0,0)表示,所以如果我們需要提高圖片的亮度(顏色接近白色),我們需要同時增加三個通道的數值,反之就是變暗。
在這裏我們添加了一個函數參數來決定要提高多少亮度,如果參數是負數的話就是減少亮度了。在每個通道都加上delta值之後,需要做的就是讓它不要低於0且不要高於255.
原圖
加亮圖 Delta = 30
4. 暖色調
當我們說一一幅暖色調的圖片的時候通常是因爲這張圖色調偏黃。我們沒有黃色的通道,但是紅色和綠色混合起來就是黃色,所以我們增加這兩個通道值,然後藍色通道值不變就好了。
我們使用一個delta參數來決定增加紅色和綠色通道的值。一張暖色的圖片能夠給人一種復古效果,如果是有沙子的圖片,圖片將會更加生動。
QImage * MainWindow::warm(int delta, QImage * origin)
{
QImage *newImage = new QImage(origin->width(), origin->height(), QImage::Format_ARGB32);
QColor oldColor;
int r,g,b;
for(int x=0; x<newImage->width(); x++)
{
for(int y=0; y<newImage->height(); y++)
{
oldColor = QColor(origin->pixel(x,y));
r = oldColor.red() + delta;
g = oldColor.green() + delta;
b = oldColor.blue();
//we check if the new values are between 0 and 255
r = qBound(0, r, 255);
g = qBound(0, g, 255);
newImage->setPixel(x,y, qRgb(r,g,b));
}
}
return newImage;
}
原圖
暖色圖 Delta = 30
5. 冷色調
如果說暖色調的圖片偏黃色,那麼冷色調的圖片應該就是偏藍色了。在這個方法裏面我們只增加藍色通道的值,紅色和綠色的值不變。
冷色調的圖片可以聯想到未來,死亡或者,冷。
QImage * MainWindow::cool(int delta, QImage * origin)
{
QImage *newImage = new QImage(origin->width(), origin->height(), QImage::Format_ARGB32);
QColor oldColor;
int r,g,b;
for(int x=0; x<newImage->width(); x++)
{
for(int y=0; y<newImage->height(); y++)
{
oldColor = QColor(origin->pixel(x,y));
r = oldColor.red();
g = oldColor.green();
b = oldColor.blue()+delta;
//we check if the new value is between 0 and 255
b = qBound(0, b, 255);
newImage->setPixel(x,y, qRgb(r,g,b));
}
}
return newImage;
}
原圖
冷色調圖 Delta = 30
6. 飽和度
我們已經說了,顏色由三個通道組成:紅,綠,藍,儘管如此,RGB不是唯一一個表示色彩的方式,在這裏,我們使用HSL格式表示色彩 - hue(色相), saturation(飽和度), lightness(明度)。
飽和的圖像擁有更加生動的顏色,通常會比較好看,但是有一點要記住:不要濫用飽和度,因爲很容易出現失真。
QImage * MainWindow::saturation(int delta, QImage * origin)
{
QImage * newImage = new QImage(origin->width(), origin->height(), QImage::Format_ARGB32);
QColor oldColor;
QColor newColor;
int h,s,l;
for(int x=0; x<newImage->width(); x++)
{
for(int y=0; y<newImage->height(); y++)
{
oldColor = QColor(origin->pixel(x,y));
newColor = oldColor.toHsl();
h = newColor.hue();
s = newColor.saturation()+delta;
l = newColor.lightness();
//we check if the new value is between 0 and 255
s = qBound(0, s, 255);
newColor.setHsl(h, s, l);
newImage->setPixel(x, y, qRgb(newColor.red(), newColor.green(), newColor.blue()));
}
}
return newImage;
}
原圖
飽和的圖片 Delta=30
7. 模糊
這個效果相對於之前的有一點點複雜。我們會用到一個卷積濾波器,根據當前像素的顏色和相鄰像素的顏色來獲得一個新的顏色。同時還有一個kernel的矩陣來決定計算中相鄰像素的影響程度。
原像素會在矩陣的中心,因此我們會使用基數行的行和列。我們不會修改邊緣的像素點,因爲那些點沒有我們需要的相鄰像素點,雖然我們也可以只使用有效的像素點。
舉了例子,讓我們來看看如何計算像素的RGB值。下面的三個舉證代表着當前像素和鄰接像素的RGB值,最中間的是當前像素。
R = 20 102 99
150 200 77
170 210 105
G = 22 33 40
17 21 33
8 15 24
B = 88 70 55
90 72 59
85 69 50
Kenel = 0 2 0
2 5 2
0 2 0
使用濾波器進行計算:
r = ( (102*2) + (150*2) + (200*5) + (77*2) + (210*2) ) / (2+2+5+2+2) = 159
g = ( (33*2) + ( 17*2) + (21*5) + (33*2) + (15*2) ) / (2+2+5+2+2) = 23
b = ( (70*2) + (90*2) + (72*5) + (59*2) + (69*2) ) / (2+2+5+2+2) = 72
由原始的RGB(200, 21, 72)得到了RGB(159, 23, 72). 發現最大的變化是紅色的通道,因爲紅色通道的值差距最大。
在修改肖像照片的時候通常會使用到模糊的技術,它能後掩蓋住皮膚的瑕疵。
QImage * MainWindow::blur(QImage * origin)
{
QImage * newImage = new QImage(*origin);
int kernel [5][5]= {{0,0,1,0,0},
{0,1,3,1,0},
{1,3,7,3,1},
{0,1,3,1,0},
{0,0,1,0,0}};
int kernelSize = 5;
int sumKernel = 27;
int r,g,b;
QColor color;
for(int x=kernelSize/2; x<newImage->width()-(kernelSize/2); x++)
{
for(int y=kernelSize/2; y<newImage->height()-(kernelSize/2); y++)
{
r = 0;
g = 0;
b = 0;
for(int i = -kernelSize/2; i<= kernelSize/2; i++)
{
for(int j = -kernelSize/2; j<= kernelSize/2; j++)
{
color = QColor(origin->pixel(x+i, y+j));
r += color.red()*kernel[kernelSize/2+i][kernelSize/2+j];
g += color.green()*kernel[kernelSize/2+i][kernelSize/2+j];
b += color.blue()*kernel[kernelSize/2+i][kernelSize/2+j];
}
}
r = qBound(0, r/sumKernel, 255);
g = qBound(0, g/sumKernel, 255);
b = qBound(0, b/sumKernel, 255);
newImage->setPixel(x,y, qRgb(r,g,b));
}
}
return newImage;
}
原圖
模糊圖
8. 銳化
像模糊中一樣,銳化一張圖片也會使用一個卷積濾波器,但是kernel矩陣是不一樣的,相鄰像素對應的值是負的。
銳化能夠處理模糊的照片,能夠提升細節。
QImage * MainWindow::sharpen(QImage * origin)
{
QImage * newImage = new QImage(* origin);
int kernel [3][3]= {{0,-1,0},
{-1,5,-1},
{0,-1,0}};
int kernelSize = 3;
int sumKernel = 1;
int r,g,b;
QColor color;
for(int x=kernelSize/2; x<newImage->width()-(kernelSize/2); x++)
{
for(int y=kernelSize/2; y<newImage->height()-(kernelSize/2); y++)
{
r = 0;
g = 0;
b = 0;
for(int i = -kernelSize/2; i<= kernelSize/2; i++)
{
for(int j = -kernelSize/2; j<= kernelSize/2; j++)
{
color = QColor(origin->pixel(x+i, y+j));
r += color.red()*kernel[kernelSize/2+i][kernelSize/2+j];
g += color.green()*kernel[kernelSize/2+i][kernelSize/2+j];
b += color.blue()*kernel[kernelSize/2+i][kernelSize/2+j];
}
}
r = qBound(0, r/sumKernel, 255);
g = qBound(0, g/sumKernel, 255);
b = qBound(0, b/sumKernel, 255);
newImage->setPixel(x,y, qRgb(r,g,b));
}
}
return newImage;
}
原圖
銳化圖
9. 添加相框
繪製一個相框是非常 常見的,我們只需要把相框在原圖上面繪製就可以了。這裏假設我們已經有一個和圖片一樣大小的相框了,不一樣的話要resize到一樣大。
QImage * MainWindow::drawFrame(QImage * origin)
{
QImage * newImage = new QImage(* origin);
QPainter painter;
painter.begin(newImage); // 以newImage爲畫布,繪製 frame.png 圖形
painter.drawImage(0,0, QImage(":images/frame.png"));
painter.end();
return newImage;
}
原圖
相框
添加相框之後
10. 金屬效果
這個例子中我們會結合幾種技術來獲得一種效果。下面是處理的步驟:
1.調整圖像的亮度,獲得一個較暗的圖片。
2.將圖像轉成灰度。
3.將灰度圖繪製在金屬的紋理上,透明度50%。
QImage * MainWindow::metal(QImage * origin)
{
QImage * newImage = new QImage(":images/metal.png");
QImage * darkImage = brightness(-100, origin);
QImage * greyImage = greyScale(darkImage);
QPainter painter;
painter.begin(newImage); // 以 紋理圖 爲畫布
painter.setOpacity(0.5);
painter.drawImage(0, 0, * greyImage);
painter.end();
delete greyImage;
delete darkImage;
return newImage;
}
原圖
金屬紋理
最終效果
11. 模糊的邊框
最後再來學習一個融合的效果,這次我們想要做的是模糊圖片外延的部分,讓視線的焦點聚集在圖片的中間。
我們將會使用一張遮罩圖片,來決定需要模糊的部分,具體的操作步驟如下:
1.從原圖獲取一張完全模糊的圖片。
2.使用QPainter的一種融合模式,通過遮罩圖片截取出一個模糊的相框。點這裏可以學習到更多的QPainter的融合模式。
3.在原圖上繪製出模糊的邊框。
QImage * MainWindow::blurFrame(QImage * origin)
{
QImage * newImage = new QImage(* origin);
QImage * blurredImage = blur(newImage);
QImage * mask = new QImage(":images/mask.png");
QPainter painter;
//Using the composition mode SourceAtop we get a blurred frame stored in QImage mask
painter.begin(mask);
painter.setCompositionMode(QPainter::CompositionMode_SourceAtop);
painter.drawImage(0, 0, * blurredImage);
painter.end();
//With our new frame we simply draw it over the original image
painter.begin(newImage);
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.drawImage(0, 0, * mask);
painter.end();
delete mask;
delete blurredImage;
return newImage;
}
原圖
遮罩
模糊的邊框
最終效果
總結
這篇文章應該可以成爲你圖像處理的入門,但是一切皆有可能。你可以修改這些方法,整合這些方法,使用其他的技術等等。想象力纔是你唯一的限制。