Tuesday, March 7, 2017

Thresholding โดยการหาค่า Threshold แบบอัตโนมัติ

ปกติเราจะต้องกำหนดค่า Threshold เพื่อทำ Thresholding ซึ่งตามความเป็นจริงถือว่ายากพอสมควร เพราะไม่รู้ว่าควรจะกำหนดค่านี้เป็นเท่าไหร่ดี เลยมีนักวิจัยคิดวิธีการที่จะคำนวณหาค่านี้โดยอัตโนมัติ และ OpenCV ได้นำมาใช้ด้วยกัน 2 วิธี คือ วิธีของ Otsu และ วิธี Triangle ลองไปอ่านหลักการกันได้ตามนี้ครับ
  • https://en.wikipedia.org/wiki/Otsu's_method
  • https://www.ncbi.nlm.nih.gov/pubmed/70454
ลองดูผลลัพธ์กันก่อน


ถ้าจะใช้งานสองวิธีนี้ใน OpenCV ก็ต้องเตรียม matrix ของรูปต้นฉบับให้เป็นแบบ CV_8UC1 หรือแบบ 8-bit 1 channel เสียก่อน (ใน reference บอกไว้) ดังนั้น เมื่อแปลง bitmap ให้เป็น Mat ด้วยคำสั่ง
Utils.bitmapToMat(bitmap, mat);

เมตริกซ์ mat ที่ได้จะเป็นแบบ CV_8UC4 โดยอัตโนมัติ เราจึงจำเป็นต้องแปลงให้เป็นแบบที่เราต้องการผ่านคำสั่ง
Imgproc.cvtColor(mat, mat1, Imgproc.COLOR_RGB2GRAY, 1);
จากนั้นค่อยมากำหนดการทำ thresholding โดยทั้งสองวิธีข้างต้นจะใช้ควบคู่กับการทำ thresholding 5 รูปแบบก่อนหน้านี้ เช่น ถ้าเราต้องการทำ binary thresholding โดยใช้ Otsu's method เพื่อกำหนดค่า threshold โดยอัตโนมัติก็จะใช้คำสั่ง
Imgproc.threshold(mat1, mat2, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
ให้สังเกตการกำหนดค่าพารามิเตอร์จะพบว่า ค่า threshold เรากำหนดให้เป็น 0 เพราะว่าเดี๋ยววิธีของ Otsu จะคำนวณให้เอง ส่วนวิธีการทำ thresholding ก็จะเป็นสองแบบรวมกัน เลยใช้รูปแบบ Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU ครับ

โค้ดทั้งหมดก็จะประมาณนี้
xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.mobile.opencv102.MainActivity">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/ivImage"
        app:srcCompat="@drawable/lenna_gray"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_weight="1" />

    <RadioGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/rgroup"
        android:layout_marginTop="24dp"
        android:layout_below="@+id/ivImage"
        android:layout_centerHorizontal="true">

        <RadioButton
            android:text="Original"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rOriginal"
            android:layout_weight="1"
            android:checked="true" />

        <RadioButton
            android:text="Binary Otsu"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rBinaryOtsu"
            android:layout_weight="1" />

        <RadioButton
            android:text="Binary Triangle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rBinaryTriangle"
            android:layout_weight="1" />
    </RadioGroup>
</RelativeLayout>

java

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.RadioGroup;

import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;

public class MainActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener{
    private Bitmap bitmap, result;
    private ImageView ivImage;
    private RadioGroup rgroup;
    private Mat mat1, mat2;

    static {
        if(OpenCVLoader.initDebug()) {
            Log.i("OpenCV", "Success");
        }
        else {
            Log.i("OpenCV", "Fail");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ivImage = (ImageView) findViewById(R.id.ivImage);
        rgroup = (RadioGroup) findViewById(R.id.rgroup);
        rgroup.setOnCheckedChangeListener(this);

        //decode resource file to bitmap with no scale
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lenna_gray);


        //temporary matrix to get pixel values from bitmap
        Mat temp = new Mat();
        //convert bitmap to matrix, this matrix will always be of type CV_8UC4
        Utils.bitmapToMat(bitmap, temp);

        //source matrix, for Otsu and Triangle thresholding must be unsigned 8-bit 1 channel (gray)
        mat1 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC1);
        //convert temp matrix to mat1 of type CV_8U1, 1 channel
        Imgproc.cvtColor(temp, mat1, Imgproc.COLOR_RGB2GRAY, 1);

        //output matrix, grayscale 8-bit 1 channel
        mat2 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC1);

        //create bitmap having width (columns) and height(rows) as matrix and in the format of 8 bit/pixel
        result = Bitmap.createBitmap(mat2.cols(), mat2.rows(), Bitmap.Config.RGB_565);
    }

    @Override
    public void onCheckedChanged(RadioGroup radioGroup, int id) {
        if(id==R.id.rOriginal) {
            ivImage.setImageResource(R.drawable.lenna_gray);
            return;
        }
        else if(id==R.id.rBinaryOtsu) {
            Imgproc.threshold(mat1, mat2, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
        }
        else if(id==R.id.rBinaryTriangle) {
            Imgproc.threshold(mat1, mat2, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_TRIANGLE);
        }
        //convert result matrix to bitmap
        Utils.matToBitmap(mat2, result);
        //show bitmap in ImageView
        ivImage.setImageBitmap(result);
    }
}

