OpenCv 的基礎(chǔ)學(xué)習(xí)目前先告一段落了瓣喊,后面我們要開始手寫一些常用的效果坝锰,且都是基于 Android 平臺的瘫絮。希望我們有一定的 C++ 和 JNI 基礎(chǔ)宾添,如果我們對這塊知識有所欠缺锭弊,大家不妨看看這個:Android進(jìn)階之旅(JNI基礎(chǔ)實戰(zhàn))
我們可能會忍不住問堪澎,做 android 應(yīng)用層開發(fā),學(xué)習(xí)圖形圖像處理到底有啥好處味滞?首先不知我們是否有在 Glide 中有看到像這樣的源碼:
private static final int GIF_HEADER = 0x474946;
private static final int PNG_HEADER = 0x89504E47;
static final int EXIF_MAGIC_NUMBER = 0xFFD8;
@NonNull
private ImageType getType(Reader reader) throws IOException {
final int firstTwoBytes = reader.getUInt16();
// JPEG.
if (firstTwoBytes == EXIF_MAGIC_NUMBER) {
return JPEG;
}
final int firstFourBytes = (firstTwoBytes << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF);
// PNG.
if (firstFourBytes == PNG_HEADER) {
// See: http://stackoverflow.com/questions/2057923/how-to-check-a-png-for-grayscale-alpha
// -color-type
reader.skip(25 - 4);
int alpha = reader.getByte();
// A RGB indexed PNG can also have transparency. Better safe than sorry!
return alpha >= 3 ? PNG_A : PNG;
}
// GIF from first 3 bytes.
if (firstFourBytes >> 8 == GIF_HEADER) {
return GIF;
}
// ....... 省略部分代碼
return ImageType.WEBP;
}
其次學(xué)習(xí) opencv 不能只停留在其 api 的調(diào)用上樱蛤,我們必須了解其內(nèi)部的實現(xiàn)的原理钮呀,最好還要能手寫實現(xiàn)。最后學(xué)習(xí)圖像圖形處理昨凡,也有利于我們后面學(xué)習(xí)音視頻的開發(fā)爽醋,能夠幫助我們更加熟悉 NDK 開發(fā),包括我們自己去閱讀 android native 層的源碼等等土匀,總之好處還是有很多的子房。
接下來我們就以 QQ 發(fā)說說處理圖片的效果為例,來手寫實現(xiàn)部分效果就轧。有些效果在之前的文章中已有講到证杭,這里就不再給代碼了,我們可以參考:《圖形圖像處理 - Android 濾鏡效果》 搭建 android ndk 開發(fā)環(huán)境和集成 opencv 大家可以參考:《NDK開發(fā)前奏 - 實現(xiàn)支付寶人臉識別功能》妒御。
1. 逆世界和鏡像
againstWorld(JNIEnv *env, jclass type, jobject bitmap) {
// bitmap -> mat
Mat src;
cv_helper::bitmap2mat(env, bitmap, src);
// 二分之一的位置
const int middleRows = src.rows >> 1;
// 四分之一的位置
const int quarterRows = middleRows >> 1;
Mat res(src.size(), src.type());
// 處理下半部分
for (int rows = 0; rows < middleRows; ++rows) {
for (int cols = 0; cols < src.cols; ++cols) {
res.at<int>(middleRows + rows, cols) = src.at<int>(quarterRows + rows, cols);
}
}
// 處理上半部分
for (int rows = 0; rows < middleRows; ++rows) {
for (int cols = 0; cols < src.cols; ++cols) {
res.at<int>(rows, cols) = src.at<int>(src.rows - quarterRows - rows, cols);
}
}
// mat -> bitmap
cv_helper::mat2bitmap(env, res, bitmap);
return bitmap;
}
2. remap 重映射
void remap(Mat &src, Mat &dst, Mat &mapX, Mat &mapY) {
// 有一系列的檢測
dst.create(src.size(), src.type());
for (int rows = 0; rows < dst.rows; ++rows) {
for (int cols = 0; cols < dst.cols; ++cols) {
int r_rows = mapY.at<int>(rows, cols);
int r_cols = mapX.at<int>(rows, cols);
dst.at<Vec4b>(rows, cols) = src.at<Vec4b>(r_rows, r_cols);
}
}
}
remap(JNIEnv *env, jclass type, jobject bitmap) {
// bitmap -> mat
Mat src;
cv_helper::bitmap2mat(env, bitmap, src);
Mat res;
Mat mapX(src.size(), src.type());
Mat mapY(src.size(), src.type());
for (int rows = 0; rows < src.rows; ++rows) {
for (int cols = 0; cols < src.cols; ++cols) {
mapX.at<int>(rows, cols) = src.cols - cols;
mapY.at<int>(rows, cols) = src.rows - rows;
}
}
remap(src, res, mapX, mapY);
// mat -> bitmap
cv_helper::mat2bitmap(env, res, bitmap);
return bitmap;
}
3. resize 插值法
我們經(jīng)常會將某種尺寸的圖像轉(zhuǎn)換為其他尺寸的圖像解愤,如果放大或者縮小圖片的尺寸,籠統(tǒng)來說的話乎莉,可以使用OpenCV為我們提供的如下兩種方式:
- resize函數(shù)送讲。這是最直接的方式,
- pyrUp( )惋啃、pyrDown( )函數(shù)哼鬓。即圖像金字塔相關(guān)的兩個函數(shù),對圖像進(jìn)行向上采樣边灭,向下采樣的操作异希。
pyrUp、pyrDown 其實和專門用作放大縮小圖像尺寸的 resize 在功能上差不多绒瘦,披著圖像金字塔的皮称簿,說白了還是在對圖像進(jìn)行放大和縮小操作。另外需要指出的是惰帽,pyrUp憨降、pyrDown 在 OpenCV 的 imgproc 模塊中的 Image Filtering 子模塊里。而 resize 在 imgproc 模塊的 Geometric Image Transformations 子模塊里该酗。關(guān)于 pyrUp授药、pyrDown 在 opencv 基礎(chǔ)學(xué)習(xí)中已有詳細(xì)介紹,這里就不再反復(fù)了呜魄。
resize( ) 為 OpenCV 中專職調(diào)整圖像大小的函數(shù)烁焙。此函數(shù)將源圖像精確地轉(zhuǎn)換為指定尺寸的目標(biāo)圖像。如果源圖像中設(shè)置了 ROI(Region Of Interest 耕赘,感興趣區(qū)域),那么 resize( ) 函數(shù)會對源圖像的 ROI 區(qū)域進(jìn)行調(diào)整圖像尺寸的操作膳殷,來輸出到目標(biāo)圖像中操骡。若目標(biāo)圖像中已經(jīng)設(shè)置 ROI 區(qū)域九火,不難理解 resize( ) 將會對源圖像進(jìn)行尺寸調(diào)整并填充到目標(biāo)圖像的 ROI 中。很多時候册招,我們并不用考慮第二個參數(shù)dst的初始圖像尺寸和類型(即直接定義一個Mat類型岔激,不用對其初始化),因為其尺寸和類型可以由 src,dsize,fx 和 fy 這其他的幾個參數(shù)來確定是掰÷嵌Γ可選的方式為:
- INTER_NEAREST - 最近鄰插值
- INTER_LINEAR - 線性插值(默認(rèn)值)
- INTER_AREA - 區(qū)域插值(利用像素區(qū)域關(guān)系的重采樣插值)
- INTER_CUBIC –三次樣條插值(超過4×4像素鄰域內(nèi)的雙三次插值)
- INTER_LANCZOS4 -Lanczos插值(超過8×8像素鄰域的Lanczos插值)
- 最近鄰插值
最簡單的圖像縮放算法就是最近鄰插值。顧名思義键痛,就是將目標(biāo)圖像各點的像素值設(shè)為源圖像中與其最近的點炫彩。算法優(yōu)點在與簡單、速度快絮短。
如下圖所示江兢,一個44的圖片縮放為88的圖片。步驟:
- 生成一張空白的8*8的圖片丁频,然后在縮放位置填充原始圖片值(可以這么理解)
-
在圖片的未填充區(qū)域(黑色部分)杉允,填充為原有圖片最近的位置的像素值。
void resize(Mat src, Mat dst, int nH, int nW) {
dst.create(nH, nW, src.type());
int oH = src.rows;
int oW = src.cols;
for (int rows = 0; rows < dst.rows; ++rows) {
for (int cols = 0; cols < dst.cols; ++cols) {
int nR = rows * (nH / oH);
int nC = cols * (nW / oW);
dst.at<Vec4b>(rows, cols) = src.at<Vec4b>(nR, nC);
}
}
}
- 雙線性插值法
如果原始圖像src的大小是3×3席里,目標(biāo)圖像dst的大小是4×4叔磷,考慮dst中(1,1)點像素對應(yīng)原始圖像像素點的位置為(0.75,0.75),如果使用最近鄰算法來計算奖磁,原始圖像的位置在浮點數(shù)取整后為坐標(biāo)(0,0)改基。
上面這樣粗暴的計算會丟失很多信息,考慮(0.75,0.75)這個信息署穗,它表示在原始圖像中的坐標(biāo)位置寥裂,相比較取(0,0)點,(0.75,0.75)貌似更接近(1,1)點案疲,那如果將最近鄰算法中的取整方式改為cvRound(四舍五入)的方式取(1,1)點封恰,同樣會有丟的信息,即丟失了“0.25”部分的(0,0)點褐啡、(1,0)點和(0,1)點诺舔。
可以看到,dst圖像上(X,Y)對應(yīng)到src圖像上的點备畦,最好是根據(jù)計算出的浮點數(shù)坐標(biāo)低飒,按照百分比各取四周的像素點的部分。
如下圖:
雙線性插值的原理相類似懂盐,這里不寫雙線性插值計算點坐標(biāo)的方法褥赊,容易把思路帶跑偏,直接就按照比率權(quán)重的思想考慮莉恼。將 ( wWX , hHY ) ( wWX , hHY ) 寫成 ( x′ + u , y′ + v ) ( x′ + u , y′ + v ) 的形式拌喉,表示將 xx 與yy 中的整數(shù)和小數(shù)分開表示 uvuv 分別代表小數(shù)部分速那。這樣,根據(jù)權(quán)重比率的思想得到計算公式
(X,Y) = (1 ? u) · (1 ? v) · (x , y) + (u ? 1) · v · ( x , y + 1) + u · (v ? 1) · (x + 1 , y) + (u · v) · (x , y)
在實際的代碼編寫中尿背,會有兩個問題端仰,一個是圖像會發(fā)生偏移,另一個是效率問題田藐。
幾何中心對齊:
由于計算的圖像是離散坐標(biāo)系荔烧,如果使用 (wWX , hHY) (wWX , hHY) 公式來計算,得到的 (X , Y) 值是錯誤的比率計算而來的汽久,即 (x + 1 , y)鹤竭、(x , y + 1)、(x + 1 , y + 1)這三組點中回窘,有可能有幾個沒有參與到比率運算當(dāng)中诺擅,或者這個插值的比率直接是錯誤的。 例如啡直,src 圖像大小是 a×aa×a烁涌,dst 圖像的大小是 0.5a×0.5a0.5a×0.5a。
根據(jù)原始公式計算(wW , hH) (wW , hH)得到(2 , 2)(2 , 2)(注意這不是表示點坐標(biāo)酒觅,而是 x 和 y 對應(yīng)的比率)如果要計算 dst 點 (0 , 0) 對應(yīng)的插值結(jié)果撮执,由于 (2 , 2) (2 , 2) 是整數(shù),沒有小數(shù)舷丹,所以最后得到 dst 點在 (0 , 0) (0 , 0) 點的像素值就是src圖像上在 (0,0)(0,0)點的值抒钱。然而,我們想要的 dst 在 (0,0)(0,0)上的結(jié)果是應(yīng)該是有 (0 , 0) (1 , 0) (0 , 1) (1 , 1) 這四個點各自按照 0.5×0.5 的比率加權(quán)的結(jié)果颜凯。 所以我們要將 dst 上面的點谋币,按照比率 (wW , hH) ( wW , hH) 向右下方向平移0.5個單位。
公式如下:
(x , y) = (XwW + 0.5(wW ? 1),YhH + 0.5(hH ? 1))
(x , y) = (XwW + 0.5(wW ? 1),YhH + 0.5(hH ? 1))
運算優(yōu)化:由計算公式可以得知症概,在計算每一個dst圖像中的像素值時會涉及到大量的浮點數(shù)運算蕾额,性能不佳”顺牵可以考慮將浮點數(shù)變換成一個整數(shù)诅蝶,即擴(kuò)大一定的倍數(shù),運算得到的結(jié)果再除以這個倍數(shù)募壕。舉一個簡單的例子调炬,計算 0.25×0.75,可以將 0.25 和 0.75 都乘上 8舱馅,得到 2×6=12缰泡,結(jié)果再除以 8282,這樣運算的結(jié)果與直接計算浮點數(shù)沒有差別代嗤。
在程序中匀谣,沒有辦法取得一個標(biāo)準(zhǔn)的整數(shù)照棋,使得兩個相互運算的浮點數(shù)都變成類似“2”和”6“一樣的標(biāo)準(zhǔn)整數(shù),只能取一個適當(dāng)?shù)闹祦肀M量的減少誤差武翎,在源碼當(dāng)中取值為 211211=2048,即 2 的固定冪數(shù)溶锭,最后結(jié)果可以通過用位移來表示除以一個 2 整次冪數(shù)宝恶,計算速度會有很大的提高。
//雙線性插值
void resize(const Mat &src, Mat &dst, Size &dsize, double fx = 0.0, double fy = 0.0){
//獲取矩陣大小
Size ssize = src.size();
//保證矩陣的長寬都大于0
CV_Assert(ssize.area() > 0);
//如果dsize為(0,0)
if (!dsize.area()) {
//satureate_cast防止數(shù)據(jù)溢出
dsize = Size(saturate_cast<int>(src.cols * fx),
saturate_cast<int>(src.rows * fy));
CV_Assert(dsize.area());
} else {
//Size中的寬高和mat中的行列是相反的
fx = (double) dsize.width / src.cols;
fy = (double) dsize.height / src.rows;
}
dst.create(dsize, src.type());
double ifx = 1. / fx;
double ify = 1. / fy;
uchar *dp = dst.data;
uchar *sp = src.data;
//寬(列數(shù))
int iWidthSrc = src.cols;
//高(行數(shù))
int iHiehgtSrc = src.rows;
int channels = src.channels();
short cbufy[2];
short cbufx[2];
for (int row = 0; row < dst.rows; row++) {
float fy = (float) ((row + 0.5) * ify - 0.5);
//整數(shù)部分
int sy = cvFloor(fy);
//小數(shù)部分
fy -= sy;
sy = std::min(sy, iHiehgtSrc - 2);
sy = std::max(0, sy);
cbufy[0] = cv::saturate_cast<short>((1.f - fy) * 2048);
cbufy[1] = 2048 - cbufy[0];
for (int col = 0; col < dst.cols; col++) {
float fx = (float) ((col + 0.5) * ifx - 0.5);
int sx = cvFloor(fx);
fx -= sx;
if (sx < 0) {
fx = 0, sx = 0;
}
if (sx >= iWidthSrc - 1) {
fx = 0, sx = iWidthSrc - 2;
}
cbufx[0] = cv::saturate_cast<short>((1.f - fx) * 2048);
cbufx[1] = 2048 - cbufx[0];
for (int k = 0; k < src.channels(); ++k) {
dp[(row * dst.cols + col) * channels + k] = (
sp[(sy * src.cols + sx) * channels + k] * cbufx[0] * cbufy[0] +
sp[((sy + 1) * src.cols + sx) * channels + k] * cbufx[0] *
cbufy[1] +
sp[(sy * src.cols + (sx + 1)) * channels + k] * cbufx[1] *
cbufy[0] +
sp[((sy + 1) * src.cols + (sx + 1)) * channels + k] * cbufx[1] *
cbufy[1]
) >> 22;
}
}
}
}
視頻地址:https://pan.baidu.com/s/1EoAEQJA2_bBOIHCpcGEkgA
視頻密碼:8ydn