ตัวอย่างการใช้ OpenCV เพื่อหา Homography

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

  1. จุดบนระนาบต้นฉบับ (srcPoints) อย่างน้อย 2 จุดขึ้นไป จำนวนจุดนี้ขึ้นอยู่กับแต่ละงานนะครับ
  2. จุดบนระนาบปลายทาง (destPoints) ซึ่งจำนวนจุดก็เท่ากับที่กำหนดไว้บนระนาบเริ่มต้น
  3. Homography เมทริกซ์ ขนาด 3x3 (เริ่มแรกก็คือเมทริกซ์ว่างๆ นั่นแหละ)

จริงๆ เราสามารถใส่ option เพิ่มได้ ไว้ไปลองกันเองนะครับ 🙂

Continue reading "ตัวอย่างการใช้ OpenCV เพื่อหา Homography"

การใช้ OpenCV สร้างกราฟ Histogram จากรูป

บล็อกนี้ผมขอเสนอวิธีใช้ฟังก์ชั่นจาก OpenCV สร้างกราฟ Histogram จากค่าสีที่ได้จากรูปที่เป็น Grayscale นะครับ ซึ่งก็สามารถนำไปประยุกต์ใช้กับรูปที่เป็น RGB หรือ HSV ได้เหมือนกันครับ

ก่อนอื่นเลย Histogram ของรูป คืออะไร.. มันก็คือกราฟที่เกิดจากการพลอตจำนวนของ pixel ที่ค่าสีนั้นๆ นั่นเอง แกน X คือค่าของสี ส่วนแกน Y คือจำนวนของ pixel (อ่านเพิ่มเติมได้ที่ Image histogram ครับ)

เมื่อเราทราบหลักการของการสร้าง Histogram จากรูปแล้ว เราก็มาดูที่โค้ดกันครับ เริ่มต้นเราก็ประกาศขนาดของ Histogram ครับ ให้มีขนาด 256 ช่อง (ค่าสีปกติจะมีค่าระหว่าง 0-255 ในที่นี้ผมจะให้ช่องหนึ่งคือค่าสีหนึ่งนะครับ ซึ่งค่านี้สามารถเปลี่ยนแปลงได้แล้วแต่งานครับ)

int bins = 256;
int hsize[] = { bins };

ในโค้ดของผมนี้ผมจะสร้าง Histogram ที่เป็น uniform นะครับ เราก็ต้องกำหนดขอบเขตของค่า x ซึ่งต้องกำหนดตาม format ดังนี้ครับ โดย ranges[] จะต้องมีจำนวน dimension เท่ากับจำนวนค่าที่เป็นคู่ที่เราประกาศไว้ (คู่ 0 กับ 255)

float xranges[] = { 0, 255 };
float* ranges[] = { xranges };

เนื่องจากรูปตัวอย่างเป็น RGB เราก็ต้องแปลงให้เป็น Grayscale ก่อนนะครับ โดย image ในที่นี้คือรูปที่เราโหลดมาตั้งแต่ต้นนะครับ

IplImage* gray = cvCreateImage( cvGetSize( image ), 8, 1 );
cvCvtColor( image, gray, CV_BGR2GRAY );

จากนั้นเราต้องสร้างเพลน (plane) เพื่อมาคำนวณกราฟกันครับ ในที่นี้มีแค่ 1

IplImage* planes[] = { gray };

แล้วเราก็คำนวณค่าของ Histogram ดังนี้ โดยค่า 1 ตัวแรกหมายถึง จำนวน dimension ครับ ส่วน CV_HIST_ARRAY หมายถึงให้ type เป็นชนิด array และค่า 1 ตัวสุดท้ายหมายถึงว่าเป็น uniform ครับ

CvHistogram* hist = cvCreateHist( 1, hsize, CV_HIST_ARRAY, ranges, 1 );
cvCalHist( planes, hist, 0, NULL );