Sunday, March 5, 2017

มาลอง Thresholding กัน

มาลองประมวลผลภาพอย่างง่ายต่อด้วยการทำ Thresholding ซึ่งก็คือการกำหนดค่า Threshold (มีทั้งแบบกำหนดเอง หรือได้จากการคำนวณโดยอัตโนมัติ) ที่มีค่าระหว่างช่วงค่าของพิกเซล 0-255 แล้วนำค่านี้ไปตัดสินใจสร้างรูปผลลัพธ์อีกที

ปกติแล้ว Thresholding มักจะใช้ในงาน segmentation คือแยกส่วนของภาพสีเทา ซึ่งมักจะแยกออกเป็นสองส่วนคือส่วนที่เราสนใจ (foreground) กับส่วนที่เราไม่สนใจ (background) ดังนั้นกระบวนการนี้จึงอาจถูกเรียกอีกชื่อว่า Binarization ก็ได้

ลองมาดูตัวอย่างผลลัพธ์ของการทำ Thresholding 5 แบบกันก่อนครับ
 


OpenCV มีคำสั่งในการทำ Thresholding คือ
public static double threshold(Mat src, Mat dst, double thresh, double maxval, int type)

-src และ dst คือ source และ destination matrix นั่นเอง
-thresh คือค่า threshold ที่เราเอาไว้แยกภาพ
-maxval คือค่าสูงสุดที่เราทำ thresholding แล้วอยากให้เป็น ปกติก็มักจะกำหนดให้เป็นค่าสูงสุดของภาพ เช่น 255
-type คือ วิธีการ thresholding มีด้วยกัน 7 แบบ ในที่นี้เราจะลอง 5 แบบ ได้แก่
(คัดลอกจาก http://docs.opencv.org/java/2.4.9/org/opencv/imgproc/Imgproc.html#threshold(org.opencv.core.Mat,%20org.opencv.core.Mat,%20double,%20double,%20int)

THRESH_BINARY
dst(x,y) = maxval if src(x,y) > thresh; 0 otherwise

THRESH_BINARY_INV
dst(x,y) = 0 if src(x,y) > thresh; maxval otherwise

THRESH_TRUNC
dst(x,y) = threshold if src(x,y) > thresh; src(x,y) otherwise

THRESH_TOZERO
dst(x,y) = src(x,y) if src(x,y) > thresh; 0 otherwise

THRESH_TOZERO_INV
dst(x,y) = 0 if src(x,y) > thresh; src(x,y) otherwise

ลองมาดูโค้ดกัน

xml (อาจจะดูแน่นไปหน่อยนะครับ)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.mobile.opencv102.MainActivity">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/ivImage"
        app:srcCompat="@drawable/lenna_gray"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_weight="1" />

    <RadioGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/rgroup"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true">

        <RadioButton
            android:text="Original"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rOriginal"
            android:layout_weight="1"
            android:checked="true" />

        <RadioButton
            android:text="Binary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rBinary"
            android:layout_weight="1" />

        <RadioButton
            android:text="Binary, Inverted"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rBinaryInverted"
            android:layout_weight="1" />

        <RadioButton
            android:text="Truncate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rTruncate"
            android:layout_weight="1" />

        <RadioButton
            android:text="To zero"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rToZero"
            android:layout_weight="1" />

        <RadioButton
            android:text="To zero, Inverted"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rToZeroInverted"
            android:layout_weight="1" />
    </RadioGroup>

    <TextView
        android:text="Threshold"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tvThreshold"
        android:textSize="18sp"
        android:layout_below="@+id/ivImage"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_marginTop="12dp" />

    <SeekBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/sbThreshold"
        android:layout_alignBottom="@+id/tvThreshold"
        android:layout_toRightOf="@+id/tvThreshold"
        android:layout_alignRight="@+id/ivImage"
        android:layout_alignEnd="@+id/ivImage"
        android:max="255"
        android:progress="127" />

</RelativeLayout>

java

package com.example.mobile.opencv102;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.RadioGroup;
import android.widget.SeekBar;

import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;

public class MainActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener, SeekBar.OnSeekBarChangeListener{
    private Bitmap bitmap, result;
    private ImageView ivImage;
    private RadioGroup rgroup;
    private SeekBar sbThreshold;
    private Mat mat1, mat2;
    private int threshold = 127;
    private int type = -1;

    static {
        if(OpenCVLoader.initDebug()) {
            Log.i("OpenCV", "Success");
        }
        else {
            Log.i("OpenCV", "Fail");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ivImage = (ImageView) findViewById(R.id.ivImage);
        rgroup = (RadioGroup) findViewById(R.id.rgroup);
        rgroup.setOnCheckedChangeListener(this);
        sbThreshold = (SeekBar) findViewById(R.id.sbThreshold);
        sbThreshold.setOnSeekBarChangeListener(this);

        //decode resource file to bitmap with no scale
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lenna_gray);

        //source matrix, unsigned 8-bit 4 channels (RGBA)
        mat1 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC4);
        //convert bitmap to matrix
        Utils.bitmapToMat(bitmap, mat1);

        //output matrix, grayscale 8-bit 4 channels
        mat2 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC4);
        //create bitmap having width (columns) and height(rows) as matrix and in the format of 8 bit/pixel
        result = Bitmap.createBitmap(mat2.cols(), mat2.rows(), Bitmap.Config.RGB_565);
    }

    public void adjust(double thres, int type) {
        //thresholding
        //public static double threshold(Mat src, Mat dst, double thresh, double maxval, int type)
        Imgproc.threshold(mat1, mat2, thres, 255, type);

        //convert result matrix to bitmap
        Utils.matToBitmap(mat2, result);
        //show bitmap in ImageView
        ivImage.setImageBitmap(result);
    }

    @Override
    public void onCheckedChanged(RadioGroup radioGroup, int id) {
        if(id==R.id.rOriginal) {
            ivImage.setImageResource(R.drawable.lenna_gray);
            type = -1;
        }
        else if(id==R.id.rBinary) {
            type = Imgproc.THRESH_BINARY;
            adjust(threshold, type);
        }
        else if(id==R.id.rBinaryInverted) {
            type = Imgproc.THRESH_BINARY_INV;
            adjust(threshold, type);
        }
        else if(id==R.id.rTruncate) {
            type = Imgproc.THRESH_TRUNC;
            adjust(threshold, type);
        }
        else if(id==R.id.rToZero) {
            type = Imgproc.THRESH_TOZERO;
            adjust(threshold, type);
        }
        else if(id==R.id.rToZeroInverted) {
            type = Imgproc.THRESH_TOZERO_INV;
            adjust(threshold, type);
        }
    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        threshold = progress;
        if(type!=-1) {
            adjust(threshold, type);
        }
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {

    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {

    }
}

อ้างอิง
  • http://docs.opencv.org/3.2.0/db/d8e/tutorial_threshold.html
  • http://docs.opencv.org/java/2.4.9/org/opencv/imgproc/Imgproc.html#threshold(org.opencv.core.Mat,%20org.opencv.core.Mat,%20double,%20double,%20int)

Friday, March 3, 2017

Matrix Scaling กับ Point operation

Pixel processing อาจแบ่งได้ออกเป็น 2 แบบหลักๆ คือ ประมวลผลแต่ละพิกเซลของใครของมัน (ขอใช้ภาษาที่เข้าใจง่ายๆ :p ) หรือ point operation กับ การประมวลผลที่ต้องใช้พิกเซลข้างๆด้วย (Neighborhood operation)

ในที่นี้จะพูดถึง point operation ก่อน ซึ่งเราได้เจอมาแล้วในหัวข้อก่อนๆ เช่น การเปลี่ยนรูปสีให้เป็นรูปสีเทา หรือ การทำ image negative

หากเราต้องการประมวลผลแต่ละพิกเซล ในแต่ละ channel RGBA ส่วนใหญ่การประมวลผลก็จะอยู่ในรูปแบบ
output = alpha*input + beta

เมื่อ output คือเมตริกซ์ผลลัพธ์ และ input คือ เมตริกซ์ตั้งต้น ส่วน alpha และ beta ก็จะเป็นตัวคูณและตัวบวกเพิ่ม เราอาจเรียกการคำนวณนี้ว่า Matrix Scaling

เมื่อค่า alpha และ beta มีค่าต่างๆกัน จะให้ผลลัพธ์ดังตัวอย่างต่อไปนี้
  • alpha = 1 และ beta != 0 จะเป็นการเพิ่มหรือลดความสว่าง (brightness) ของรูป
  • alpha >0 และ beta = 0 จะเป็นการเพิ่มหรือลดค่า contrast ของรูป 
  • alpha = -1 และ beta = 255 เป็นการทำ image negative
ใน OpenCV เราสามารถใช้คำสั่ง
input.converTo(Mat output, int type, double alpha, double beta);

โดยที่ type ก็จะเป็นชนิดของเมตริกซ์ผลลัพธ์ที่ต้องการ เช่น CvType.CV_8UC4 เป็นต้น หรือกำหนดให้เป็นค่าติดลบเช่น -1 ก็ได้ ถ้าอยากให้เมตริกซ์ผลลัพธ์เป็นชนิดเดียวกับเมตริกซ์ตั้งต้น

ข้อดีของคำสั่งนี้ใน OpenCV คือมันจะปรับค่าของพิกเซลให้อยู่ในช่วง 0-255 โดยอัตโนมัติ

เราจะมาลองใช้คำสั่งนี้กันอีกครั้ง เพื่อปรับค่า brightness และ contrast ของรูป ดังนี้



โค้ดก็ประมาณนี้ครับ
xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.mobile.opencv102.MainActivity">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:id="@+id/ivImage"
        app:srcCompat="@drawable/lenna256" />

    <RadioGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/ivImage"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="25dp"
        android:orientation="horizontal"
        android:id="@+id/rgroup">

        <RadioButton
            android:text="Origin"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rbtOrigin"
            android:layout_weight="1"
            android:checked="true" />

        <RadioButton
            android:text="Brightness"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rbtBright"
            android:layout_weight="1" />

        <RadioButton
            android:text="Contrast"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rbtContrast"
            android:layout_weight="1" />

    </RadioGroup>

</RelativeLayout>

java

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.RadioGroup;

import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;

public class MainActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener{
    private Bitmap bitmap;
    private ImageView ivImage;
    private RadioGroup rgroup;

    static {
        if(OpenCVLoader.initDebug()) {
            Log.i("OpenCV", "Success");
        }
        else {
            Log.i("OpenCV", "Fail");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ivImage = (ImageView) findViewById(R.id.ivImage);
        rgroup = (RadioGroup) findViewById(R.id.rgroup);
        rgroup.setOnCheckedChangeListener(this);
        //decode resource file to bitmap with no scale
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lenna256);
    }

    public void adjust(double alpha, double beta) {
        //source matrix, unsigned 8-bit 4 channels (RGBA)
        Mat mat1 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC4);
        //convert bitmap to matrix
        //Be careful that the output Mat have the same size as the input Bitmap
        //and of the 'CV_8UC4' type, RGBA format.
        Utils.bitmapToMat(bitmap, mat1);

        //output matrix, grayscale 8-bit 1 channel
        Mat mat2 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC3);
        //Method 1: use built-in class, fastest
        //increase brightness or contrast or both
        //out = alpha*(in) + beta
        mat1.convertTo(mat2, CvType.CV_8UC3, alpha, beta);

        //Method 2: single loop
        //it requires converting mat data type from byte to double
        //otherwise the mat.get() gives the wrong pixel value
//        mat1.convertTo(mat1, CvType.CV_64FC4);
//        //create temp arrays to keep pixels for both original and output
//        double[] source = new double[(int)(mat1.total()*mat1.channels())];
//        double[] dest = new double[(int)(mat2.total()*mat2.channels())];
//
//        //dump matrix to array
//        mat1.get(0, 0, source);
//        int j=0;
//        double alpha=1, beta=100;
//        //source RGBA, move every four bytes
//        for(int i=0;i<source.length;i+=4) {
//            //change brightness and contrast
//            dest[j] = alpha*source[i] + beta;
//            dest[j+1] = alpha*source[i+1] + beta;
//            dest[j+2] = alpha*source[i+2] + beta;
//            //dest RGB, mover every three bytes
//            j+=3;
//        }
//        //put the modified temp array to matrix, this step will clamp the pixel to <= 255
//        mat2.put(0, 0, dest);

        //create bitmap having width (columns) and height(rows) as matrix and in the format of 8 bit/pixel
        //ARGB_8888 and RGB565 are normal bitmap formats
        Bitmap result = Bitmap.createBitmap(mat2.cols(), mat2.rows(), Bitmap.Config.RGB_565);
        //convert result matrix to bitmap
        Utils.matToBitmap(mat2, result);
        //show bitmap in ImageView
        ivImage.setImageBitmap(result);
    }

    @Override
    public void onCheckedChanged(RadioGroup radioGroup, int id) {
        if(id==R.id.rbtOrigin) {
            ivImage.setImageResource(R.drawable.lenna256);
        }
        else if(id==R.id.rbtBright) {
            adjust(1,100);
        }
        else if(id==R.id.rbtContrast) {
            adjust(2,0);
        }
    }
}

