Thursday, April 15, 2010

Learning OpenCV: Cartesian, Polar, and Log Polar

ตอนนี้จะเป็นการ convert ระนาบจาก Cartesian กับ Polar

void cvCartToPolar(
    const CvArr* x,
    const CvArr* y,
    CvArr* magnitude,
    CvArr* angle = NULL,
    int angle_in_degrees = 0
);
void cvPolarToCart(
    const CvArr* magnitude,
    const CvArr* angle,
    CvArr* x,
    CvArr* y,

    int angle_in_degrees = 0
);

x,y คือพิกัดในระนาบ xy
magnitude, angle คือพิกัดในระนาบ polar
angle_in_degree เซ็ตว่ามุกในระนาบ polar จะเป็น degree หรือ radius

ส่วนการแปลง Cartesian ไปเป็น log polar ใช้ฟังก์ชันดังนี้

void cvLogPolar(
    const CvArr* src,
    CvArr* dst,
    CvPoint2D32f center,
    double m,
    int flags = CV_INTER_LINEAR | CV_WARP_FILL_OUTLIERS
);
src คือจุดในระนาบ x,y
dst ตือผลลัพธ์ในระนาบ log polar
center กำหนดจุดกึ่งกลางของภาพ src
m เป็นการขยาย scale
flags คือวิธีการ interpolation (CV_INTER_NN, CV_INTER_LINEAR, CV_INTER_AREA, CV_INTER_CUBIC) รวมกับ(โดยใช้ bitwise or) flags CV_WARP_FILL_OUTLIERS หรือ CV_WARP_INVERSE_MAP

ลองมาดูข้อดีของระนาบแบบ log polar ที่เกี่ยวข้อกับ การ หมุน และการ scale

ในรูปจะเห็นภาพสี่เหลี่ยมเส้นทึบโดนหมุน(เส้นเทา)และขยาย(เส้นประ) ผลลัพธ์ที่ได้ในระนาบ log polar จะเป็นการเลื่อนกราฟไปในแนวตั้งและแนวนอน (จากคุณสมบัติของ logarithm  กับการ transform แบบ linear transform) ซึ่งจริงๆ จะดูจากภาพก็ได้ว่า สี่เหลี่ยมทีเทาเกิดจากการหมุน ระยะห่างเท่าเดิมแต่มุมเพิ่มขึ้น ดังนั้นการเลื่อนกราฟไปในแนวตั้งในระนาบ log polar ก็เป็นเรื่องปกติ เพราะจุดเปลี่ยนแค่มุม ส่วนการขยายภาพ ก็จะเห็นได้ว่ามุมแต่มุมยังอยู่เท่าเดิมแต่ระยะห่างจาก center เพิ่มขึ้น การ shift กราฟไปทางขวาก็สมเหตุผล


การประยุกต์ใช้จะเห็นได้ว่าการ scale ภาพและการหมุนภาพ ไม่ใช่ปัญหาในการวิเคราห์ภาพอีกต่อไปถ้าอยู่ในระนาบ log polar การ matching ระหว่าง กราฟสามเส้นทางด้านขวาว่าเป็นสี่เหลี่ยมเหมือนกันง่ายกว่าการ matching สี่เหลี่ยมสามรูปทางด้านซ้ายมาก

Learning OpenCV: Stretch, Shrink, Warp, and Rotate

การ ทำ geometric transform รูปภาพใน planar นั้นอาจจะใช้ matrix 2x3 ที่เรียกว่า affine transformation หรือ 3x3 ที่เรียกว่า perspective transformation(หรือเรียกว่า homography)  มองง่ายๆ ดังรูป




การทำ affine จะสามารถ transform รูปสี่เหลี่ยมผืนผ้าไปเป็นรูปสี่เหลี่ยมด้านขนาน  ในขณะที่ homography สามารถ transform ไปเป็นรูปสี่เหลี่่ยมคางหมูได้ (เหมือนมุมมองเปลี่ยน)

ประโยชน์หนึ่งของการใช้ transformation คือการที่วัตถุอันเดียวกันถูกมองด้วยมุมมองที่ต่างกัน เช่นภาพถ่ายของวัตถุเดียวกันแต่คนละมุม ซึ่ง affine transformation สามารถใช้ได้ในหลายกรณี ยกเว้นบางกรณีทีต้องใช้ perspective transformation(homography) แต่ homography มีพารามิเตอร์มากกว่า


affine transformation คือการ transform แบบ linear transformation ตามด้วยการ translation ดังนั้นปกติในภาพ 2 มิติการทำ linear transformation สามารถใช้ matrix 2x2 แทนได้ และ ใช้ matrix 2x1 แทนการ translation ได้ (เลยเป็นที่มีของ matrix 2x3 ในที่กล่าวมาในตอนแรก) ส่วน perspective คือการหมุนในระนาบสามมิติแล้ว map กลับมาที่ระนาบสองมิติ (เลยทำไมต้อง 3x3)

การ transform ทั้ง affine และ perspective สามารถนำมาใช้ได้ทั้งการ transform แบบ dense (ภาพหรือว่าจุดที่ต่อกันเป็นภาพ) หรือแบบ sparse (list ของจุด) เราลองมาดูเป็นกรณีไป

Dense Affine Transform
เริ่มดูจากคำสั่ง

void cvWarpAffine(
    const CvArr* src,
    CvArr* dst,
    const CvMat* map_matrix,
    int flags = CV_INTER_LINEAR | CV_WARP_FILL_OUTLIERS,
    CvScalar fillval = cvScalarAll(0)
);

