Android Bitmap亮度调节、灰度化、二值化、相似距离实现

2,304 阅读16分钟

前言

计算机图像处理是非常挑战的领域,图片算法有很多种,作为非专业的Android开发者,我们除了利用第三方算法库外,还可以自己实现一些效果。当然,opencv可以处理很多图像。但是在一些简单的场景中,其实不需要open cv这么强大的东西,我们完全可以自行实现一些效果。

我们以常用的四个场景展开

  • 灰度化:彩色图片转灰白图片
  • 亮度调节:通过一些手段调高图片亮度
  • 二值化:一般用于目标检测和目标追踪、汉明距离计算,当然,还可以用于图像反选等场景。
  • pHash相似度计算:用于相似照片检索

下面,是本篇实现的效果。

WechatIMG6107.jpg

亮度调节

亮度调节其实有两个思路,一个是将图片的颜色格式转为其他格式,如hsl、hsv,其中HSL颜色格式在之前的点阵体文章中谈及过,我们只需要将L分量设置为50%,就能实现最高亮度。

企业微信20240330-171010@2x.png

不过,这种转换相对来说还是过于复杂,因为调整之后,还需要转为rgb格式。

其实,我们知道,如果red-green-blue 同步递增,其最终颜色是偏向白色的,白色自然是亮色,因此我们同步调整red-green-blue色值,理论上图片亮度就会显著提高。

这里我们可以使用Color Matrix来实现,我们可以看到 4x5的矩阵,色值增量是最后一列 。

注意:4x5 的矩阵是提供更多的一列,方便矩阵加法运算

下面是ColorMatrix 单一像素转换矩阵计算,当然,实际绘制时所有的像素都会参与运算。

企业微信20240331-074959@2x.png

然而,ColorMatrix 并没有提供修改最后一列数据的方法,不过没关系,我们自定义矩阵,设置进去即可,当然,我们可以不用关注alpha,只需要给red\green\blue三个通道增加50即可

final float[] a = new float[20];
a[0] = a[6] = a[12] = a[18] = 1;
a[4] = a[9] = a[14] = 50;  // red \ green \ blue 色值增加 50
ColorMatrix cm = new ColorMatrix(a);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);

注意:另一种方式是利用LightingColorFilter也可以实现亮度增加,不过话说回来,LightingColorFilter屏蔽了一些矩阵操作,实际上我觉得你应该按照本篇的方法,这样你就能明明白白知道为什么是第5列是增量了。

展示效果如下:

从下面右侧的一列我们可以看到,这种亮度调节是有效的,那能不能单独调节每个颜色通道呢,答案是可以的,但是,这种情况只会增强其中某一种颜色的亮度。

企业微信20240330-172657@2x.png

灰度化

实际上,灰度化应用也很广泛,实现灰度效果的方式很多,比如HSL、HSV、YUV颜色格式,其中,HSV和HSL只需要将色彩饱和度设置为0即可实现灰度化。

企业微信20240330-173448@2x.png

另一种可行的方案是,利用YUV转换时的30-59-11公式,当然下面是0.299-0.587-0.114,我们转换图像计算出Y分量之后,不用关注U、V分量,或者U=0,V=0时就是灰度图,让RED=GREEN=BLUE=Y 分量即可实现灰度化。

    Y =  0.299*R + 0.587*G + 0.114*B;
    U = -0.169*R - 0.331*G + 0.5 *B;
    V =  0.5  *R - 0.419*G - 0.081*B;
    
YUV转RGB:

    R = Y + 1.4075 * V;  
    G = Y - 0.3455 * U - 0.7169*V;  
    B = Y + 1.779 * U;  

不过,如果能使用矩阵那自然还得使用矩阵,使用30-59-11公式修改单个像素实在是太慢了,这里我们选择Color Matrix实现,将饱和度设置为0.

ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);

不过,话说回来,30-59-11公式也可以用,但是矩阵肯定性能更好一点,下面,我们把30-59-11公式使用的机会留给二值化。

二值化

二值化是一种非常重要的图像,比如服装染色、目标检测和目标跟踪、人物环境替换、动作捕捉等,因为只有两种颜色,相比来说可以剔除很多干扰因素。

亮度分界二值化算法

下面,我们利用颜色亮度为128分界实现二值化,小于128的强制设置为黑色,大于128的设置为白色,简单粗暴,但是受限于明暗度的问题,一些重要的细节可能被忽视掉。

另外,我们使用 (red * 38 + green * 75 + blue * 15) >> 7 来减少浮点运算

