Friday, April 16, 2010

Learning OpenCV: Moments

Moments เป็นวิธีหนึ่งที่ง่ายที่สุดในการเปรียบเทียบ contour โดยการเปรียบเทียบ contour moments
ลองมาดูนิยามของคำว่า moment กันก่อน

m(p,q) = ผลรวมของ (I(x,y)*x^p*y^q) ในทุกจุด

ในกรณีที่ p=q=0 m(0,0) และเป็นรูป binary จะหมายถึงความยาวของ contour ถ้าเป็นรูปปิดจะหมายถึงพื้นที่ (จำนวนจุดทุกจุดในเส้นรอบปิด เท่ากับพื้นที่)


ฟังก์ชันที่ใช้ในการหา moment คือ


void cvContoursMoments(
    CvSeq* contour,
    CvMoments* moments
)



โดย CvMoments นิยามดังนี้


typedef struct CvMoments {
    // spatial moments
    double m00, m10, m01, m20, m11, m02, m30, m21, m12, m03;
    // central moments
    double mu20, mu11, mu02, mu30, mu21, mu12, mu03;
    // m00 != 0 ? 1/sqrt(m00) : 0
    double inv_sqrt_m00;
} CvMoments;

cvContoursMoments จะคำนวนเฉพาะ spatial moments ให้เท่านั้น เราสามารถหา moment ได้โดยใช้คำสั่ง
cvGetSpatialMoment


moments ที่กล่าวมายังใช้ัในการเปรียบเทียบ contour ได้ไม่ดีนัก ปกติแล้วเรามักจะต้องการ normalized moments (contour ที่รูปร่างเหมือนกันแต่ขนาดต่างกันจะได้เปรียบเทียบกันได้) และ moment ที่คำนวณได้จะขึ้นกับ ระนาบที่เลือกใช้ เพราะฉะนั้นถ้า contour เกิดการหมุน moments ก็จะไม่ match กัน

OpenCV มีทางเลือกในการหา moments คือ

  • cvMoments เหมือน cvContoursMoments แต่รับ input เป็นภาพ สามารถตั้งให้เป็น binary image ได้
  • cvGetCentralMoment การคำนวณคล้ายกับ cvMoments เพียงแต่ ลบค่า เฉลี่ยในแกน x,y ลงไป มีผลทำให้เกิดคุณสมบัติ transition invariant (ไม่เปลี่ยนแปลงไปตามการเลื่อน)
  • cvGetNormalizedCentralMoment เหมือน cvGetCentralMoment ยกเว้นมีการ normalized การ normalized นี้ทำให้เกิด invariant to scale (moment ของ contour ที่คล้ายๆ กัน แต่ขนาดไม่เท่ากันจะถูก normalized ให้ใกล้ๆ กัน)
  • cvGetHuMoments เกิดจากผลรวมของ normalized central moment ผลที่ได้ จะได้ค่าที่ invariant to scale, rotation, and reflection (ผลจาก Hu Moment เลเวลหนึ่งจะ invariant to rotate  เลเวลเจ็ด จะ invariant to skew จึงสามารถนำมาใช้ในการเปรียบเทียบได้ดีกว่า moment ธรรมดา)

Matching with Hu Moments

การ matching contours ใน OpenCV ใช้คำสั่ง


double cvMatchShapes(
    const void* object1,
    const void* object2,
    int method,
    double parameter = 0
);

object1, object2 สามารถเป็น image หรือ contour ได้ ถ้าเป็น image ฟังก์ชันนี้จะคำนวณ moments ให้ก่อน แล้วนำมาเปรียบเทียบ
parameter ตอนนี้ไม่ได้ใช้
method เลือกได้ระหว่าง


  • CV_CONTOURS_MATCH_I1
  • CV_CONTOURS_MATCH_I2
  • CV_CONTOURS_MATCH_I3

รายละเอียดดูได้จากในหนังสือ (เพราะไม่ได้บอกถึงผลที่ได้จากการ คำนวณแต่ละแบบ)

Learning OpenCV: Contour Summary Characteristics and Geometry

คุณลักษณะของ contour ที่สำคัญๆ ที่ OpenCV สามารถหาได้

  • cvArcLength ใช้หาความยาวของ contour
  • cvContourArea หาพื้นที่ของ contour
  • cvBoundingRect หา bounding box ของ contour เป็นสี่เหลี่ยมแนวขนานกับแกน x
  • cvMinAreaRect2 หา bounding box โดยสี่เหลี่ยมที่ bound จะมีมุมใดๆ ก็ได้
  • cvMinEnclosingCircle หา bounding circle
  • cvFitEllipse2 หา fitting ellipse

Geometry
เป็นฟังกขันในการคำนวณเกี่ยวกับ bounding box ทั้งหลาย


  • cvMaxRect หา bounding box ระหว่าง CvRect สองตัว
  • cvBoxPoints หาจุดทั้งสี่มุมจาก CvRect 
  • cvPointSeqFromMat สร้าง sequence ของจุดจาก metric
  • cvPointPolygonTest ใช้ในการทดสอบว่าจุด อยู่ใน polygon หรือไม่ 




Learning OpenCV: Freeman Chain Codes, Polygon Approximations and Dominant point

Freeman Chain Codes
ปกติวิธีในการเก็บ contour จาก cvFindContours (ค่า default คือ CV_CHAIN_APPROX_SIMPLE) อีกวิธีหนึ่งคือ Freeman chains โดยกำหนดใน mode  CV_CHAIN_CODE ลองดูภาพตัวอย่าง

จากรูป (a) เราจะแบ่งทิศทางออกเป็นแปดทิศตามรูป ส่วนใน (b) แสดงการใช้ freeman codes ในการลากแทนที่ contour ของรถถัง โดยเริ่มจากท้ายตัวถัง