src, dst คือภาพต้นฉบับและผลลัพธ์เหมือนเช่นเคย
map_matrix คือ matrix 2x3 ที่จะใช้ transform แบบ affine
flag จะบอกถึงวิธีการคำนวณในกรณีที่ pixel ไม่สามารพ map ได้โดยตรง และสามารถเพิ่ม(ด้วย boolean or) ตัวเลือกต่อไปนี้

  • CV_WARP_FILL_OUTLIERS fill ส่วนที่ไม่สามารถ map ได้ด้วย fillval
  • CV_WARP_INVERSE_MAP ให้ map จาก dst ไป src แทน
ในหนังสือแนะนำให้ใช้วิธีการทำ Dense Affine Transformation อย่างอื่นคือ
void cvGetQuadrangleSubPix(
    const CvArr* src,
    CvArr* dst,
    const CvMat* map_matrix
);

ซึ่่งจะเห็นว่ามีพารามิเตอร์น้อยกว่า (header ในการประมวลผลก็จะน้อยกว่า) และสามารถ transform ได้ทีละหลายๆภาพ(หนังสือไม่ได้บอกว่าทำยังไง)
ปัญหาคือ ถ้าไม่ต้องการคำนวณ map_matrix เอง OpenCV ก็มีฟังก์ชันในการคำนวณให้

CvMat* cvGetAffineTransform(
    const CvPoint2D32f* pts_src,
    const CvPoint2D32f* pts_dst,
    CvMat* map_matrix
);

โดย pts_src จะเป็นจุดในต้นฉบับสามจุด (ซึ่งเพียงพอกับการกำหนดมุมของสี่เหลี่ยมผืนผ้า) และ pts_dst จะเป็นจุดในผลลัพธ์ที่ต้องการสามจุดเช่นกัน (ก็เพียงพอกับการกำหนดมุมของสี่เหลี่ยมด้านขนาน) ผลลัพธ์ที่ได้จะอยู่ใน map_matrix อีกฟังก์ชันหนึ่งที่กล่าวถึงในหนังสือคือ cv2DRotationMatrix ซึ่งคำนวณ map_matrix จากการหมุนรูปภาพ (ซึ่งเข้าใจว่าไม่สามารถแทนการ stretch กับ shrink ได้)

Sparse Affine Transformations

เป็นการ affine transform กลุ่มของจุด
void cvTransform(
    const CvArr* src,
    CvArr* dst,
    const CvMat* transmat,
    const CvMat* shiftvec = NULL
);

ในหนังสือแนะนำวิธีการใช้ฟังก์ชันนี้สองวิธี แต่ความเห็นส่วนตัวแนะนำวิธีที่ ใช้ transmat เป็น matrix 2x3 เช่นเดียวกับการทำ Dense Affine Transformation 


Dense Perspective Transformation

void cvWarpPerspective(
    const CvArr* src,
    CvArr* dst,
    const CvMat* map_matrix,
    int flags = CV_INTER_LINEAR + CV_WARP_FILL_OUTLIERS,
    CvScalar fillval = cvScalarAll(0)
);



การใช้งานแบบเดียวกับ Dense Affine Translation ทุกอย่างยกเว้น map_matrix ต้องเป็น 3x3

และหา map_matrix ได้ด้วย


CvMat* cvGetPerspectiveTransform(
    const CvPoint2D32f* pts_src,
    const CvPoint2D32f* pts_dst,
    CvMat* map_matrix
)

คล้ายๆ การหา map_matrix ของ Dense Affine Transformation เพียงแต่ ต้องกำหนดสี่จุด (เพราะว่า สามจุดไม่เพียงพอต่อการกำหนดสี่เหลี่ยมคางหมูได้อีกต่อไป)


Sparse Perspective Transformation

เปิดมาด้วยฟังก์ชันเลย

void cvPerspectiveTransform(
    const CvArr* src,
    CvArr* dst,
    const CvMat* mat
);

โดยที่ถ้า mat เป็น matrix 3x3 จะเป็นการ project ภาพสองมิติไปยังสองมิติ(เนื้อหาในตอนนี้) ถ้าเป็น 4x4 จะเป็นการ project จาก 4 มิติไปยัง 3 มิติ (ยังหาอ่านไม่เจอ)

Learning OpenCV: Convolution (cont)

Canny Edge Detection
แนวคิดของ canny edge detection คือการหาอนุพันธ์เพื่่อหาจุดสุดสุดและต่ำสุดของข้อมูล โดยเชื่่อว่าจุดนั้นคือ edge (ดังที่ได้อธิบายไปในตอนที่แล้ว) ที่นี้ข้อแตกต่างก็คือ ในขณะที่ Laplace ใช้ผลรวมของสองแกน x,y ของ canny หาอนุพันธ์ของแกนสี่แกน (มุมเฉียงอีกสองแกน) วิธีการเรียกก็เหมือนกับ Laplace


void cvCanny(
    const CvArr* img,
    CvArr* edges,
    double lowThresh,
    double highThresh,
    int apertureSize = 3
);

ค่าที่ได้จะเป็น array ของ edge ที่ detect ได้ (ซึ่งสามารถสร้างเป็น contour)
โดยถ้าค่า gradient ที่ได้มากว่า highThresh จะถือว่าเป็น edge ถ้านอ้ยว่า lowThresh จะถือว่าไม่ใช่ edge ค่าที่อยู่ระหว่างนั้น ให้ดู pixel ข้างเคียง โดยถือว่าเป็น edge ถ้าข้อมูลข้างเคียงสูงกว่า highThresh

