出品:1Z實驗室 (1ZLAB: Make Things Easy)
概要
下面的這張圖片是十字激光的圖像, 當前我們的任務就是要識別十字的四個端點還有十字中心。
這個缺口就是螺絲的孔位,所以你獲得的圖像可能是片段的,不一定連續。
我們用不同的顏色標示不同的端點, 空心圓代表交點。
算法流程講解
1-圖像二值化
二值化排除其他干擾。
binary = cv2.inRange(gray, 200, 255)
2-擬合線段與直線
用HoughLineP
進行線段擬合。
line_segs = cv2.HoughLinesP(binary, rho=2,theta=0.1, threshold=100)
len(line_segs)
HoughLineP
返回的是數組,每個元素是Tuple
類型的數據。還是要打印一下這個數據結構。
import math
for lseg in line_segs:
#
x1,y1,x2,y2 = lseg[0]
# 計算權重
weight = math.sqrt(math.pow(x1-x2, 2) + math.pow(y1-y2, 2))
print('x1: {}, y1: {}, x2: {}, y2: {}, weight: {}'.format(x1, y1, x2, y2, weight))
打印結果
線段的標示方法是記錄線段的兩個端點, 從(x1, y1)
點到(x2, y2)
點。
數組的個數,取決于你的調參,從十幾個到幾百個不等。
x1: 817, y1: 408, x2: 1068, y2: 164, weight: 350.0528531522061
x1: 525, y1: 93, x2: 868, y2: 468, weight: 508.20665088131227
x1: 630, y1: 202, x2: 818, y2: 408, weight: 278.89065957826557
x1: 488, y1: 56, x2: 814, y2: 412, weight: 482.7131653477042
x1: 816, y1: 407, x2: 960, y2: 267, weight: 200.83824337013107
x1: 714, y1: 520, x2: 714, y2: 520, weight: 0.0
x1: 845, y1: 391, x2: 1113, y2: 131, weight: 373.39523296367884
x1: 698, y1: 275, x2: 817, y2: 404, weight: 175.50498568416796
....
把線段的長度 作為這個線段的權重 Weight。
weight = math.sqrt(math.pow(x1-x2, 2) + math.pow(y1-y2, 2))
每個線段都可以求解出這個線段所在直線的k
還有b
.
y = k*x + b
求解k
,b
的算法比較簡單,需要借助初中所學的知識。
def calculate_line(x1, y1, x2, y2):
'''
計算直線
如果直線水平或者垂直,統一向一個方向傾斜特定角度。
TODO 這里面沒有考慮水平或者垂直的情況
'''
if x1 > x2:
x1,y1,x2,y2 = x2,y2,x1,y1
if x1 == x2 or y1 == y2:
# 有時候會出現單個像素點 x1 = x2 而且 y1 = y2
print('x1:{} y1:{} x2:{} y2:{}'.format(x1, y1, x2, y2))
k = (y1 - y2) / (x1 - x2)
b = (y2 * x1 - y1*x2) / (x1 - x2)
return k,b
3-合并線段
我們需要把這幾百個線段擬合成兩條直線, 這個2 屬于我們的先驗知識。
遍歷所有的線段,求解其k
, b
還有對應的weight
。
然后我們通過字典數據結構來存放合并過后的直線。 數據結構細節如下:
參數 | 備注 |
---|---|
cur_k | 當前合并的直線的K值 |
cur_b | 當期合并的直線的b值 |
k_sum | K值的帶權累加和 |
b_sum | b值的帶權累加和 |
weight_sum | 權重和 |
x1 | 合并后大線段的左側端點的x坐標 |
y1 | 合并后大線段的左側端點的y坐標 |
x2 | 合并后大線段的右側端點的x坐標 |
y2 | 合并后大線段的右側端點的y坐標 |
所有合并后的直線/線段,存放在lines
里面, 每個線段在合并的時候,遍歷lines里面合并后的線段,通過k
的差值(max_k_distance
)判斷是否屬于同一個直線,如果里面都沒有的話就另外添加一個。
lines = []
# 最小權值
min_weight = 20
# 相同k之間最大的差距
max_k_distance = 0.3
for lseg in line_segs:
# 獲取線段端點值
x1,y1,x2,y2 = lseg[0]
if x1 > x2:
x1, y1, x2, y2 = x2, y2, x1, y1
# 計算權重
weight = math.sqrt(math.pow(x1 - x2, 2) + math.pow(y1 - y2, 2))
if weight != 0 and weight > min_weight:
# 計算K與b
k, b = calculate_line(x1, y1, x2, y2)
# print('k: {:.2f}, b: {:.2f}, weight: {:.2f}'.format(k, b, weight))
if len(lines) == 0:
# 初次填充line
line = {}
line['cur_k'] = k
line['cur_b'] = b
line['k_sum'] = k * weight
line['b_sum'] = b * weight
line['weight_sum'] = weight
line['x1'] = x1
line['y1'] = y1
line['x2'] = x2
line['y2'] = y2
lines.append(line)
continue
# 根據k的差異做加權
# 首先獲取lines數組里面k舉例最近的那個
neighbor_line = min(lines, key=lambda line:abs(line['cur_k'] - k))
if abs(neighbor_line['cur_k'] - k) < max_k_distance:
# 小于最大k差值,認為是同一條線
neighbor_line['weight_sum'] += weight
neighbor_line['k_sum'] += k * weight
neighbor_line['b_sum'] += b * weight
neighbor_line['cur_k'] = neighbor_line['k_sum'] / neighbor_line['weight_sum']
neighbor_line['cur_b'] = neighbor_line['b_sum'] / neighbor_line['weight_sum']
if neighbor_line['x1'] > x1:
neighbor_line['x1'] = x1
neighbor_line['y1'] = y1
if neighbor_line['x2'] < x2:
neighbor_line['x2'] = x2
neighbor_line['y2'] = y2
else:
# 添加另外一條線
# 初次填充line
line = {}
line['cur_k'] = k
line['cur_b'] = b
line['k_sum'] = k * weight
line['b_sum'] = b * weight
line['weight_sum'] = weight
line['x1'] = x1
line['y1'] = y1
line['x2'] = x2
line['y2'] = y2
lines.append(line)
做完上述的操作之后,我們獲取的lines
的長度可能不是2, 可能會大于2. 所以我們可以根據weight_sum
對lines
進行重新排序, 然后截取前兩個,作為激光十字的兩條直線。
# 根據權重對lines數組進行排序, 取前兩個(lines的長度有可能大于2)
sorted_lines = sorted(lines, key=lambda line: line['weight_sum'])[::-1]
line1 = sorted_lines[0]
line2 = sorted_lines[1]
[{'b_sum': -3304027.8377846032,
'cur_b': -482.1075439824276,
'cur_k': 1.0900334200603314,
'k_sum': 7470.32650483927,
'weight_sum': 6853.300428555484,
'x1': 478,
'x2': 1001,
'y1': 54,
'y2': 597},
{'b_sum': 8948293.312710544,
'cur_b': 1209.7121822845368,
'cur_k': -0.9799324921216083,
'k_sum': -7248.603010345705,
'weight_sum': 7397.043233715087,
'x1': 599,
'x2': 1113,
'y1': 607,
'y2': 129}]
4-計算交點
def calculate_intersection(line1, line2):
a1 = line1['y2'] - line1['y1']
b1 = line1['x1'] - line1['x2']
c1 = line1['x2'] * line1['y1'] - line1['x1'] * line1['y2']
a2 = line2['y2'] - line2['y1']
b2 = line2['x1'] - line2['x2']
c2 = line2['x2'] * line2['y1'] - line2['x1'] * line2['y2']
if (a1 * b2 - a2 * b1) != 0 and (a2 * b1 - a1 * b2) != 0:
cross_x = int((b1*c2-b2*c1)/(a1*b2-a2*b1))
cross_y = int((c1*a2-c2*a1)/(a1*b2-a2*b1))
return (cross_x, cross_y)
return None
計算交點:
(cx, cy) = calculate_intersection(line1, line2)
print('cx: {} cy: {}'.format(cx, cy))
cx: 816 cy: 405
5-信息可視化
在畫面上繪制四個端點與中心交點:
canvas = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)
# 繪制第一條線
pt_radius = 20
cv2.circle(canvas, (line1['x1'], line1['y1']),pt_radius, (255, 0, 0), thickness=-1)
cv2.circle(canvas, (line1['x2'], line1['y2']),pt_radius, (0, 255, 0), thickness=-1)
cv2.circle(canvas, (line2['x1'], line2['y1']),pt_radius, (0, 255, 255), thickness=-1)
cv2.circle(canvas, (line2['x2'], line2['y2']),pt_radius, (0, 0, 255), thickness=-1)
cv2.circle(canvas, (cx, cy), 40, (255, 0, 255), thickness=20)
plt.imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
plt.show()
推廣
出品:1Z實驗室 (1ZLAB: Make Things Easy)
掃碼關注微信公眾號1Z實驗室, 回復關鍵詞激光十字源碼 獲取源碼。
1Z實驗室 Make Things Easy . 致力于在機器人+計算機視覺+人工智能的重疊區域, 制作小白友好的教程.