ในการอ่านค่า freeman chains ใช้ฟังก์ชันดังนี้
  • cvStrarReadChainPoints
  • cvReadChainPoint
 คำสั่งแรกเป็นการเริ่มอ่านค่า chain (เหมือน fopen) แล้วทำการอ่านค่า chain ต่อไปเรื่อยๆ ด้วย cvReadChainPoint จนกว่าจะเจอค่า NULL ในทำนองเดียวกับ CvSeqReader



Polygon Approximations

ปกติ Polygon Approximations จะใช้ในการลดจำนวน vertics OpenCV ใช้วิธี Douglas-Peucker (DP) approximation ในฟังก์ชันที่เรียกว่า cvApproxPoly()

CvSeq* cvApproxPoly(
    const void* src_seq,
    int header_size,
    CvMemStorage* storage,
    int method,
    double parameter,
    int recursive = 0
);


ผลลัพธ์ที่ได้จะเป็น CvSeq* ที่ชี้ไปยัง contour แรก (เราสามารถหา contour ต่อไปได้เอง) 

src_seq คือ sequence ของ contour ที่จะใช้ในการหา approximation
header_size = sizeof(CvContour)
storage เป็น memory storage ที่ต้องใช้ในการคำนวณ
method  เป็น CV_POLY_APPROX_DP (อย่างที่กล่าวไว้ในข้างต้น และตอนนี้มีวิธีเดียว)
recursive จะบอกว่าให้ค่า  approximation เฉพาะ contour แรกที่ชี้(head) หรือจะให้หาทุก contour
parameter เป็นค่าที่กำหนดเป็น threshold สำหรับวิธีการหา approximation

ก่อนอื่นลองดูวิธีการหา approximation เพื่อที่จะเข้าใจถึง parameter


(a) เป็น image, (b) คือ contour ของ (a) ต่อไปเราจะหา extreme point แล้วเชื่อมด้วยเช่น (c)  หลังจากนั้นจะหาจุดใน contour ที่ไกลจากเส้นที่สุด (ดูใน (c)) หลังจากนั้นลากเส้นเชื่อม (d) แล้วหาจุดที่ไกลจากสามเหลี่ยมใน (d)  ทำไปเรื่อยๆ จนในที่สุด เส้นที่หาได้น้อยกว่าค่าทีระบุใน parameter ก็หยุด


Dominant point

คือจุดที่มีข้อมูลเกี่ยวกับ curve มากกว่า จุดอื่น(นัยว่าสำคัญกว่าจุดอื่นเพราะมีข้อมูลเยอะกว่า ถ้าจะลดจำนวน vertices จุดทีเป็น dominant point ไม่น่าโดน) ฟังก์ชันที่ใช้หา dominant point ใน OpenCV คือ 

CvSeq* cvFindDominantPoints(
    CvSeq* contour,
    CvMemStorage* storage,
    int method = CV_DOMINANT_IPAN,
    double parameter1 = 0,
    double parameter2 = 0,
    double parameter3 = 0,
    double parameter4 = 0
);


contour คือ contour ที่จะหา dominant point
storage คือ memory storage ที่ใช้ในการคำนวณ
method ตอนนี้มีค่า CV_DOMINANT_IPAN หมายถึง algorithm IPAN

แนวคิดคือ IPAN จะพยายาม scan ไปทั่ว contour เพื่อจะสร้างสามเหลี่ยมโดยใช้จุดใน contour ลักษณะของสามเหลี่ยมจะกำหนดโดย ขนาดสองด้าน และ มุม (ด้านมุมด้าน เพียงพอที่จะกำหนดสามเหลี่ยมได้ ยังจำได้ไหม) 

parameter1, parameter2 หมายถึง ระยะทางที่สั้นสุด และระยะทางที่ยาวที่สุด
parameter3 หมายถึง neighbor distance
parameter4 หมายถึง maximum angle θ

หลังจากที่ IPAN สร้างสามเหลี่ยมที่แบบที่ ด้านสองด้าน มีค่าระหว่าง paramter1และ parameter2 โดยที่มุมระหว่างสองด้านน้อยกว่า parameter4 หลังจากนั้น จุดที่มีมุมที่น้อยที่สุดในระหว่างจุดที่ใกล้เคียงกัน (กำหนดความใกล้เคียงกันโดย parameter3) จะคงอยู่ที่เหลือจะถูกกำจัดออกไป
โดยปกติค่า parameter1-4 จะกำหนดเป็น 7,9,9,150 ตามลำดับ






Learning OpenCV: Contour

จากตอนก่อนๆ เรื่องเกี่ยวกับการ detect edge โดยใช้ canny ขั้นตอนต่อไปคือการรวมจุดที่ประกอบเป็น edge นั้นรวมกัน เรียกว่า contour แน่นอน OpenCV มีฟังก์ชันสำหรับหา contour ไว้ให้

ก่อนอื่นมาดูนิยามของ contour ก่อน contour คือ list ของจุดที่แทน curve ในรูปภาพ ในกรณีของ Contour ใน OpenCV จะเก็บไว้ใน sequence ลองดูภาพประกอบ



ในภาพบนเป็นภาพที่เราต้องการหา contour ซึ่งประกอบด้วยส่วน A-E ส่วนด้านล่างคือ contour ที่ OpenCV หามาได้ (โดยฟังกชัน cvFindContours) ซึ่งกำกับไว้ด้วยคำว่า cX หรือ hX โดย c หมายถึง contour, h หมายถึง hole ตัวที่เป็นเส้นประ เป็น exterior boundaries ของพื้นที่สีขาว (ที่ไม่ใช่ 0 ก็คือกรอบของพื้นที่ขาว)   ตัวเส้นจุดนั้นเป็น interior boundaries ของพื้นที่สีขาว หรือ exterior boundary ของพื้นที่ดำ (คืออยู่ในพื้นที่ขาว, หรือว่าเป็นกรอบของพื้นที่สีดำ) ซึ่ง OpenCV จะแยกความแตกต่างระหว่างสองตัวนี้