Hough Transform
เป็นการค้นหาเส้นตรงที่อยู่ในรูปภาพโดยมีแนวคิดที่ว่า จุดใดๆ ในภาพอาจจะเป็นส่วนหนึ่งของเส้นตรงได้
ดังนั้นถ้ากำหนดพารามิเตอร์ของเส้นตรงเช่น เส้นตรงกำหนดได้ด้วย a,b (ความชัน กับ จุดตัดแกน x) ดังนั้น จุดในระนาบ x,y ใดๆ ในภาพก็สามารถ map ไปยังระนาบ a,b ได้ (แน่นอนไม่ได้เป็น one-to-one แต่เป็น one-to-many) เมื่อ map หลายจุดเข้าไป ในระนาบ จุดที่ซ้ำกัน ก็ให้รวมค่าซะ (เราเรียก plane นี้ว่า accumulator plane) ค่าที่เป็น local maximum (เนื่องจากจุดใกล้ๆ เป็นเส้นตรงที่คล้ายๆ กันไม่จำเป็นต้องนับทั้งหมด) ก็ถือว่าเป็นเส้นที่อยู่ในภาพ แต่จริงๆ แล้วการใช้ระนาบ a,b ไม่ค่อยเวิร์คเพราะว่าค่า a มีได้อนันต์ จึงใช้วิธีกำหนดเส้นโดยใช้ polar (ρ,θ) (จริงๆ polar กำหนดเส้นตรงไม่ได้ แต่ในที่นี้กำหนดให้เส้นตรงนั้นตั้งฉากกับมุม θ) ใน OpenCV จะคำนวณค่า local max ในระนาบ   (ρ,θ) ออกมาให้

OpenCV สนับสนุน Hough Line Transform สองชนิด ชนิดแรกคือ standard Hough Transform (SHT) ดังที่กล่าวไปแล้ว อีกชนิด คือ progressive probabilistic Hough Transform (PPHT) โดยใช้ความน่าจะเป็นในการคำนวณ local max โดยไม่จำเป็นต้องคำนวณทุกจุดใน accumulator plane เพราะถือว่าถ้า local max สูงพอ การคำนวณขาดไปบางจุดค่า local max ก็ยังต้องสูงอยู่ดี

CvSeq* cvHoughLines2(

    CvArr* image,
    void* line_storage,
    int method,
    double rho,
    double theta,
    int threshold,
    double param1 = 0,
    double param2 = 0
);

image คือรูปภาพที่จะ transform
line_storage คือที่เก็บผลลัพธ์ อาจจะเป็น memory storage หรือ CvArr ขนาด Nx1 (N คือขนาดมากสุดของผลลัพธ์)
method คือ CV_HOUGH_STANDARD, CV_HOUGH_PROBABILISTIC, หรือ CV_HOUGH_
MULTI_SCALE สำหรับ SHT, PPHT และ multi scale variant ของ SHT
rho, theta คือขนาดของของระนาบ  (ρ,θ)
threshold  คือขนาดที่กำหนดไว้ เพื่อที่จะนับว่าเป็น line ข้อควรระวังคือค่านี้ไม่ normalize เพราะฉะนั้นในกรณีที่รูปภาพใหญ่ขึ้นสำหรับ SHT ค่านี้อาจจะต้องตั้งสูงขึ้น

SHT ไม่ใช้ค่า param1, param2
PPHT ใช้ค่า param1 สำหรับความยาวสั้นสุดของเส้นผลลัพธ์ (สั้นมากก็ไม่น่าจะนับเป็นเส้นตรง) param2 จะเป็นระยะห่างต่ำสุดที่จะไม่เชื่อมเส้นตรงสองเส้นที่อยู่ในแนวเดียวกันด้วยกัน(คือถ้าระยะห่างสั้นกว่า param2 ก็เขื่อมเส้นตรงสองเส้นนี้เป็นเส้นเดียวกันซะ)
multi scale HT ค่า param1, param2 จะใช้ในการ กำหนดรายละเอียดการค้นหาเส้น โดยหลังจากคำนวณหาเส้นแล้วจะทำการ ตรวจสอบผลของเส้นโดยการ เพิ่ม resolution ใน rho ด้วย param1 และ theta ด้วย param2 (อันนี้หนังสืออธิบายว่าผลลัพธ์สุดท้าย rho จะเท่ากับ rho/param1 และ theta จะเท่ากับ theta/param2 ถ้าละเอียดขึ้นมันน่าจะคูณกันนี่นา)

ค่าที่ return จากฟังก์ชัน ขึ้นอยู่กับ พารามิเตอร์ line_storage ถ้าเป็น matrix array ฟังก์ชันจะคืนค่า NULL แล้วค่าของ  (ρ,θ) จะอยู่ใน 2 channel ใน array ถ้าเป็น PPHT จะเป็น array 4 channel เก็บค่า x,y ของจุดต้นและจุดปลายของเส้น ทั้งสองกรณีขนาด rows จะถูกอัพเดตโดยฟังกช์นเองเพื่อให้สอดคล้องกับจำนวนเส้นที่หาได้

ในกรณีที่ line_storage เป็น memory storage ฟังก์ชันจะคืนค่า pointer ไปยัง sequence ตัวอย่างการอ้างถึง line ที่สนใจใน sequence

float* line = (float*) cvGetSeqElem( return_line , i );

return_line คือ pointer ของ sequence ที่ได้มาจากฟังก์ชัน line[0] จะเป็นค่า ρ line[1] จะเป็นค่า θ (สำหรับ SHT, MSHT ในกรณีของ PPHT ต้อง casting ให้เป็น CvPoint)


Hough Circle Transform

เป็นแนวคิดในการหาวงกลมคล้ายๆ กับ การหา Hough Line Transform แต่เปลี่ยนจาก accumulate plane เป็น accumulate volume เพราะต้องการค่า x,y และ r แต่จริงๆ แล้ว OpenCV ไม่ได้ใช้วิธีนี้แต่ใช้วิธีที่เรียกว่า Hough Gradient Method

