Một số thuật toán tạo hiệu ứng cho ảnh Bitmap trên Android (P1)

Xin chào mọi người!

Trong bài tháng trước mình đã giới thiệu về xử lý ảnh Bitmap trong Android, cách để thao tác tới từng điểm ảnh trong Android. Và tháng này mình sẽ giới thiệu các thuật toán tạo hiệu ứng trên ảnh Bitmap như hiệu ứng đèn noen, emboss, Sketch, TV Screen...

I. CONVOLUTION MATRIX

Convolution Matrix hay còn gọi là Ma trận nhân chập là khái niệm cơ bản trong xử lý tín hiệu số. Các hiệu ứng biến đổi trên ảnh đa phần là sẽ áp dụng một ma trận nhân chập đặc biệt nào đó biến đổi tín hiệu ban đầu (ảnh gốc) thành tín hiệu mong muốn (ảnh sau xử lý)

conv.png

Công thức tính nhân chập:

conv.png

		int A, R, G, B;
        int i, j, k, l;
        int h = inp.getHeight();
        int w = inp.getWidth();
        int[] pixels = new int[w * h];
        int[] pxResult = new int[w * h];
        inp.getPixels(pixels, 0, w, 0, 0, w, h);
        Bitmap bmOut = Bitmap.createBitmap(w, h, inp.getConfig());
        int half_MTS = Matrix_Size / 2;
        int maxH = h - half_MTS;
        int maxW = w - half_MTS;
        int matrixValue; //Convolution Matrix Value
        int pos; //Current Pixel Position
        int conPos; //Convolution Matrix Position
        for (i = half_MTS; i < maxH; i++) {
            for (j = half_MTS; j < maxW; j++) {
                R = G = B = 0;
                pos = i * w + j;
                A = (pixels[pos] >> 24) & 0xFF;
                for (k = -half_MTS; k <= half_MTS; k++) {
                    for (l = -half_MTS; l <= half_MTS; l++) {
                        matrixValue = Matrix[k + half_MTS][l + half_MTS];
                        conPos = (i + k) * w + (j + l);
                        R += ((pixels[conPos] >> 16) & 0xFF) * matrixValue;
                        G += ((pixels[conPos] >> 8) & 0xFF) * matrixValue;
                        B += (pixels[conPos] & 0xFF) * matrixValue;
                    }
                }

                R = normalize(R / Factor + Offset);
                G = normalize(G / Factor + Offset);
                B = normalize(B / Factor + Offset);
                pxResult[pos] = (A << 24) | (R << 16) | (G << 8) | B;
            }
        }
        bmOut.setPixels(pxResult, 0, w, 0, 0, w, h);

II. CÁC HIỆU ỨNG TRÊN ẢNH BITMAP

1. NEON

neon xedap.JPEG

Hiệu ứng đèn neon về mặt bản chất được thực hiện bằng cách dùng toán tử Sobel phát hiện các biên cục bộ của ảnh, sau đó với các biên đã được phát hiện ta tô màu xanh lá của đèn neon cho nó còn lại nền là màu đen, như vậy ảnh thu được sẽ có hiệu ứng đèn neon ở các biên của ảnh.

Toán tử Sobel là 2 nhân chập có ma trận hệ số như sau:

sobel.png

Trong đó:

