Value của convolution ở một điểm cụ thể được tính bởi đầu tiên đặt kernel anchor ở đỉnh của một pixel trên image với phần còn lại của kernel nằm chồng các local pixel tương ứng trong ima
Trang 1Trong chương này ta sẽ quan sát image transforms, mà các methods để thay đổi một image thành một biểu diễn thay thế của data Chẳng hạn hầu hết example phổ biến của một transform là gì đó giống Fourier transform, mà trong đó image được chuyển thành biểu diễn thay thế của data trong image gốc Kết quả của
operation này được lưu trong một OpenCV “image” structure, nhưng các “pixel” riêng trong image mới này biểu diễn các thành phần spectral của input gốc hơn là các thành phần không gian ta được dùng để nghĩvề
Có một số trong các transform hữu ích mà đến lặp lại trong computer vision OpenCV cung cấp các thực hiện hoàn chỉnh của vài trong những cái phổ biến cũng như dựng các block để giúp bạn thực hiện các image transform riêng
Convolution
Convolution là cơ sở của nhiều trong các transformation mà ta thảo luận trong chương này Về khái quát,
thuật ngữ này có nghĩa ta làm mọi phần của một image Trong ý thức này, nhiều trong các operation ta xemxét trong chương 5 có thể cũng được hiểu như các trường hợp đặc biệt của tiến trình tổng quát hơn của
convolution Điều một convolution cụ thể “làm” được xác định bởi dạng của Convolution kernel được dùng Kernel này thực sự chỉ là một fixed size array của các hệ số cùng với một anchor point trong array
đó, mà điển hình được đặt ở tâm Size của array* được gọi support của kernel Figure 6-1 mô tả một 3-by-3
convolution kernel với anchor đặt ở center của array Value của convolution ở một điểm cụ thể được tính bởi đầu tiên đặt kernel anchor ở đỉnh của một pixel trên image với phần còn lại của kernel nằm chồng các local pixel tương ứng trong image Cho mỗi kernel point, ta bây giờ có một value cho kernel ở điểm đó và một value cho image ở image point tương ứng Ta nhân những cái này cùng nhau và cộng kết quả; kết quả này được đặt trong image kết quả ở vị trí tương ứng với vị trí của anchor trong input image Tiến trình này được lặp lại cho mỗi point trong image bởi quét kernel qua toàn bộ image
Figure 6-1 Một 3-by-3 kernel cho một vi phân Sobel; lưu ý rằng anchor point ở tâm của kernel
Ta có thể, dĩ nhiên, mô tả procedure này theo dạng của một phương trình Nếu ta định nghĩa image là I(x, y), kernel là G(i, j) (trong đó 0 < i< M i –1 và 0 < j < M j –1), và điểm anchor được định vị ở (a i , a j) theo tọa
độ trong kernel, thì convolution H(x, y) được định nghĩa bởi biểu thức sau:
− +
= 1
0
1 0
) , ( ) ,
( )
j
a i x I y
x
H
Nhận thấy rằng số các operation, tối thiểu ở cái nhìn đầu tiên, dường như là số các pixel trong image được nhận bởi số các pixel trong kernel.* Điều này có thể là nhiều tính toán và do đó không là thứ bạn muốn làmvới vài “for” loop và nhiều pointer de-referencing Trong những trường hợp như thế này, tốt hơn để
Trang 2OpenCV làm việc cho bạn và lấy thuận lợi của các tối ưu hiện được lập trình vào OpenCV Cách OpenCV làm điều này là với cvFilter2D():
void cvFilter2D(
const CvArr* src, CvArr* dst, const CvMat* kernel, CvPoint anchor = cvPoint(-1,-1) );
Ở đây ta tạo một matrix với size thích hợp, điền nó với các hệ số, và sau đó chuyển nó cùng nhau với các source và ảnh đíchs vào cvFilter2D() Ta có thể cũng tùy chọn chuyển vào một CvPoint để nhận diện vị trí của
center của kernel, nhưng value mặc định (bằng với cvPoint(-1,-1)) được hiểu như nhận biết ở center của kernel Kernel có thể có size chẵn nếu anchor point của nó được định nghĩa; ngược lại, nó nên là size lẻ
width và length thêm của convolution kernel Nhưng các size của src và dst có thể là giống trong OpenCV vì, bởi mặc định, ưu tiên trong convolution OpenCV tạo các virtual pixel qua việc tái tạo qua biên của src
image sao cho các pixel biên trong dst có thể được điền vào Việc tái tạo được làm như input(–dx, y) = input(0, y), input(w + dx, y) = input(w – 1, y), và vân vân Có vài thay thế với hành động mặc định này; ta
sẽ thảo luận chúng trong phần tiếp
Ta chú ý rằng các hệ số của convolution kernel nên luôn luôn là các số floatingpoint Điều này có nghĩa rằng bạn nên dùng CV_32FC1 khi cấp phát matrix đó
Convolution Boundaries
Một vấn đề mà đến tự nhiện với các convolution là cách handle các biên Ví dụ, khi dùng convolution kernel vừa mô tả, điều xảy ra khi point được convolved ở cạnh của image? Hầu hết các function tạo sẵn củaOpenCV thực hiện dùng của cvFilter2D() phải handle điều này theo cách này hay cách khác Tương tự, khi làm các convolution riêng, bạn sẽ cần biết cách giải quyết điều này hiệu quả Giải pháp đến theo dạng của
động đêm boundary theo cách này hay cách khác:
void cvCopyMakeBorder(
const CvArr* src, CvArr* dst, CvPoint offset, int bordertype, CvScalar value = cvScalarAll(0) );
hình, nếu kernel là N-by-N (cho N lẻ) thì bạn sẽ muốn một boundary mà là rộng (N – 1)/2 trên tất cả các cạnh hay, tương đương, một image mà rộng hơn và cao hơn N – 1 so với gốc Trong trường hợp này bạn
nên đặt offset thành cvPoint((N-1)/2,(N-1)/2) sao cho biên sẽ là chẵn trên tất cả các cạnh.* Kiểu border có thể làmột trong IPL_BORDER_CONSTANT hay IPL_BORDER_REPLICATE (xem Figure 6-2)
Trong trường hợp đầu tiên, value argument sẽ được hiểu như value mà với tất cả pixels trong boundary nên được đặt Trong trường hợp thứ hai, row hay column ở cạnh của gốc được tái tạo thành cạnh của image lớn hơn Lưu ý rằng border của pattern image kiểm tra là gì đó huyền ảo (kiểm tra image trên phải trong Figure6-2); trong image mẫu kiểm tra, có một border tối rộng một pixel ngoại trừ nơi các circle pattern đến gần border ở đó nó chuyển thành trắng Có hai border types khác nhau được định nghĩa,
nhưng có thể được hỗ trợ trong tương lai
Trang 3Figure 6-2 Mở rộng image border Cộ trái cho thấy IPL_BORDER_CONSTANT nới một zero value được dùng để điền cho các border Cột phải cho thấy IPL_BORDER_REPLICATE nơi border pixels được tái tạo theo các hướng ngang và dọc
Ta đề cập trước đây rằng, khi bạn làm các lời gọi đến các OpenCV libraery functions mà thực hiện
convolution, các library function này gọi cvCopyMakeBorder() để lấy việc của chúng làm Trong hầu hết các trường hợp border type được gọi là IPL_BORDER_REPLICATE, nhưng đôi khi bạn sẽ không muốn nó được làm theo cách đó Đâu là một dịp khác nơi bạn có thể muốn dùng cvCopyMakeBorder() Bạn có thể tạo một image lớn hơn một tí với border bạn muốn, gọi bất cứ routine trên image đó, và sau đó cắt lại phần bạn quan tâm ban đầu Cách này, định biên tự động của OpenCV sẽ không ảnh hưởng các pixel bạn quan tâm
Gradients và Sobel Derivatives
Một trong các convolution hầu hết cơ bản và quan trọng là tính các đạo hàm (hay xấp xỉ chúng) Có nhiều cách để làm điều này, nhưng chỉ vài được phù hợp tốt cho trường hợp được cho
Nhìn chung, hầu hết operator phổ biến dùng để biểu diễn vi phân là Sobel derivative [Sobel68] operator
(xem Figures 6-3 và 6-4) Các Sobel operator tồn tại cho bất kỳ bậc đạo hàm cũng như for các vi phân riêngphần hỗn hợp (chẳng hạn ∂2/ ∂ x ∂ y)
Trang 4Figure 6-3 Hiệu ứng của Sobel operator khi dùng với xấp xỉ một vi phân đầu tiên theo x-dimension
cvSobel(
const CvArr* src, CvArr* dst, int xorder, int yorder, int aperture_size = 3 );
Ở đâu, src và dst là image input và output của bạn, và xorder và yorder là các thứ tự của vi phân Điển hình bạn
sẽ dùng 0, 1, hay hầu hết ở 2; một 0 value nhận biết không vi phân theo hướng đó.* aperture_size parameter nên là lẻ và là width (và height) của square filter Hiện tại, aperture_sizes 1, 3, 5, và 7 được hỗ trợ Nếu src là8-bit thì dst phải có độ sâu IPL_DEPTH_16S để tránh tràn Sobel derivatives có đặc tính tốt mà chúng có thể được định nghĩa cho các kernel ở bất kỳ size, và những kernel này có thể được xây dựng nhanh chóng và lặp lại Các kernel lớn hơn cho một xấp xỉ tốt hơn cho vi phân vì các kernel nhỏ hơn rất nhậy với noise
Trang 5Figure 6-4 The effect of the Sobel operator khi used to approximate a đầu tiên derivative in the y-dimension
Để hiểu điều này chính xác hơn, ta phải thấy rõ rằng một Sobel derivative không thực sự một vi phân Điềunày là vì Sobel operator được định nghĩa trên một không gian rời rạc Điều Sobel operator thực sự biểu
diễn là một điều chỉnh thành một đa thức Mà là, Sobel derivative của bậc hai theo x-direction không thực
sự là vi phân thứ hai; nó là điều chỉnh tại chỗ thành một hàm parabolic Điều này giải thích một nguyên nhân có thể muốn dùng một kernel lớn hơn: mà kernel lớn hơn sẽ tính điều chỉnh trên số lớn hơn các pixel
Scharr Filter
Thật ra, có nhiều cách để thích hợp một vi phân trong trường hợp của một discrete grid Phía dưới của xấp
xỉ dùng cho Sobel operator mà nó ít chính xác cho các kernel nhỏ Cho các kernel lớn, nơi nhiều điểm được
dùng trong xấp xỉ, vấn đề này là ít ý nghĩa Sự không chính xác này không cho thấy trực tiếp cho các X và
Y filters được dùng trong cvSobel(), vì chúng chính xác được sắp với các trục x- và y- Sự khó khăn đến khi bạn muốn làm các đo lường image mà là các xấp xỉ của các đạo hàm hướng (chẳng hạn hướng của image gradient bởi dùng arctangent của các đáp ứng y/x filter).
Để đặt điều này trong ngữ cảnh, một example cụ thể nơi bạn có thể muốn các đo lường image của kiểu này
sẽ ở tiến trình thu thập thông tin hình dáng từ một object bởi tập hợp một histogram của các góc gradient quanh object Chẳng hạn một histogram là cơ sở mà trên nhiều bộ phân loại hình dáng chung được huấn luyện và hoạt động Trong trường hợp này, các đo lường không chính xác của gradient angle sẽ giảm hiện suất nhận diện của bộ phân loại
Cho một 3-by-3 Sobel filter, các không chính xác là rõ ràng hơn gradient angle từ ngang hay dọc OpenCV
để ý sự không chính xác này cho các bộ lọc Sobel nhỏ (nhưng nhanh) 3-by-3 bởi một dùng một ít mờ của giá trị aperture_size đặc biệt CV_SCHARR trong cvSobel() function Scharr filter không chỉ nhanh mà còn chính xác hơn Sobel filter, do đó nó sẽ luôn luôn được dùng nếu bạn muốn làm các đo lường image dùng một 3-by-3 filter Các hệ số filter cho Scharr filter được thấy trong Figure 6-5 [Scharr00]
Trang 6Figure 6-5 3-by-3 Scharr filter dùng flag CV_SHARR
Laplace
OpenCV Laplacian function (đầu tiên được dùng trong vision bởi Marr [Marr82]) thực hiện một tương tự
rời rạc của Laplacian operator:*
2
2 2
)
(
y
f x
f f
∂
∂ +
void cvLaplace(
const CvArr* src, CvArr* dst, int apertureSize = 3
);
một trong một 8-bit (unsigned) image hay a 32-bit (floating-point) image Đích phải là một 16-bit (signed) image hay 32-bit (floating-point) image Aperture này chính xác là giống aperture xuất hiện trong các đạo hàm Sobel và, về hiệu quả, cho size của vùng mà trên đó các pixel được lấy mẫu trong tính toán của đạo hàm bậc hai
Laplace operator có thể được dùng trong muôn vàn hoàn cảnh Một ứng dụng chung là dò các “blob.” Nhờ
lại dạng của Laplacian operator mà là tổng các đạo hàm bậc hai theo trục x và trục y Điều này có nghĩa
rằng một điểm đơn hay bất kỳ blob nhỏ (nhỏ hơn aperture) mà được bao quanh bởi các giá trị cao hơn sẽ hướng đến cực đại function này Ngược lại, một điểm hay blob nhỏ mà được bao quanh bởi các giá trị thấp hơn sẽ hướng đến tối đa phần âm của function này
Với điều này trong đầu, Laplace operator có thể cũng được dùng như một kiểu của edge detector Để thấy cách điều này được làm, lưu ý đạo hàm đầu tiên của một function, mà sẽ (dĩ nhiên) là lớn bất kể function là thay đổi nhanh Quan trọng đều, nó sẽ phát triển nhanh như ta tiếp cận một sự không liên tục giống cạnh và
co rút nhanh như ta chuyển qua sự không liên tục Do đó đạo hàm sẽ ở cực đại địa phương đâu đó bên trongdải này Do đó ta có thể quan sát các số 0 của đạo hàm bậc hai cho các vị trí của cực đại địa phương như thế Hiểu không? Các cạnh trong ảnh gốc sẽ là những 0 của Laplacian Không may, cả các cạnh quan trọng
và ít s nghĩa sẽ là những số 0 của Laplacian, nhưng điều này không là một vấn đề vì ta có thể đơn giản lọc
ra các pixel mà cũng có các giá trị lớn của đạo hàm đầu tiên (Sobel) Figure 6-6 cho thấy một ví dụ dùng Laplacian trên một image cùng nhau với các chi tiết của đạo hàm đầu tiên và bậc hai và các zero crossing của chúng
Canny
Method vừa được mô tả để tìm các cạnh được tinh chỉnh xa hơn bởi J Canny năm 1986 thành cái mà bây
giờ phổ biến được gọi là Canny edge detector [Canny86] Một trong các khác biệt giữa Canny algorithm và
thuật toán dựa Laplace đơn giản hơnvới phần trước là, trong thuật toán Canny, các đạo hàm bậc một được
tính theo x và y và sau đó kết hợp thành bốn đạo hàm hướng Các điểm nơi những đạo hàm hướng này là
cực trị địa phương sau đó là các ứng viên cho tập hợp thành các cạnh
Trang 7Figure 6-6 Laplace transform (trên phải) của racecar image: zooming in trên bánh (vòng tròn trắng) và lưu ý chr hướng x, ta thấy mô tả (qualitative) của sự sáng cũng như đạo hàm bậc một và bậc hai (thấp hơn ba cell); những số 0 trong đạo hàm bậc hai tương ứng vứi các cạnh, và 0 tương ứng đạo hàm bậc một lớn là một cạnh mạnh
Tuy nhiên, hầu hết hướng mới ý nghĩa với thuật toán Canny là nó cố tập hợp các pixel ứng viên cạnh riêng
rẽ thành các contour.* những contours này được hình thành bởi áp dụng một hysteresis threshold cho các
pixel Điều này có nghĩa rằng có hai thresholds, một trên hơn và một thấp hơn Nếu một pixel có gradient lớn hơn upper threshold, thì nó được chấp nhận như một edge pixel; nếu một pixel bên dưới lower
threshold, nó bị loại Nếu gradient của pixel ở giữa các threshold, thì nó được chấp nhậnchỉ nếu nó được nối với một pixel mà trên threshold cao Canny khuyến cáo một ratio của high:low threshold giữa 2:1 và 3:1 Các hình 6-7 và 6-8 cho thấy các kết quả của áp dụng cvCanny() với một mẫu kiểm tra và một ảnh dùng các high:low hysteresis threshold ratios 5:1 và 3:2, tương ứng
Trang 8Figure 6-7 Các kết quả của Canny edge detection cho hai ảnh khác nhau khi các high và low thresholds được đặt thành 50 và 10, tương ứng
grayscale (nhưng mà sẽ thực sự là một Boolemột image) Hai arguments tiếp là các low và high thresholds,
và argument cuối là một aperture khác Như thường, đây là aperture được dùng bởi các Sobel derivative operators mà được gọi bên trong của thực hiện cvCanny()
Hough Transforms
Hough transform* là một method để tìm các đường thẳng, đường tròn, hay các dạng đơn giản trong một
image Hough transform ban đầu là một biến đổi thẳng, mà là cách tương đối nhanh để tìm một ảnh nhị phân cho các đường thẳng Transform này có thể được tổng quát xa hơn cho các trường hợp hơn chỉ cho các đường đơn giản
Hough Line Transform
Lý thuyết cơ bản của Hough line transform là bất kỳ điểm trong ảnh nhị phân có thể là phần của vài tập các
đường có thể Nếu ta tham số hóa mỗi đường bởi, ví dụ, một slope a và một intercept b, thì điểm a trong một ảnh gốc được biến đổi thành một quỹ tích các điểm trong mặt phẳng (a, b) tương ứng với tất cả các
đường đi qua điểm đó (xem Figure 6-9) Nếu ta chuyển mọi nonzero pixel trong input image thành một tập các điểm trong output image và tộng trên tất cả các đóng góp, thì các đường mà xuất hiện trong input
(chẳng hạn (x, y) plane) image sẽ xuất hiện như cực trị địa phương trong output (chẳng hạn (a, b) plane) image Vì ta đang cộng các hợp thành từ mỗi điểm, (a, b) plane nhìn chung được gọi là accumulator plane.
Trang 9Figure 6-8 các kết quả của Canny edge detection cho hai image khác nhau khi các high và low thresholds được đặt thành 150 và 100, tương ứng
Điều có thể xảy ra với bạn rằng dạng slope-intercept không thực sự là cách tốt nhất để biểu diễn tất cả các đường đi qua một điểm (vì mật độ khác nhau đáng kể của các đường như một function của slope, và sự thậtliên qua rằng interval của các slope có thể đi từ –∞ đến +∞) Nó là cho nguyên nhân này mà sự tham số hóathực của transform image dùng trong tính toán số là gì đó khác Sự tham số hóa ưa thích hơn biểu diễn mỗi
đường như một điểm trong tọa độ cực (ρ, θ), với đường ngầm định bằng đầu là đường đi qua điểm nhận
biết nhưng vuông góc với radial từ gốc đến điểm đó (xem Figure 6-10) Phương trình cho một đường thẳng
là :
) sin(
)
cos( θ y θ
x
Trang 10Figure 6-9 Hough line transform tìm thấy nhiều đường trong mỗi image; vài trong các đường được thấy như mong đợi, nhưng những cái khác có thể không
Figure 6-10 Một point (x0, y0) trong image plane (panel a) ngụ ý nhiều đường mỗi cái được tham số bởi một ρ và θ (panel b) khác nhau; những đường này mỗi cái được ngụ ý các điểm trong (ρ, θ) plane, mà được lấy cùng nhau hình thành đường cong hình dáng đặc tính (panel c)
Thuật toán Hough transform OpenCV không thực hiện tính toán này rõ ràng với user Thay vào đó, nó đơn
giản trả về cực trị địa phương trong (ρ, θ) plane Tuy nhiên, bạn sẽ cần hiểu tiến trình này để hiểu các
argument cho OpenCV Hough line transform function
OpenCV hỗ trợ hai kiểu khác nhau của Hough line transform: standard Hough transform (SHT) [Duda72]
và progressive probabilistic Hough transform (PPHT).* SHT là thuật toán ta vừa xem xét PPHT là một
biến thể của thuật toán này mà, cùng với các thứ khác, tính một mở rộng cho các đường cụ thể thêm vào đóvới hướng (như được thấy trong Figure 6-11) Nó là “có khả năng” vì, hơn việc lũy kế mọi điểm có khả năng trong mặt phẳng lũy kế, nó lũy kế chỉ một phần của chúng Ý tưởng là nếu đỉnh sẽ là đủ cao đại khái, sau khi đánh nó chỉ một phần của thời gian sẽ để để tìm thấy nó; Kết quả của ước đoán này có thể là giảm thực chất trong thời gian tính Cả hai thuật toán này được truy cập với cùng OpenCV function, qua các phương tiện của vài các argument phụ thuộc vào method mà đang được dùng
CvSeq* cvHoughLines2(
Trang 11trong chương 8) hay một N-by-1 matrix array phẳng (số các hàng, N, sẽ phục vị để giới hạm số cực đại các
đường trả về) Argument tiếp theo, method, có thể là CV_HOUGH_STANDARD,
biến thể multiscale của SHT Hai argument tiếp theo, rho và theta, đặt phân giải mong muốn cho các đường (chẳng hạn phân giải của mặt phẳng lỹ kế) Các đơn vị của rho là pixel và đơn vị của theta là radians; Do đó, mặt phẳng lũy kế có thể được nghĩ như histogram hai chiều với các cell của các pixel rho
chiều với các theta radian Giá trị threshold là giá trị trong mặt phẳng lũy kế mà phải đạt được cho routine để report một đường thẳng
Argument cuối là một tí mẹo trong thực tế; không tiêu chuẩn hóa, do đó bạn sẽ mong tỉ lệ nó bằng image size cho SHT Nhớ rằng argument này, về hiệu quả, nhận diện số các điểm (the edge image) mà phải hỗ trợ đường cho đường này được trả về
Figure 6-11 Canny edge detector (param1=50, param2=150) được chạy đầu tiên, với các kết quả được thấy trong gray, và progressive probabilistic Hough transform (param1=50, param2=10) được chạy tiếp theo, với các kết quả được chồng theo màu trắng; bạn có thể thấy rằng các đường mạnh nhìn chung được nhấc bởi Hough transform
Các tham số param1 và param2 không được dùng bởi SHT Cho PPHT, param1 đặt chiều dài tối thiểu của một đoạn thẳng mà sẽ được trả về, và param2 đặt sự chia tách giữa các đoạn collinear đòi hỏi cho thuật toán không hợp chúng thành một đoạn đơn dài hơn Cho multiscale HT, hai parameters được dùng để nhận diện các phân giải cao hơn mà các parameter cho các đường thẳng nên được tính Multiscale HT đầu tiên tính các vị trí các đường với độ chính xác được cho bởi các tham số rho và theta và sau đó tiếp tục tinh chỉnh các
Trang 12kết quả này bởi một factor của param1 và param2, tương ứng (chẳng hạn phân giải cuối cùng trong rho là rho
được dẫn xuất bởi param1 và phân giải cuối trong theta được chia bởi param2)
Cái function trả về phụ thuộc vào cách nó được gọi Nếu giá trị line_storage là một matrix array, thì giá trị thực trả về sẽ là NULL Trong trường hợp này, matrix nên là type CV_32FC2 nếu SHT hay multi-scale HT đang được dùng và nên là CV_32SC4 nếu PPHT đang được dùng Trong hai trường hợp đầu tiên, các giá trị
ρ- và θ-cho mỗi đường thẳng sẽ được đặt trong hai channels của array Trong trường hợp của PPHT, bốn channels sẽ giữ các giá trị x- và y- của các điểm bắt đầu và kết thúc của các đoạn trả về Trong tất cả những
trường hợp này, số các hàng trong array sẽ được cập nhật bởi cvHoughLines2() để phản ảnh đúng số các đường trả về
Nếu giá trị line_storage là một một pointer đến một memory store,* thì giá trị trả về sẽ là một pointer đến
chuỗi này bằng một lệnh như
float* line = (float*) cvGetSeqElem( lines , i);
trong đó cá đường là giá trị trả về từ cvHoughLines2() và i là index của đường thẳng mong muốn Trong trường hợp này, đường thẳng sẽ là một pointer đến data cho đường thẳng đó, với line[0] và line[1] là các giá trị
floating-point ρ và θ (cho SHT và MSHT) hay các CvPoint structure cho các điểm cuối của các đoạn (cho PPHT)
Hough Circle Transform
Hough circle transform [Kimme75] (xem Figure 6-12) làm việc theo cách đại khái tương tự với các Hough
line transform veaf được mô tả Nguyên nhân nó chỉ “đại khái” là—nếu một cái cố làm điều tương tự chính
xác —mặt phẳng lũy kế sẽ phải phải được thay bằng volume lỹ kế với ba chiều: một cho x, một cho y, và một cái khác cho circle radius r Điều này có nghĩa các yêu cầu bộ nhớ lớn hơn nhiều và tốc độ chậm hơn
nhiều Thực hiện của circle transform trong OpenCV tránh vấn đề này bởi dùng một phương pháp thông
minh hơn gọi là Hough gradient method.
Hough gradient method làm việc như sau đầu tiên image được chuyển qua một edge detection phase (trongtrường hợp này, cvCanny()) Tiếp theo, cho mọi nonzero point trong edge image, local gradient được xem
xét (gradient được tính bởi tính toán đầu tiên các đạo hàm bậc một Sobel x- và y-qua cvSobel()) Dùng gradient này, mọi điểm dọc đường này được nhận biết bởi slope này—từ một khoảng cách tối thiểu cụ thể đến một tối đa cụ thể—được tăng theo accumulator Ở cùng thời điểm, vị trí của mỗi cái trong những nonzero pixels này trong edge image được lưu ý Các tâm ứng viên sau đó được chọn từ những điểm này theo lũy kế này (hai chiều) mà là cả trên vài threshold cho trước và lớn hơn tất cả trong các láng giềng tức thời của chúng Những tâm ứng viên này được lưu theo thứ tự giảm dần của các giá trị lũy kế của chúng, sao cho các tâm với hầu hết pixel hỗ trợ xuất hiện đầu tiên Tiếp theo, cho mỗi tâm, tất cả các nonzero pixel(nhớ rằng danh sách này được dựng trước) được xem xét Những pixels này được lưu theo khoảng cách đếntâm Làm việc với các khoảng cách nhỏ nhất đến bán kính lớn nhất, một bán kính đơn được chọn mà được
hỗ trợ tốt nhất bởi các nonzero pixel Một tâm được giữ nếu nó có đủ hỗ trợ từ các nonzero pixel theo edge image và nếu nó đủ khoảng cách từ bất kỳ tâm được chọn trước đây
Thực hiện này cho phép thuật toán chạy nhanh hơn nhiều và, có lẽ quan trọng hơn, giúp giải vấn đề của bùng nổ dân số thưa thời ngược lại của một lũy kế ba chiều, mà sẽ dẫn đến nhiều nhiễu và render các kết quả không ổn định Mặt khác, thuật toán này có vài khuyết điểm mà bạn nên biết
Trang 13Figure 6-12 Hough circle transform tìm thấy vài circles trong mẫu kiểm tra và (correctly) tìm thấy không trong photograph
Đầu tiên, dùng các đạo hàm Sobel để tính local gradient—và sự chiếm giữ kèm theo mà điều này có thể được xem tương đương với một tiếp tuyến địa phương—không một xác nhận ổn định số Nó có thể là đúng
“hầu hết mọi lúc,” nhưng bạn nên mong điều này tạo vài noise trong output
Thứ hai, tập toàn bộ các nonzero pixel trong edge image được quan tâm cho mọi tâm ứng cử; do đó, nếu bạn làm ngưỡng lũy kế quá thấp, thuật toán sẽ mất nhiều thời gian để chạy
Thứ ba, vì một đường tròn được chọn cho mỗi tâm, nếu có các đường tròn đồng tâm thì bạn sẽ lấy chỉ một trong chúng
Cuối cùng, vì các tâm được xem theo thứ tự tăng dần của các giá trị lũy kế kết hợp với chúng và vì các tâm mới không được giữ nếu chúng qua gần với các tâm được chấp nhận trước, có một khuynh hướng dẫn đến giữ các đường tròn lớn hơn khi nhiều đường tròn đồng tâm hay gần đồng tâm (nó chỉ là một “thiên hướng”
vì nhiễu đến từ các đạo hàm Sobel; trong ảnh làm trơn ở phân giải vô cùng, nó sẽ là một chắc chắn.)Với tất cả điều này trong đầu, hãy tiến vào OpenCV routine mà làm tất cả điều này cho ta:
Hough circle transform function cvHoughCircles() có các argument tương tự với line transform Input image
một ảnh nhị phân
grayscale image tổng quát hơn
trả về Nếu an array được dùng, nó nên là cột đơn type CV_32FC3; ba channels sẽ được dùng để encode vị trí của đường tròn và bán kính của nó Nếu memory storage được dùng, thì các đường tròn sẽ được làm
Trang 14thành một chuỗi OpenCV và một pointer đến chuỗi đó sẽ được trả về bởi cvHoughCircles() (Cho trước một array pointer value cho circle_storage, giá trị trả về của cvHoughCircles() là NULL.) Method argument phải luôn luôn được đặt thành CV_HOUGH_GRADIENT Parameter dp là phân giải của ảnh lũy kế được dùng Parameter này cho phép ta tạo một bộ lũy kế của một phân giải thấp hơn input image (thật ý nghĩa để làm điều này vì không có lý do để mong các đường tròn tòn tạo trong image để rơi tự nhiên vào cùng số các nhóm như width hay height của image tự nó.) Nếu dp được đặt thành 1 thì các phân giải sẽ là giống; nếu đặtthành một số lớn hơn (chẳng hạn 2), thì phân giải lũy kế sẽ nhỏ hơn bởi factor đó (trong trường hợp này, một nửa) Giá trị của dp không thể nhỏ hơn 1 Parameter min_dist là khoảng cách tối thiểu mà phải tồn tại giữahai đường tròn để thuật toán lưu ý chúng để phân biệt các đường tròn.
Cho trường hợp (hiện tại được đòi hỏi) của method đang được đặt thành CV_HOUGH_GRADIENT, hai argument tiếp theo, param1 và param2, là edge (Canny) threshold và accumulator threshold, tương ứng Bạn
có thể nhớ lại Canny edge detector thực sự lấy hai threshold khác nhau chính nó Khi cvCanny() được gọi
bên trong, threshold đầu tiên (cao hơn) được đặt thành value của param1 chuyển vào cvHoughCircles(), và threshold thứ hai (thấp hơn) được đặt chính xác một nửa giá trị đó Parameter param2 là cái được dùng để threshold bộ lũy kế và là chính xác tương tự với threshold argument của cvHoughLines()
Hai parameters cuối là các bán kính cực tiểu và cực đại của các đường tròn có thể được thấy Điều này có nghĩa rằng những cái này là radii của các đường tròn mà bộ lũy kế có một biểu diễn Example 6-1 cho thấy một program ví dụ dùng cvHoughCircles()
Example 6-1 Dùng cvHoughCircles để return một chuỗi các đường tròn tìm thấy trong một grayscale image
#include <cv.h>
#include <highgui.h>
#include <math.h>
int main(int argc, char** argv) {
IplImage* image = cvLoadImage(
argv[1], CV_LOAD_IMAGE_GRAYSCALE );
CvMemStorage* storage = cvCreateMemStorage(0);
cvSmooth(image, image, CV_GAUSSIAN, 5, 5 );
CvSeq* results = cvHoughCircles(
image, storage, CV_HOUGH_GRADIENT, 2,
image->width/10 );
for( int i = 0; i < results->total; i++ ) {
float* p = (float*) cvGetSeqElem( results, i );
CvPoint pt = cvPoint( cvRound( p[0] ), cvRound( p[1] ) );
cvCircle(
image, pt, cvRound( p[2] ), CV_RGB(0xff,0xff,0xff) );
} cvNamedWindow( “cvHoughCircles”, 1 );
cvShowImage( “cvHoughCircles”, image);
cvWaitKey(0);
}
Điều đáng phản ảnh tạm thời về sự thật rằng, không vần đề gì với các mẹo ta thực hiện, không có lấy quanh
yêu cầu rằng các đường tròn được mô tả bởi ba bậc tự do (x, y, và r), ngược với chỉ hai bậc tự do (ρ và θ)
cho các đường thẳng Kết quả sẽ bất biến mà bất kỳ thuật toán tìm đường tròn đòi hỏi nhiều memory và thời gian tính toán hơn các thuật toán tìm đường thẳng ta xem xét trước đây Với điều này trong ý nghĩ, nó
là ví dụ tốt để bao tham số bán kính chặt như các trường hơpj cho phép để giữ những chi phí này dưới điều khiển.* Hough transform được mở rộng cho các hình tùy ý bởi Ballard năm 1981 [Ballard81] về cơ bản bởiquan tâm các đối tượng như tập các gradient edge
Remap
Phía bên dưới, nhiều trong các biến đổi theo sau có một phần tử giải thích nào đó Về cụ thể, chúng sẽ lấy các pixel từ một nơi trong image và chiếu chúng vào một nơi khác Trong trường hợp này, sẽ luôn luôn có vài chiếu trơn, mà sẽ làm điều ta cần, nhưng nó sẽ không luôn luôn là đáp ứng pixel một một Ta đôi khi muốn hoàn thành nội suy này một cách lập trình; mà là, ta thích áp dụng vài thuật toán đã biết mà sẽ xác
Trang 15định phép chiếu Trong các trường hợp khác, tuy nhiên, ta thích tự làm phép chiếu này Trước khi đi sâu vào vài method mà sẽ tính (và áp dụng) những phép chiếu cho ta, hãy tốn ít thời gian để quan sát đáp ứng function để áp dụng các phép chiếu mà những method khác này dựa vào OpenCV function ta muốn được gọi là cvRemap():
void cvRemap(
const CvArr* src, CvArr* dst, const CvArr* mapx, const CvArr* mapy, int flags = CV_INTER_LINEAR | CV_WARP_FILL_OUTLIERS, CvScalar fillval = cvScalarAll(0)
);
Hai arguments đầu tiên của cvRemap() là các ảnh nguồn và ảnh đích, tương ứng Hiển nhiên, những cái này nên là cùng size và số các channel, nhưng chúng có thể có bất kỳ data type Điều quan trọng phải lưu ý rằng hai cái không thể là cùng ảnh.* Hai argument tiếp, mapx và mapy, nhận diện nơi bất kỳ pixel cụ thể được định vị lại Những cái này nên là cùng size như ảnh nguồn và ảnh đích, nhưng chúng là single-channel và thường của data type float (IPL_DEPTH_32F) Các phép chiếu không nguyên là OK, và
(làm đúng các méo) các ảnh được calibrated và stereo images Ta sẽ thấy các function trong các Chương 11
và 12 mà chuyển đổi các méo camera được tính toán và sắp xếp thành các tham số mapx và mapy Argumenttiếp theo chứa flags mà bảo cvRemap() độ chính xác mà nội suy được làm Bất kỳ một trong các giá trị này kêtrong Table 6-1 sẽ làm việc
Table 6-1 cvWarpAffine() additional flags values
Nội suy là một vấn đề quan trọng ở đây Các pixel trong ảnh nguồn nằm trên một integer grid; ví dụ, ta có thể refer đến một pixel ở vị trí (20, 17) Khi những vị trí nguyên này được chiếu vào ảnh mới, có thể có các gap—một trong vì các vị trí pixel nguồn nguyên được chiếu thành các vị trí float trong ảnh đích và phải được làm tròn thành vị trí pixel nguyên gần nhất hay vì có vài vị trí mà không các pixel mà tất cả được chiếu (nghĩ về gấp đôi kích thước ảnh bởi kéo dãn nó; sau đó mọi pixel đích sẽ được để trống) Những vấn
đề này nhìn chung được xem như các vấn đề forward projection Để làm việc các vấn đề làm tròn như thế
và các gap đích, ta thực sự giải quết vấn đề theo hướng khác: ta bước qua mỗi pixel của ảnh đích và hỏi,
“Pixel nào trong source được cần điền vào pixel đích này?” những source pixels này sẽ hầu hết luôn luôn làcác vị trí pixel phân đoạn do đó ta phải nội suy các source pixels để dẫn xuất giá trị đúng cho giá trị đích Method mặc định là nội suy bilinear, nhưng bạn có thể chọn các method khác (như được thấy trong Table 6-1)
Bạn có thể cũng thêm (dùng OR operator) flag CV_WARP_FILL_OUTLIERS, mà hiệu quả là điền các pixel trong ảnh đích mà không là đích của bất kỳ pixel trong input image với giá trị nhận diện bởi argument cuối cùng fillval Theo cách này, nếu bạn chiếu tất cả của image của bạn thành một vòng tròn theo tâm sau đó bên ngoài của vòng tròn đó sẽ tự động được tô bằng màu đen (hay bất kỳ màu khác mà bạn tưởng tượng)
Stretch, Shrink, Warp, và Rotate
Trong phần này ta chuyển đến thao tác hình học của ảnh.* Các thao tác như thế gồm kéo dãn theo nhiều
cách, mà bao gồm cả resizing đồng dạng hay không đồng dạng (cái sau được biết là warping) Có nhiều
những nguyên nhân để thực hiện những tác vụ này: ví dụ, warping và quay một image sao cho nó có thể được xếp chồng trên tường trong một cảnh tồn tại, hay làm lớn nhân tạo một tập các ảnh huấn luyện dùng
để nhận dạng đối tượng.† Các function mà có thể stretch, shrink, warp, và/hoặc rotate một image được gọi
là các geometric transform (cho một bộc lộ sớm, thấy [Semple79]) Cho các vùng phẳng, có hai hương vị của các geometric transform: các transform mà dùng một 2-by-3 matrix, mà được gọi affine transforms; và các transform dựa trên a 3-by-3 matrix, mà được gọi perspective transforms hay homographies Bạn có thể
nghĩ biến đổi sau như một method để tính cách mà trong mặt phẳng trong ba chiều được nhận thức bởi một người quan sát cụ thể, người không thể quan sát thẳng trên mặt phẳng đó Một biến đổi affine là bất kỳ biếnđổi mà có thể được nhằm theo dạng của một tích matrix theo sau bởi một tổng vector Trong OpenCV kiểu chuẩn của biểu diễn một biến đổi như thế là một 2-by-3 matrix Ta định nghĩa:
Trang 16Điều được thấy dễ dàng hiệu quả của biến đổi affine A · X + B chính xác tương đương với mở rộng vector
X thành vector X´ và đơn giản nhân trái X´ bởi T Các biến đổi affine có thể được thấy như sau Bất kỳ
hình bình hành ABCD trong một mặt phẳng có thể được chiếu vào bất kỳ hình bình hành khác A'B'C'D' bởi
vài biến đổi affine Nếu các vùng của những hình bình hành này là khác không, thì biến đổi affine ngầm định được định nghĩa duy nhất bởi (ba cạnh của) hai hình bình hành Nếu bạn thích, bạn có thể nghĩ một biến đổi affine như vẽ ảnh của bạn thành một trang cao su lớn và sau đó biến dạng trang giấy bởi ấn hay kéo* trên các cạnh để làm các kiểu khác nhau của các hình bình hành
Khi ta có nhiều ảnh mà ta biết là các view khác nhau một ít của cùng đối tượng, ta có thể muốn tính các biến đổi thực mà liên quan các view khác nhau Trong trường hợp này, các biến đổi affine thường được dùng để mô hình các view vì, có ít parameter hơn, chúng dễ để giải quyết Phía dưới là sự thật rằng các méo mó phối cảnh có thể được mô hình bởi một homography,† do đó các biến đổi affine sản ra một biểu diễn mà không thể thích hợp tất cả các quan hệ có thể giữa các view Mặt khác, cho các thay đổi nhỏ trong viewpoint méo kết quả là affine, do đó trong vài trường hợp một biến đổi affine có thể là đủ
Các biến đổi Affine có thể chuyển các hình chữ nhật thành các hình bình hành Chúng có thể nén hình dángnhưng phải giữ các cạnh song song; chúng có thể quay nó và/hoặc tỉ lệ nó Các biến đổi phối cảnh cung cấpnhiều linh hoạt hơn; một biến đổi phối cảnh có thể chuyển hình chữ nhật thành hình thanh Dĩ nhiên, do cáchình bình hành cũng là hình thang, các biến đổi affine là tập con của các biến đổi phối cảnh Figure 6-13 cho thấy các ví dụ của các biến đổi affine và phối cảnh khác nhau
Affine Transform
Có hai trường hợp mà đến khi làm việc với cac biến đổi affine Trong trường hợp đầu tiên, ta có một image (hay một vùng quan tâm) ta thích biến đổi; trong trường hợp thứ hai, ta có một danh sách các điểm mà ta muốn tính kết quả của một biến đổi
Dense affine transformations
Trong trường hợp đầu tiên, các định dạng input và output hiển nhiên là ảnh, và yêu cầu ngầm định là việc
warping giả sử các pixel là một dense representation của ảnh nằm dưới Điều này có nghĩa rằng image
warping phải handle cần thiết các nội suy sao cho các output image là trơn và trông tự nhiên Hàm biến đổi affine được cấp bởi OpenCV cho các biến đổi dense là cvWarpAffine()
Figure 6-13 Các biến đổi Affine và phối cảnh
void cvWarpAffine(
const CvArr* src,
Trang 17CvArr* dst,
const CvMat* map_matrix,
int flags = CV_INTER_LINEAR | CV_WARP_FILL_OUTLIERS,
CvScalar fillval = cvScalarAll(0)
);
Ở đây src và dst refer đến một array hay image, mà có thể là một trong một hay ba channels và của bất kỳ
type (cung cấp chúng là cùng type và size).* map_matrix là 2-by-3 matrix ta giới thiệu trước mà định lượng biến đổi mong muốn next-tolast argument, flags, điều khiển method nội suy cũng như một trong hay cả option thêm sau (như bình thường, kết hợp với Boolean OR)
Điều đáng biết mà cvWarpAffine() liên quan khó khăn kết hợp thực sự Một thay thế là dùng
thể handle trường hợp đặc biệt khi ảnh nguồn là 8-bit và ảnh đích là 32-bit floating-point image Nó cũng
sẽ handle các multichannel image
void cvGetQuadrangleSubPix(
const CvArr* src, CvArr* dst, const CvMat* map_matrix );
Điều cvGetQuadrangleSubPix() làm là tính tất cả các điểm trong dst bởi chiếu chúng (bằng nội suy) từ các điểm trong src mà được tính bởi áp dụng biến đổi affine bởi nhận với 2-by-3 map_matrix (Chuyển đổi các vị trí trong dst thành tọa độ homogeneous để phép nhân được làm tự động.)
Một tính chất của cvGetQuadrangleSubPix() là có một phép chiếu phụ áp dụng bởi function Về cụ thể, các điểm kết quả trong dst được tính tùy theo công thức:
) ,
( )
,
11
'' 10 0
' 01
'
00x a y b a x a y b a
10
0 01
00
b a
a
b a
) 1 ) ( ( ''
''
dst height y
dst width x
y x
Nhận xét thấy phép chiếu từ (x, y) thành (x˝, y˝) có hiệu ứng mà—ngay cả nếu phép chiếu M là phép chiếu
đồng nhất—các điểm trong ảnh đích ở tâm sẽ được lấy từ ảnh gốc ở gốc Nếu cvGetQuadrangleSubPix() cần
Computing the affine map matrix
OpenCV cung cấp hai functions giúp bạn tạo map_matrix Cái đầu tiên được dùng khi bạn hiện có hai ảnh
mà bạn biết được liên quan bởi một biến đổi affine hay bạn thích xấp xỉ theo cách đó:
Example 6-2 cho thấy ít code mà dùng những function này Trong example ta lấy các tham số matrix
của ta) và sau đó chuyển thành matrix biến đổi thực dùng cvGetAffineTransform() Ta sau đó làm một affine