ในการคำนวณโดยใช้ cvCalHist นั้น เราเซตค่าที่ 3 ว่าให้เป็น 1 แปลว่า ถ้าสมมุติว่าเราวนลูปอ่านค่าสีจากรูปมาเรื่อยๆ เราสามารถบวกค่า pixel เพิ่มได้ในกราฟ Histogram อันเดิมครับ แต่ถ้าเป็น 0 ก็แปลว่าไม่ต้องบวกเพิ่ม ส่วนค่าสุดท้าย ถ้าไม่ใช่ NULL เราจะนำแค่จุด pixel ที่ไม่ใช่ 0 และมีการ mask ไว้ในรูปมาคำนวณครับ

ขั้นตอนต่อไป เราก็ต้องสร้างรูปขึ้นมาเพื่อแสดงผลครับ ในที่นี้เรากำหนดให้มีความสูงแค่ 50 พอครับ

IplImage* imgHistogram = cvCreateImage( cvGetSize( bins, 50 ), 8, 1 );
cvRectangle( imgHistogram, cvPoint( 0, 0 ), cvPoint( 256, 50 ), CV_RGB( 255, 255, 255 ), -1 ); // ค่าสุดท้ายคือค่า thickness ครับ ถ้าเป็น -1 แสดงว่าให้เป็นค่าสูงสุดเลย (เพื่อที่ว่าเราจะระบายสีขาวให้เต็มสีเหลี่ยมเลยครับ)

ขั้นตอนสุดท้ายเราก็วาดกราฟครับ และแสดงผล วิธีการก็คือดึงค่าออกมาทีละค่านั่นเอง แต่เนื่องจากเราเซตความสูงไว้ที่ 50 เราจะต้อง normalize ด้วยนะครับ แต่การที่เราจะ normalize ได้เราต้องมีค่าสูงสุดของ Histogram ก่อนครับ หาได้ดังนี้

float max_value = 0, min_value = 0;
cvGetMinMaxHistValue( hist, &min_value, $max_value );

และสุดท้ายจริงๆ ก็ดังนี้ครับ

cvNamedWindow( "histogram", 1 );
for( int i = 0; i < bins; i++ ) {
  float value = cvQueryHistValue_1D( hist, i );
  int normalized = cvRound( value * 50 / max_value );
  cvLine( imgHistogram, cvPoint( i, 50 ), cvPoint( i, 50 - normalized ), CV_RGB( 0, 0, 0 ) );
}
cvShowImage( "histogram", imgHistogram );

หวังว่าจะเป็นจุดอ้างอิงสำหรับผู้ที่เริ่มต้นสนใจในด้าน Image Processing นะครับ

Source code: hist.cc

credit: Isaias Gonzalez (siderevs at gmail dot com)

แนะนำการใช้ Perl เพื่อหาผลการทดลอง

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

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

บล็อกนี้ผมขอแนะนำการใช้ Perl ครับ ในกรณีของผม ผมใช้ Perl script ไฟล์เดียว ใส่ค่าพารามิเตอร์ครั้งแรก เราก็แค่รอให้ผลออกมา ถ้าไม่ดี เราก็รันคำสั่งเดียว เปลี่ยนค่าพารามิเตอร์ และก็รอผล ก็ดูง่ายๆ ใช่เปล่าครับ แต่สำหรับคนที่ไม่เคยทำ และจะลองมาทำดูก็จะค่อนข้างเสียเวลาในการศึกษา เสียเวลาไป google หาโค้ด หาตัวอย่าง ผมเลยเอาโค้ดที่ผมคิดว่าใช้บ่อยๆ มารวมไว้ครับ