public static Bitmap grayBitmap2BinaryBitmap(Bitmap graymap, boolean isReverse) {
    //得到图形的宽度和长度
    int width = graymap.getWidth();
    int height = graymap.getHeight();
    //创建二值化图像
    Bitmap binarymap = graymap.copy(Bitmap.Config.ARGB_8888, true);
    //依次循环,对图像的像素进行处理
    for (int i = 0; i < width; i++) {
        for (int j = 0; j < height; j++) {
            //得到当前像素的值
            int col = binarymap.getPixel(i, j);
            //得到alpha通道的值
            int alpha = Color.alpha(col);
            //得到图像的像素RGB的值
            int red = Color.red(col);
            int green = Color.green(col);
            int blue = Color.blue(col);
            // 用公式X = 0.3×R+0.59×G+0.11×B计算出X代替原来的RGB
            //int gray = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11);
            int gray = (red * 38 + green * 75 + blue * 15) >> 7;  //降低浮点运算

            //对图像进行二值化处理
            if (gray > 128) {
                gray = isReverse ? 0xFF000000 : 0xFFFFFFFF;
            } else {
                gray = isReverse ? 0xFFFFFFFF : 0xFF000000;
            }
            //设置新图像的当前像素值
            binarymap.setPixel(i, j, gray);
        }
    }
    return binarymap;
}

效果

企业微信20240330-175147@2x.png

平均值二值化算法

但是128是经验值,理论上还可以选择平均值,这样可能选择出整体图片的亮度平均,不过,有一定的不确定性就是,色彩占比越小,丢失的细节可能越多。

/**
 * 平均灰度算法获取二值图
 *
 * @param srcBitmap 图像像素数组地址( ARGB 格式)
 * @return Bitmap
 */
public static Bitmap grayAverageBitmap2BinaryBitmap(Bitmap srcBitmap) {
    int width = srcBitmap.getWidth();
    int height = srcBitmap.getHeight();

    double pixel_total = width * height; // 像素总数
    if (pixel_total == 0) return null;

    Bitmap bitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);

    long sum = 0;  // 总灰度
    int threshold = 0;    // 阈值

    for (int i = 0; i < pixel_total; i++) {

        int x = i % width;
        int y = i / width;

        int pixel = bitmap.getPixel(x, y);

        // 分离三原色及透明度
        int alpha = Color.alpha(pixel);
        int red = Color.red(pixel);
        int green = Color.green(pixel);
        int blue = Color.blue(pixel);

        int gray = (red * 38 + green * 75 + blue * 15) >> 7;
        if (alpha == 0 && gray == 0) {
            gray = 0xFF;
        }

        if(gray > 0xFF){
            gray = 0xFF;
        }
        bitmap.setPixel(x, y, gray | 0xFFFFFF00);
        sum += gray;
    }
    // 计算平均灰度
    threshold = (int) (sum / pixel_total);

    for (int i = 0; i < pixel_total; i++) {

        int x = i % width;
        int y = i / width;
        int pixel = bitmap.getPixel(x, y) & 0x000000FF;
        int color = pixel <= threshold ? 0xFF000000 : 0xFFFFFFFF;
        bitmap.setPixel(x, y, color);
    }
    return bitmap;
}

不过,上述代码中我们setPixel进行了与0xFFFFFF00的位或运算,主要原因是Bitmap是ARGB_8888,不满足的话存储数据可能异常,因此有必要转换一下,不过读取的时候需要0x000000FF位与运算。

企业微信20240330-180555@2x.png

为了优化细节丢失的问题,日本学者提出了OTSU算法,具体原理是统计0-255每种阶梯的亮度,然后通过方差计算出相关权重。

OTSU 二值化算法


/**
 * OTSU 算法获取二值图
 *
 * @param srcBitmap 图像像素数组地址( ARGB 格式)
 * @return 二值图像素数组地址
 */
