OpenCv 的基础学习目前先告一段落了,后面我们要开始手写一些常用的效果,且都是基于 Android 平台的。希望我们有一定的 C++ 和 JNI 基础,如果我们对这块知识有所欠缺,大家不妨看看这个:Android进阶之旅(JNI基础实战)

我们可能会忍不住问,做 android 应用层开发,学习图形图像处理到底有啥好处?首先不知我们是否有在 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;
  }

其次学习 opencv 不能只停留在其 api 的调用上,我们必须了解其内部的实现的原理,最好还要能手写实现。最后学习图像图形处理,也有利于我们后面学习音视频的开发,能够帮助我们更加熟悉 NDK 开发,包括我们自己去阅读 android native 层的源码等等,总之好处还是有很多的。

接下来我们就以 QQ 发说说处理图片的效果为例,来手写实现部分效果。有些效果在之前的文章中已有讲到,这里就不再给代码了,我们可以参考:《图形图像处理 - Android 滤镜效果》 搭建 android ndk 开发环境和集成 opencv 大

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 插值法

我们经常会将某种尺寸的图像转换为其他尺寸的图像,如果放大或者缩小图片的尺寸,笼统来说的话,可以使用OpenCV为我们提供的如下两种方式:

  1. resize函数。这是最直接的方式,
  2. pyrUp( )、pyrDown( )函数。即图像金字塔相关的两个函数,对图像进行向上采样,向下采样的操作。

pyrUp、pyrDown 其实和专门用作放大缩小图像尺寸的 resize 在功能上差不多,披着图像金字塔的皮,说白了还是在对图像进行放大和缩小操作。另外需要指出的是,pyrUp、pyrDown 在 OpenCV 的 imgproc 模块中的 Image Filtering 子模块里。而 resize 在 imgproc 模块的 Geometric Image Transformations 子模块里。关于 pyrUp、pyrDown 在 opencv 基础学习中已有详细介绍,这里就不再反复了。

resize( ) 为 OpenCV 中专职调整图像大小的函数。此函数将源图像精确地转换为指定尺寸的目标图像。如果源图像中设置了 ROI(Region Of Interest ,感兴趣区域),那么 resize( ) 函数会对源图像的 ROI 区域进行调整图像尺寸的操作,来输出到目标图像中。若目标图像中已经设置 ROI 区域,不难理解 resize( ) 将会对源图像进行尺寸调整并填充到目标图像的 ROI 中。很多时候,我们并不用考虑第二个参数dst的初始图像尺寸和类型(即直接定义一个Mat类型,不用对其初始化),因为其尺寸和类型可以由 src,dsize,fx 和 fy 这其他的几个参数来确定。可选的方式为:

  • INTER_NEAREST - 最近邻插值
  • INTER_LINEAR - 线性插值(默认值)
  • INTER_AREA - 区域插值(利用像素区域关系的重采样插值)
  • INTER_CUBIC –三次样条插值(超过4×4像素邻域内的双三次插值)
  • INTER_LANCZOS4 -Lanczos插值(超过8×8像素邻域的Lanczos插值)
  1. 最近邻插值
    最简单的图像缩放算法就是最近邻插值。顾名思义,就是将目标图像各点的像素值设为源图像中与其最近的点。算法优点在与简单、速度快。

如下图所示,一个44的图片缩放为88的图片。步骤:

  • 生成一张空白的8*8的图片,然后在缩放位置填充原始图片值(可以这么理解)
  • 在图片的未填充区域(黑色部分),填充为原有图片最近的位置的像素值。
    图形图像处理 - 手写 QQ 说说图片处理效果_QQ
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,目标图像dst的大小是4×4,考虑dst中(1,1)点像素对应原始图像像素点的位置为(0.75,0.75),如果使用最近邻算法来计算,原始图像的位置在浮点数取整后为坐标(0,0)。

上面这样粗暴的计算会丢失很多信息,考虑(0.75,0.75)这个信息,它表示在原始图像中的坐标位置,相比较取(0,0)点,(0.75,0.75)貌似更接近(1,1)点,那如果将最近邻算法中的取整方式改为cvRound(四舍五入)的方式取(1,1)点,同样会有丢的信息,即丢失了“0.25”部分的(0,0)点、(1,0)点和(0,1)点。

可以看到,dst图像上(X,Y)对应到src图像上的点,最好是根据计算出的浮点数坐标,按照百分比各取四周的像素点的部分。
如下图:
图形图像处理 - 手写 QQ 说说图片处理效果_QQ_02
双线性插值的原理相类似,这里不写双线性插值计算点坐标的方法,容易把思路带跑偏,直接就按照比率权重的思想考虑。将 ( wWX , hHY ) ( wWX , hHY ) 写成 ( x′ + u , y′ + v ) ( x′ + u , y′ + v ) 的形式,表示将 xx 与yy 中的整数和小数分开表示 uvuv 分别代表小数部分。这样,根据权重比率的思想得到计算公式

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

在实际的代码编写中,会有两个问题,一个是图像会发生偏移,另一个是效率问题。

几何中心对齐:

由于计算的图像是离散坐标系,如果使用 (wWX , hHY) (wWX , hHY) 公式来计算,得到的 (X , Y) 值是错误的比率计算而来的,即 (x + 1 , y)、(x , y + 1)、(x + 1 , y + 1)这三组点中,有可能有几个没有参与到比率运算当中,或者这个插值的比率直接是错误的。 例如,src 图像大小是 a×aa×a,dst 图像的大小是 0.5a×0.5a0.5a×0.5a。

根据原始公式计算(wW , hH) (wW , hH)得到(2 , 2)(2 , 2)(注意这不是表示点坐标,而是 x 和 y 对应的比率)如果要计算 dst 点 (0 , 0) 对应的插值结果,由于 (2 , 2) (2 , 2) 是整数,没有小数,所以最后得到 dst 点在 (0 , 0) (0 , 0) 点的像素值就是src图像上在 (0,0)(0,0)点的值。然而,我们想要的 dst 在 (0,0)(0,0)上的结果是应该是有 (0 , 0) (1 , 0) (0 , 1) (1 , 1) 这四个点各自按照 0.5×0.5 的比率加权的结果。 所以我们要将 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))

运算优化:由计算公式可以得知,在计算每一个dst图像中的像素值时会涉及到大量的浮点数运算,性能不佳。可以考虑将浮点数变换成一个整数,即扩大一定的倍数,运算得到的结果再除以这个倍数。举一个简单的例子,计算 0.25×0.75,可以将 0.25 和 0.75 都乘上 8,得到 2×6=12,结果再除以 8282,这样运算的结果与直接计算浮点数没有差别。

在程序中,没有办法取得一个标准的整数,使得两个相互运算的浮点数都变成类似“2”和”6“一样的标准整数,只能取一个适当的值来尽量的减少误差,在源码当中取值为 211211=2048,即 2 的固定幂数,最后结果可以通过用位移来表示除以一个 2 整次幂数,计算速度会有很大的提高。

//双线性插值
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防止数据溢出
        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;
    //宽(列数)
    int iWidthSrc = src.cols;
    //高(行数)
    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);
        //整数部分
        int sy = cvFloor(fy);
        //小数部分
        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;
            }
        }
    }
}