圖形圖像處理 - 手寫 QQ 說說圖片處理效果

OpenCv 的基礎(chǔ)學(xué)習(xí)目前先告一段落了柬批,后面我們要開始手寫一些常用的效果,且都是基于 Android 平臺(tái)的。希望我們有一定的 C++ 和 JNI 基礎(chǔ)氮帐,如果我們對(duì)這塊知識(shí)有所欠缺锻霎,大家不妨看看這個(gè):Android進(jìn)階之旅(JNI基礎(chǔ)實(shí)戰(zhàn))

我們可能會(huì)忍不住問,做 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)部的實(shí)現(xiàn)的原理奄容,最好還要能手寫實(shí)現(xiàn)。最后學(xué)習(xí)圖像圖形處理产徊,也有利于我們后面學(xué)習(xí)音視頻的開發(fā)昂勒,能夠幫助我們更加熟悉 NDK 開發(fā),包括我們自己去閱讀 android native 層的源碼等等舟铜,總之好處還是有很多的戈盈。

接下來我們就以 QQ 發(fā)說說處理圖片的效果為例,來手寫實(shí)現(xiàn)部分效果谆刨。有些效果在之前的文章中已有講到塘娶,這里就不再給代碼了,我們可以參考:《圖形圖像處理 - Android 濾鏡效果》 搭建 android ndk 開發(fā)環(huán)境和集成 opencv 大家可以參考:《NDK開發(fā)前奏 - 實(shí)現(xiàn)支付寶人臉識(shí)別功能》痊夭。

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) {
    // 有一系列的檢測(cè)
    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)常會(huì)將某種尺寸的圖像轉(zhuǎn)換為其他尺寸的圖像刁岸,如果放大或者縮小圖片的尺寸,籠統(tǒng)來說的話她我,可以使用OpenCV為我們提供的如下兩種方式:

  1. resize函數(shù)虹曙。這是最直接的方式,
  2. pyrUp( )番舆、pyrDown( )函數(shù)酝碳。即圖像金字塔相關(guān)的兩個(gè)函數(shù),對(duì)圖像進(jìn)行向上采樣恨狈,向下采樣的操作疏哗。

pyrUp、pyrDown 其實(shí)和專門用作放大縮小圖像尺寸的 resize 在功能上差不多拴事,披著圖像金字塔的皮沃斤,說白了還是在對(duì)圖像進(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ù)會(huì)對(duì)源圖像的 ROI 區(qū)域進(jìn)行調(diào)整圖像尺寸的操作缩抡,來輸出到目標(biāo)圖像中。若目標(biāo)圖像中已經(jīng)設(shè)置 ROI 區(qū)域包颁,不難理解 resize( ) 將會(huì)對(duì)源圖像進(jìn)行尺寸調(diào)整并填充到目標(biāo)圖像的 ROI 中瞻想。很多時(shí)候,我們并不用考慮第二個(gè)參數(shù)dst的初始圖像尺寸和類型(即直接定義一個(gè)Mat類型娩嚼,不用對(duì)其初始化)蘑险,因?yàn)槠涑叽绾皖愋涂梢杂?src,dsize,fx 和 fy 這其他的幾個(gè)參數(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插值)
  1. 最近鄰插值
    最簡(jiǎn)單的圖像縮放算法就是最近鄰插值佃迄。顧名思義,就是將目標(biāo)圖像各點(diǎn)的像素值設(shè)為源圖像中與其最近的點(diǎn)贵少。算法優(yōu)點(diǎn)在與簡(jiǎn)單呵俏、速度快。

如下圖所示春瞬,一個(gè)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);
        }
    }
}
  1. 雙線性插值法

如果原始圖像src的大小是3×3,目標(biāo)圖像dst的大小是4×4萄涯,考慮dst中(1,1)點(diǎn)像素對(duì)應(yīng)原始圖像像素點(diǎn)的位置為(0.75,0.75)绪氛,如果使用最近鄰算法來計(jì)算,原始圖像的位置在浮點(diǎn)數(shù)取整后為坐標(biāo)(0,0)涝影。

上面這樣粗暴的計(jì)算會(huì)丟失很多信息枣察,考慮(0.75,0.75)這個(gè)信息,它表示在原始圖像中的坐標(biāo)位置燃逻,相比較取(0,0)點(diǎn)序目,(0.75,0.75)貌似更接近(1,1)點(diǎn),那如果將最近鄰算法中的取整方式改為cvRound(四舍五入)的方式取(1,1)點(diǎn)伯襟,同樣會(huì)有丟的信息猿涨,即丟失了“0.25”部分的(0,0)點(diǎn)、(1,0)點(diǎn)和(0,1)點(diǎn)姆怪。

可以看到叛赚,dst圖像上(X,Y)對(duì)應(yīng)到src圖像上的點(diǎn)澡绩,最好是根據(jù)計(jì)算出的浮點(diǎn)數(shù)坐標(biāo),按照百分比各取四周的像素點(diǎn)的部分俺附。
如下圖:



雙線性插值的原理相類似肥卡,這里不寫雙線性插值計(jì)算點(diǎn)坐標(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)重比率的思想得到計(jì)算公式

(X,Y) = (1 ? u) · (1 ? v) · (x , y) + (u ? 1) · v · ( x , y + 1) + u · (v ? 1) · (x + 1 , y) + (u · v) · (x , y)