public static Bitmap bitmap2OTSUBitmap(Bitmap srcBitmap) {

    int width = srcBitmap.getWidth();
    int height = srcBitmap.getHeight();

    double pixel_total = width * height; // 像素总数
    if (pixel_total == 0) return null;

    Bitmap bitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);


    long sum1 = 0;  // 总灰度值
    long sumB = 0;  // 背景总灰度值
    double wB = 0.0;        // 背景像素点比例
    double wF = 0.0;        // 前景像素点比例
    double mB = 0.0;        // 背景平均灰度值
    double mF = 0.0;        // 前景平均灰度值
    double max_g = 0.0;     // 最大类间方差
    double g = 0.0;         // 类间方差
    int threshold = 0;    // 阈值
    double[] histogram = new double[256];// 灰度直方图,下标是灰度值,保存内容是灰度值对应的像素点总数

    // 获取灰度直方图和总灰度
    for (int i = 0; i < pixel_total; i++) {
        int x = i % width;
        int y = i / width;
        int pixel = bitmap.getPixel(x, y);
        // 分离三原色及透明度
        int alpha = Color.alpha(pixel);
        int red = Color.red(pixel);
        int green = Color.green(pixel);
        int blue = Color.blue(pixel);

        int gray = (red * 38 + green * 75 + blue * 15) >> 7;
        if (alpha == 0 && gray == 0) {
            gray = 0xFF;
        }
        if(gray > 0xFF){
            gray = 0xFF;
        }
        bitmap.setPixel(x, y, gray | 0xFFFFFF00);

        // 计算灰度直方图分布,Histogram 数组下标是灰度值,保存内容是灰度值对应像素点数
        histogram[gray]++;
        sum1 += gray;
    }

    // OTSU 算法
    for (int i = 0; i < 256; i++) {
        wB = wB + histogram[i]; // 这里不算比例,减少运算,不会影响求 T
        wF = pixel_total - wB;
        if (wB == 0 || wF == 0) {
            continue;
        }
        sumB = (long) (sumB + i * histogram[i]);
        mB = sumB / wB;
        mF = (sum1 - sumB) / wF;
        g = wB * wF * (mB - mF) * (mB - mF);
        if (g >= max_g) {
            threshold = i;
            max_g = g;
        }
    }

    for (int i = 0; i < pixel_total; i++) {
        int x = i % width;
        int y = i / width;
        int pixel = bitmap.getPixel(x, y) & 0x000000FF;
        int color = pixel <= threshold ? 0xFF000000 : 0xFFFFFFFF;
        bitmap.setPixel(x, y, color);
    }

    return bitmap;
}

效果如下

企业微信20240330-183600@2x.png

从上图我们看到OTSU算法保留的细节还是比较多的。

反向二值化

反向二值化其实只是很好将黑白颜色调换即可,上面的逻辑中我们其实已经实现了,通过参数控制就能实现不同的反向二值化图片。

if (gray > 128) {
    gray = isReverse ? 0xFF000000 : 0xFFFFFFFF;
} else {
    gray = isReverse ? 0xFFFFFFFF : 0xFF000000;
}

企业微信20240330-184454@2x.png

算法评价

在二值化图片中,细节其实很重要,如何尽可能保留细节,其实也有难度,因此常规的方式就是调整算法,然后对比选出最佳的图片。

那么如何判定细节的充足程度呢,有这样一套算法叫做“汉明距离”,很多系统工具类产品中都有这种算法,用于高级别的相似图片检索,我们可以利用这种算法,对比灰度图和生成的二值化图片,如果距离越短,意味着相似度越高。

这里我们可以参考 《pHash》的实现,通过汉明距离比较图片。

本篇代码

下面是完整的本篇代码

public class BitmapUtil {

    public static Bitmap bitmap2GrayBitmap(Bitmap bmSrc) {
        // 得到图片的长和宽
        int width = bmSrc.getWidth();
        int height = bmSrc.getHeight();
        // 创建目标灰度图像
        Bitmap bmpGray = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
        // 创建画布
        Canvas c = new Canvas(bmpGray);
        Paint paint = new Paint();
        ColorMatrix cm = new ColorMatrix();
        cm.setSaturation(0);
        ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
        paint.setColorFilter(f);
        c.drawBitmap(bmSrc, 0, 0, paint);
        return bmpGray;
    }

    /**
     * 提高图片亮度
     *
     * @param bitmap
     * @return
     */
    public static Bitmap bitmap2LightBitmap(Bitmap bitmap) {
        //得到图像的宽度和长度
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Bitmap outputBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        //依次循环对图像的像素进行处理
        final float[] a = new float[20];
        a[0] = a[6] = a[12] = a[18] = 1;
        a[4] = a[9] = a[14] = 50;
        ColorMatrix cm = new ColorMatrix(a);
        Paint paint = new Paint();
        ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
        paint.setColorFilter(f);
        Canvas c = new Canvas(outputBitmap);
        c.drawBitmap(bitmap, 0, 0, paint);
        return outputBitmap;
    }

