Tuesday, July 18, 2017

Markov โมเดล แบบบ้านๆ ตอนที่ 2 - 1st order Markov assumption

เราลองมาดูตัวอย่างจาก Reference เดิมเพิ่มเติมกันครับ

ตัวอย่างที่แล้ว เราคาดการณ์เหตุการณ์ที่สามและสอง จากเหตุการณ์ที่ 1

จะเป็นอย่างไร ถ้าเราจะคาดการณ์เหตุการณ์ใดๆ จากเหตุการณ์ตั้งต้น เช่น เหตุการณ์ที่สามจากเหตุการณ์ที่ 1 เลย

สมมติว่าเรายังใช้ความน่าจะเป็นจาก finite state automaton รูปนี้
ตัวอย่างที่ 2 ถ้าวันนี้มีหมอก จงหาความน่าจะเป็นที่ฝนจะตกในวันมะรืน

ก่อนอื่นลองพิจารณาดูก่อนว่า จะมีกี่หนทางที่จะเกิดเหตุการณ์นี้ได้ จากสภาพอากาศ
วันนี้ -> พรุ่งนี้ -> มะรืน
ก็จะเป็นไปได้สามรูปแบบคือ
1. หมอก -> หมอก -> ฝน
2. หมอก -> ฝน -> ฝน
3. หมอก -> แดด -> ฝน

นั่นคือ ความน่าจะเป็นที่โจทย์ถาม ก็คือผลรวมของความน่าจะเป็นในแต่ละช่องทาง

P(W_3 = rainy | W1 = foggy)
 = P(W3 = rainy, W2= foggy | W1 = foggy) + P(W3 = rainy, W2= rainy | W1 = foggy) + P(W3 = rainy, W2= sunny | W1 = foggy)

คล้ายๆตัวอย่างที่แล้ว โดยใช้การประมาณ Markov เรารู้ว่า
 P(W3 = rainy, W2= foggy | W1 = foggy)
=  P(W3 = rainy | W2= foggy , W1 = foggy) * P(W2 = foggy | W1= foggy)
=  P(W3 = rainy | W2= foggy) * P(W2 = foggy | W1= foggy)

ดังนั้น ย้อนกลับไปสมการก่อนหน้านี้ จึงได้ว่า
P(W3 = rainy | W1 = foggy)
=  P(W3 = rainy | W2= foggy) * P(W2 = foggy | W1= foggy) + P(W3 = rainy | W2= rainy) * P(W2 = rainy | W1= foggy) + P(W3 = rainy | W2= sunny) * P(W2 = sunny | W1= foggy)
= (0.3*0.5) + (0.6*0.3) + (0.05*0.2)
= 0.15 + 0.18 + 0.01
= 0.34

Markov โมเดล แบบบ้านๆ ตอนที่ 1 - 1st order Markov assumption

วันนี้มาทางวิชาการกันหน่อย ผมจะขอสรุปเนื้อหาของ Markov models ตามเอกสารอ้างอิงนี้
Eric Fosler-Lussier, Markov Models and Hidden Markov Models: A Brief Tutorial, December 1998

ซึ่งเขียนอธิบายไว้ดีมาก รวมกับความเข้าใจของผมเอง อาจจะมีคำบัญญัติภาษาไทยแบบที่ผมเขียนเองบ้าง ขอให้ดูความหมายภาษาอังกฤษกำกับเพื่อเทียบกับตำราเล่มอื่นนะครับ

ก่อนอื่นคือเราอาจจะมีคำถามว่า Markov model มีไว้ทำอะไร ถ้าเอาแบบง่ายๆสั้นๆ ก็คือ เป็นโมเดลทางคณิตศาสตร์แบบหนึ่ง ที่ไว้คาดการณ์หรือพยากรณ์เหตุการณ์ในอนาคต จากข้อมูลในอดีต


ผมจะลองยกตัวอย่างจากเอกสารอ้างอิงนะครับ

สมมติว่าเราอยากพยากรณ์อากาศของวันพรุ่งนี้ ถ้าเราไม่มีข้อมูลภาพถ่ายดาวเทียมหรือข้อมูลทางภูมิศาสตร์อื่นๆเลย เราอาจจะสามารถทำได้โดยการใช้ข้อมูลของวันที่ผ่านๆมา

ถ้าสภาพอากาศมีแค่สามแบบ คือ มีแดด (sunny) มีฝน (rainy) และ มีหมอก (foggy) ตลอดวันในแต่ละวัน

เมื่อเราลองเก็บสถิติสภาพอากาศในแต่ละวัน แล้วคำนวณหาค่าความน่าจะเป็นของสภาพอากาศวันพรุ่งนี้ เมื่อทราบสภาพอากาศของวันนี้ เราอาจจะได้ตารางความน่าจะเป็นแบบมีเงื่อนไข (conditional probability) ดังข้างล่าง