Thursday, March 2, 2017

Lookup table และการวัดเวลาในการประมวลผลคำสั่ง

การทำ pixel processing สามารถใช้ lookup table เพื่อมาแก้ปัญหาได้ ตัว lookup table ก็คือตารางค่าที่มีการคำนวณไว้ล่วงหน้าแล้ว ถ้าสมมติว่าพิกเซลของรูปต้นฉบับมีค่าเท่าไหร่ เราก็สามารถเอาไปเทียบใน lookup table เพื่อหาผลลัพธ์ได้ทันที วิธีนี้ก็จะประหยัดเวลาในการคำนวณ โดยเฉพาะถ้าการคำนวณนั้นซับซ้อนไปได้มาก

ลองเทียบดูกับโจทย์ image negative ในตัวอย่างที่แล้ว เราต้องการคำนวณพิกเซลใหม่ โดย 255 - พิกเซลเก่า ซึ่งต้องทำทุกๆพิกเซล แต่เนื่องจากว่าพิกเซลมีค่าแค่ 256 ค่า คือจาก 0-255 ดังนั้น ถ้าสร้าง lookup table ไว้แบบนี้

index : 0       1      2       3    ... 255
value : 255  254  253  252  ...   0

ถ้าพิกเซลต้นฉบับมีค่าเป็น 0 ก็เอาไปเทียบในตารางข้างต้น จะได้ผลลัพธ์เป็น 255 ทันทีโดยไม่ต้องคำนวณ