在實(shí)際的代碼編寫中,會(huì)有兩個(gè)問題沮稚,一個(gè)是圖像會(huì)發(fā)生偏移,另一個(gè)是效率問題册舞。

幾何中心對(duì)齊:

由于計(jì)算的圖像是離散坐標(biāo)系蕴掏,如果使用 (wWX , hHY) (wWX , hHY) 公式來計(jì)算,得到的 (X , Y) 值是錯(cuò)誤的比率計(jì)算而來的调鲸,即 (x + 1 , y)盛杰、(x , y + 1)、(x + 1 , y + 1)這三組點(diǎn)中藐石,有可能有幾個(gè)沒有參與到比率運(yùn)算當(dāng)中即供,或者這個(gè)插值的比率直接是錯(cuò)誤的。 例如于微,src 圖像大小是 a×aa×a逗嫡,dst 圖像的大小是 0.5a×0.5a0.5a×0.5a。

根據(jù)原始公式計(jì)算(wW , hH) (wW , hH)得到(2 , 2)(2 , 2)(注意這不是表示點(diǎn)坐標(biāo)株依,而是 x 和 y 對(duì)應(yīng)的比率)如果要計(jì)算 dst 點(diǎn) (0 , 0) 對(duì)應(yīng)的插值結(jié)果驱证,由于 (2 , 2) (2 , 2) 是整數(shù),沒有小數(shù)恋腕,所以最后得到 dst 點(diǎn)在 (0 , 0) (0 , 0) 點(diǎn)的像素值就是src圖像上在 (0,0)(0,0)點(diǎn)的值抹锄。然而,我們想要的 dst 在 (0,0)(0,0)上的結(jié)果是應(yīng)該是有 (0 , 0) (1 , 0) (0 , 1) (1 , 1) 這四個(gè)點(diǎn)各自按照 0.5×0.5 的比率加權(quán)的結(jié)果荠藤。 所以我們要將 dst 上面的點(diǎn)伙单,按照比率 (wW , hH) ( wW , hH) 向右下方向平移0.5個(gè)單位。

公式如下:
(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ùn)算優(yōu)化:由計(jì)算公式可以得知哈肖,在計(jì)算每一個(gè)dst圖像中的像素值時(shí)會(huì)涉及到大量的浮點(diǎn)數(shù)運(yùn)算吻育,性能不佳∧党梗可以考慮將浮點(diǎn)數(shù)變換成一個(gè)整數(shù)扫沼,即擴(kuò)大一定的倍數(shù)出爹,運(yùn)算得到的結(jié)果再除以這個(gè)倍數(shù)。舉一個(gè)簡(jiǎn)單的例子缎除,計(jì)算 0.25×0.75严就,可以將 0.25 和 0.75 都乘上 8,得到 2×6=12器罐,結(jié)果再除以 8282梢为,這樣運(yùn)算的結(jié)果與直接計(jì)算浮點(diǎn)數(shù)沒有差別。

在程序中轰坊,沒有辦法取得一個(gè)標(biāo)準(zhǔn)的整數(shù)铸董,使得兩個(gè)相互運(yùn)算的浮點(diǎn)數(shù)都變成類似“2”和”6“一樣的標(biāo)準(zhǔn)整數(shù),只能取一個(gè)適當(dāng)?shù)闹祦肀M量的減少誤差肴沫,在源碼當(dāng)中取值為 211211=2048粟害,即 2 的固定冪數(shù),最后結(jié)果可以通過用位移來表示除以一個(gè) 2 整次冪數(shù)颤芬,計(jì)算速度會(huì)有很大的提高悲幅。

//雙線性插值
void resize(const Mat &src, Mat &dst, Size &dsize, double fx = 0.0, double fy = 0.0){
    //獲取矩陣大小
    Size ssize = src.size();
    //保證矩陣的長(zhǎng)寬都大于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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市站蝠,隨后出現(xiàn)的幾起案子汰具,更是在濱河造成了極大的恐慌,老刑警劉巖菱魔,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件留荔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡澜倦,警方通過查閱死者的電腦和手機(jī)聚蝶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來藻治,“玉大人既荚,你說我怎么就攤上這事《把蓿” “怎么了恰聘?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)吸占。 經(jīng)常有香客問我晴叨,道長(zhǎng),這世上最難降的妖魔是什么矾屯? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任兼蕊,我火速辦了婚禮,結(jié)果婚禮上件蚕,老公的妹妹穿的比我還像新娘孙技。我一直安慰自己产禾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布牵啦。 她就那樣靜靜地躺著亚情,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哈雏。 梳的紋絲不亂的頭發(fā)上楞件,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音裳瘪,去河邊找鬼土浸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛彭羹,可吹牛的內(nèi)容都是我干的黄伊。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼派殷,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼毅舆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起愈腾,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎岂津,沒想到半個(gè)月后虱黄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吮成,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了粱甫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泳叠。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖茶宵,靈堂內(nèi)的尸體忽然破棺而出危纫,到底是詐尸還是另有隱情,我是刑警寧澤乌庶,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布种蝶,位于F島的核電站,受9級(jí)特大地震影響瞒大,放射性物質(zhì)發(fā)生泄漏螃征。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一透敌、第九天 我趴在偏房一處隱蔽的房頂上張望盯滚。 院中可真熱鬧踢械,春花似錦、人聲如沸魄藕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)泼疑。三九已至德绿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間退渗,已是汗流浹背移稳。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留会油,地道東北人个粱。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像翻翩,于是被迫代替她去往敵國(guó)和親都许。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容