วิธีการของ Hough Gradient Method คือ นำภาพมาผ่านกระบวนการ edge detection ก่อน(คือ cvCanny) ต่อมาทุกๆ จุดที่ไม่ใช้ 0 ใน edge image จะมาดูค่า gradient (ที่ได้มาจากอนุพันธ์ลำดับแรกใน cvSobel) โดย

  1. ทุกจุดบนเส้นนี้(เส้นที่ผ่านจุดใน edge มีความชันทีได้มากจาก gradient โดยมีระยะห่างต่ำสุดและสูงสุดกำหนดไว้ จะถูกเพิ่มใน  accumulator)
  2. จุดที่เป็น local max ใน accumulator น่าจะมีสิทธิ์ เป็นจุดศูนย์กลาง นำมาเรียงจากมากไปน้อย
  3. ไล่ขนาดวงกลมไปเพื่อให้ตัดจุดที่ไม่ใช้ 0 ใน canny image วงกลมที่ตัดจุดมากพอและห่างจากวงกลมที่เคยหาได้แล้ว จะถูกเก็บ
  4. ไล่ไปจนครบทุก candidate ที่จะเป็น center


CvSeq* cvHoughCircles(

    CvArr* image,

    void* circle_storage,
    int method,
    double dp,
    double min_dist,
    double param1 = 100,
    double param2 = 300,
    int min_radius = 0,
    int max_radius = 0
)

image หมายถึง ภาพต้นฉบับ
circle_storage ถ้าเป็น CvArr จะได้ค่ามาเป็น array 3 channel แทน x,y,r ตามลำดับและฟังก์ชันจะคืนค่า NULL ถ้าเป็น memory storage ฟังก์ชันจะคืนค่า pointer ไปยัง sequence (คล้ายๆ อันที่แล้ว)
dp จะกำหนดตัวหารเพื่อลดขนาดของ accumulator image
min_dist เป็นค่าระยะห่างต่ำสุดที่จะแยกว่าวงกลมสองวงเป็นคนละวง
param1 คือ cvCanny threshold (จริงๆ canny ต้องการ threshold สองค่า แต่ cvHoughtCircles จะกำหนดค่าที่สองให้เป็ฯ param1/2)
param2 คือ accumulator threshold
min_radius, max_radius คือรัศมีของวงกลมที่เล็กสุดและใหญ่สุด

Learning OpenCV: Convolution

การ convolution image คือการทำอะไรสักอย่างกับทุกส่วนของ image ในปกติที่ผ่านมา เราะจะใช้ convolution kernel ซึ่งก็คือ อะเรย์ขนาดคงที่ที่มีค่าสัมประสิทธิ์อยู่ และมีจุด anchor point (ปกติจะอยู่ตรงกลาง) ขนาดของ array เราจะเรียกว่า support ของ kernel แน่นอนว่า เราไม่จำเป็นต้องวนลูปจัดการทุก pixel ในภาพ OpenCV ได้เตรียมฟังก์ชันไว้ให้แล้ว

void cvFilter2D(
    const CvArr* src,
    CvArr* dst,
    const CvMat* kernel,
    CvPoint anchor = cvPoint(-1,-1)

);

ค่าทุกอย่างดูตรงไปตรงมาดี ในกรณีของ anchor point ค่า default จะหมายถึงตรงกลาง ของ kernel
ในกรณีที่เราต้องการ convolute เอง เราอาจจำเป็นที่ต้องจัดการเรื่องขอบของภาพเอง โดยใช้คำสั่ง

cvCopyMakeBorder ซึ่งจะขอข้ามไปในรายละเอียด


Sobel
การ convolute ที่กล่าวถึงอันแรกคือ sobel derivative


cvSobel(
    const CvArr* src,
    CvArr* dst,
    int xorder, //0,1,2
    int yorder, //0,1,2
    int aperture_size = 3 //1,3,5,7
);
คื่อการคำนวณอนุพันธ์อันดับ xorder ในแกน x และ yorder ในแกน y

แต่ในความเป็นจริงแล้ว OpenCV ไม่ได้ทำการหาอนุพันธ์แต่ใช้วิธีการประมาณค่าอนุพันธ์เอา ดังนั้นบล็อก kernel ที่ใหญ่กว่าจะให้ค่าที่ถูกต้องมากกว่า (ไม่ความเสี่ยงต่อ noise น้อยกว่า) ใน OpenCV สามารถใช้ filter พิเศษที่เรียกว่า scharr filter โดยกำหนด ค่า CV_SCHARR ให้กับ aperture_size ใน cvSobel

Laplace
OpenCV implement Laplace operator  ไว้เป็นอนุพันธ์ลำดับสองของทั้งแกน x และ y ดูเผินๆ คล้ายๆ กับ sobel ที่ xorder=yorder=2 ซึ่งจริงๆ แล้ว ที่พารามิเตอร์นั้น OpenCV ใช้ Laplace Operator ในการคำนวณ
ลองดูฟังก์ชันจะเห็นว่าคล้ายกับ sobel มาก


void cvLaplace(
    const CvArr* src,
    CvArr* dst,
    int apertureSize = 3
);



ความสำคัญของการใช้ Laplace มา detect edge คือ จากการใช้อนุพันธ์อันดับสอง(ตามนิยาม) จุดที่ได้ค่าเท่ากับศูนย์ เป็นไปได้สามแบบคือ local maximum, local minimum, และเส้นโค้งที่มีความเว้าน้อยมากๆ (เรียกว่าอะไรนะ)  ของอนุพันธ์อันดับหนึ่ง (ตัดกรณีหลังทิ้งไป)

