Tuesday, March 22, 2011

ImageJ 7: Pixel processing using Lookup Table

ในการประมวลผลทีละพิกเซล บางครั้งถ้าการประมวลผลซับซ้อนและมีพิกเซลจำนวนมาก ก็จะต้องเกิด computing cost + time cost ที่สูง

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

ตัวอย่างเช่น รูปสีเทารูปหนึ่งมีขนาด 512x512 พิกเซล แต่ละพิกเซลต้องการการประมวลผล 2 วินาที เวลาที่ใช้ทั้งหมดคือ 2^19 วินาที

ถ้าใช้ Lookup Table ขนาด 1x256 จะใช้เวลาในการประมวลผล 2^9 วินาที บวกกับการให้ค่าจากตารางอีกนิดหน่อย รวมแล้วก็จะน้อยกว่าวิธีปกติมาก

ลองสังเกตการเพิ่ม contrast ของรูป 50% และหา inverse โดยใช้วิธีปกติ และ ใช้ Lookup Table

1. วิธีปกติ
public void run(ImageProcessor ip){
 int MN = ip.getPixelCount();
 for(int p=0;p<MN;p++){
  int v = ip.get(p);
  v = (int) Math.round(1.5*v); //50% increase of contrast
  if(v>255)    //clamp max
   v = 255;
  v = 255-v;   //inverse
  ip.set(p,v);
 }
}

2. Lookup Table
public void run(ImageProcessor ip){
 int K = 256; //gray level
 int[] LUT = new int[K]; //lookup table
 for(int i=0;i<K;i++){
  int v = (int) Math.round(i*1.5);    //increase contrast 50%
  if(v>255)   //clamp max
  v = 255;
  v = 255-v;  //inverse
  LUT[i] = v;
 }

 //look for value in lookup table
 int MN = ip.getPixelCount();
 for(int p=0;p<MN;p++){
  ip.set(p,LUT[ip.get(p)]);
 }
 //ip.applyTable(LUT); is better for this loop
}

ซึ่งจากการเปรียบเทียบเมื่อทดสอบบนรูปสีเทาขนาด 256x256 พิกเซล พบว่า ใช้เวลาในการประมวลผล = 0.011 และ 0.006 วินาทีตามลำดับ

หมายเหตุ เราสามารถใช้คำสั่งเพื่อให้ค่าจาก Lookup Table โดยตรงแทนลูปที่สอง โดยใช้คำสั่ง ip.applyTable(LUT); ได้

Monday, March 21, 2011

ImageJ 6: Basic Gray-level Thresholding

การทำ Gray-level Thresholding ก็คือการเปลี่ยนจากรูปสีเทาให้เป็นรูปขาวดำ โดยอาศัยค่า Threshold เป็นตัวแบ่ง ด้วยหลักการง่ายๆ เช่น

ถ้าเลือกค่า Threshold เป็น 100 ดังนั้น ทุกพิกเซลที่มีค่าน้อยกว่าหรือเท่ากับ 100 ให้มีค่าเป็น 0 (ดำ) นอกนั้นให้มีค่าเป็น 255 (ขาว) เช่น


หลังจากทำ Thresholding

สำหรับ ImageJ มีฟังก์ชันในคลาส ImageProcessor ชื่อ void threshold(int) เพื่อทำหน้าที่นี้

ลองดูตัวอย่างครับ
import ij.IJ;
import ij.ImagePlus;
import ij.plugin.filter.PlugInFilter;
import ij.process.ImageProcessor;

public class My_Threshold implements PlugInFilter
{
 public int setup(String arg, ImagePlus imp)
 {
  return DOES_8G;  //accept only 8-bit grayscale image
 }

public void run(ImageProcessor ip)
{
 int th = (int) IJ.getNumber("Threshold value (0-255)",100);
 if(th==IJ.CANCELED) //if click cancel
  return;

 if((th<0) || (th>255)){
  IJ.error("Error","0-255 only");
  return;
 }

 ip.threshold(th);
 //thresholding
 /*
 int MN = ip.getPixelCount();
 for(int p=0;p<MN;p++){
  if(ip.get(p)<=th)
  ip.set(p,0);
  else
  ip.set(p,255);
 }
 */
 }
}