สมมติว่ารูปมีขนาด 256x256 พิกเซล หรือทั้งหมด 65536 พิกเซล แทนที่จะคำนวณทุกพิกเซล ก็เหลือแค่การคำนวณเฉพาะ 256 พิกเซลใน lookup table ที่เหลือก็แค่เปรียบเทียบ ซึ่งจะประหยัดเวลาไปได้มาก

ดังนั้น โค้ดการใช้ lookup table กับปัญหา image negative ก็จะประมาณนี้

//need to convert original matrix to have the same type of output matrix
mat1.convertTo(mat1, CvType.CV_8UC3);
//create lookup matrix 1 row 256 columns (index 0-255)
Mat lookup = Mat.zeros(1, 256, CvType.CV_8UC1);
//an array of 256 members, index 0 is for pixel value 0
double[] temp = new double[256];
//loop to assign inverse to array
for(int i=0;i<256;i++) {
 temp[i] = 255-i;
}
//put array to lookup matrix
lookup.put(0, 0, temp);
//perform lookup table processing for all channels of mat1 and put result into mat2
Core.LUT(mat1, lookup, mat2);

แล้วเราจะทราบได้อย่างไรว่ามันใช้เวลาประมาณเท่าไหร่
หลักการก็ง่ายๆ วัดเวลาก่อน process กับหลัง process เสร็จ แล้วหาความต่างของเวลา ด้วยโค้ด