แนวคิดคือภาพน่าจะมีการเปลี่ยนแปลงไม่มากนักแต่จะเริ่มเปลี่ยนแปลงมากขึ้นเมื่อเข้าใกล้ขอบ (ดังนั้นข่วยนี้อนุพันธ์อันดับหนึ่งจะลดลงหรือเพิ่มขึ้น) จนถึงขอบและหลังจากนั้นการเปลี่ยนแปลงจะลดลงเมื่อออกจากขอบ (อนุพันธ์จะเพิ่มขึ้นหรือลดลงเข้าใกล้ศูนย์) ดังนั้นช่วงสูดสุดหรือต่ำสุดน่าจะหมายถึงขอบของภาพ ซึ่งสามารถตรวจสอบได้โดยใช้อนุพันธ์ลำดับสองมาช่วย (ก็ Laplace ไง) ลองดูตัวอย่างในหนังสือ Learning OpenCV ของ OReilly ดู





จากรูปข่วยเส้นสีเทาจะเป็นช่วงที่ อนุพันธ์อันดับสองเป็นศูนย์ที่น่าจะหมายถึงขอบ ซึ่งสัมพันธ์กับ อนุพันธ์ลำดับหนึ่งที่มีค่าสุดสุดหรือต่ำสุด  และในช่วงที่อนุพันธ์อันดับหนึ่งมากกว่า threshold ที่กำหนดไว้ จะหมายถึงค่า strong edge คือมีการเปลี่ยนแปลงถึงขนาดที่เรากำหนด เนื่องจากการเปลี่ยนแปลงเล็กๆ น้อยๆ  ก็อาจจะทำให้เกิด local max, local min ได้ จะต้องกำจัดส่วนนี้ไปด้วย

Learning OpenCV: Threshold

เรื่องสำคัญในการทำ CV อีกเรื่องคือ threshold คงไม่ต้องอธิบายมากับความหมาย เรามาดูวิธีการใช้ดีกว่า
เริ่มต้นที่ definition เช่นเคย

double cvThreshold(
    CvArr* src,
    CvArr* dst,
    double threshold,
    double max_value,
    int threshold_type
);

src คือต้นฉบับ
dst คือผลลัพธ์
double คือ threshold ที่ตั้งไว้
max_value คือค่าคงที่ใช้ในการตั้งค่าผลลัพธ์
threshold_type คือวิธีการคำนวณ threshold ใชัหลักดังนี้ (ให้ M=max_value)

  • CV_THRESHOLD_BINARY              dst = (src>T)?M:0
  • CV_THRESHOLD_BINARY_INV     dst = (src>T)?0:M
  • CV_THRESHOLD_TRUNC               dst = (src>T)?M:src
  • CV_THRESHOLD_TOZERO_INV    dst = (src>T)?0:src
  • CV_THRESHOLD_TOZERO             dst = (src>T)?src:0
และยังมี adaptive threshold ที่ค่า threshold จะเปลี่ยนค่าเองไปได้เรื่อย ซึ่งจะมีประโยชน์ในกรณีที่ภาพต้นแบบนั้นเจอแสดงแบบ gradient ทำให้ค่าต่างกันมากตาม gradient ของภาพ การเรียกใช้ก็จะคล้ายๆ กัน

void cvAdaptiveThreshold(
    CvArr* src,
    CvArr* dst,
    double max_val,
    int adaptive_method = CV_ADAPTIVE_THRESH_MEAN_C
    int threshold_type = CV_THRESH_BINARY,
    int block_size = 3,
    double param1 = 5
);

ในฟังก์ชันนี้ค่า threshold ที่จุด x,y ใดๆ จะคำนวณมาจาก ค่าเฉลี่ยของ pixel ในสี่เหลี่ยมจัตตุรัสที่มีขนาดกำหนดไว้ใน block_size แล้วลบด้วยค่า param1 โดยการเฉลี่ย pixel นั้นมีสองวิธีกำหนดไว้ใน adaptive_method คือ CV_ADAPTIVE_THRESH_MEAN_C จะถ่วงน้ำหนักเท่ากันทุก pixel ในขณะที่ถ้ากำหนดเป็น CV_ADAPTIVE_THRESH_GAUSSIAN_C จะถ่วงน้ำหนัก pixel โดย ใช้ gaussian function จากระยะห่าง 




Open CV: Resize Image and Image Pyramids

Resize Image

การ resize image ใน OpenCV จริงๆ แล้วไม่น่าจะมีอะไรยุ่งยากในการใช้ ลองดูคำสั่งกันก่อน


void cvResize(
    const CvArr*  src,
    CvArr*           dst,
    int                    interpolation = CV_INTER_LINEAR
);

src, dst คือภาพต้นฉบับและผลลัพธ์โดยดูจากขนาดของภาพใน header (IplImage) ควรระวังการตั้ง ROI ในภาพต้นฉบับ พารามิเตอร์สุดท้ายคือฟังก์ชันในการประมาณค่า interpolate ค่า pixel โดยมีค่าเป็น

  1. CV_INTER_NN nearest neighbor
  2. CV_INTER_LINEAR Bilinear
  3. CV_INTER_AREA Pixel area re-sampling
  4. CV_INTER_CUBIC Bicubic interpolation

Image Pyramids

Image pyramids คือ collection ของ image  โดยเริ่มจาก image เริ่มต้นแล้วลดขนาดไปเรื่อยๆ จนถึงจุดที่ต้องการ ในหนังสือกล่าวถึง image pyramid สองแบบคือ gaussian pyramid กับ laplacian pyramid