Hx dùng để phát hiện biên theo hướng ngang
Hy dùng để phát hiện biên theo hướng dọc
		int[] xSobel = new int[]{-1, -2, -1,
                                  0, 0, 0,
                                  1, 2, 1};
        int[] ySobel = new int[]{-1, 0, 1,
                		 		 -2, 0, 2,
               					 -1, 0, 1};

        int index = 0;
        int oriGray = 0;
        int afterGray = 0;
        int maskSize = 3;
        int halfMaskSize = maskSize / 2;

        int xVal = 0;
        int yVal = 0;
        float threshold = 110;
        int i, j, m, n;
        int oriPos, pos;
        int width = inp.getWidth();
        int height = inp.getHeight();
        Bitmap bmOut = Bitmap.createBitmap(width, height, inp.getConfig());
        int[] pixels = new int[width * height];
        inp.getPixels(pixels, 0, width, 0, 0, width, height);
        int[] oriPixels = new int[width * height];
        inp.getPixels(oriPixels, 0, width, 0, 0, width, height);
        for (i = halfMaskSize; i < height - halfMaskSize; i++) {
            for (j = halfMaskSize; j < width - halfMaskSize; j++) {
                index = 0;
                xVal = yVal = 0;
                for (m = -halfMaskSize; m <= halfMaskSize; m++) {
                    for (n = -halfMaskSize; n <= halfMaskSize; n++) {
                        oriPos = (i + m) * width + j + n;
                        oriGray = grayscale(oriPixels[oriPos]);
                        xVal += oriGray * xSobel[index];
                        yVal += oriGray * ySobel[index];
                        index++;
                    }
                }

                afterGray = Math.abs(xVal) + Math.abs(yVal);
                if (afterGray > 255)
                    afterGray = 255;
                if (afterGray < 0)
                    afterGray = 0;

                pos = i * width + j;
                if (afterGray > threshold) {
                    pixels[pos] = Color.rgb(neonR, neonG, neonB);
                } else {
                    pixels[pos] = Color.rgb(1, 1, 1);
                }
            }
        }
        bmOut.setPixels(pixels, 0, width, 0, 0, width, height);

2. EMBOSS

emboss.JPEG

Hiệu ứng emboss là một kỹ thuật trong đồ họa máy tính mà với mỗi điểm ảnh của ảnh gốc được thay thế bởi một điểm ảnh nổi bật hoặc một điểm ảnh chìm. Vùng ảnh có độ tương phản thấp sẽ được thay thế bởi nền xám.

Ảnh sau khi đi qua bộ lọc Emboss sẽ thể hiện tốc độ thay đổi màu tại mỗi nơi của ảnh gốc. Nơi có tốc độ thay đổi màu thấp biến thành nền và nơi có tốc độ thay đổi màu cao biến thành các cạnh nổi.

Ảnh kết quả giống với việc dập nổi một tờ giấy hoặc kim loại vì vậy hiệu ứng này có tên là dập nổi – emboss

Bản chất là dùng ma trận nhân chập với ma trận hệ số như sau:

emboss.png

		//Sử dụng ConvolutionMatrix
	 	int[][] config = new int[][]{
                {-1, -1, 0},
                {-1,  0, 1},
                { 0,  1, 1}
        };

        embossMatrix = new ConvolutionMatrix(config, 1, 128);
        return embossMatrix.convolute(inp);

3. TV SCREEN

Hiệu ứng màn hình TV CRT không dùng nhân chập hay bộ lọc mà dùng thuật toán riêng dưới đây để có được hiệu ứng biến bức ảnh thành các dòng điểm ảnh với ba màu đỏ, xanh lá, xanh da trời như trên một màn hình CRT.

Thuật toán xử lý từng lượt 4 dòng từ trên xuống dưới, hay nói cách khác là xử lý 4 điểm ảnh nằm trên 4 dòng liền nhau. Đọc giá trị R, G, B của 4 điểm ảnh trên cộng lại từng màu sau đó chia cho 4, được giá trị R, G, B trung bình cho 4 điểm. Tiếp theo lần lượt đưa giá trị R,G,B trên vào 3 dòng đầu, dòng 1 chỉ lấy giá trị R đưa vào điểm ảnh kết quả, các giá trị khác bằng 0, tương tự dòng 2 chỉ lấy giá trị G, dòng 3 chỉ lấy giá trị B, còn lại dòng 4 thì các điểm ảnh được giữ nguyên theo ảnh gốc. Kết quả thu được giống như màn hình TV CRT.

 		int R, G, B;
        int i, j, k;
        int w = inp.getWidth();
        int h = inp.getHeight();
        Bitmap bmOut = Bitmap.createBitmap(w, h, inp.getConfig());
        int[] pixels = new int[w * h];
        inp.getPixels(pixels, 0, w, 0, 0, w, h);
        int index;

        for (i = 0; i < w; i++) {
            for (j = 0; j < h; j += gap) {
                R = G = B = 0;
                for (k = 0; k < gap; k++) {
                    index = (j + k) * w + i;
                    if (index < pixels.length) {
                        R += ((pixels[index] >> 16) & 0xFF) / gap;
                        G += ((pixels[index] >> 8) & 0xFF) / gap;
                        B += (pixels[index] & 0xFF) / gap;
                    }
                }

                R = normalize(R);
                G = normalize(G);
                B = normalize(B);

                for (k = 0; k < gap; k++) {
                    index = (j + k) * w + i;
                    if (index < pixels.length) {
                        if (k == 0) {
                            pixels[index] = Color.rgb(R, 0, 0);
                        }
                        if (k == 1) {
                            pixels[index] = Color.rgb(0, G, 0);
                        }
                        if (k == 2) {
                            pixels[index] = Color.rgb(0, 0, B);
                        }
                    }
                }
            }
        }
        bmOut.setPixels(pixels, 0, w, 0, 0, w, h);