    public static Bitmap grayBitmap2BinaryBitmap(Bitmap graymap, boolean isReverse) {
        //得到图形的宽度和长度
        int width = graymap.getWidth();
        int height = graymap.getHeight();
        //创建二值化图像
        Bitmap binarymap = graymap.copy(Bitmap.Config.ARGB_8888, true);
        //依次循环,对图像的像素进行处理
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                //得到当前像素的值
                int col = binarymap.getPixel(i, j);
                //得到alpha通道的值
                int alpha = Color.alpha(col);
                //得到图像的像素RGB的值
                int red = Color.red(col);
                int green = Color.green(col);
                int blue = Color.blue(col);
                // 用公式X = 0.3×R+0.59×G+0.11×B计算出X代替原来的RGB
                //int gray = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11);
                int gray = (red * 38 + green * 75 + blue * 15) >> 7;  //降低浮点运算

                //对图像进行二值化处理
                if (gray > 128) {
                    gray = isReverse ? 0xFF000000 : 0xFFFFFFFF;
                } else {
                    gray = isReverse ? 0xFFFFFFFF : 0xFF000000;
                }
                //设置新图像的当前像素值
                binarymap.setPixel(i, j, gray);
            }
        }
        return binarymap;
    }

    /**
     * 平均灰度算法获取二值图
     *
     * @param srcBitmap 图像像素数组地址( ARGB 格式)
     * @return Bitmap
     */
    public static Bitmap grayAverageBitmap2BinaryBitmap(Bitmap srcBitmap) {
        int width = srcBitmap.getWidth();
        int height = srcBitmap.getHeight();

        double pixel_total = width * height; // 像素总数
        if (pixel_total == 0) return null;

        Bitmap bitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);

        long sum = 0;  // 总灰度
        int threshold = 0;    // 阈值

        for (int i = 0; i < pixel_total; i++) {

            int x = i % width;
            int y = i / width;

            int pixel = bitmap.getPixel(x, y);

            // 分离三原色及透明度
            int alpha = Color.alpha(pixel);
            int red = Color.red(pixel);
            int green = Color.green(pixel);
            int blue = Color.blue(pixel);

            int gray = (red * 38 + green * 75 + blue * 15) >> 7;
            if (alpha == 0 && gray == 0) {
                gray = 0xFF;
            }

            if(gray > 0xFF){
                gray = 0xFF;
            }
            bitmap.setPixel(x, y, gray | 0xFFFFFF00);
            sum += gray;
        }
        // 计算平均灰度
        threshold = (int) (sum / pixel_total);

        for (int i = 0; i < pixel_total; i++) {

            int x = i % width;
            int y = i / width;
            int pixel = bitmap.getPixel(x, y) & 0x000000FF;
            int color = pixel <= threshold ? 0xFF000000 : 0xFFFFFFFF;
            bitmap.setPixel(x, y, color);
        }
        return bitmap;
    }

    /**
     * OTSU 算法获取二值图
     *
     * @param srcBitmap 图像像素数组地址( ARGB 格式)
     * @return 二值图像素数组地址
     */
    public static Bitmap bitmap2OTSUBitmap(Bitmap srcBitmap) {

        int width = srcBitmap.getWidth();
        int height = srcBitmap.getHeight();

        double pixel_total = width * height; // 像素总数
        if (pixel_total == 0) return null;

        Bitmap bitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);


        long sum1 = 0;  // 总灰度值
        long sumB = 0;  // 背景总灰度值
        double wB = 0.0;        // 背景像素点比例
        double wF = 0.0;        // 前景像素点比例
        double mB = 0.0;        // 背景平均灰度值
        double mF = 0.0;        // 前景平均灰度值
        double max_g = 0.0;     // 最大类间方差
        double g = 0.0;         // 类间方差
        int threshold = 0;    // 阈值
        double[] histogram = new double[256];// 灰度直方图,下标是灰度值,保存内容是灰度值对应的像素点总数

        // 获取灰度直方图和总灰度
        for (int i = 0; i < pixel_total; i++) {
            int x = i % width;
            int y = i / width;
            int pixel = bitmap.getPixel(x, y);
            // 分离三原色及透明度
            int alpha = Color.alpha(pixel);
            int red = Color.red(pixel);
            int green = Color.green(pixel);
            int blue = Color.blue(pixel);

            int gray = (red * 38 + green * 75 + blue * 15) >> 7;
            if (alpha == 0 && gray == 0) {
                gray = 0xFF;
            }
            if(gray > 0xFF){
                gray = 0xFF;
            }
            bitmap.setPixel(x, y, gray | 0xFFFFFF00);

            // 计算灰度直方图分布,Histogram 数组下标是灰度值,保存内容是灰度值对应像素点数
            histogram[gray]++;
            sum1 += gray;
        }

        // OTSU 算法
        for (int i = 0; i < 256; i++) {
            wB = wB + histogram[i]; // 这里不算比例,减少运算,不会影响求 T
            wF = pixel_total - wB;
            if (wB == 0 || wF == 0) {
                continue;
            }
            sumB = (long) (sumB + i * histogram[i]);
            mB = sumB / wB;
            mF = (sum1 - sumB) / wF;
            g = wB * wF * (mB - mF) * (mB - mF);
            if (g >= max_g) {
                threshold = i;
                max_g = g;
            }
        }

        for (int i = 0; i < pixel_total; i++) {
            int x = i % width;
            int y = i / width;
            int pixel = bitmap.getPixel(x, y) & 0x000000FF;
            int color = pixel <= threshold ? 0xFF000000 : 0xFFFFFFFF;
            bitmap.setPixel(x, y, color);
        }

        return bitmap;
    }
}