*notes ในความเป็นจริงระหว่างภาพ binary (ภาพขาวดำ) และ edge ที่ detect ได้จาก canny จะมีความแตกต่างกันเล็กน้อยในผลลัพธ์ เพราะจริงๆ แล้ว cvFindContours ไม่รู้จักว่า edge คืออะไร แต่จะมองเห็นเป็นพื้นที่สีขาวบางๆ (ไม่ได้เข้าใจว่ามันเป็น edge แต่อย่างไร) ผลลัพธ์ที่ได้ก็คือ ใน edge วงรอบปิดหนึ่งๆ จะได้ exterior contour และ interior contour มาสองอันที่ขนาดใกล้กันมาก

ลองมาดูฟังก์ชันกันซะที

int cvFindContours(
    IplImage* img,
    CvMemStorage* storage,
    CvSeq** firstContour,
    int headerSize = sizeof(CvContour),
    CvContourRetrievalMode mode = CV_RETR_LIST,
    CvChainApproxMethod method = CV_CHAIN_APPROX_SIMPLE
);

img คือรูปภาพซึ่ง OpenCV จะใช้พื้นที่นี้ในการคำนวณด้วย ถ้าต้องการเก็บข้อมูลส่วนนี้ให้ copy ไว้ก่อน
storage เป็น memory storage ที่เราจองไว้
firstContour เป็นผลลัพธ์ที่ได้จากฟังก์ชัน
headerSize ใช้ในการบอกถึงขนาด object ที่จะ allocate ปกติเป็น sizeof(CvContour) หรือ sizeof(CvChain)
mode มีตัวเลือกอยู่ 4 ตัว

  • CV_RETR_EXTERNAL จะหาเฉพาะ extrme outer contours
  • CV_RETR_LIST หาทุก contour และแทนด้วย list
  • CV_RETR_CCOMP หาทุก contour และแทนด้วยต้น list สอบระนาบ ระนาบบนมีแต่ cX ระนาบล่างเป็น hX
  • CV_RETR_TREE  หาทุก contour และจัดเรียงเป็น tree


ลองดูตัวอย่างผลที่ได้จากการหา contour ในรูปแรกกัน



ตัวเลือกเหล่านี้จะบอกว่าเราต้องการหา contour แบบไหนและผลลัพธ์แบบไหน ซึ่งจริงๆ วิธีการลิงก์ linked list ของ sequence ด้วย h_prev, h_next, v_prev, v_next นั้นกำหนดได้จาก mode ในการหา contour

method คือวิธีการประมาณค่า contour

  • CV_CHAIN_CODE ผลลัพธ์ contour จะอยู่ในรูปแบบ Freeman chain code วิธีอื่นจะเป็น polygons (sequence of vertics)
  • CV_CHAIN_APPROX_NONE แปลง chian code ให้เป็น จุด
  • CV_CHAIN_APPROX_SIMPLE 
  • CV_CHAIN_APPROX_TC89_L1  CV_CHAIN_APPROX_TC89_KCOS ใช้วิธี Teh-Chin approximation algorithm
  • CV_LINK_RUN  method นี้จะใช้ได้กับ mode CV_RETR_LIST เท่านั้น

ซึ่งวิธีการหา contour นอกจาก cvFindContours แล้ว ยังสามารถใช้กลุ่มฟังก์ชันเหล่านี้ในการหา contour ได้

  • cvStartFindContours
  • cvFindNextContour
  • cvSubstituteContour
  • cvEndFindContour
  • cvApproxChains