ก่อนที่จะใช้ Perl script ผมแนะนำให้

  1. เขียนโปรแกรมให้รับค่าพารามิเตอร์จาก command line ให้ได้ก่อน ถ้าเป็นภาษา C/C++ ก็ใช้พวก argc, argv และ getopt (ลองดูโค้ดตัวอย่างได้ที่  ตัวอย่าง getopt ครับ
  2. ตอนที่แสดงผลจากโปรแกรมนั้น ให้เราแสดงออกมาทาง stdout หรือจะเขียนลงไฟล์ก็ได้ครับ
  3. output ที่ออกมา เราควรจะออกแบบสักหน่อย เพื่อให้โปรแกรมต่อไปสามารถรับจาก command line ได้

ต่อไปนี้ก็จะเป็นโค้ดตัวอย่างที่ผมใช้บ่อยๆ ครับ

การรับพารามิเตอร์จาก command line ใช้ดังนี้

my $myVariable = $ARGV[0];

เริ่มจากเลข 0 ครับ ถ้ามี 3 ค่า จะเป็นดังนี้

my $myVariable1 = $ARGV[0];
my $myVariable2 = $ARGV[1];
my $myVariable3 = $ARGV[2];

ส่วนคำสั่ง Perl สำหรับรันโปรแกรมที่เราเขียนขึ้นเองต่างหากก็ตามนี้

system( "myProgram $param01 $param02" );

เปิดโฟลเดอร์เพื่อแสดงชื่อไฟล์ทั้งหมด (รวมทั้งชื่อโฟลเดอร์ด้วย ฉะนั้นแนะนำว่าให้ชื่อไฟล์มี extension ไว้ แล้วแก้คำสั่ง grep เพื่อกรองเอาแต่ไฟล์ครับ)

opendir( DIR, $folder_name ) or die "couldn't open $folder_name\n";
# ไม่เอา . ไม่เอา .. และไม่เอา .svn (เผื่อว่าเราใช้ svn)
my @files = grep { $_ ne '.' && $_ ne '..' && $_ ne '.svn' } readdir DIR;
for $file ( sort @files ) {
  print $file . "\n";
}

ถ้าต้องการที่จะเรียงไฟล์ตามตัวอักษรไม่ใช่ตามแบบ string ก็ใช้ตามนี้ครับ

for $file ( sort { $a <=> $b } @files ) { ... }

ส่วนฟังก์ชั่นตัด extension ของไฟล์

sub without_ext {
  my ( $file ) = @_;
  return substr( $file, 0, rindex( $file, '.' ) );
}

วิธีใช้ก็แค่โยนชื่อไฟล์เข้าไปแบบนี้ without_ext ( $file )

และฟังก์ชั่นตัดเอามาแต่ extension ของไฟล์

sub ext_only {
  my ( $file ) = @_;
  return substr( $file, rindex( $file, '.' ) + 1 );
}

วิธีใช้ก็แค่โยนชื่อไฟล์เข้าไปแบบนี้ ext_only( $file ) เช่นกัน

การเปิดไฟล์สำหรับอ่านและการอ่านไฟล์

open( FILE, $filename ) or die( "Cannot open file" );
@data = <FILE>;
foreach $text_line ( @data ) {
  print $text_line . "\n";
}

การเปิดไฟล์สำหรับเขียนลงไฟล์ก็แค่เพิ่ม ">" ลงไปก่อนหน้าชื่อไฟล์ครับ ดังนี้

open( FILE, ">$filename" ) or die( "Cannot open file" );
print FILE "test\n";

การอ่านข้อมูลจาก stdout

# ต้องใส่ | ข้างหลังด้วย แปลว่าให้อ่านจาก pipe
open( OUTPUT, "myProgram $param01 $param02 |" );
while( <OUTPUT> ) {
  # แบบนี้จะตัด input โดยใช้การขึ้นบรรทัดใหม่เป็นตัวกำหนด ถ้าต้องการตัดโดยใช้ tab ก็เปลี่ยนจาก "\n" เป็น "\t"
  @output = split( "\n", $_ );
  foreach $val ( @output ) {
    print $val . "\t";
  }
}

สามารถหาข้อมูลเพิ่มเติมเกี่ยวกับ open() ได้ ที่นี่ ครับ

หวังว่าจะมีประโยชน์สำหรับหลายๆ ท่านนะครับ 🙂

การวัดประสิทธิภาพของโมเดล (Two-class prediction)

การที่เราสร้างโมเดลๆ หนึ่งขึ้นมาทำนายผลนั้น เราจะต้องมีการวัดประสิทธิภาพของโมเดลนั้นด้วย เรื่องการวัดผลนี้ค่อนข้างสำคัญมาก ในงานวิจัยที่เราจะต้องนำเสนอสิ่งที่เราคิดค้นออกมาว่าดีแค่ไหน ในบล็อกนี้จะขอพูดถึงการวัดผลโดยใช้ปัญหาที่เรียกว่า two-class prediction (binary classification) ซึ่งเป็นการวัดประสิทธิภาพที่งานวิจัยส่วนใหญ่นิยมใช้กัน

ในปัญหานี้เราจะบอกว่าผลลัพธ์ที่ได้ว่าเป็นคลาส Positive หรือ Negative ซึ่งผลลัพธ์ที่ได้สามารถมีได้ 4 แบบ คือ

  1. True Positive (TP) หมายความว่า เราได้ผลลัพธ์จากการทำนายคือ p และค่าจริงๆ ก็คือ p ด้วย
  2. False Positive (FP) หมายความว่า เราได้ผลลัพธ์จากการทำนายคือ p แต่ว่าค่าจริงๆ แล้วคือ n
  3. True Negative (TN) หมายความว่า เราได้ผลลัพธ์จากการทำนายคือ n และค่าจริงๆ ก็คือ n ด้วย
  4. False Negative (FN) หมายความว่า เราได้ผลลัพธ์จากการทำนายคือ n แต่ว่าค่าจริงๆ แล้วคือ p

จะเห็นได้ว่า TN จะกลับกันกับ TP และ FN จะกลับกันกับ FP ตามลำดับ

หลายๆ คนจะค่อนข้างสับสนเวลาที่คำนวณค่าจริงๆ ก็จะอธิบายจากความเข้าใจของผมนะครับ และคิดว่าน่าจะเข้าใจง่ายที่สุดแล้ว (เครดิต: @b39ppr) ยกตัวอย่าง เช่น เวลาที่เราต้องการจะทำนายว่า พฤติกรรมของคนๆ นี้ผิดปกติหรือไม่? หรืออีกนัยหนึ่งคือ เราต้องการที่จะตรวจจับพฤติกรรมที่ผิดปกติของคนๆ หนึ่ง

ในที่นี้จะได้ว่า พฤติกรรมที่ผิดปกติคือคลาส Positive ส่วนพฤติกรรมปกติคือคลาส Negative จะได้ว่า

  • TP คือ ระบบเราตรวจจับได้ว่าคนๆ นี้ผิดปกติ และ คนๆ นี้ได้มีพฤติกรรมผิดปกติจริงๆ
  • FP คือ ระบบเราตรวจจับได้ว่าคนๆ นี้ผิดปกติ แต่ว่า จริงๆ แล้ว เค้าไม่ได้ทำอะไรผิดเลย
  • TN คือ ระบบเราไม่ได้ตรวจจับอะไรเลย และ คนๆ นี้ก็ไม่ได้ทำผิด
  • FN คือ ระบบเราไม่ได้ตรวจจับอะไรเลย แต่ว่า คนๆ นี้ จริงๆ แล้วเค้ามีพฤติกรรมที่ผิดปกติ!

พอนึกออกรึเปล่าครับ? ผมจะขอยกตัวอย่างอีกสักตัวอย่าง เช่น เราเขียนโมเดลระบบตรวจจับสแปมเมล ถ้าเจอสแปมเมลให้ลบทิ้งเลย (ระบบออกแนวโหดหน่อย)

ในที่นี้จะได้ว่า เมลที่เป็นสแปมคือคลาส Positive ส่วนเมลปกติคือคลาส Negative

  • TP คือ ระบบทำนายว่าเมลนี้เป็นสแปม และ มันก็เป็นสแปมจริงๆ
  • FP คือ ระบบทำนายว่าเมลนี้เป็นสแปม แต่ว่า จริงๆ แล้วเมลนี้เป็นเมลปกติ เช่น ข่าวสารทั่วไป เมลเกี่ยวกับงานต่างๆ
  • TN คือ ระบบทำนายว่าเมลนี้เป็นเมลปกติ และ เมลนี้ก็ปกติจริงๆ
  • FN คือ ระบบทำนายว่าเมลนี้เป็นเมลปกติ แต่ว่า จริงๆ แล้วมันเป็นสแปม!

ค่าที่ได้จาก 2 ตัวอย่างข้างต้นนี้ สามารถช่วยให้เราวิเคราะห์และนำไปพัฒนาโมเดลให้ดียิ่งขึ้นได้อีก จากตัวอย่างแรกจะเห็นได้ว่าค่า FN นั้นสำคัญมาก ถ้าค่า FN เราสูงไปแสดงว่า เราตรวจจับคนร้ายไม่ได้เลย และจากตัวอย่างที่สองจะเห็นได้ว่าค่า FP นั้นสำคัญ เนื่องจากเป็นระบบแนวโหดที่เวลาเจอสแปมแล้วจะลบทิ้งเลย ดังนั้นถ้าค่า FP สูง ระบบเราก็อาจจะลบเมลสำคัญๆ ของเราทิ้งไปนั่นเอง

มีสรุปเพิ่มเติมจากคุณ pat หรือคุณ @b39ppr นั่นเอง ดังนี้ครับ (มีคำถามให้คิดเล่นตอนท้าย)

การอ่าน (T,F)(P,N) แบบไม่งง

  • ดูตัวหลังก่อน ว่า P หรือ N
    ถ้า P คือ model ของเรา predict ว่ามันเป็น positive (เป็นคนร้าย, เป็น spam)
  • ต่อไปดูตัวหน้า ว่า T หรือ F
    ถ้า T คือ ที่ model ของเรา predict ไปนั้น ถูกต้องแล้ว ถ้า F คือ predict ผิด

ตัวอย่าง: ระบบตรวจหา spam mail

  1. ตรวจหาอะไรก็ให้สิ่งนั้นเป็น positive
    จะตรวจหา spam เมื่อคิดว่าเจอ spam ก็คือเจอกรณี positive
  2. จะคำนวณ FP ทำอย่างไร?
    ตอบ สำหรับ FP นั้น
    2.1 ตัวหลังเป็น P ให้นับจำนวน mail ที่เรา predict ว่าเป็น “positive” (เป็น spam)
    2.2 ตัวหน้าเป็น F ให้ดูว่าที่ predict ว่าเป็น spam ไปนั้น มีอันไหน “ผิด” บ้าง

เอ้า ลองคิดกันหน่อย:
มี email ใหม่ ส่งมาหาเราทั้งหมด 10 ฉบับ เข้ามาใน inbox 8 ส่วนอีก 2 เข้าไปอยู่ในกล่อง junk mail 8 mail ใน inbox นั้นมาจากคนรู้จักซะ 5 ที่เหลือเป็นเมล์ขายของ ไม่อยากได้ ส่วนในกล่อง junk mail ดันมีเมล์สำคัญจากเพื่อนอยู่ 1 ฉบับ ลองบอกค่า TP, FP, TN, FN ของระบบกรอง spam ตัวนี้มาหน่อย

หวังว่าจะมีประโยชน์นะครับ 🙂

ข้อมูลเพิ่มเติม: Receiver Operating Characteristic (ROC)