pHash相似距离

本篇我们提到图片相似度算法,其实很多系统工具产品都有类似的算法实现,知晓这种算法之后,你也可以自己检索出手机中的相似照片,以便于清理磁盘空间。

这里我们也将代码贴出来,供大家参考。

pHash 算法源码

下面是完整的源码

public class pHash {
  
  /**
   * pHash算法流程
   * 1.缩小图片,最佳大小为32*32
   * 2.转化成灰度图
   * 3.转化为DCT图
   * 4.取dct图左上角8*8的范围
   * 5.计算所有点的平均值
   * 6.8*8的范围刚好64个点,计算出64位的图片指纹,如果小于平均值记为0,反之记为1,指纹顺序可以随机,但是每张图片的指纹的顺序应该保持一致
   * 7.最后比较两张图片指纹的汉明距离,越小表示越相识
   * 
   */

    //获取指纹,long刚好64位,方便存放
    public static long dctImageHash(Bitmap src, boolean recycle) throws IOException {
        //由于计算dct需要图片长宽相等,所以统一取32
        int length = 32;

        //缩放图片
        Bitmap bitmap = scaleBitmap(src, recycle, length);

        //获取灰度图
        int[] pixels = createGrayImage(bitmap, length);

        //先获得32*32的dct,再取dct左上角8*8的区域
        return computeHash(DCT8(pixels, length));
    }

    private static int[] createGrayImage(Bitmap src, int length) {
        int[] pixels = new int[length * length];
        src.getPixels(pixels, 0, length, 0, 0, length, length);
        src.recycle();
        for (int i = 0; i < pixels.length; i++) {
            int gray = computeGray(pixels[i]);
            pixels[i] = Color.rgb(gray, gray, gray);
        }
        return pixels;
    }

    //缩放成宽高一样的图片
    private static Bitmap scaleBitmap(Bitmap src, boolean recycle, float length) throws IOException {
        if (src == null) {
            throw new IOException("invalid image");
        }
        int width = src.getWidth();
        int height = src.getHeight();
        if (width == 0 || height == 0) {
            throw new IOException("invalid image");
        }
        Matrix matrix = new Matrix();
        matrix.postScale(length / width, length / height);
        Bitmap bitmap = Bitmap.createBitmap(src, 0, 0, width, height, matrix, false);
        if (recycle) {
            src.recycle();
        }
        return bitmap;
    }

    //计算hash值
    private static long computeHash(double[] pxs) {
        double t = 0;
        for (double i : pxs) {
            t += i;
        }
        double median = t / pxs.length;
        long one = 0x0000000000000001;
        long hash = 0x0000000000000000;
        for (double current : pxs) {
            if (current > median)
                hash |= one;
            one = one << 1;
        }
        return hash;
    }

    /**
     *计算灰度值
     * 计算公式Gray = R*0.299 + G*0.587 + B*0.114
     * 由于浮点数运算性能较低,转换成位移运算
     * 向右每位移一位,相当于除以2
     * 
     */
    private static int computeGray(int pixel) {
        int red = Color.red(pixel);
        int green = Color.green(pixel);
        int blue = Color.blue(pixel);
        return (red * 38 + green * 75 + blue * 15) >> 7;
    }
    
    //取dct图左上角8*8的区域
    private static double[] DCT8(int[] pix, int n) {
        double[][] iMatrix = DCT(pix, n);

        double px[] = new double[8 * 8];
        for (int i = 0; i < 8; i++) {
            System.arraycopy(iMatrix[i], 0, px, i * 8, 8);
        }
        return px;
    }