4. SKETCH

Thuật toán tạo ảnh dạng vẽ chì thực hiện trên ảnh xám vì vậy cần chuyển ảnh về dạng ảnh xám trước khi xử lý.

Thuật toán sẽ tính toán trên 2 điểm ảnh, một điểm ảnh đang xét và một điểm ảnh nằm kề phía dưới bên phải điểm ảnh đang xét. Chuyển 2 điểm ảnh về dạng ảnh đa mức xám, tính hiệu 2 giá trị đó nếu lớn hơn một ngưỡng cho trước thì đặt giá trị điểm ảnh đang xét là màu chì, ngược lại đặt màu nền là màu trắng.

	 	int w = inp.getWidth();
        int h = inp.getHeight();
        int[] pixels = new int[w * h];
        inp.getPixels(pixels, 0, w, 0, 0, w, h);
        int[] originPixels = new int[w * h];
        inp.getPixels(originPixels, 0, w, 0, 0, w, h);
        int pos;
        int i, j;
        int sketchColor = Color.parseColor("#5c6274");
        Log.v("Sketch Color", sketchColor + "");
        int centerGray;
        int rightBottomIndex;
        int rightBottomGray;
        Bitmap bmOut = Bitmap.createBitmap(w, h, inp.getConfig());
        for (i = 1; i < h - 1; i++) {
            for (j = 1; j < w - 1; j++) {
                pos = i * w + j;
                centerGray = getGray(originPixels[pos]);
                rightBottomIndex = (i + 1) * w + j + 1;
                if (rightBottomIndex < pixels.length) {
                    rightBottomGray = getGray(originPixels[rightBottomIndex]);
                    if (Math.abs(centerGray - rightBottomGray) >= threshold) {
                        pixels[pos] = sketchColor;
                    } else {
                        pixels[pos] = Color.rgb(255, 255, 255);
                    }
                }
            }
        }
        bmOut.setPixels(pixels, 0, w, 0, 0, w, h);

5. INVERT

Thuật toán biến đổi âm bản chỉ dựa vào một điểm ảnh, không phụ thuộc các điểm ảnh xung quanh.

			Y(m,n) = L – X(m,n)

        Với - Y(m,n) là giá trị mức xám kết quả của điểm ảnh sau biến đổi
            - X(m,n) là giá trị mức xám ban đầu của điểm ảnh
            - L là giá trị mức xám tối đa mà một điểm ảnh thể hiện (ảnh 8bit sẽ là 2^8 - 1 = 255)

6. SMOOTHING

Làm trơn ảnh hay làm mịn ảnh làm cho bức ảnh mịn hơn san những điểm ảnh nhiễu trong bức ảnh gần bằng với các điểm ảnh xung quanh. Có thể áp dụng bộ lọc trung bình hoặc áp dụng nhân chập dưới đây với hệ số factor là 15 và offset là 1:

smoothing.png

 		int[][] config = new int[][]{
                {1, 1, 1},
                {1, 7, 1},
                {1, 1, 1}
        };

        smMatrix = new ConvolutionMatrix(config, 15, 1);
        return smMatrix.convolute(inp);

Thôi, thư đã dài, à quên bài đã dài mình xin dừng bút tại đây. Chúc các bạn sang năm mới thật nhiều hạnh phúc! :sexy:

Github: https://github.com/phamxuanlu/PhotoEffectsEditor