Gaussian Pyramids จะเริ่มต้นที่ภาพต้นแบบเรียกว่าเลเยอร์ที่ 0 (แทนด้วย G0) เราจะสร้าง เลเยอร์ีที่ G(i+1) จากเลเยอร์ Gi โดยการ convolute Gi ด้วย gaussian kernel แล้วลบแถวคู่และหลักคู่ของภาพออกไป จะได้ภาพที่ขนาดเหลือหนึ่งในสี่ของเดิม ทำได้โดยใช้คำสั่ง

void cvPyrDown(
    IplImage* src,
    IplImage* dst,
    IplFilter filter = IPL_GAUSSIAN_5x5
);

ถ้าต้องการ ขยายภาพขึ้นมาใช้คำสั่ง

void cvPyrUp(
    IplImage* src,
    IplImage* dst,
    IplFilter filter = IPL_GAUSSIAN_5x5
);

แต่ภาพที่ได้จะคำสั่ง cvPyrUp จะไม่เท่ากับภาพก่อนหน้า เพราะว่ามีข้อมูลที่หายไปจากการ down sampling collection ของข้อมูลเหล่านี้จะเรียกว่า Laplicain Pyramids ลองดูรูปภาพตัวอย่างจากหนังสือ Learning OpenCV ของ OReilly


ไอ้ส่วนที่วงสีแดงไว้ผมว่าลูกศรน่าจะกลับทิศนะ
แล้วปัญหาคือไอ้ Image Pyramids นี่เกี่ยวกับ image segmentation ยังไงล่ะ


เขาบอกว่า การทำ image pyramids ทำให้การ segment สามารถ segment ที่ภาพเล็กๆ ก่อนแล้วค่อย map กลับเป็นภาพใหญ่ทีหลัง แน่นอน OpenCV ได้ใส่ฟังก์ชันนี้มาใ้ห้ท่านแล้ว

void cvPyrSegmentation(
    IplImage* src,
    IplImage* dst,
    CvMemStorage* storage,
    CvSeq** comp,
    int level,
    double threshold1,
    double threshold2
);

src, dst คือต้นฉบับและผลลัพธ์(เนื่องจาก OpenCV ใช้ dst ในการคำนวณด้วย ดังนั้นยังไงก็ต้องใส่) int คือจำนวน level ของ pyramids (จะต้องระวังด้วย ว่าขนาดของความกว้างและความยาวของภาพใน src จะต้องหารด้วย 2^(levlel-1) ลงตัว)

stroage เป็น memory storage ที่ OpenCV ต้องการใช้ (รายละเอียดใน Memory Storage)

comp จะเป็น sequence ที่สร้างมาจาก cvPyrSegmentation เป็นข้อมูลเกี่ยวกับ connected component (cvConnectedComp) ซึ่งอธิบายโดย
typedef struct CvConnectedComponent {
    double area;            // พื้นที่ของ component
    CvScalar value;       // สีเฉลี่ยของพื้นที่นั้น
    CvRect rect;           // กรอบสี่เหลี่ยมรอบ component นั้น
    CvSeq* contour;    // ในฟังก์ชัน cvPyrSegmentation ไม่ได้เซต แต่หมายถึง sequence ของ contour(ไว้อธิบายในเรื่อง contour)
};

ลองมาดูตัวอย่างเพื่อความเข้าใจในการ segmentation ด้วย cyPyrSegmentation 

