使用 OpenCV 測量物體尺寸
共 16659字,需瀏覽 34分鐘
·
2024-04-21 10:05
點擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時間送達(dá) ![]()
-
導(dǎo)入模塊和參數(shù)初始化 -
圖像加載和預(yù)處理 -
尋找紙張輪廓 -
透視變換 -
尋找物體輪廓 -
邊長計算 -
將所有內(nèi)容放在一個函數(shù)中
1. 導(dǎo)入模塊及參數(shù)初始化
cv2,numpy而matplotlib僅用于顯示圖像。
# Codeblock 1import cv2import numpy as npimport matplotlib.pyplot as plt
SCALE主要用于我們不希望生成的圖像太小。同時,和PAPER_W表示PAPER_H紙張寬度和高度(以毫米為單位)。
# Codeblock 2SCALE = 3PAPER_W = 210 * SCALEPAPER_H = 297 * SCALE
2.圖像加載和預(yù)處理
load_image(),show_image()我認(rèn)為這兩個函數(shù)的名稱是不言自明的。它們的詳細(xì)信息可以在 Codeblock 3 中看到。您可以在下面看到我定義了參數(shù)scale(小寫),其中我的目的是使輸入圖像變得有點小,因為原始圖像分辨率非常高。但是,從技術(shù)上講,您也可以通過傳遞大于 1 的值來使其更大。
show_image()另一方面,該函數(shù)實現(xiàn)cv2.cvtColor() 了將顏色通道從 BGR 轉(zhuǎn)換為 RGB 的功能。這種轉(zhuǎn)換是必要的,因為 Matplotlib 在顏色通道順序方面與 OpenCV 的工作方式不同。
# Codeblock 3def load_image(path, scale=0.7): img = cv2.imread(path) img_resized = cv2.resize(img, (0,0), None, scale, scale)return img_resized
def show_image(img): img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.figure(figsize=(6,8)) plt.xticks([]) plt.yticks([]) plt.imshow(img) plt.show()
scale為其默認(rèn)值(0.7),這將導(dǎo)致加載的圖像大小為 1120×840 px(原始大小為 1600×1200 px)。
# Codeblock 4img_original = load_image(path='images/1.jpeg')show_image(img_original)print(img_original.shape)
img_original。接下來要做的步驟是使用一系列圖像處理技術(shù)對該圖像進(jìn)行預(yù)處理,即灰度轉(zhuǎn)換(#1)、模糊(#2)、Canny 邊緣檢測(#3)、擴(kuò)張(#5)和閉合(#6)。所有這些步驟都包含在preprocess_image()Codeblock 5 中的函數(shù)中。
# Codeblock 5def preprocess_image(img, thresh_1=57, thresh_2=232): img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) #1 img_blur = cv2.GaussianBlur(img_gray, (5,5), 1) #2 img_canny = cv2.Canny(img_blur, thresh_1, thresh_2) #3
kernel = np.ones((3,3)) #4 img_dilated = cv2.dilate(img_canny, kernel, iterations=1) #5 img_closed = cv2.morphologyEx(img_dilated, cv2.MORPH_CLOSE, kernel, iterations=4) #6
img_preprocessed = img_closed.copy()
img_each_step = {'img_dilated': img_dilated, 'img_canny' : img_canny, 'img_blur' : img_blur, 'img_gray' : img_gray}
return img_preprocessed, img_each_step
#2的參數(shù)分別表示高斯濾波器大小和高斯分布標(biāo)準(zhǔn)差。同時,行中的和是 Canny 邊緣檢測器用來捕獲弱邊緣和強(qiáng)邊緣的兩個閾值。在本例中,我決定將 設(shè)置為 57 和232,這些數(shù)字是通過反復(fù)試驗確定的。接下來,我們初始化一個 3×3 內(nèi)核,其中內(nèi)核的所有元素都設(shè)置為 1(標(biāo)記為 的行)。然后,這個全 1 內(nèi)核將在擴(kuò)張和閉合過程中用作結(jié)構(gòu)元素。(5,5)1thresh_1thresh_2#3thresh_1thresh_2#4
preprocess_image()函數(shù)返回兩個值:完全預(yù)處理的圖像(存儲在 中img_preprocessed)和每個預(yù)處理階段的結(jié)果(img_each_step以字典形式存儲在 中)。該函數(shù)的輸出如圖 3 所示。要進(jìn)入下一個過程的圖像是img_preprocessed(最右邊)。
# Codeblock 6img_preprocessed, img_each_step = preprocess_image(img_original)show_image(img_each_step['img_gray'])show_image(img_each_step['img_blur'])show_image(img_each_step['img_canny'])show_image(img_each_step['img_dilated'])show_image(img_preprocessed)
cv2.findContours()( ) 來執(zhí)行此操作#1。我們還將其cv2.RETR_EXTERNAL作為參數(shù)的輸入?yún)?shù)傳遞,因為我們只對捕獲圖像中檢測到的最外層輪廓(即紙張)感興趣。現(xiàn)在,計算器對象將被忽略。然后,將使用( )mode繪制檢測到的輪廓本身。cv2.drawContours()#2
# Codeblock 7def find_contours(img_preprocessed, img_original, epsilon_param=0.04):contours, hierarchy = cv2.findContours(image=img_preprocessed, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE) #1img_contour = img_original.copy()cv2.drawContours(img_contour, contours, -1, (203,192,255), 6) #2polygons = []for contour in contours:epsilon = epsilon_param * cv2.arcLength(curve=contour, closed=True) #3polygon = cv2.approxPolyDP(curve=contour, epsilon=epsilon, closed=True) #4polygon = polygon.reshape(4, 2) #5polygons.append(polygon)for point in polygon: img_contour = cv2.circle(img=img_contour, center=point, radius=8, color=(0,240,0), thickness=-1) #6return polygons, img_contour不僅如此,這里我還嘗試使用cv2.approxPolyDP()(#4)來近似輪廓形狀。這行代碼的目的是獲取紙張輪廓的四個角,然后將坐標(biāo)存儲在其中polygon。我們需要注意的一件事是epsilon行中使用的參數(shù)#3。較小的 epsilon 值往往會導(dǎo)致檢測到更多的角,而另一方面,較大的 epsilon 會導(dǎo)致函數(shù)cv2.approxPolyDP()捕獲多邊形的一般形狀,即較少的角。注意 epsilon 值很重要,因為我們需要確保函數(shù)將準(zhǔn)確捕獲四個角點。
#5),因為 的原始輸出cv2.approxPolyDP()格式為(number of corners, 1, 2),其中中間軸在我們的例子中無關(guān)緊要。因此,我們可以放心地將其丟棄。除了函數(shù)之外find_contours(),我們還將使用cv2.circle()( #6) 顯示角。然后,此函數(shù)將返回角坐標(biāo)本身 ( polygon) 和具有突出顯示輪廓的圖像 ( img_contour)。
find_contours()函數(shù)。在這里我決定將設(shè)置epsilon_param為 0.04。如果您使用自己的圖片,則可能需要更改此值,特別是如果光照特性與我的圖像不同。下面代碼塊的輸出(顯示在圖 4 中)顯示了輪廓和角落的樣子。我也打印了存儲在中的坐標(biāo)polygon[0]。如果您想知道,使用索引器 0 是因為該find_contours()函數(shù)本質(zhì)上能夠捕獲多個輪廓,而在這種情況下,我們唯一的外部輪廓是紙張本身。除此之外,可能值得注意的是,存儲在中的坐標(biāo)polygons是(x,y)格式,而不是(y,x)。
# Codeblock 8polygons, img_contours = find_contours(img_preprocessed, img_original,epsilon_param=0.04)show_image(img_contours)polygons[0]
重新排序坐標(biāo)
cv2.approxPolyDP()。為了使這四個點都有序,我們需要創(chuàng)建一個專門的函數(shù)來執(zhí)行此操作。我正在談?wù)摰暮瘮?shù)稱為,reorder_coords()其詳細(xì)信息可在 Codeblock 9 中看到。
# Codeblock 9def reorder_coords(polygon): rect_coords = np.zeros((4, 2))
add = polygon.sum(axis=1) rect_coords[0] = polygon[np.argmin(add)] # Top left rect_coords[3] = polygon[np.argmax(add)] # Bottom right
subtract = np.diff(polygon, axis=1) rect_coords[1] = polygon[np.argmin(subtract)] # Top right rect_coords[2] = polygon[np.argmax(subtract)] # Bottom left
return rect_coords
polygon[0]如 Codeblock 10 所示。
# Codeblock 10rect_coords = np.float32(reorder_coords(polygons[0]))rect_coords
rect_coords)將作為變換的源,而變換目標(biāo)(paper_coords)由實際紙張大小決定,即PAPER_W和PAPER_H。查看 Codeblock 11 以了解我如何手動排列 存儲的點。這里要記住的一件事是,目標(biāo)坐標(biāo)的順序需要與源坐標(biāo)的順序完全匹配,這就是我們實現(xiàn)重新排序源坐標(biāo)的功能paper_coords的原因。reorder_coords()
# Codeblock 11paper_coords = np.float32([[0,0], # Top left [PAPER_W,0], # Top right [0,PAPER_H], # Bottom left [PAPER_W,PAPER_H]]) # Bottom rightpaper_coords
cv2.getPerspectiveTransform()后續(xù)代碼塊中的函數(shù)返回錯誤。
創(chuàng)建變換矩陣
rect_coords和paper_coords。我們現(xiàn)在可以使用它們來扭曲原始圖像,使用cv2.getPerspectiveTransform()和,cv2.warpPerspective()如下面的代碼塊 12 所示。為了讓代碼更簡潔,我將它們放在另一個名為的函數(shù)中warp_image()。
# Codeblock 12def warp_image(rect_coords, paper_coords, img_original, pad=5):
matrix = cv2.getPerspectiveTransform(src=rect_coords, dst=paper_coords) #1img_warped = cv2.warpPerspective(img_original, matrix,(PAPER_W, PAPER_H)) #2
warped_h = img_warped.shape[0]warped_w = img_warped.shape[1]img_warped = img_warped[pad:warped_h-pad, pad:warped_w-pad] #3
return img_warped
cv2.getPerspectiveTransform()( #1) 創(chuàng)建所謂的變換矩陣。變換矩陣本身的尺寸為 3 × 3,它存儲了有關(guān)如何將圖像從一個角度變換到另一個角度的信息。然后,該矩陣將實際用于使用cv2.warpPerspective()( #2) 變換原始圖像。然后,我們將變換后的圖像存儲在名為 的變量中img_warped。除了函數(shù)之外warp_image(),我們還將丟棄圖像邊界附近的區(qū)域 ( #3),因為生成的圖像可能在邊緣處包含一個狹窄的不需要的黑色區(qū)域。
warp_image()在 Codeblock 13 中演示,其中輸出顯示在圖 7 中。
# Codeblock 13img_warped = warp_image(rect_coords, paper_coords, img_original)show_image(img_warped)print(img_warped.shape)
cv2.RETR_EXTERNAL我們在 Codeblock 7 中實現(xiàn)的輪廓。
# Codeblock 14img_warped_preprocessed, _ = preprocess_image(img_warped)show_image(img_warped_preprocessed)
# Codeblock 15polygons_warped, img_contours_warped = find_contours(img_warped_preprocessed, img_warped,epsilon_param=0.04)show_image(img_contours_warped)polygons_warped[0]
polygons_warped,這四個坐標(biāo)值其實就是用來估算邊長的坐標(biāo)值。
6. 邊長計算
#2)。同時,可以通過計算物體左上角和右上角之間的相同距離度量來獲得寬度(#3)。此外,我們需要記住,返回的檢測到的角find_contours()仍然是無序的。因此,我們需要reorder_coords()事先調(diào)用( )。我想在 Codeblock 16 中強(qiáng)調(diào)的最后一件事是,估計的高度和寬度現(xiàn)在存儲在按相應(yīng)順序#1命名的數(shù)組中( )sizes#4
# Codeblock 16def calculate_sizes(polygons_warped):
rect_coords_list = []for polygon in polygons_warped:rect_coords = np.float32(reorder_coords(polygon)) #1rect_coords_list.append(rect_coords)
heights = []widths = []for rect_coords in rect_coords_list:height = cv2.norm(rect_coords[0], rect_coords[2], cv2.NORM_L2) #2width = cv2.norm(rect_coords[0], rect_coords[1], cv2.NORM_L2) #3
heights.append(height)widths.append(width)
heights = np.array(heights).reshape(-1,1)widths = np.array(widths).reshape(-1,1)
sizes = np.hstack((heights, widths)) #4
return sizes, rect_coords_list
sizes, rect_coords_list = calculate_sizes(polygons_warped)sizes
轉(zhuǎn)換為毫米
convert_to_mm()(Codeblock 17)。此函數(shù)通過取紙張長度(以毫米為單位)和像素(以像素為單位)的比率來工作。然后,我們可以通過將比率值(和)與仍以像素為單位的計算器大小(和#1)#2相乘來獲得實際的毫米長度。scale_hscale_w#3#4
# Codeblock 17def convert_to_mm(sizes_pixel, img_warped):warped_h = img_warped.shape[0]warped_w = img_warped.shape[1]
scale_h = PAPER_H / warped_h #1scale_w = PAPER_W / warped_w #2
sizes_mm = []
for size_pixel_h, size_pixel_w in sizes_pixel:size_mm_h = size_pixel_h * scale_h / SCALE #3size_mm_w = size_pixel_w * scale_w / SCALE #4
sizes_mm.append([size_mm_h, size_mm_w])
return np.array(sizes_mm)
sizes_mm = convert_to_mm(sizes, img_warped)sizes_mm
write_size()。初始化函數(shù)后,我們可以直接調(diào)用它,如#1代碼塊 18 中所示。此代碼的輸出如圖 12 所示。
# Codeblock 18def write_size(rect_coords_list, sizes, img_warped):
img_result = img_warped.copy()
for rect_coord, size in zip(rect_coords_list, sizes):
top_left = rect_coord[0].astype(int) top_right = rect_coord[1].astype(int) bottom_left = rect_coord[2].astype(int)
cv2.line(img_result, top_left, top_right, (255,100,50), 4) cv2.line(img_result, top_left, bottom_left, (100,50,255), 4)
cv2.putText(img_result, f'{np.int32(size[0])} mm', (bottom_left[0]-20, bottom_left[1]+50), cv2.FONT_HERSHEY_DUPLEX, 1, (100,50,255), 1)
cv2.putText(img_result, f'{np.int32(size[1])} mm', (top_right[0]+20, top_right[1]+20), cv2.FONT_HERSHEY_DUPLEX, 1, (255,100,50), 1)
return img_result
img_result = write_size(rect_coords_list, sizes_mm, img_warped) #1show_image(img_result)print(img_result.shape)
7. 將所有內(nèi)容放在一個函數(shù)中
measure_size()在 Codeblock 19 中將其命名為該函數(shù)。使用此函數(shù),我們可以簡單地將要處理的圖像與參數(shù)一起傳遞,以完成我們剛剛完成的所有工作。
# Codeblock 19def measure_size(path, img_original_scale=0.7,PAPER_W=210, PAPER_H=297, SCALE=3, paper_eps_param=0.04, objects_eps_param=0.05, canny_thresh_1=57, canny_thresh_2=232):
PAPER_W = PAPER_W * SCALEPAPER_H = PAPER_H * SCALE
# Loading and preprocessing original image.img_original = load_image(path=path, scale=img_original_scale)img_preprocessed, img_each_step = preprocess_image(img_original, thresh_1=canny_thresh_1, thresh_2=canny_thresh_2)
# Finding paper contours and corners.polygons, img_contours = find_contours(img_preprocessed, img_original, epsilon_param=paper_eps_param)
# Reordering paper corners.rect_coords = np.float32(reorder_coords(polygons[0]))
# Warping image according to paper contours.paper_coords = np.float32([[0,0], [PAPER_W,0], [0,PAPER_H],[PAPER_W,PAPER_H]])img_warped = warp_image(rect_coords, paper_coords, img_original)
# Preprocessing the warped image.img_warped_preprocessed, _ = preprocess_image(img_warped)
# Finding contour in the warped image.polygons_warped, img_contours_warped = find_contours(img_warped_preprocessed, img_warped,epsilon_param=objects_eps_param)
# Edge langth calculation.sizes, rect_coords_list = calculate_sizes(polygons_warped)sizes_mm = convert_to_mm(sizes, img_warped)img_result = write_size(rect_coords_list, sizes_mm, img_warped)
return img_result
measure_size()創(chuàng)建了函數(shù),我們現(xiàn)在就可以在幾個測試用例上測試它。圖 13 所示的輸出表明,我們估算物體大小的方法對于不同的矩形物體非常有效。
# Codeblock 20show_image(measure_size('images/1.jpeg'))show_image(measure_size('images/2.jpeg'))show_image(measure_size('images/3.jpeg'))show_image(measure_size('images/4.jpeg'))show_image(measure_size('images/5.jpeg'))show_image(measure_size('images/6.jpeg', objects_eps_param=0.1))
一些局限
下載1:OpenCV-Contrib擴(kuò)展模塊中文版教程
在「小白學(xué)視覺」公眾號后臺回復(fù):擴(kuò)展模塊中文教程,即可下載全網(wǎng)第一份OpenCV擴(kuò)展模塊教程中文版,涵蓋擴(kuò)展模塊安裝、SFM算法、立體視覺、目標(biāo)跟蹤、生物視覺、超分辨率處理等二十多章內(nèi)容。
下載2:Python視覺實戰(zhàn)項目52講 在「小白學(xué)視覺」公眾號后臺回復(fù):Python視覺實戰(zhàn)項目,即可下載包括圖像分割、口罩檢測、車道線檢測、車輛計數(shù)、添加眼線、車牌識別、字符識別、情緒檢測、文本內(nèi)容提取、面部識別等31個視覺實戰(zhàn)項目,助力快速學(xué)校計算機(jī)視覺。
下載3:OpenCV實戰(zhàn)項目20講 在「小白學(xué)視覺」公眾號后臺回復(fù):OpenCV實戰(zhàn)項目20講,即可下載含有20個基于OpenCV實現(xiàn)20個實戰(zhàn)項目,實現(xiàn)OpenCV學(xué)習(xí)進(jìn)階。
交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細(xì)分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進(jìn)入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~
評論
圖片
表情