ImageJ 5: Contrast and Brightness

ในหัวข้อนี้เราจะลองปรับค่า contrast และ brightness ของภาพ

contrast คือ ความแตกต่างของเฉดสี เช่น
contrast สูง คือ เฉดสีต่างกันมาก

contrast ต่ำ คือ เฉดสีใกล้เคียงกัน

brightness คือ ความมืดหรือความสว่างของภาพ เช่น
brightness สูง คือ ภาพสว่าง

brightness ต่ำ คือ ภาพมืด

นั่นคือ
การปรับ contrast คือ การคูณหรือหารค่าพิกเซล
การปรับ brightness คือ การบวกหรือลบค่าพิกเซล

ลองพิจารณารหัสโปรแกรมในการปรับค่า contrast ดังนี้
//Plugin to change contrast
import ij.IJ;
import ij.ImagePlus;
import ij.plugin.filter.PlugInFilter;
import ij.process.ImageProcessor;

public class My_Contrast implements PlugInFilter
{
 public int setup(String arg, ImagePlus imp)
 {
  return DOES_8G;  //accept only 8-bit grayscale image
 }

 public void run(ImageProcessor ip)
 {
  double ad = IJ.getNumber("Adjust percent of contrast",50);  
  if(ad==IJ.CANCELED) //if click cancel
   return;

  int MN = ip.getPixelCount();
  for(int p=0;p<MN;p++)
  {
   //contrast
   int v = (int)(ip.get(p)*(1+ad/100));
   if(v>255) //clamp max value
    v = 255;
   if(v<0)  //clamp min value
    v = 0;
   ip.set(p,v);
  }
 }
}

ส่วนการปรับค่า brightness ก็สามารถทำได้ดังนี้
 public void run(ImageProcessor ip)
 {  
  double ad = IJ.getNumber("Adjust value of brigtness",50);
  if(ad==IJ.CANCELED) //if click cancel
   return;

  int MN = ip.getPixelCount();
  for(int p=0;p<MN;p++)
  {
   //brightness
   int v = (int) (ip.get(p) + ad);
   if(v>255) //clamp max value
    v = 255;
   if(v<0)  //clamp min value
    v = 0;
   ip.set(p,v);
  }
 }

Sunday, March 20, 2011

ImageJ 4: Message

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

ใน ImageJ เรามีคำสั่งสำหรับการแสดงข้อความหรือตัวแปร เช่น
IJ.log(String) ตัวอย่างเช่น
 public void run(String arg) 
 {
  // log
  IJ.log("Use log method to debug variable");
  int a = 99;
  IJ.log("a = "+a);  
 }

ซึ่งจะได้ผลลัพธ์ คือ

หากต้องการแสดงข้อความที่ status bar ก็จะสามารถใช้คำสั่ง
IJ.showStatus(String) เช่น
 public void run(String arg) 
 { 
  // show text in status bar
  IJ.showStatus("Message at status bar");
 }

ซึ่งจะได้ผลลัพธ์

 ImageJ ยังสนับสนุนการแสดงข้อความเป็น dialog box ในหลายรูปแบบ เช่น
-แสดงข้อความผิดพลาด ใช้คำสั่ง IJ.error(String Title, String ErrorMessage); เช่น
IJ.error("Error","Need user input");
จะได้ผลลัพธ์เป็น

หากต้องการให้ผู้ใช้ป้อนค่าตัวอักษร สามารถใช้คำสั่ง String IJ.getString(String Title, String DefaultString) เช่น
String name = IJ.getString("Please enter your name: ","Your name here");
ก็จะเกิดกล่องข้อความดังนี้

จากนั้นเราก็สามารถใช้ตัวแปรที่รับมาได้ เช่น ใช้แสดงในกล่องข้อความใหม่ โดยใช้คำสั่ง IJ.showMessage(String Title, String message) เช่น
IJ.showMessage("Finished",name+" is running ImageJ");
จะได้ผลลัพธ์คือ

Sunday, March 13, 2011

ImageJ 3: Histogram