long tstart = System.nanoTime();
//------------------------
//our algorithm here
//------------------------
long tend = System.nanoTime();
double elapse = (tend-tstart)/1000000000.0;
Log.i("time", "Processing time = "+elapse+" seconds");

ผมลองวัดเวลาโดยประมาณของแต่ละวิธีในการทำ image negative ในมือถือที่ใช้ทดสอบ ได้ประมาณนี้ครับ
1. ใช้คำสั่ง
Core.bitwise_not(mat1, mat2);
ใช้เวลาเฉลี่ย 0.0072 วินาที

2. ใช้คำสั่ง
mat1.convertTo(mat2, CvType.CV_8UC3, -1, 255);
ใช้เวลาเฉลี่ย 0.027 วินาที

3. ใช้การวนลูปแถวและคอลัมน์ ใช้เวลาเฉลี่ย 3.35 วินาที

4. ใช้การ dump ไปประมวลผลใน array  ใช้เวลาเฉลี่ย 0.142 วินาที

5. ใช้ lookup table ข้างต้น  ใช้เวลาเฉลี่ย 0.0103 วินาที

เรียงตามลำดับความเร็ว คือ วิธีที่ 1 5 2 4 3

ลองพิจารณาดูแต่ละวิธี แล้วเลือกใช้งานตามความชอบเลยครับ ส่วนถ้าให้ผมเลือก ผมคงเลือกคำสั่งที่ OpenCV มีให้ก่อน เช่น วิธีที่ 1 หรือ 2 ถ้าใช้ไม่ได้ค่อยลองวิธี 5 4 และ 3 ตามลำดับครับ

