【实战篇:粘连物体分割——利用几何分割实现硬币分割检测】
實戰篇:粘連物體分割——利用角點檢測、定ROI區域、透視變換、幾何分割實現硬幣分割計數
- 一、背景
- 二、思路
- 三、代碼
- 四、效果
- 五、聲明
- 六、其他文章
一、背景
????前面分享過一篇文章,是利用幾何分割的方法,實現了 瓶蓋的分割檢測瓶蓋的分割檢測,本文主要利用該文章算法,應用到硬幣檢測當中(因為圖像分辨率有一些不一樣,質量也有點不同,所有需要稍微調參),經過稍微調參之后,這里附上可以直接運行的代碼。
圖1 瓶蓋檢測效果 圖2 硬幣檢測二、思路
一、基于****Harrs****的角點檢測
圓形相粘物體之間會存在凹凸區域、可以通過對相連區域進行角點檢測、或者凹凸點檢測。檢測點是否齊全決定分割的準確度。為了保證分割效果,角點檢測階段經可能檢測多一些角點。
二、均值去噪點
對所有點求最近點求最小歐式距離,通過對距離進行求均值以及中位數。其中均值指標適用于雜點較多以及距離較大的情況。對于雜點較多,距離較大的情況。通過均值指標或者中位數指標能去掉部分雜點。
三、ROI取樣
為了更進一步篩選掉部分雜點,以及更好第判斷兩點之間是否為物體相連部分,因為通過相連部分的兩個角點,進行旋轉90°獲得一個正方向的ROI區域,假設ROI區域為相連部分,其ROI區域中的填充率較大。因此利用本特征進行ROI取樣。其中只有正方形ROI才能更好地衡量其填充率。
四、坐標變換
在對圖像坐標進行運算的時候,需要進行坐標變換,將坐標原點移動到圖像的中心。
五、旋轉矩陣獲取
對獲取到兩個角點,通過坐標變換、旋轉矩陣的獲取,使圖像中任意點能繞一點進行旋轉一定角度。
六、透視變換
因為需要計算填充率需要進行透視變換,求ROI輪廓面積與ROI面積之比,因為不同四邊形的角度不一樣,所以需要對所有點進行排序,同時又一種特殊情況就是四邊形對角線如果是豎直的情況,則不需要及進行排序,不然會出現矯正錯誤。
七、填充率計算
通過取樣的ROI進行計算二值化輪廓面積與ROI面積之比,獲取填充率,設定一個值,假如大于閾值,就進行分割。
三、代碼
????將下面代碼放在.py文件當中,讀取圖片,運行之后,便會在當前路徑自動創建文件夾將所有圖片保存里面。
""" 作者:馮耿鑫 時間:2021/1/9 功能:對相連的圓形物體進行分割 思路:=>>創新:形態學操作的小技巧可以定義一個卷積核、然后在本卷積核上畫圓,就是一個圓形的卷積了=>>基于Harris角點檢測、得出dist圖像,因為再拐角處會有很多個角點,為了只求一個,所以進行二值化,膨脹,求拐點的形心。=>>對角點進行x方向的排序=>>進行坐標變換、以及旋轉矩陣求出垂直的另一條直線=>>進行透視變換,矯正ROI區域,需要通過透視變換來求得,其中ROI的透視變換用到了坐標排序,其中需要注意一種對角線豎直的情況,然后求包含物體的飽和率,從而進行篩選。=>>利用連通域進行顏色顯示 """# -*- coding:utf-8 -*- import cv2 as cv import numpy as np import cv2 import math import os # 創建文件進行圖片保存 def make_dir_save_img(path,img, binary,open,color,result ):if not os.path.exists(name): # 判斷是否存在os.makedirs( name) # 不存在就創建文件夾if not os.path.exists("roi\\"+name): # 判斷是否存在os.makedirs("roi\\" +name) # 不存在就創建文件夾cv.imwrite(name+"\\img.png",img)cv.imwrite(name+"\\binary.png",binary)cv.imwrite(name+"\\open.png",open)cv.imwrite(name+"\\color.png",color)cv.imwrite(name+"\\result.png",result)class SegmentationConnectObject(object):def __init__(self,img,binary):self.img = img # 原圖self.binary = binary # 二值化圖片self.number = 0 # 第幾個輪廓self.answer = False # 一開始默認是不是相連的self.H,self.W,self.C = img.shape# 尋找距離最小的兩個點def main_find_mindist_points(self):""":function: 用來尋找兩個最近的點,用來進行區域分析:return:""""""=>>圓形卷積核進行形態學操作,消除雜點噪聲、以及光滑變換<<="""k2 = np.zeros((6, 6), np.uint8) # <==定義一個卷24x24的卷積核cv2.circle(k2, (3, 3), 3, (1, 1, 1), -1, cv2.LINE_AA) # <==在這個卷積核上進行畫一個鵑形的卷積核k1 = cv.getStructuringElement(cv.MORPH_ELLIPSE,(3,3))self.binary= cv.morphologyEx(self.binary, cv.MORPH_DILATE, k1) # 進行開操作,也就是先腐蝕后膨脹open = cv.morphologyEx(self.binary, cv.MORPH_OPEN, k2) # 進行開操作,也就是先腐蝕后膨脹"""=>>利用Harris進行角點檢測<<= """harris = cv2.cornerHarris(open, 2, 5, 0.04) #<== 進行角點檢測,blockSize:角點檢測中要考慮的領域大小||ksize - Sobel:求導中使用的窗口大小||k - Harris:角點檢測方程中的自由參數, 取值參數為[0, 04, 0.06]harris = cv2.dilate(harris, None) # 對角點進行一個簡單的膨脹、不然的話輪廓會不好尋找# img[harris > 0.2 * harris.max()] = [0, 0, 255] #<== 通過角點檢測之后只有邊緣像素是有值的,拐角的地方是比較大,所以利用這個條件進行顯示pix_max = 0.2 * harris.max() # <==獲取選擇角點的閾值ret, binary_p = cv.threshold(harris, pix_max, 255, cv.THRESH_BINARY) # 因為選擇出來的像素太多了,又類似與輪廓,所以我們直接進行閾值分割發現輪廓binary_p = np.uint8(binary_p) # 二值化前需要對位數進行轉換contours = cv2.findContours(binary_p, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0] # 發現角點的輪廓,用來發現其角點的質心"""=>>找到所有的可能坐標點<<= """points = [] # 用來儲存所有的角點坐標for c in contours: # 橫向# 獲取矩形框的四個參數mm = cv.moments(c) # 幾何重心的獲取cx, cy = int(mm['m10'] / mm['m00']), int(mm['m01'] / mm['m00'])points.append((int(cx), int(cy))) # 將坐標保留在points列表中cv.circle(self.img, (int(cx), int(cy)), 3, (0, 0, 255), -1)"""=>>對所有的角點排序,方向為x從小到大<<= """points_sorted_x = self.sort_x(points)# for i, p in enumerate(points_sorted_x):# cv.circle(self.img, p, 4, (255, 0, 0), -1) # 畫出點# cv.putText(self.img,str(i) , p, cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 2)"""=>>因為有很多角點,如何獲得最小相連的兩個點呢,通過遍歷所有點,獲取最小距離用來求平均值,用來作為指標作為閾值,篩選太遠的點<<= """points_sorted_x_2 = points_sorted_x.copy() # 因為要遍歷兩次,所以賦值一份方便后面更改distance_1 = [] # 所有點都進行遍歷、儲存每個點相連做近的點for p1 in points_sorted_x: # p1作為父點x1, y1 = p1 # p1的坐標distance_2 = [] # 用來儲存所有子點p2到p1的距離,然后獲取最小距離給diatance_1for p2 in points_sorted_x_2: # p2作為父點x2, y2 = p2 # p2的坐標if x1 == x2 and y1 == y2: # 因為兩個列表是一樣的,所以會有遇到相同的點,需要跳過,不然distance_2中最小的都是0continue # 循環到原來的帶點就不進行計算else:l = pow(abs(x1 - x2) ** 2 + abs(y1 - y2) ** 2, 0.5) # 計算父點與子點的歐式距離distance_2.append(l) # 將所有歐式距離保存在distance_2中distance_1.append(min(distance_2)) # 獲取每個父點到子點的最小歐式距離mean_dist = np.mean(distance_1) # 這里設置了兩個指標,一個是平均值,適合密集點median_dist = np.median(distance_1) # 一個是中間數,適合雜點較少情況"""=>>上面根據模型求出距離指標,下面將通過設定閾值進行求解<<= """choose = [] # choose列表使用來記錄已經檢測完畢的兩個點,用來判斷,如果沒有檢測成功,就繼續檢測,如果檢測成功,那就跳過避免重復檢測for number, p1 in enumerate(points_sorted_x): # 遍歷父點x1, y1 = p1 # 父點坐標for p2 in points_sorted_x: # 遍歷子點x2, y2 = p2 # 子點坐標if x1 == x2 and y1 == y2: # 過濾相同的點continueelse: # 求歐式距離l = pow(abs(x1 - x2) ** 2 + abs(y1 - y2) ** 2, 0.5)if l > mean_dist * 0.3 and l < mean_dist * 1.8: #<<== 設定約束條件,如果在這個閾值范圍就可以進行后續的分割功能if p1 in choose or p2 in choose: # 如果點在choose中就代表這兩個點已經檢測成功continueself.check_connect(x1, y1, x2, y2) #<<==判斷是否連接函數if self.answer: # 當分割成功的時候,用來記錄p1,p2這兩個點choose.append(p1)choose.append(p2)self.answer = False # 需要重新賦值,不然邊True之后就會一直默認正確color,result = self.connect_domain()return self.img,self.binary, open,color,result# 冒泡排序對角點坐標進行排序def sort_x(self,points):"""function:冒泡排序算法實現對x方向進行排序"""l = len(points)for i in range(l - 1):for j in range(l - 1 - i):# if points[j][1]>points[j+1][1]:# temp = points[j]# points[j] = points[j+1]# points[j+1] = tempif points[j][0] > points[j + 1][0]:temp = points[j]points[j] = points[j + 1]points[j + 1] = tempreturn points# 通過旋轉矩陣,實現任一點的旋轉def rota(self,x1, y1, x2, y2):""":function:以任意點為中線,通過坐標平移,然后通過旋轉,再平移回來,最終完成旋轉。:return:"""# 獲取直線的中點cx, cy = (x1 + x2) / 2, (y1 + y2) / 2# 偏移矩陣C = np.array([[cx], [cy]])# 旋轉角度degree = math.radians(90)# 旋轉矩陣A = np.array([[math.cos(degree), -math.sin(degree)],[math.sin(degree), math.cos(degree)]])# 輸入坐標X1 = np.array([[x1], [y1]])X2 = np.array([[x2], [y2]])# 進行偏移,將中間點轉換為中間坐標X1 = X1 - CX2 = X2 - C# 利用矩陣的乘積求出旋轉坐標Y1 = np.dot(A, X1)Y2 = np.dot(A, X2)# 轉換絕對坐標的形式Y1 = Y1 + CY2 = Y2 + Cout_x1, out_y1, out_x2, out_y2 = int(Y1.ravel()[0]), int(Y1.ravel()[1]), int(Y2.ravel()[0]), int(Y2.ravel()[1])return out_x1, out_y1, out_x2, out_y2# 檢查是否為相連物體def check_connect(self,x1, y1, x2, y2):""":function:檢查這兩個點是否為相連接的兩個點""""""==>>因為后面需要用到圖像坐標的各種運算,所以需要先進行坐標變換<<=="""x1, y1 = self.change_coordinate_lt_center(x1, y1) # 將第一個點也就是父點轉換為笛卡爾坐標系x2, y2 = self.change_coordinate_lt_center(x2, y2) # 將第二個點也就是子點轉換為笛卡爾坐標系"""==>>進行旋轉90°,分別獲得父、子的旋轉坐標<<=="""x3, y3, x4, y4 = self.rota(x1, y1, x2, y2) #x3,y3是父點的逆時針旋轉點、x4,y4是子點的旋轉坐標點"""==>>轉為圖像坐標系<<=="""x1, y1 = self.change_coordinate_center_lt(x1, y1) # 將父點坐標轉為圖像坐標x2, y2 = self.change_coordinate_center_lt(x2, y2) # 將子點坐標轉為圖像坐標x3, y3 = self.change_coordinate_center_lt(x3, y3) # 將父點坐標旋轉坐標轉為圖像坐標x4, y4 = self.change_coordinate_center_lt(x4, y4) # 將子點坐標旋轉坐標轉為圖像坐標"""==>>進行透視變換、因為是傾斜的矩形,必須透視變換,不然的話沒辦法求比例<<=="""pts = [(x1, y1), (x3, y3), (x2, y2), (x4, y4)]"""==>>進行透視變換、因為是傾斜的矩形,必須透視變換,不然的話沒辦法求比例<<=="""self.Perspective_transformation(pts)"""==>>answer表示的是,檢測區域為相連接部分,<<=="""if self.answer:arrPt = np.array(pts, np.int32).reshape((-1, 1, 2)) # 將坐標轉換為n行兩列的形式cv.polylines(img, [arrPt], True, (255, 0, 255), 1)# 圖片坐標系轉為笛卡爾坐標系def change_coordinate_lt_center(self,x_in, y_in):"""x_out = x_in-1/2Wy_out = -(y_in-1/2H) = 1/2H-y_in"""x_out = x_in - 1 / 2 * self.Wy_out = 1 / 2 * self.H - y_inreturn x_out, y_out# 笛卡爾坐標系轉為圖片的坐標系def change_coordinate_center_lt(self,x_in, y_in):"""x_out = x_in+1/2Wy_out = -(y_in-1/2H) = 1/2H-y_in =>>y_in = 1/2H-y_out ==>> y_out = 1/2H-yin"""x_out = x_in + 1 / 2 * self.Wy_out = 1 / 2 * self.H - y_inreturn int(x_out), int(y_out)def order_points(self,pts):# initialzie a list of coordinates that will be ordered# such that the first entry in the list is the top-left,# the second entry is the top-right, the third is the# bottom-right, and the fourth is the bottom-leftrect = np.zeros((4, 2), dtype="float32")# the top-left point will have the smallest sum, whereas# the bottom-right point will have the largest sums = pts.sum(axis=1)rect[0] = pts[np.argmin(s)]rect[2] = pts[np.argmax(s)]# now, compute the difference between the points, the# top-right point will have the smallest difference,# whereas the bottom-left will have the largest differencediff = np.diff(pts, axis=1)rect[1] = pts[np.argmin(diff)]rect[3] = pts[np.argmax(diff)]# return the ordered coordinatesreturn rect# 進行透視變換def Perspective_transformation(self,pts):"""==>>獲取四個點的坐標,依次是父點、父點旋轉點、子點、子點旋轉點、同時對對角線豎直的情況進行單獨分析<<=="""(x1, y1), (x2, y2), (x3, y3), (x4, y4) = pts[0], pts[1], pts[2], pts[3]pts1 = np.float32(pts) # 透視變換前坐標需要轉換為32位if x1 == x3 or x2 == x4: # 有一張特殊情況,就是對角線是豎直線,這樣的經過排序之后就會出現變形,漏檢測,rect = pts1else:rect = self.order_points(pts1)(tl, tr, br, bl) = rect"""==>>計算ROI區域的長寬<<=="""widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))maxWidth = max(int(widthA), int(widthB))heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))maxHeight = max(int(heightA), int(heightB))"""==>>獲取變換后圖片的坐標點、獲得旋轉矩陣、同時進行透視變換<<=="""dst = np.array([[0, 0],[maxWidth - 1, 0],[maxWidth - 1, maxHeight - 1],[0, maxHeight - 1]], dtype="float32") # in the top-left, top-right, bottom-right, and bottom-leftmatrix = cv2.getPerspectiveTransform(rect, dst) # 獲得旋轉矩陣roi = cv.warpPerspective(self.binary, matrix, (maxHeight, maxWidth)) # 獲取roi區域"""==>>對獲取到的目標區域進行面積統計<<=="""contours = cv2.findContours(roi, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0] # 發現最外邊輪廓area_list = [] #定義一個列表用來儲存所有的面積for cnt in contours:area_list.append(cv.contourArea(cnt))if len(area_list) == 0:max_cnts = 0 # 如果區域沒有面積,sum會報錯,所以需要單獨賦值為0else:max_cnts = sum(area_list) # 獲取面積綜合area = maxWidth * maxHeight # ROI的一個面積ratio = max_cnts / area # 二值化面積比if ratio > 0.8:self.number +=1cv.circle(self.img, ((x1 + x3) // 2, int(y1 + y3) // 2), 4, (0, 0, 255), -1)cv.circle(self.img, (x1, y1), 3, (255, 0, 0), -1)cv.circle(self.img, (x2, y2), 3, (255, 0, 0), -1)cv.circle(self.img, (x3, y3), 3, (255, 0, 0), -1)cv.circle(self.img, (x4, y4), 3, (255, 0, 0), -1)cv.line(self.img, (x1, y1), (x2, y2), (0, 255, 255), 1)cv.line(self.img, (x3, y3), (x4, y4), (0, 255, 255), 1)cv.line(self.binary, (x1, y1), (x3, y3), (0, 0, 0),2)cv.putText(self.img, str(self.number), ((x1 + x3) // 2, int(y1 + y3) // 2 - 10), cv.FONT_HERSHEY_SIMPLEX, 0.8,(255, 0, 0), 1)roi_img = cv.warpPerspective(self.img, matrix, (maxHeight, maxWidth))cv.imwrite(".\\roi\\" +name+"\\"+ str(self.number) + ".png", roi_img)self.answer = Truedef connect_domain(self):# # 連通域分析num_labels, labels, stats, centers = cv2.connectedComponentsWithStats(self.binary, connectivity=8)# 利用連通域進行不同輪廓畫出不同顏色color = np.zeros((self.H, self.W, 3), np.uint8)for i in range(1, num_labels):mask = labels == icolor[:, :, 0][mask] = np.random.randint(0, 255)color[:, :, 1][mask] = np.random.randint(0, 255)color[:, :, 2][mask] = np.random.randint(0, 255)result = cv2.addWeighted(img, 0.8, color, 0.5, 0) # 圖像權重疊加for i in range(1, len(centers)):cv2.drawMarker(result, (int(centers[i][0]), int(centers[i][1])), (255, 0, 0), 1, 10, 2)return color,resultdef get_binary(img):# # 圖像預處理# # blurred = cv.pyrMeanShiftFiltering(img, 10, 100) # 邊緣保留濾波能夠進行去噪是同時有效地保留邊緣# # gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY) # 進行灰度化為二值化做準備# # ret, binary = cv.threshold(gray, thresh=70, maxval=255, type=cv.THRESH_BINARY) # 固定閾值二值化,將大于thresh得像素點設置為maxval,# ## blurred = cv.pyrMeanShiftFiltering(img, 5, 50) # 邊緣保留濾波能夠進行去噪是同時有效地保留邊緣# #獲取灰度圖#### # h, s, v = cv.split(hsv)# ret, binary = cv.threshold(blurred, 200, 255, cv.THRESH_BINAR)blurred = cv.pyrMeanShiftFiltering(img, 5, 80) # 邊緣保留濾波能夠進行去噪是同時有效地保留邊緣gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY) # 進行灰度化為二值化做準備ret, binary = cv.threshold(gray, thresh=0, maxval=255, type=cv.THRESH_BINARY|cv.THRESH_OTSU) # 固定閾值二值化,將大于thresh得像素點設置為maxval,# imgHsv = cv.cvtColor(blurred, cv.COLOR_BGR2HSV) #轉換為HSV色彩空間# lower = np.array([0, 79, 0])# upper = np.array([179, 255, 255])# binary = cv2.inRange(imgHsv,lower,upper) #獲取灰度圖return binaryif __name__ == '__main__':path = "3.jpg"name = os.path.splitext(path)[0] # 文件名img = cv.imread(path) # 讀取圖片# 獲取二值化圖片binary = get_binary(img)# 創建實例seg = SegmentationConnectObject(img,binary)# 調用第一個函數開始執行功能,返回二值化、開操作、黑底顏色、結果、原圖img, binary,open,color,result = seg.main_find_mindist_points()# 進行圖片保存make_dir_save_img(path,img, binary,open,color,result )cv.namedWindow("img",0)cv.imshow("img",img)cv.namedWindow("binary",0)cv.imshow("binary",binary)cv.namedWindow("open",0)cv.imshow("open",open)cv.namedWindow("color",0)cv.imshow("color",color)cv.namedWindow("result",0)cv.imshow("result",result)cv.waitKey(0)cv.destroyAllWindows()四、效果
圖3.1 二值化操作(分割線畫在了上面) 圖3.2 開操作 圖3.3 粘連位置角點ROI提取 圖3.4 分割效果圖 圖3. 5 連通域分析染色圖 圖3. 6 圖片融合五、聲明
本次分享內容主要根據前面瓶蓋分割項目進行調參,應用硬幣識別當中,代碼以及算法仍有不少需要優化的地方,可在評論區留下您寶貴的意見,大家一起討論進步,謝謝大家的支持!!!
六、其他文章
1.理論系列:
第一章:pycharm、anaconda、opencv、pytorch、tensorflow、paddlex等環境配置大全總結【圖像處理py版本】
第二章:OpenCv算法的基本介紹與應用
第三章:OpenCv圖片、視頻讀寫操作與基本應用
第四章:OpenCv閾值分割/二值化(單通道、多通道圖片)總結
2.項目系列:
項目一:四六級改卷系統
項目二:實戰篇:粘連物體分割——利用幾何分割實現瓶蓋分割檢測
==》項目三:實戰篇:粘連物體分割——利用幾何分割實現硬幣分割檢測
項目四:實戰篇:粘連物體分割——利用幾何分割實現細胞分割檢測
項目五:實戰篇:粘連物體分割——利用分水嶺算法實現糖豆分割檢測
總結
以上是生活随笔為你收集整理的【实战篇:粘连物体分割——利用几何分割实现硬币分割检测】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 前端工程师等级评定
- 下一篇: js引擎执行js代码的过程