ตัวอย่างนี้จะเป็นการลองสร้าง Histogram ของรูปสีเทารูปนี้ครับ


ผลลัพธ์ที่อยากได้คือ Histogram ประมาณนี้

ImageJ มีฟังก์ชันในการคำนวณ Histogram ให้คือ getHistogram(); ซึ่งจะใช้หรือเขียนฟังก์ชันเองก็ได้ตามตัวอย่างต่อไปนี้ครับ
//Plugin to compute histogram
import ij.ImagePlus;
import ij.plugin.filter.PlugInFilter;
import ij.process.ImageProcessor;
import ij.process.ByteProcessor;

public class My_Histogram implements PlugInFilter 
{
 public int setup(String arg, ImagePlus im) 
 {
  return DOES_8G + NO_CHANGES;  //accept only 8-bit grayscale image
 }

 public void run(ImageProcessor ip) 
 {
  int[] H = new int[256];
  /*
  int MN = ip.getPixelCount();
  for(int p=0;p<MN;p++)
  {
   int v = ip.get(p);
   H[v] += 1;
  }
  */
  H = ip.getHistogram(); //built-in method   
 }
}

ส่วนการแสดงผล จำเป็นต้องสร้างหน้าต่างขึ้นมาใหม่ แล้ววาดจุดสีดำลงไปครับ ซึ่งโค้ดทั้งหมดต่อไปนี้ ยังอยู่ใน method run() ครับ
//find max of histogram bin
  int max=H[0];
  for(int b=1;b<256;b++)
  {
   if(max<H[b])
    max=H[b];
  }
  
  //create the image of histogram
  //prepare blank area of w=256 h=100
  ImageProcessor histip = new ByteProcessor(256,100); 
  histip.setValue(255) ; // white = 255 
  histip.fill(); // clear this image 
  //draw histogram bins    
  for(int b=0;b<256;b++)
  {
   int hh = (int)(100.0*H[b]/max); //normalized bin's height
   for(int r=0;r<hh;r++)
   {
    histip.set(b,99-r,0); //plot black points
   }
  }
  
  // display the histogram image: 
  ImagePlus histim = new ImagePlus("Histogram", histip); 
  histim.show();

ImageJ 2: Invert an image

เรายังอยู่ที่การ invert image ครับ แต่จะลองใช้เทคนิคในการเข้าถึงพิกเซลต่างๆกัน ลองดูตัวอย่างโคัดครับ ในที่นี้มีอย่างน้อย 4 วิธี ดูรายละเอียดตามที่หมายเหตุได้ครับ

//Plugin to invert a grayscale image
import ij.ImagePlus;
import ij.plugin.filter.PlugInFilter;
import ij.process.ImageProcessor;
import ij.process.ByteProcessor;

// _ in the class name = plugin
public class My_Invert implements PlugInFilter 
{
 public int setup(String arg, ImagePlus im) 
 {
  return DOES_8G;  //accept only 8-bit grayscale image
 }

 public void run(ImageProcessor ip) 
 {  
  //1. use getPixel, putPixel to access each pixel
  //they check image boundary+invoke function call, slowest
  int w = ip.getWidth();
  int h = ip.getHeight();
  //for each pixel
  for(int x=0;x<w;x++)
   for(int y=0;y<h;y++)
    ip.putPixel(x,y,255-ip.getPixel(x,y));
    
  /*
  //2. use get,set which do not check boundary
  //still invoke function, faster
  int w = ip.getWidth();
  int h = ip.getHeight();
  //for each pixel
  for(int x=0;x<w;x++)
   for(int y=0;y<h;y++)
    ip.set(x,y,255-ip.get(x,y));
  */
  
  /*  
  //3. use get,set with 1d indices
  //only 1 loop, invoke function, more faster
  int MN = ip.getPixelCount(); //total pixels
  for(int p=0;p<MN;p++)
   ip.set(p, 255-ip.get(p));
  */
  
  /*
  //4. use getPixels to directly process pixel array
  //direct access, no function invocation
  //fastest but more memory need
  //check if ip is ByteProcessor (0-255)
  if(!(ip instanceof ByteProcessor))
   return;
  if(!(ip.getPixels() instanceof byte[]))
   return;
  //dump to array but by reference!!!
  byte[] pixels = (byte[]) ip.getPixels(); 
  int MN = ip.getPixelCount();
  for(int p=0;p<MN;p++)
  {
   int v = 0xFF & pixels[p]; //convert to integer
   v = 255-v;
   pixels[p] = (byte) (0xFF & v); //to byte
  }
  */      
 }
}