เล่นกับ Pixel ต่อ การกลับสีรูป (Image negative / inverse)

ลองมาเล่นกับ Pixel อีก โดยการกลับสีรูป ซึ่งหลักการก็ค่อนข้างง่าย คือ พิกเซลมีค่าเป็นเท่าไหร่ ก็เปลี่ยนเป็นค่าตรงกันข้าม เช่น จาก 0 ก็เปลี่ยนเป็น 255 หรือจาก 255 ก็เป็น 0

พูดง่ายๆก็คือ การกลับสีรูปคือการคำนวณ 255-pixel นั่นเอง



ลองดูขั้นตอนกันครับ

สมมติว่าเราจะใช้ interface ง่ายๆเหมือนตัวอย่างที่แล้ว

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.mobile.opencv102.MainActivity">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:id="@+id/ivImage"
        app:srcCompat="@drawable/lenna256" />

    <ToggleButton
        android:text="ToggleButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:id="@+id/tbProcess"
        android:layout_below="@+id/ivImage"
        android:layout_centerHorizontal="true"
        android:onClick="process" />

</RelativeLayout>

ในส่วนของโค้ด ก็จะมีขั้นตอนหลักๆคล้ายตัวอย่างที่แล้ว ผมจะขอสรุปย่อๆดังนี้
1. สร้าง matrix สำหรับรูปต้นฉบับและรูปผลลัพธ์ mat1 และ mat2
Mat mat1 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC4);
Mat mat2 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC3);

สังเกตว่าในที่นี้ matrix ผลลัพธ์เป็นชนิด CV_8UC3 เพราะเราจะไม่ใช้ alpha channel

2. กลับสีรูป ทำได้อย่างน้อยสามวิธีเช่น
2.1 ใช้คำสั่งสำเร็จรูป
จะใช้
Core.bitwise_not(mat1, mat2);
หรือ
mat1.convertTo(mat2, CvType.CV_8UC3, -1, 255);
ก็ได้ มีความหมายเหมือนเอา 255 ไปลบทุก pixel

2.2 วนลูปแต่และแถวแต่ละคอลัมน์

int row = mat2.rows();
int col = mat2.cols();
for(int r=0;r<row;r++) {
 for(int c=0;c<col;c++) {
  //read pixel
  double[] pixelSet = mat1.get(r, c);
  //find average of RGB
  double[] pixel = new double[3];
  pixel[0] = 255-pixelSet[0];
  pixel[1] = 255-pixelSet[1];
  pixel[2] = 255-pixelSet[2];
  //put average value of pixel to matrix
  mat2.put(r, c, pixel);
 }
}