    /**
     * 离散余弦变换
     * 
     * 计算公式为:系数矩阵*图片矩阵*转置系数矩阵
     *
     * @param pix 原图像的数据矩阵
     * @param n   原图像(n*n)
     * @return 变换后的矩阵数组
     */
    private static double[][] DCT(int[] pix, int n) {
        double[][] iMatrix = new double[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                iMatrix[i][j] = (double) (pix[i * n + j]);
            }
        }
        double[][] quotient = coefficient(n);   //求系数矩阵
        double[][] quotientT = transposingMatrix(quotient, n);  //转置系数矩阵

        double[][] temp;
        temp = matrixMultiply(quotient, iMatrix, n);
        iMatrix = matrixMultiply(temp, quotientT, n);
        return iMatrix;
    }

    /**
     * 矩阵转置
     *
     * @param matrix 原矩阵
     * @param n      矩阵(n*n)
     * @return 转置后的矩阵
     */
    private static double[][] transposingMatrix(double[][] matrix, int n) {
        double nMatrix[][] = new double[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                nMatrix[i][j] = matrix[j][i];
            }
        }
        return nMatrix;
    }

    /**
     * 求离散余弦变换的系数矩阵
     *
     * @param n n*n矩阵的大小
     * @return 系数矩阵
     */
    private static double[][] coefficient(int n) {
        double[][] coeff = new double[n][n];
        double sqrt = Math.sqrt(1.0 / n);
        double sqrt1 = Math.sqrt(2.0 / n);
        for (int i = 0; i < n; i++) {
            coeff[0][i] = sqrt;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < n; j++) {
                coeff[i][j] = sqrt1 * Math.cos(i * Math.PI * (j + 0.5) / n);
            }
        }

        return coeff;
    }

    /**
     * 矩阵相乘
     *
     * @param A 矩阵A
     * @param B 矩阵B
     * @param n 矩阵的大小n*n
     * @return 结果矩阵
     */
    private static double[][] matrixMultiply(double[][] A, double[][] B, int n) {
        double nMatrix[][] = new double[n][n];
        double t;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                t = 0;
                for (int k = 0; k < n; k++) {
                    t += A[i][k] * B[k][j];
                }
                nMatrix[i][j] = t;
            }
        }
        return nMatrix;
    }

    /**
     * 计算两个图片指纹的汉明距离
     *
     * @param hash1 指纹1
     * @param hash2 指纹2
     * @return 返回汉明距离 也就是64位long型不相同的位的个数
     */
    public static int hammingDistance(long hash1, long hash2) {
        long x = hash1 ^ hash2;
        final long m1 = 0x5555555555555555L;
        final long m2 = 0x3333333333333333L;
        final long h01 = 0x0101010101010101L;
        final long m4 = 0x0f0f0f0f0f0f0f0fL;
        x -= (x >> 1) & m1;
        x = (x & m2) + ((x >> 2) & m2);
        x = (x + (x >> 4)) & m4;
        return (int) ((x * h01) >> 56);
    }
}

pHash 用法

下面是pHash的用法,具体流程很简单,我们主要关注两张图片之间的距离

val dctImageHash1 = pHash.dctImageHash(bitmap1, true)
val dctImageHash2 = pHash.dctImageHash(bitmap2, true)

val distance = pHash.hammingDistance(dctImageHash1, dctImageHash2)
Log.d(TAG, "dis2 =  $distance")

当然,提起相似度,其实还有个特征筛选法,对于同一台手机,如果拍照的时间间隔足够小,那么受到光照、仰角等因素影响其实会很小,进而两张图之间的大小其实是差不多的。

特征筛选法可以通过 【时间] + 【大小】 + 【扩展名】实现相似图片的过滤,不过话说回来,pHash性能虽然差,但是精度够高,对于转码图片(大小可能会变化很大),也有较高的精确到,因此,pHash比较适合后台扫描筛选,而特征筛选法比较适合快速筛选的场景。

另外,这种算法还有个优点是,对于旋转了N*90度的图片,仍然可以计算出相似度。

总结

到这里本篇就结束了,通过本篇我们可以了解到一些比较有价值的图片效果实现,分别是亮度调节、灰度化、二值化。

在后续的内容中,我们也会继续关注Canvas相关的绘制,如AGSL、RenderNode,我们文章的路线会向Jet Compose转移,希望大家继续关注。