CvMemStorage* storage = cvCreateMemStorage(0);
CvSeq* comp = NULL;
cvPyrSegmentation( src, dst, storage, &comp, 4, 200, 50 );
int n_comp = comp->total;
for( int i=0; i
     CvConnectedComp* cc = (CvConnectedComp*) cvGetSeqElem( comp, i );
do_something_with( cc );

cvReleaseMemStorage( &storage );


จากโคด เราจะได้ sequence ของ connected component ซึ่งส่งต่อให้ฟังก์ชัน do_something_with

Learning OpenCV: more about image segmentation in OpenCV

ต่อจากเรื่อง image morphology ยังมีเรื่องค้างอีกนิดหน่อยเีกี่ยวกับการ segment ข้อมูล
เรื่องการคือการทำ Flood Fill แนวคิดของ flood fill นี้อาจจะเคยเห็นกันมาแล้วบ้างจากโปรแกรมแต่งรูปต่างๆ
คือจะมีเลือกจุดในรูปภาพ (seed point) แล้วทั้ง seed point และจุดรอบข้างที่มีสีเหมือนกัน(หรือใกล้เคียงกันตามกำหนด) จะถูกแทนที่ด้วยสีที่กำหนด และประเด็นคืออะไร จริงๆ แล้วประเด็นสำคัญคือ ผลที่ได้จาก flood ่่fill operation นั้นจะเป็นพื้นที่ๆ ต่อเนื่องกันพื้นที่หนึ่ง เรียกได้ว่าเป็นการ segment ด้วยสี ลองดูการประกาศฟังก์ชัน


void cvFloodFill(
    IplImage* img,
    CvPoint seedPoint,
    CvScalar newVal,
    CvScalar loDiff = cvScalarAll(0),
    CvScalar upDiff = cvScalarAll(0),
    CvConnectedComp* comp = NULL,
    int flags = 4,
    CvArr* mask = NULL
);

พารามิเตอร์จะเป็น img รูปภาพที่จะ fill, seedPoint คือตำแหน่ง seed point  newVal คือสีที่จะ fill loDiff กับ upDiff คือผลต่างค่าที่จะยอมรับได้ที่จะต้องแทนที่สี(หรือว่าเป็นพื้้นที่เดียวกันนั่นเอง) ปกติผลต่างนัั้นจะคำนวณระหว่าง pixel ที่ติดกันยกเว้นถ้ามี flag CV_FLOODFILL_FIXED_RANGE จะคำนวณผลต่างจาก seed point เท่านั้น

CvConnectedComp จะเก็บข้อมูลสถิติเกี่ยวกับพื้นที่ๆ ถูก fill จะยกยอดไปอธิบายในเรื่อง Image Pyramids


mask ถ้ากำหนดจะต้องมีขนาด 1 channel และขนาดกว้างและยาวกว่า img (เพื่อการคำนวณ filter ภายใน) โดย pixel (x+1,y+1) ใน mask จะ map กับ x,y ใน img โดยการทำงาน Flood Fill จะ fill img เฉพาะในส่วนที่ mask ตรงตำแหน่งเป็น 0 
ผลการ Flood Fill สามารถ กำหนดให้ fill ทั้ง  img และ mask หรือ fill เฉพาะ mask อย่างเดียวก็ได้


flag จะประกอบด้วยข้อมูลสามส่วน
        แปดบิทล่าง จะมีค่าเป็น 4,8 ใช้กำหนดทิศทางในการ fill โดย 4 จะเป็นการ fill เฉพาะแนวตั้งและแนวนอนและ 8 จะ fill รอบด้านทั้งแปดทิศ
        แปดบิทกลาง จะกำหนดว่า mask จะถูกกำหนดว่า mask จะถูก fill ด้วยค่าอะไรถ้า set เป็น 0 จะถูก fill ด้วยค่า 1s(127)
        แปดบิดบน จะเป็นการเซตค่า CV_FLOODFILL_FIXED_RANGE (ที่อธิบายไปแล้ว) และ CV_FLOODFILL_MASK_ONLY fill เฉพาะ mask เท่านั้น

ลองยกตัวอย่าง flag ถ้าเราต้องการให้  fill ทั้งแปดทิศ, fill เฉพาะ mask, fill mask ด้วยค่า 47

flag = 8 | CV_FLOODFILL_MASK_ONLY | CV_FLOODFILL_FIXED_RANGE | (47<<8)

(น่าจะไม่มีปัญหา bitwise or และ shift bit ธรรมดา)

Learning OpenCV: Sequences

ประเภทข้อมูล sequence เป็นข้อมูลที่เก็บไว้ใน memory storage (จากตอนที่แล้ว) ตัวมันเองเป็น linked list ชนิดหนึ่งที่ชี้ไปยังโครงสร้างข้อมูลแบบอื่น ลองดูการประกาศ sequence


typedef struct CvSeq {
    int flags; // miscellaneous flags
    int header_size; // size of sequence header
    CvSeq* h_prev; // previous sequence
    CvSeq* h_next; // next sequence
    CvSeq* v_prev; // 2nd previous sequence
    CvSeq* v_next // 2nd next sequence
    int total; // total number of elements
    int elem_size; // size of sequence element in byte
    char* block_max; // maximal bound of the last block
    char* ptr; // current write pointer
    int delta_elems; // how many elements allocated
    // when the sequence grows
    CvMemStorage* storage; // where the sequence is stored
    CvSeqBlock* free_blocks; // free blocks list
    CvSeqBlock* first; // pointer to the first sequence block
}

จะเห็นมีข้อมูลที่สำคัญอยู่คือ total ที่บอกจำนวนในลิสต์ กับ pointer สำหรับ linked list สี่ตัวคือ h_prev, h_next, v_prev, v_next เป็น linked list ที่ชี้ได้สี่ทิศเลยทีเดียว

โดยตามปกติเช่นเดียว linked list ทั่วไป sequence จะสามารถจัดการข้อมูลที่อยู่บนหัวท้ายได้ดีกว่าข้อมูลที่อยู่ตรงกลาง 

โดยปกติ ฟังก์ชันใน OpenCV จะสร้าง sequence ให้เราโดยอัตโนมัติ เราแค่เรียกใช้ sequence ให้ถูกก็พอ ในกรณีที่ต้องการสร้างและลบ sequence เอง ใช้คำสั่งต่อไปนี้


CvSeq* cvCreateSeq(
    int seq_flags,
    int header_size,
    int elem_size,
    CvMemStorage* storage
);

แล้วลบด้วย


void cvClearSeq(
    CvSeq* seq
);



sequence ใช้หน่วยความจำใน memory storage (ที่ผ่าน parameter ใน cvCreateSeq) ในตอนที่ใช้คำสั่ง cvClearSeq หน่วยความจำจะไม่ถูก recycle (อ้างอิงจากตอนที่แล้ว เรื่อง memory storage)


คำสั่งจริงๆ ที่น่าจะใช้ของ sequence น่าจะเป็นการเข้าถึง object ใน sequence มากกว่า ลองดูคำสั่งตัวอย่างในหนังสือ


for( int i=0; itotal; ++i ) {
    CvPoint* p = (CvPoint*)cvGetSeqElem ( seq, i );
    printf(“(%d,%d)\n”, p->x, p->y );
}

จะเป้นการใช้คำสั่ง cvGetSeqElem โดยใส่ sequence เป็นพารามิเตอร์และ index i ที่ต้องการอย่าลืม casting ให้เป็นข้อมูลทีเราต้องการด้วย


คำสั่งอื่นๆ ในการจัดการ sequence จะขอลิสต์ไว้ ต่อไปนี้

คำสั่งในการ ค้นหา element ใน sequence

  • cvSeqElemIdx
คำสั่งในการเข้าถึง manipulate sequence

  • cvCloneSeq
  • cvSeqSlice
  • cvRemoveSlice
  • cvInsertSlice
การเรียง sequence การแบ่ง sequence และการค้นหาข้อมูลใน sequence

  • cvSeqSort
  • cvSeqSearch
  • cvSeqInvert
  • cvSeqPartition
ในฟังก์ชันส่วนนี้จะต้องกำหนดฟังก์ชันสำหรับเปรียบเทียบด้วย เพื่อที่ OpenCV จะได้เข้าใจว่า object สองตัวเรียงกันอย่างไร (เรียงตาม x หรือ y ใครที่เคยเขียน quick sort ใน C น่าจะนึกภาพออก) โดยกำหนดฟังก์ชันในรูปแบบนี้

typedef int (*CvCmpFunc)(const void* a, const void* b, void* userdata );

การใช้ sequence เป็น stack (สองหัวด้วยนะ)

  • cvSeqPush
  • cvSeqPop
  • cvSeqPushFront
  • cvSeqPopFront
  • cvSeqPushMulti
  • cvSeqPopMulti


การแทรก element กลาง sequence

  • cvSeqInsert
  • cvSeqRemove


การเปลี่ยนขนาด memory block size (จะได้ใช้เหรอ)

  • cvSetBlockSize
การแปลงระหว่าง array กับ sequence

  • cvCvtSeqToArray
  • cvMakeSeqHeaderForArray

ส่วนสุดท้ายคือการอ่านเขียน sequence ซึ่งมีประสิทธิภาพมากกว่าการ insert การ ใช้ฟังก์ชัน cvGetSeqElem โดยใช้วิธีสร้างตัว sequence reader กับ sequence writer ขึ้นมา การจัดการดูแล้วเหมือนการอ่านเขียนไฟล์ใน C

คำสั่งที่เกี่ยวข้องกับ sequence writer
  • cvStartWriteSeq
  • cvStartAppendToSeq
  • cvEndWriteSeq
  • cvFluseSeqWriter
  • CV_WRITE_SEQ_ELEM
  • CV_WRITE_SEQ_ELEM_VAR
คำสั่งที่เกี่ยวข้องกับ sequence reader
  • cvStartReadSeq
  • cvGetSeqReaderPos
  • cvSetSeqReaderPos
  • CV_NEXT_SEQ_ELEM
  • CV_PREV_SEQ_ELEM
  • CV_READ_SEQ_ELEM
  • CV_REV_READ_SEQ_ELEM
เนื่องจากการเขียนใน sequence writer น่าจะสำคัญจะอาจจะได้ใช้ เลยยกตัวอย่างในหนังสือมาเสียหน่อย





We will create a writer and append a
hundred random points drawn from a 320-by-240 rectangle to the new sequence.
CvSeqWriter writer;
cvStartWriteSeq( CV_32SC2, sizeof(CvSeq), sizeof(CvPoint), storage, &writer );
for( i = 0; i < 100; i++ )
{
    CvPoint pt; pt.x = rand()%320; pt.y = rand()%240;
    CV_WRITE_SEQ_ELEM( pt, writer );
}
CvSeq* seq = cvEndWriteSeq( &writer );


Learning OpenCV: Memory Storage in OpenCV

ข้ามมาว่ากันด้วยเรื่อง memory storage กันดีกว่า OpenCV มี dynamic object
ที่เรียกว่า memory storage ซึ่งก็คือ linked lists(ไม่เคยใช้ตั้งแต่จบ data structure ตอนปีสอง) ที่จะ allocate, de-allocate memory block เนื่องจากใน OpenCV
เองมีส่วนการทำงานที่ต้องใช้ memory ส่วนนี้อยู่หลายคำสั่ง คำสั่งที่เกี่ยวกับ memory storage มีประมาณนี้

cvMemStorage* cvCreateMemStorage(int block_size = 0);
void cvReleaseMemStorage(CvMemStorage** storage);
void cvClearMemStorage(CvMemStorage* storage);
void* cvMemStorageAlloc(CvMemStorage* storage,size_t size)

เราใช้คำสั่ง cvMemStorage ในการสร้าง linked list โดยกำหนดขนาด block ของ memory ซึ่งคิดว่าค่า default ที่กำหนดให้ขนาด block = 64k น่าจะโอเค (เพราะไม่มีความรู้ด้านนี้ 555) หลังจบก็ใช้คำสั่ง cvReleaseMemStorage ตาม step การ alloc, dealloc ใน C

มีอย่างหนึ่งที่น่าสังเกตคือคำสั่ง cvClearMemStorage ที่จะช่วยคืนพื้นที่ที่สร้างขึ้นจากคำสั่ง cvCreateMemStorage นี้ เพราะว่าการลบ object ใดๆ ที่อยู่ในพื้นที่นี้ (object เหล่านี้จะเกิดขึ้นในบางฟังก์ชันของ OpenCV เช่น sequences ที่จะกล่าวถึง) จะเป็นการลบ object นั้นทิ้งไปเท่านั้นไม่ได้คืนพื้นที่หน่วยความจำ (ก็หน่วยความจำนี่ cvCreateMemStorage เป็นคนจองนี่ object ในนั้นก็ชี้ไปยังพื้นที่ในนั้น อีกอย่าง C ไม่มีการ garbage collection ด้วย)
ดังนั้นถ้าต้องการจะ recycle memory ก็ใช้คำสั่ง cvClearMemStorage ได้ (จะได้ใช้ถึงขั้นนั้นไหม)

ส่วนคำสั่งสุดท้ายคือ cvMemStorageAlloc เป็นจองเนื้อที่ในหน่วยความจำที่อยู่ใน storage (ยังไม่รู้เลยว่าจะใช้ตอนไหน)