Android實現二值點陣圖識別

Robod丶發表於2020-09-16

好好學習,天天向上

本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star

前言

我這幾天在做一個東西,就是一張像二維碼這樣的 n*n 的只有兩種顏色的點陣圖,識別出哪個方塊是深色的,哪個方塊是淺色的。就像下面這張圖?

我一開始想的是,既然是影像識別,那不是OpenCV嘛。但是我不會呀,所以就開始研究,發現Android要使用OpenCV還涉及到JNI,NDK。算了算了好複雜,還是直接在網上找現成的輪子吧。找了一圈後,發現沒有找到...既然沒有輪子,那就自己造?

然後想來想去,發現被自己蠢哭了,這個貌似不需要用到OpenCV吧,是我把問題想複雜了。然後開始第二種方案:比如這張圖是 24*24 的,那我就把這樣圖縮小到24px * 24px,那不就正好一個畫素對應一個格子嘛,哈哈哈✌。但最後還是淘汰了這個方案,因為我最終要實現的是拍照再裁切成肉眼的1:1的照片進行識別,因為肉眼裁切或者拍照的時候手機歪了一點點,很難做到正好1:1。這樣的話壓縮到一個格子對應一個畫素這樣高的精度很大可能性會造成誤差。

然後方案就進化到了第三代,不去進行壓縮了,直接計算出每個格子中心畫素的座標,然後判斷色值就可以得到這個格子是什麼顏色的了。因為不壓縮的話每個格子就會佔很多個畫素,就算圖片不是正好的1:1,也不太會出現誤差。

正文

廢話了一大堆,介紹完方案就來說一下具體實現吧。首先需要明確一個問題:如何去計算某個格子的中心畫素的位置,用一個公式就可以得出:

(2 * n+1) * length /(2 * num)

簡單解釋一下:當知道一條邊的長度和格子數後就可以計算出每個格子佔多少個畫素 length/num,求某一個格子的中心座標,比如第10個格子,因為從0開始的。所以就是 (2*(10-1)+1) 個半個格子的長度。

先灰度圖片,目的是減少誤差,然後將Bitmap圖片用一個二維陣列表示,二維陣列的每個元素的值就是對應圖片中點的色值。然後通過計算每個方格中心點的座標,將值從二維陣列中取出,放入存放每個方格中心點色值的陣列newPx中。最後再遍歷一遍newPx,當大於某個值就說明是該方塊是白色的,小於某個值則說明該方塊是黑色的。那麼這個某個值是多少呢,就是所有方格中心畫素色值最大值與最小值的平均值。

原理介紹完了,完整程式碼如下:

/**
 * 展示圖片
 * @param imagePath 圖片的路徑
 */
private void displayImage(String imagePath) {
    if (imagePath != null) {
        Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
        Bitmap greyBitmap = convertGreyImg(bitmap);
        String pixels = getBitString(greyBitmap, gridNum);
        picturePixels.setText(pixels);
    } else {
        Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
    }
}

/**
 * 獲取轉化後的表示方格顏色的字串,黑色用 ● 表示,白色用 ○ 表示
 * @param img
 * @param num 方格數,總方格數為num * num
 * @return
 */
private String getBitString(Bitmap img, int num) {
    int width = img.getWidth();//原影像寬度
    int height = img.getHeight();//原影像高度
    int[] oldPx = new int[width * height];//用來儲存原圖每個畫素點的顏色資訊
    int[] newPx = new int[num * num];     //用來存放每個方格中心點畫素顏色值
    int index = 0;
    img.getPixels(oldPx, 0, width, 0, 0, width, height);//獲取原圖中的畫素資訊
    int[][] oldPxTwo = twoArray(oldPx,width,height);  //原圖每個畫素點顏色資訊的二維陣列
    int minValue = Integer.MAX_VALUE;
    int maxValue = Integer.MIN_VALUE;
    //迴圈
    for (int i = 0; i < num; i++) {
        int row = getCoordinate(i,width,num);
        for (int j = 0; j < num; j++) {
            int col = getCoordinate(j,height,num);
            minValue = Math.min(minValue,oldPxTwo[row][col]);
            maxValue = Math.max(maxValue,oldPxTwo[row][col]);
            newPx[index++] = oldPxTwo[row][col];
        }
    }
    StringBuilder pixels = new StringBuilder();
    int middleValue = (minValue + maxValue)/2;
    for (int i = 0; i < newPx.length; i++) {
        if (i>0 && i%num==0) pixels.append("\n");
        if (newPx[i] > middleValue) {
            pixels.append("○");
        } else {
            pixels.append("●");
        }
    }
    return pixels.toString();
}

/**
 * 獲取方塊中心點 橫/縱 座標
 * @param n         第幾個方塊,從0開始
 * @param length    橫向或者縱向的長度
 * @param num      橫向或者縱向方塊數
 * @return          座標值
 */
private int getCoordinate(int n,int length,int num) {
    return (2*n+1)*length /(2*num);
}

/**
 * 一維陣列轉化為二維陣列
 * @param arr
 * @param width   縱向的方塊數,多少行
 * @param height
 * @return
 */
public int[][] twoArray(int[] arr,int width,int height) {
    int[][] result = new int[height][width];
    int k = 0;
    for (int i = 0;i<height;i++) {
        for (int j = 0;j<width;j++) {
            result[i][j] = arr[k++];
        }
    }
    return result;
}

/**
 * 將彩色圖轉換為灰度圖
 * @param img 點陣圖
 * @return 返回轉換好的點陣圖
 */
public Bitmap convertGreyImg(Bitmap img) {
    int width = img.getWidth();         //獲取點陣圖的寬
    int height = img.getHeight();       //獲取點陣圖的高

    int[] pixels = new int[width * height]; //通過點陣圖的大小建立畫素點陣列

    img.getPixels(pixels, 0, width, 0, 0, width, height);
    int alpha = 0xFF << 24;
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            int grey = pixels[width * i + j];

            int red = ((grey & 0x00FF0000) >> 16);
            int green = ((grey & 0x0000FF00) >> 8);
            int blue = (grey & 0x000000FF);

            grey = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11);
            grey = alpha | (grey << 16) | (grey << 8) | grey;
            pixels[width * i + j] = grey;
        }
    }
    Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    result.setPixels(pixels, 0, width, 0, 0, width, height);
    return result;
}

這個轉化為灰度圖的方法是我在網上找的。

展示一下效果:

總結

好了,到這裡就介紹完了,聽我介紹完是不是覺得挺簡單的。因為本篇文章主要是講如何講如何識別點陣圖,所以呼叫相簿等內容就沒有說,其實我這個Demo呼叫相簿的程式碼也是直接從《第一行程式碼》中拿來用的,不是自己寫的。程式碼還可以再優化一下的,比如一位陣列轉二維陣列可以和灰度圖片一起實現,不過這些就留給小夥伴們自己去實現啦!

都看完了,來個 "" "" "" 鼓勵一下我唄...

本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star

相關文章