2.3 dump matrix มาใส่ array

mat1.convertTo(mat1, CvType.CV_64FC4);
//create temp arrays to keep pixels for both original and output
double[] source = new double[(int)(mat1.total()*mat1.channels())];
double[] dest = new double[(int)(mat2.total()*mat2.channels())];

//dump matrix to array
mat1.get(0, 0, source);
int j=0;
//source RGBA, move every four bytes
for(int i=0;i<source.length;i+=4) {
 //negative
 dest[j] = 255-source[i];
 dest[j+1] = 255-source[i+1];
 dest[j+2] = 255-source[i+2];
 //dest RGB, mover every three bytes
 j+=3;
}
//put the modified temp array to matrix
mat2.put(0, 0, dest);

โค้ดรวมๆก็เป็นแบบนี้ครับ

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.ToggleButton;

import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;

public class MainActivity extends AppCompatActivity {
    private Bitmap bitmap;
    private ToggleButton tbProcess;
    private ImageView ivImage;

    static {
        if(OpenCVLoader.initDebug()) {
            Log.i("OpenCV", "Success");
        }
        else {
            Log.i("OpenCV", "Fail");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ivImage = (ImageView) findViewById(R.id.ivImage);
        tbProcess = (ToggleButton) findViewById(R.id.tbProcess);

        //decode resource file to bitmap with no scale
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lenna256);
        //if you don't want Android to auto resize the bitmap according to device resolution
//        BitmapFactory.Options option = new BitmapFactory.Options();
//        option.inScaled = false;
//        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lenna256, option);
    }

    public void process(View view) {
        if(!tbProcess.isChecked()) {
            ivImage.setImageResource(R.drawable.lenna256);
            return;
        }
        //source matrix, unsigned 8-bit 4 channels (RGBA)
        Mat mat1 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC4);
        //convert bitmap to matrix
        //Be careful that the output Mat have the same size as the input Bitmap
        //and of the 'CV_8UC4' type, RGBA format.
        Utils.bitmapToMat(bitmap, mat1);

        //output matrix, grayscale 8-bit 3 channels
        Mat mat2 = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC3);
        //negative or inverse (255-pixel)
        //Method 1: use built-in class, fastest
//        Core.bitwise_not(mat1, mat2);
        //out = a(in) + b = -(in) + 255 = 255 - in
//        mat1.convertTo(mat2, CvType.CV_8UC3, -1, 255);

        //Method 2: 2D array, simple but slow
//        int row = mat2.rows();
//        int col = mat2.cols();
//        for(int r=0;r<row;r++) {
//            for(int c=0;c<col;c++) {
//                //read pixel
//                double[] pixelSet = mat1.get(r, c);
//                //find average of RGB
//                double[] pixel = new double[3];
//                pixel[0] = 255-pixelSet[0];
//                pixel[1] = 255-pixelSet[1];
//                pixel[2] = 255-pixelSet[2];
//                //put average value of pixel to matrix
//                mat2.put(r, c, pixel);
//            }
//        }

        //Method 3: single loop, more complex but faster
        //it requires converting mat data type from byte to double
        //otherwise the mat.get() gives the wrong pixel value
        mat1.convertTo(mat1, CvType.CV_64FC4);
        //create temp arrays to keep pixels for both original and output
        double[] source = new double[(int)(mat1.total()*mat1.channels())];
        double[] dest = new double[(int)(mat2.total()*mat2.channels())];

        //dump matrix to array
        mat1.get(0, 0, source);
        int j=0;
        //source RGBA, move every four bytes
        for(int i=0;i<source.length;i+=4) {
            //negative
            dest[j] = 255-source[i];
            dest[j+1] = 255-source[i+1];
            dest[j+2] = 255-source[i+2];
            //dest RGB, mover every three bytes
            j+=3;
        }
        //put the modified temp array to matrix
        mat2.put(0, 0, dest);

        //create bitmap having width (columns) and height(rows) as matrix and in the format of 8 bit/pixel
        //ARGB_8888 and RGB565 are normal bitmap formats
        Bitmap result = Bitmap.createBitmap(mat2.cols(), mat2.rows(), Bitmap.Config.RGB_565);
        //convert result matrix to bitmap
        Utils.matToBitmap(mat2, result);
        //show bitmap in ImageView
        ivImage.setImageBitmap(result);
    }
}