ความหมายของตารางจะเป็นดังตัวอย่างต่อไปนี้ เช่น
-ถ้าวันนี้มีแดด โอกาสที่พรุ่งนี้จะฝนตกคือ 0.05
ซึ่งเราสามารถเขียนเป็นสมการได้ดังรูป
 P(W2 = rainy | W1=sunny)
 เมื่อ W2 และ W1 แทนสถานะ (สภาพอากาศ) ณ วันพรุ่งนี้และวันนี้ตามลำดับ

ให้สังเกตว่าผลรวมของความน่าจะเป็นในแต่ละแถวจะเท่ากับ 1

ซึ่งก็อาจจะเอามาเขียนเป็น finite state automaton หรือ state transition diagram ได้ตามรูปนี้


อย่างไรก็ตาม ข้อมูลนี้ก็ใช้พยากรณ์ได้วันต่อวัน ถ้าต้องการพยากรณ์ข้อมูลถัดไปอีกสองวัน (วันมะรืน) เมื่อทราบข้อมูลวันนี้ ก็จะทำไม่ได้โดยตรง นั่นคือ ต้องเก็บข้อมูลเพิ่มและคำนวณหาความน่าจะเป็นใหม่อีก ซึ่งจำนวนข้อมูลที่ต้องเก็บจะเท่ากับ จำนวนสถานะยกกำลังด้วยจำนวนวัน ข้อมูล ซึ่งในที่นี้เรามีสามสถานะ และสนใจสามวัน ก็จะต้องเก็บอย่างต่ำ 3^3 ข้อมูลเพื่อมาคำนวณหาความน่าจะเป็น

เพื่อที่จะทำให้งานง่ายลง เราสามารถใช้การประมาณมาทดแทน โดยกำหนดว่าถ้าต้องการพยากรณ์ข้อมูลวันนี้ ให้ใช้ข้อมูลของวันก่อนหน้านี้เท่านั้น นั่นคือ
P(Wn | Wn-1, Wn-2, ..., W1) มีค่าประมาณ P(Wn | Wn-1)

การประมาณนี้มีชื่อเรียกว่า การประมาณ Markov ในลำดับชั้นแรก (First-order Markov assumption) หรือเรียกสั้นๆว่า การประมาณ Markov (Markov assumption) ซึ่งจะทำให้เราลดการเก็บข้อมูลจาก สถานะ^n เหลือแค่ สถานะ^2 เพราะสนใจข้อมูลแค่สองเวลา

กลับมาที่ตัวอย่างเรื่องสภาพอากาศ ถ้าเราใช้การประมาณ Markov แล้วเราจะสามารถตอบคำถาม "สภาพอากาศของวันมะรืนจะเป็นอย่างไร ถ้าทราบสภาพอากาศของวันนี้"

การตอบคำถามข้างต้น เราสามารถใช้หลักการของความน่าจะเป็นมาคำนวณได้ ดังตัวอย่างต่อไปนี้ ซึ่งอ้างอิงตารางความน่าจะเป็นที่แสดงไว้ข้างต้น ซึ่งมีข้อมูลสภาพอากาศแค่สองวัน


ตัวอย่างที่ 1 ถ้าวันนี้มีแดด โอกาสที่พรุ่งนี้จะมีแดด และ วันมะรืนจะฝนตกจะเป็นเท่าใด
เราสามารถแปลงโจทย์เป็นดังนี้
P(W3 = rainy, W2 = sunny | W1 = sunny)

ซึ่งจะมีค่าเท่ากับ
P(W3 = rainy | W2 = sunny, W1 = sunny) * P(W2 = sunny | W1 = sunny)

เมื่อเราใช้ การประมาณ Markov นิพจน์แรกก็จะถูกประมาณให้เหลือแค่ P(W3 = rainy | W2 = sunny) และค่าทั้งหมดก็จะเปลี่ยนเป็น
P(W3 = rainy | W2 = sunny) * P(W2 = sunny | W1 = sunny)

ซึ่งเมื่อแทนค่าจากตารางด้านบน จะได้คำตอบคือ 0.05 * 0.8 = 0.04
นั่นคือ ถ้าวันนี้มีแดด โอกาสที่พรุ่งนี้จะมีแดด และ วันมะรืนจะฝนตกจะเป็น 0.04

เราสามารถคิดอีกแบบ โดยใช้ state diagram ข้างต้นมาคำนวณ โดยคูณความน่าจะเป็นตามการเปลี่ยนสถานะดังรูป คือคูณเส้นสีแดง (พรุ่งนี้มีแดดเมื่อวันนี้มีแดด) กับเส้นสีน้ำเงิน (พรุ่งนี้ฝนตกเมื่อวันนี้มีแดด)


วันนี้ก็คงพอแค่นี้ก่อนนะครับ เดี๋ยวบทความถัดไปลองมาดูตัวอย่างเพิ่มเติมกัน

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);
        }
    }
}