ตามทฤษฎีแล้วแต่ละวิธีจะให้ความเร็วต่างกัน ซึ่งจะเห็นชัดขึ้นเมื่อรูปมีขนาดใหญ่ขึ้นครับ

ImageJ 1: Invert an image

ImageJ เป็นซอฟต์แวร์ทางด้านการประมวลผลภาพที่มีประสิทธิภาพ ไม่เสียค่าใช้จ่าย และใช้งานได้ไม่ยากครับ

ในที่นี้เราจะลองศึกษาการประยุกต์ใช้งาน โดยสร้าง plugin ซึ่งเนื้อหาโดยส่วนใหญ่ในหัวข้อนี้และถัดๆไป ผมจะอ้างอิงจากเอกสารในเว็บ http://rsbweb.nih.gov/ij/ และหนังสือ Wilhelm Burger and Mark James Burge, Digital Image Processing: An Algorithmic Introduction using Java, Springer, 2008.

ซึ่งไม่ได้มีเจตนาจะละเมิดลิขสิทธิ์ใดๆทั้งสิ้น

ถ้าพร้อมแล้ว ดาวน์โหลด ImageJ ได้ที่ http://rsbweb.nih.gov/ij/ ครับ

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

เราจะลองสร้าง plugin เพื่อเพิ่มความสามารถในการประมวลผลภาพตามที่เราต้องการ ในครั้งนี้เราจะลอง invert รูปสีเทาครับ ที่เราเรียกว่าทำ image negative

ขั้นตอนในการสร้าง PlugIn
1. สร้างโฟลเดอร์ หรือ ไฟล์จาวา ในไดเรกทอรี ImageJ/plugins ครับ ในที่นี้เราจะสร้างโฟลเดอร์ชื่อ My และสร้างไฟล์ชื่อ My_Invert.java ไว้
โปรดสังเกตว่า เราใช้เครื่องหมาย _ เพื่อแสดงว่าไฟล์นี้เป็น plugin และจะถูกโหลดเข้าไปใน ImageJ โดยอัตโนมัติ
2. ใช้ IDE ตัวไหนก็ได้แก้ไขไฟล์จาวา ชื่อคลาสต้องตรงกับชื่อไฟล์ plugin
3. ใช้ ImageJ คอมไพล์ไฟล์นั้น
4. แ้ก้ไขไฟล์ถ้ามีข้อผิดพลาด
5. ถ้าไม่มีข้อผิดพลาดก็จะใช้ plugin นั้นได้เลยทันที

ลองมาดูโค้ดของ plugin invert กันครับ
//Plugin to invert a grayscale image
import ij.ImagePlus;
import ij.plugin.filter.PlugInFilter;
import ij.process.ImageProcessor;
import ij.process.ByteProcessor;

// _ in the class name = plugin
public class My_Invert implements PlugInFilter 
{
 public int setup(String arg, ImagePlus im) 
 {
  return DOES_8G;  //accept only 8-bit grayscale image
 }

 public void run(ImageProcessor ip) 
 {  
  //1. use getPixel, putPixel to access each pixel
  //they check image boundary+invoke function call, slowest
  //0.015 second
  int w = ip.getWidth();
  int h = ip.getHeight();
  //for each pixel
  for(int x=0;x<w;x++)
   for(int y=0;y<h;y++)
    ip.putPixel(x,y,255-ip.getPixel(x,y));
        
 }
}

ทดลองโหลดรูปใดๆ ขึ้นมาก่อน เช่น

จากนั้น คอมไพล์โดยใช้เมนู Plugin / Compile and Run...

ผลลัพธ์ที่ได้

จะเห็นได้ว่าเรามีการเข้าถึงและประมวลผลพิกเซลได้โดยสะดวกครับ