แนวคิดของวิธีนี้คือเราจะไม่หา contour ออกมาทีเดียว เราจะออกมาทีละตัว โดยตัวแรกเราจะใช้คำสั่ง cvStartFindContours ซึ่งจะให้ผลลัพธ์มาเป็น CvSequenceScanner (คล้ายๆ กับการ เปิด file ก่อนด้วย fopen)
ซึ่งใน CvSequenceScanner เก็บ state เกี่ยวกับ contour ที่เราหาไปแล้ว (อย่าสับสนระหว่าง CvSequenceScanner กับ CvSeqReader, CvSeqReader ไว้ใช้อ่าน elements ที่เรียงเป็น sequence แต่ CvSequenceScanner  เป็น list ของ contour (ซึ่งเป็น list ของ sequece อีกที)

เราสามารถหา contour ต่อไปได้เรื่อยๆ ด้วยคำสั่ง cvFindNextContour จนกว่าจะเจอค่า NULL หรือสั่งจบการหาเองด้วย cvEndFindContour (เหมือน fclose)

ฟังก์ชัน cvSubstituteContour ใช้ในการแทน contour ในตำแหน่งที่ปัจจุบันด้วย contour ใหม่ หรือจะใช้ลบ contour ปัจจุบันอยู่ก็ได้ (โดยใช้ค่า NULL ไปใน contour ที่จะแทนที่)

ฟังก์ชัน  cvApproxChains ใช้ในการแปลง Freeman chians ไปเป็น polygon (จริงๆ มีการประมาณค่าด้วย)


Drawing Contours
ฟังก์ชันในการวาด contour คือ

void cvDrawContours(
    CvArr* img,
    CvSeq* contour,
    CvScalar external_color,
    CvScalar hole_color,
    int max_level,
    int thickness = 1,
    int line_type = 8,
    CvPoint offset = cvPoint(0,0)
);

img คือ image ที่จะวาด contour
contour คือ root node ของ contour tree
external_color, hole_color เป็นสี่ที่จะใช้ fill ระหว่าง external กับ hole
max_level จะเป็นการสั่งให้วาด contour ใน contour tree เท่ากับจำนวนชั้นที่กำหนด(ในกรณีของ CV_RETR_LIST มีชั้นเดียว ค่า 0 ก็พอ ในกรณี CV_RETR_CCOMP มีสองชั้นถ้าใส่หมดก็จะต้องให้ค่าเป็น 1 ยังมีค่าที่ติดลบสำหรับใช้ในกรณี CV_RETR_CCOMP และ CV_RETR_TREE)
thickness คือความกว้างของเส้นที่จะวาด ถ้าให้ค่าเป็น
line_type ประเภทของเส้นที่จะวาด
offset ใช้ในการกำหนดตำแหน่งการวาดแทนที่จะวาดเป็น absolute coordinate สามารถใช้ได้ในกรณีที่เรา หา contour โดยใช้ ROI


Learning OpenCV: Template Matching



วิธีนี้ไม่ใช้ histogram แต่ใช้วิธีการ match patch ที่มีรูปภาพ (ในกรณีของ back project patch นั้นจะเป็นแค่กรอบ ภาพนั้นใช้ภาพจากภาพ test) การ match patch ของรูปภาพ กับ ภาพที่จะหานั้นจะใช้วิธีที่จะกล่าวถึงต่อไปในตอนนี้

ก่อนอื่นมาดูฟังก์ชันในการ match template กันก่อน

void cvMatchTemplate(
    const CvArr* image,
    const CvArr* templ,
    CvArr* result,
    int method
);

image คือภาพที่จะหา template
templ คือ template ที่จะหาในภาพ
result คือผลลัพธ์
method คือวิธีในการคำนวณการ matching มีดังนี้

  • Square difference matching method (method = CV_TM_SQDIFF)(หาค่าผลต่างกำลังสอง)
  • Correlation matching methods (method = CV_TM_CCORR)(หา correlation)
  • Correlation coefficient matching methods (method = CV_TM_CCOEFF)

ยังมี normalized version ของสามวิธีนี้ ซึ่งจะช่วยลดผลการผิดพลาดเนื่องจากสภาพแสงไม่เท่ากัน (ในตัวอย่าง histogram matching EMD) คือ

  • CV_TM_SQDIFF_NORMED
  • CV_TM_CCORR_NORMED
  • CV_TM_CCOEFF_NORMED


วิธี CV_TM_SQDIFF จะใช้เวลาน้อยสุดแต่ผลที่ได้จะค่อนข้างแย่ตรงช้ามกับ CV_TM_CCORR_NORMED แต่ก็ต้อง trade-off

เช่นเดียวกับ cvCalcBackProjectPatch เราจะหาตำแหน่งของ วัตถุได้โดยใช้คำสั่ง cvMinMaxLoc (แต่ต้องระวังหน่อยเพราะว่า mothod ที่ต่างกันค่าที่ต้องการก็จะต่างกันเช่น CV_TM_SQDIFF_NORMED ต้องหาค่า min ส่วน CV_TM_CCORR_NORMED หรือ CV_TM_CCOEFF_NORMED ต้องหาค่า max)

เพื่อต้องการหาค่า match ที่ถูกต้อง(ไม่บังเอิญเจอ error) จึงมีแนวคิดที่ว่า บริเวิณรอบๆ จุดที่ match เจอน่าจะมีค่าค่อนข้างสูง(เพราะภาพความมีความต่อเนื่อง) ดังนั้นวิธีการ smooth ภาพผลลัพธ์ก่อนที่จะใช้ cvMinMaxLoc ในการหาจุดสุดสุดต่ำสุดน่าจะช่วยได้

Learning OpenCV: Back Projection

back projection
เป็นวิธีในการบันทึกว่า pixel หรือว patch ของ pixel เข้ากันได้กับ histogram ของ
model (ตัวอย่าง)  อย่างเช่นถ้าเรามี histogram ของสีผิวคน เราสามารถใช้ back project
หาตแหน่งของสีผิวคนในภาพได้

void cvCalcBackProject(
    IplImage** image,
    CvArr* back_project,
    const CvHistogram* hist
);
image คือภาพ ที่จะหาว่ามีข้อมูลใน histogram อยู่หรือไม่
hist คือ histogram ที่จะหา
back_project เป็น CvArr ขนาดเท่า image

ค่าใน back_project จะเป็นค่าที่อยู่ใน bin (ใน hist ที่ associated กับจุดนั้นๆ) ในกรณีที่ hist นั้น normalized ค่านี้อาจถือได้ว่า เป็นค่า ความน่าจะเป็นแบบมีเงื่อนไข (conditional probability value) อธิบายได้ว่าคือความน่าจะเป็นที่ pixel นั้นๆ จะอยู่ในเงื่อนไขเดียวกับ hist


ซ้ายบนคือ histogram ของมือ(ไม่ใช่ของขวาบน)
ขวาบนคือภาพที่เราจะทดสอบ
ซ้ายล่างคือ histogram ของภาพที่จะทดสอบ(ภาพขวาบน)
ขวาล่างคือค่าความน่าจะเป็น p(C|F)

ลองดูตัวอย่าง ถ้า C เป็นสีของ pixel และ F คือความน่าจะเป็นที่สีนั้นจะเป็นสีผิวหนัง ดังนั้น probability map(back_project) อันนี้ ให้ค่า p(C|F)

เพราะว่าค่าตรงนี้คือค่าใน bin หรือความถี่ ของ histogram ของผิว ที่ associate กับจุดนั้น ความหมายของความถี่คือจำนวน ครั้งที่เกิดขึ้นของ สีนั้นๆ ใน histogram p(C)เมื่อ histogram นี่เป็น histogram ที่คำนวณจากภาพผิวหนัง(ในตัวอย่างคือภาพมือ) เพราะฉะนั้น ค่าที่ได้คือ p(C|F)

ความน่าจะเป็นที่จะเป็นเป็น C เมื่อวัตถุนั้นเป็นผิวหนัง(F)  ในทางกลับกันเราต้องการหา ความน่าจะเป็นที่ pixel ตรงนั้นจะเป็นผิวหนัง(F) ในเงื่อนไขที่ว่า pixel ตรงนั้นมีสี C หรือ p(F|C) แต่ความน่าจะเป็นทั้งสองนี้สัมพันธ์กันโดย bayes's theorem  ดังนั้น ถ้าเรารู้ความน่าจะเป็นที่จะเจอสีผิวในภาพ p(C)  และความน่าจะป็นในการจะเจอบริเวณส่วนที่เป็นเนื้อในภาพ p(F) เราสามารถคำนวณหา p(F|C) ได้

จริงๆ ตรงนี้เห็นว่ามันแปรผันตรงกัน ค่า p(C|F) มากก็น่าจะหมายถึง ค่า p(F|C) มาก และน่าจะเป็นจริงในทางกลับกัน


ข้อควรระวังในการใช้ฟังก์ชันนี้คือถ้า back_project เป็น bytes image(ไม่ใช่ float) hist จะต้องไม่ถูก normalized หรือไม่ก็ scale ขึ้นมา เพราะการ normailized ทำให้ค่าสูงสุดใน hist คือหนึ่งค่าที่ต่ำกว่านั้นจะถูกปัดเป็น 0 ในภาพแบบ bytes



patch based back projection

เราใช้ back projection ในการดูว่า pixel น่าจะเป็นวัตถุที่เราสนใจขนาดไหน (วัตถุที่เราสนใจถูกแทนด้วย histogram) ซึ่งไม่ตรง(กันทีเดียว)กับการคำนวณความน่าจะเป็นที่วัตถุนั้นจะปรากฏในภาพ ทางเลือกอีกทางคือ กำหนด sub region ของภาพ แล้วคำนวณหา feature histogram ของ sub region  แล้วดูว่า match กับ histogram ของ model (ตัวอย่าง, วัตถุที่ต้องการหา) หลังจากนั้น เราจะ associate แต่ละ sub region ด้วยความน่าจะเป็นที่ sub region นั้นจะเป็น วัตถุที่เราสนใจ

ในขณะที่ cvCalcBackProject คำนวนความน่าจะเป็นของ pixel จะเป็นส่วนหนึ่งของ object ที่สนใจ  cvCalcBackProjectPatch จะคำนวณความน่าจะเป็น ที่ patch จะมี object ที่สนใจอยู่ โดย cvCalcBackProjectPatch จะคำนวณพื้นที่ patch นั้นไปทั่ว image แล้วคำนวณค่าใน patch นั้นเพื่อมาใส่ในผลลัพธ์(คล้ายๆ กับการทำ filter) โดยเหตุผลคือ ณ จุดนั้นๆ เพียงจุดเดียวไม่สามารถ ระบุถึง feature บางอย่างบริเวณจุดนั้นได้(เช่น texture ไม่สามารถระบุได้โดยใช้จุดเพียงจุดเดียว)

ลองดูพารามิเตอร์ของฟังก์ชัน

void cvCalcBackProjectPatch(
    IplImage** images,
    CvArr* dst,
    CvSize patch_size,
    CvHistogram* hist,
    int method,
    float factor
);

hist คือ histogram ของ object
dst เป็นผลลัพธ์ ที่มี 1 channel ขนาดลดลงตามขอบเนื่องจากการรันของ patch
patch_size คือ ขนาดของ patch
method เป็น method ที่เดียวกับที่ใช้เปรียบเทียบ histogram ในฟังกชัน cvCompareHist
factor คือระดับการ normalized ถ้าต้องการให้ภาพผลลัพธ์เห็นชัดขึ้น(ขยายความสว่าง) ก็สามารถปรับค่านี้ได้

ผลที่ได้คือความน่าจะเป็นของวัตถุ ในตำแหน่งต่างๆ หลังจากนั้น เราอาจจะใช้ คำสั่ง cvMinMaxLoc ในการหาตำแหน่งของภาพที่มีค่า สุงสุดหรือต่ำสุด

Learning OpenCV: Comparing Two Histograms

เรื่องสำคัญใน CV เรื่องหนึ่งคือการเปรียบเทียบ histogram ซึ่ง OpenCV มีฟังก์ชัน


double cvCompareHist(
    const CvHistogram* hist1,
    const CvHistogram* hist2,
    int method
);

method หมายถึง algorithm ที่ใช้ในการคำนวณเปรียบเทียบความแตกต่างของ histogram มีสี่วิธีคือ

  1. Correlation (method = CV_COMP_CORREL)
  2. Chi-square (method = CV_COMP_CHISQR)
  3. Intersection (method = CV_COMP_INTERSECT)
  4. Bhattacharyya distance (method = CV_COMP_BHATTACHARYYA)
histogram ต้อง normalized ก่อนจะมาเปรีบบเทียบ

ผลที่ได้ขึ่นอยู่กับ method ที่ใช้ในฟังก์ชัน เช่น exact match CV_COMP_CORREL, CV_COMP_INTERSECT จะได้ค่า 1, CV_COMP_CHISQR, CV_COMP_BATTACHARYYA ได้ค่า 0

ลองดูผลการคำนวณ เปรีบยเทียบ สี่วิธีกับ EMD (Earth Mover's Distance)


ปัญหาที่เกิดขึ้นคือจาก model ต้นแบบ(แถวแรก) กับ model ทดสอบที่สาม (total mis-match) ในแง่ของ CV จริงๆ แล้วอาจจะสื่อถึงสิ่งเดียวกันก็ได้ เช่น ภาพถ่ายมือ ในที่ๆ แสดงไม่เท่ากัน จึงการเลื่อนของ histogram   ถ้าเลื่อน histogram ทดสอบอันที่สามมาทางซ้าย จะได้ผล exact match  ซึ่ง EMD จะให้ค่าของจำนวน bins ที่ต้อง shift ให้ histogram เพื่อให้เกิด exact match ในกรณีตารางตัวอย่างนี้ จะเห็นได้ว่า EMD สามารถ ทายค่าของ Total mis-match ได้ถูกต้อง ในแง่ของความคล้ายระหว่างสอง histogram ในขณะที่ อีกสี่วิธีบอกไม่ได้ การคำนวณหา EMD มีฟังก์ขันแบบง่ายดังนี้ (ในหนังสือไม่ได้อธิบายพารามิเตอร์ของแบบยากไว้ และคิดว่าไม่น่าจำเป็น)

float cvCalcEMD2(
    const CvArr* signature1,
    const CvArr* signature2,
    int distance_type
);

distance_type ในที่นี้มีสองประเภทคือ Manhattan distance (CV_DIST_L1) หรือ Euclidean distance (CV_DIST_L2)
signature1, signature2 คือ CvArr ที่จะเปรียบเทียบหา EMD โดย เป็น Array ที่ประกอบด้วยค่าตามด้วยพิกัด
เช่นในกรณีที่มีมิติเดียว(ตังตัวอย่าง)  ตัว model จะได้ signature เป็น [1,0:0,1] ([ค่าหนึ่ง, ในตำแหน่งที่ศูนย์ และ : ค่าศูนย์, ในตำแหน่งที่หนึ่ง]) half match จะได้ signature เป็น  [0.5,0:0.5,1] ([ค่า 0.5, ในตำแหน่งที่ศูนย์ และ : ค่า 0.5, ในตำแหน่งที่หนึ่ง])  ในกรณีของ 3 มิติ จะได้ signature เป็น [value, x : y,z]


ลองดูตัวอย่าง 7-3 ในหนังสือ Learning OpenCV ซึ่งจะแสดงขันตอน การเปรียบเทียบ histogram โดยใช้ EMD เริ่มตั้งแต่การสร้าง signature และคำนวณค่า EMD





Learning OpenCV: Histogram Equalization

จริงๆ เรื่องนี้อยุ่ใน Image Transformation แต่เป็นการ transform histogram เลยมาอธิบายหลังจากที่ได้รู้จัก histogram กันก่อน (จริงๆ ก็ไม่จำเป็นเท่าไร)

การทำ histogram equalization คือการ mapping distribution อันหนึ่ง ให้เป็น distribution อีกอันหนึ่ง โดยให้ ค่า intensity เสมอกันเท่าที่เป็นไปได้  ซึ่งในการ remap นั้น เขาแนะนำ cumulative distribution function เป็น mapping function  ลองยกตัวอย่าง distribution ที่เป็น gaussian และ cumulative gaussian distribution

เมื่อนำ cumulative distribution มา map แล้วจะได้ distribution ที่ค่อนข้าง uniform


แต่ในกรณีภาพทั่ว equalized distribution อาจจะไม่ uniform แบบนี้ก็ได้

Learning OpenCV: Histogram

คงรู้จักกันดีว่า histogram คืออะไร มาดูการใช้งานใน OpenCV เลยดีกว่า

ใน OpenCV เราสร้าง histogram ด้วยคำสั่ง

CvHistogram* cvCreateHist(
    int dims,
    int* sizes,
    int type,
    float** ranges = NULL,
    int uniform = 1
);

dims คือจำนวนมิติของ histogram
sizes คือขนาดในแต่ละมิติ
type อาจะเป็น CV_HIST_ARRAY เพื่อเก็บข้อมูล histogram ใน dense multidimensional matrix structure (CvMatND) หรือ CV_HIST_SPARSE เพื่อเก็บข้อมูล histogram ใน sparse matrix representation (CvSparseMat)
uniform บอกว่า ข้อมูลใน bins (แต่และช่องของ histogram) เป็น uniform หรือไม่ และยังมีผลต่อความหมายของพารามิเตอร์ ranges  ถ้าตั้งค่าไม่เท่ากับศูนย์ bins จะ uniform
ranges ในกรณีที่ uniform=1 ranges เป็น array ของค่าคู่ลำดับ จำนวนคู่ลำดับจะเท่ากับจำนวนมิติ ในกรณี uniform =0 จะเป็นอะเรย์ของช่วงของแต่ละมิติ (งงล่ะสิ ดูตัวอย่างโลด)


uniform = 1 dims=1 (histogram 1 มิติ) sizes[0]=2  ranges[0] = [0,10]
จะหมายถึง histogram 1 มิติ มี 2 bins bin แรกเก็บค่า [0,5) bin สองเก็บค่า [5,10]


uniform = 0 dims=1 (histogram 1 มิติ) sizes[0]=5 ranges[0]=[0,2,4,5,7,12]
จะหมายถึง histogram 1 มิติ 5 bins  เก็บค่า [0,2), [2,4), [4,5), [5,7) ,[7,12]

เราสามารถให้ OpenCV ตั้ง range ได้โดยใช้คำสั่ง cvSetHistRanges  และเราสามารถ clear histogram เพื่อนับค่าใหม่ได้โดยใช้คำสั่ง cvCreateHist และ release histogram โดยใช้คำสั่ง cvReleaseHist 


Accessing Histogram
OpenCV มีฟังก์ชันในการเข้าถึงข้อมูลใน historgram อยู่สองชุด

  1. cvQueryHistValue_1D, cvQueryHistValue_2D, cvQueryHistValue_nD
  2. cvGetHistValue_1D, cvGetHistValue_2D, cvGetHistValue_nD
ถ้าต้องการเข้าถึงข้อมูลตรงเพื่อประสิทธิภาพ เราก็สามารถเข้าถึงข้อมูลโดยตรงได้ โดยอ้างถึง CvHistogram ก่อนอื่นมาดู struct ของ histogram กันก่อน

typedef struct CvHistogram
{
    int type;
    CvArr* bins;
    float thresh[CV_MAX_DIM][2]; // for uniform histograms
    float** thresh2; // for nonuniform histograms
    CvMatND mat; // embedded matrix header
     // for array histograms
}
CvHistogram;

ในกรณีที่เป็น sparse histogram mat น่าจะเป็น CvSparseMat 

ยกตัวอย่างในกรณีที่่ histogram เป็นแบบ dense ข้อมูลของ histogram จะเก็บในข้อมูล CvMatND เราก็สามารถอ้างถึงข้อมูลตรงนี้ได้ เช่น
  • หาจำนวนมิติได้ โดยใช้ histogram->mat.dims
  • หาจำนวน bins ในมิติ i โดยใช้ histogram->dim[i].size;
  • ในกรณี uniform หา lower, upper bound ของ bin ในมิติ ที่ i โดย histogram->thresh[ i ][ 0 ], histogram->thresh[ i ][ 1 ]
  • ในกรณี non uniform หา lower, upper bound ของ bin j ในมิติที่ i โดย histogram->thresh2[ i ][ j ], histogram->thresh2[ i ][ j +1]


Basic Manipulations with Histograms


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

เริ่มต้นที่การสร้าง histogram จาก image กันก่อน ด้วยคำสั่ง


void cvCalcHist(
    IplImage** image,

    CvHistogram* hist,
    int accumulate = 0,
    const CvArr* mask = NULL
);

image เป็นภาพที่ต้องการหา histogram (channel เดียวเท่านั้น) (จริงๆ ใช้ CvMat ก็ได้)
hist ผลลัพธ์
accumulate จะตั้งว่าให้เคลียร์ histogram ใหม่หรือไม่(ในกรณีต้องการคำนวณ histogram รวมหลายๆ ภาพ)
mask จะกำหนด pixel ที่จะคำนวณ histogram ถ้าไม่ระบุก็จะหมายถึงทุก pixel

หลังจากคำนวณ histogram แล้วเราสามารถใช้คำสั่งเหล่านี้เพื่อ manipulate histogram ได้

  • cvCopyHist  สำเนาค่าไปยัง histogram ใหม่
  • cvGetMinMaxHistValue หาค่าสูงสุดต่ำสุดของ histogram
  • cvThreshHist ตัดค่าใน bin ที่น้อยกว่า threshold ให้เป็น 0
  • cvNormalizeHist normalize histogram



Learning OpenCV: The rest of Transformation

ยังเหลือ Transform อีกสองแบบที่กล่าวไ้ว้คือ Integral Images กับ Distance Transform ซึ่งมองภาพไม่ออก เอาไว้ช่วยในการคำนวณล้วนๆ (ประมาณเดียวกับ transform ช่วงหลังๆ ) เข้าเรื่องเลยดีกว่า

Integral Images 

เป็นการ transform โดยการคำนวณค่าผลรวม, ผลรวมของกำลังสอง, ผลรวมของภาพที่่หมุนแล้ว (ไม่รู้เรื่องใช่มะ ดูภาพดีกว่า)

ทางซ้ายจะเป็น matrix ทางขวาจะเป็น matrix ที่เป็นค่ารวม (สังเกตว่า จะมีขนาดใหญ่กว่า) ค่าในแถวบน และหลักซ้ายจะเท่าเดิม  ส่วนค่าใน pixel ใดๆ จะได้มาจากผลรวมทั้งหมดจากแถวแรก ถึงแถวมัน จากหลักแรกถึงหลักมัน เช่น 25 นี่ได้มาจาก 1+2+2+20  80 ได้มากจาก 1+2+5+2+20+50 แต่จริงแล้วในการคำนวณค่า 80 ในแถวสองไม่จำเป็นต้องบวกหมดทั้งหกตัว จะใช้แค่ 50(ตัวมันเอง)+25(ซ้ายของมัน)+8(บนของมัน)-3(ซ้ายบน) ดังนั้นเวลาคำนวณ ตารางนี้ใช้เวลาเป็น O(N)

ประโยชน์หนึ่งในการคำนวณ integral image คือถ้าเราต้องการหาผลรวมในพื้นที่ (เพื่อทำค่าเฉลี่ย ความแปรปรวน) ในส่วนหนึ่งของ image ยกตัวอย่างคือ พื้นที่ที่ล้อมรอบด้วยวงเส้นทึบ ปกติการคำนวณจะใช้ระยะเวลาขึ้นกับขนาดพื้นที่ที่ต้องการคำนวณ O(N^2) ผลการคำนวณจากด้านซ้ายจะได้
20+50+20+50+100+50+20+50+20 = 380 แต่ในการคำนวณด้านขวาจะใช้ตัวเลขสี่ตัวเท่านั้นตลอดไม่ขึ้นกับขนาด ทำให้ O(1) 398-10-9+1 =380 (คงไม่งง) ฟังก์ชันของ OpenCV คือ


void cvIntegral(
    const CvArr* image,
    CvArr* sum,
    CvArr* sqsum = NULL,
    CvArr* tilted_sum = NULL
);

image คือภาพต้นฉบับ
sum คือผลลัพธ์แบบตารางในรูปด้านขวา
sqsum คือคล้ายๆ อันที่แล้วที่ยกกำลังสองก่อนบวก
tilted_sum คือหมุน 45 องศา(คือการ transpose หรือเปล่า) แล้ว sum



Distance Transform

มาถึงการ transform แบบสุดท้าย (ในหนังสือ) นิยามของการ transform แบบนี้คือในภาพผลลัพธ์ค่าที่ได้ในจุด x,y จะเป็นระยะห่างระหว่างจุด x,y ในภาพต้นฉบับกับจุด pixel ที่เป็น 0 ที่ใกล้ที่สุด (ลองคิดดูว่าถ้าหา edge detection เช่น canny แล้วกลับค่าให้ edge เป็น 0 ส่วนที่ไม่ใช่ edge มีค่า แล้วทำ distance transform ค่ายิ่งมากก็น่าจะเป็น centroid ของวัตถุ)

ในการคำนวณของ OpenCV จะใช้วิธีคำนวณระยะจริงๆ หรือว่าจะใช้ kernel ในการช่วยก็ได้


Void cvDistTransform(
    const CvArr* src,
    CvArr* dst,
    int distance_type = CV_DIST_L2,
    int mask_size = 3,
    const float* kernel = NULL,
    CvArr* labels = NULL
);

src, dst คือต้นฉบับและผลลัพธ์
distance_type คือวิธีการคำนวณหาระยะ (CV_DIST_L2 หมายถึงคำนวณระยะแบบ Cartesian)
mask_size คือขนาดของ mask ถ้าไม่ต้องการใช้ mask ใส่ CV_DIST_MASK_PRECISE
kernel ในกรณีที่เราจะทำ mask เอง (distance_type = CV_DIST_USER)
labels เป็นตัวเก็บไว้ว่าระยะห่างที่ใกล้ที่สุดที่คำนวณมาแต่ละ pixel ในผลลัพธ์คำนวณเทียบกับจุด zero ใดในภาพต้นฉบับ

Learning OpevCV: Discrete Fourier Transform and Discrete Cosine Transform

มาถึงการ transform ที่มองไม่เห็นภาพเท่า ไม่เข้าใจตั้งแต่เรียน EE math แล้วว่าทำไปเพื่ออะไร มาดูตัวอย่างก็เข้าใจขึ้นมานิดนึง ว่าบางทีการคำนวณในระนาบ x,y มันลำบาก เลยต้อง transform แล้วค่อยคำนวณ ดูอย่างในตอน Log Polar

Discrete Fourier Transform
เอาไว้ transform จากโดเมนที่เป็น discrete ซึ่ง  ปกติตามสูตร(หาได้ทั่วไป) ค่า O(big oh) จะเป็น N^2 มันเลยมีวิธีคิดได้หลายวิธี เช่น fast Fourier Transform (FFT) ซึ่งได้ประมาณ O(N*logN) ฟังก์ชัน cvDFT สามารถคำนวณ FFT สำหรับ CvArr มิติเดียวหรือสองมิติ (ในกรณีที่ CvArr เป็นสองมิติ สามารถเลือกได้ว่า จะคำนวณ FFT สองมิติ หรือมอง CvArr เป็น array มิติเดียวหลายตัว ซึ่งจะคำนวณเร็วทำทีละตัว)

ตามปกติ algorithm ในการคำนวณ DFT จะมีเคสพิเศษที่คำนวณได้รวดเร็วกว่าการคำนวณแบบปกติ ในกรณีของ OpenCV ก็เช่นกัน ถ้า CvArr มีขนาด 2^m*3^n*5^n  จะสามารถคำนวณได้ดีเป็นพิเศษ(เข้าใจว่าในกรณีนี้จะใข้ FFT คำนวณ)
ลองมาดูฟังก์ชันการคำนวณ DFT กัน

void cvDFT(
    const CvArr* src,
    CvArr* dst,
    int flags,
    int nonzero_rows = 0
);

src, dst คือตันฉบับและผลลัพธ์ ถ้าต้นฉบับมี channel (แสดงถึง มีแต่ ส่วน real) ผลลัพธ์จะมีการ packing เป็นพิเศษ(นัยว่าเพื่อประหยัดเนื้อที่)
flags

  • CV_DXT_FORWARD  
  • CV_DXT_INVERSE  คำนวณย้อนกลับ(ค่าไม่เท่าเดิมนะ ถ้าจะได้เท่าเดิมต้อง scale กันก่อน)
  • CV_DXT_SCALE ใช้ในการ scale ค่า
  • CV_DXT_INV_SCALE, CV_DXT_INVERSE_SCALE รวมสองอันข้างบนไว้
  • CV_DXT_ROWS บอก cvDFT ว่า CvArr สองมิตินั่นคือ CvArr มิติเดียวหลายตัว
nonzero_rows บอกจำนวนแถวที่ padding ค่า 0 ไว้

จากที่บอกไป cvDFT สามารถคำนวณ CvArr บางขนาดได้ดีกว่าบางขนาด ซึ่งเราสามารถหาได้จากฟังก์ชัน cvGetOptimalDFTSize() ซึ่งจะให้ค่าที่ optimal ออกมา เราก็จึงต้องทำการแปลง CvArr ของเราให้ใหญ่ขึ้นตามขนาดนั้น แล้วก็ padding แถวที่เหลือด้วยค่า 0 แล้วก็ใช้ nonzero_rows บอกให้ cvDFT รู้ว่า padding กี่แถวเพื่อจะได้ละเลยไปซะทำให้การทำงานเร็วขึ้น (padding column ไม่เกี่ยว) ซึ่งค่านี้จำเป็นทั้งสำหรับการทำ forward และ inverse 

Spectrum Multiplication


Convolution and DFT
ในหัวข้อนี้จะแสดงเรื่องประโยชน์ของ DFT (เออ เพิ่งเจอตัวแรก) คือการ convolute ภาพต่างๆ นั้นมันจะยากวิธีง่ายๆ คือก็ transform ภาพโดย DFT ทำแบบเดียวกับ kernel แล้วเอาสองตัวนี้มา Multiplication กันหลังจากนั้นก็ inverse transform ผลลัพธ์ (มันจะ่ง่ายกว่าเหรอ) ลองดูในตัวอย่าง 6-5 ในหนังสือ Learning OpenCV ของ Oreilly 


Discrete Cosine Transform
ใช้เหมือนกับ DFT เพียงแต่ว่า DCT ไม่มีส่วน imagination (i) 

void cvDCT(
    const CvArr* src,
    CvArr* dst,
    int flags
);

พารามิเตอร์ใช้แบบเดียวกับ DFT (ตอนนี้ง่ายแฮะ)