使用 YOLO11 和霍夫變換追蹤站臺黃線穿越者
大家都知道,在地鐵站等車時,廣播里總是提醒我們:“別越過黃線哦!”但是,總有那么一些人,因?yàn)榉中幕蛘卟蛔⒁?,就站在了黃線邊上,甚至跨了過去。這可是很危險的!今天,就讓我?guī)Т蠹铱纯?,怎么用人工智能來做個智能監(jiān)控系統(tǒng),一旦有人跨過黃線,系統(tǒng)就能立刻發(fā)出警告,保護(hù)大家的安全。
這篇文章,咱們分三步走:
- 第一步:聊聊怎么用YOLO11來檢測和跟蹤站臺上的行人。
- 第二步:看看怎么用Hough變換和OpenCV技術(shù)找出站臺邊的黃線,并找到它的方程。
- 第三步:把上面兩個技術(shù)結(jié)合起來,做個AI系統(tǒng),專門盯著那些等車時越過黃線的人。系統(tǒng)會一幀一幀地檢查,一旦發(fā)現(xiàn)有人越線,就會發(fā)出警告。在實(shí)際應(yīng)用中,系統(tǒng)會在遵守黃線的人周圍畫個綠框,一旦有人越線,框就變紅。如果真的用在車站,這個系統(tǒng)還能發(fā)出聲音警報,提醒大家注意安全。
1. 使用YOLO11檢測和跟蹤行人
目標(biāo)檢測模型,就是幫我們找出圖像或視頻里的對象在哪兒,是啥。結(jié)果就是一堆框框,把檢測到的對象框起來,還標(biāo)上類別和置信度。這個技術(shù)特別適合用來找那些靠近軌道的人,不用知道他們具體長啥樣。YOLO11有五個預(yù)訓(xùn)練模型,專門干這個的。下面的腳本,咱們就用最小的那個yolo11n.pt,來識別圖像里的人,給他們畫框。用ultralytics庫的plot()函數(shù),直接在圖像上畫框,特別方便。具體代碼如下所示:
from ultralytics import YOLO
import argparse
import cv2
if __name__ == '__main__':
''' Apply bbox to detected persons '''
parser = argparse.ArgumentParser()
parser.add_argument('--image_path', type=str, default="resources/images/frame_yellow_line_0.png")
opt = parser.parse_args()
image_path = opt.image_path
# Load a pretrained YOLO11n model
model = YOLO("yolo11n.pt")
image = cv2.imread(image_path)
results = model.predict(image)
# check that person has index name=0
print("results[0].names: ", results[0].names)
# iter over results. If there is only one frame then results has only one component
for image_pred in results:
class_names = image_pred.names
boxes = image_pred.boxes
# iter over the detected boxes and select thos of the person if exists
for box in boxes:
if class_names[int(box.cls)] == "person":
print("person")
print("person bbox: ", box.xyxy)
image_pred.plot()
image_pred.show()
上面的程序輸入圖片后得到如下的結(jié)果:
YOLO11給檢測到的人畫的框。用ultralytics庫的plot()函數(shù)畫的。
用同樣的模型,我還能提取每個人的框的坐標(biāo),用OpenCV畫線。下面的腳本,效果和之前的差不多。畫框的方法在后面的部分會用到。具體代碼如下所示:
from ultralytics import YOLO
import cv2
font = cv2.FONT_HERSHEY_DUPLEX
# Load a pretrained YOLO11n model
model = YOLO("yolo11n.pt")
path_image = "resources/images/frame_yellow_line_900.png"
image = cv2.imread(path_image)
annotated_frame = image.copy()
# set in the predict function the interested classes to detect. Here I want to detect persons, whose index is 0
results = model.predict(image, classes=[0], conf=0.54)
image_pred = results[0]
boxes = image_pred.boxes
# iter over all the detected boxes of persons
for box in boxes:
x1 = int(box.xyxy[0][0])
y1 = int(box.xyxy[0][1])
x2 = int(box.xyxy[0][2])
y2 = int(box.xyxy[0][3])
coords = (x1, y1 - 10)
text = "person"
print("x1: {} - y1: {} - x2: {} - y2: {}".format(x1, y1, x2, y2))
color = (0, 255, 0) # colors in BGR
thickness = 3
annotated_frame = cv2.rectangle(image, (x1, y1), (x2, y2), color, thickness)
annotated_frame = cv2.putText(annotated_frame, text, coords, font, 0.7, color, 2)
annotated_frame_path = "/home/enrico/Projects/VideoSurveillance/resources/images/annotated_frame_900.png"
cv2.imwrite(annotated_frame_path, annotated_frame)
用坐標(biāo)和OpenCV畫的YOLO11檢測到的人的框
姿態(tài)檢測
姿態(tài)檢測模型在某些情況下特別有用,比如我們需要知道人身體的某個部位在哪兒。YOLO11就有一套預(yù)訓(xùn)練模型,專門干這個的。這些模型會輸出一系列關(guān)鍵點(diǎn),代表圖像里人的關(guān)鍵部位。每個人身上,YOLO11能找到17個關(guān)鍵點(diǎn)。下面的腳本,我展示了怎么在圖像里提取這些關(guān)鍵點(diǎn)。YOLO11有五個預(yù)訓(xùn)練的姿態(tài)估計(jì)模型。這次,因?yàn)橛行┤丝赡茈x相機(jī)比較遠(yuǎn),我用了更強(qiáng)的模型yolo11m-pose.pt。用這些關(guān)鍵點(diǎn),我們還能畫個框,把人框起來。這個框是通過取x和y坐標(biāo)的最小值和最大值,連起來形成一個封閉人的矩形。具體代碼如下所示:
from ultralytics import YOLO
import cv2
font = cv2.FONT_HERSHEY_DUPLEX
# Load a pretrained YOLO11n-pose Pose model
model = YOLO("yolo11m-pose.pt")
# Run inference on an image
path_image = "resources/images/frame_yellow_line_900.png"
image = cv2.imread(path_image)
cv2.imwrite(annotated_frame_bbox_path, annotated_frame_bbox)
annotated_frame_keypoints = image.copy()
annotated_frame_bbox = image.copy()
results = model(image) # results list
# extract keypoints
keypoints = results[0].keypoints
conf = keypoints.conf
xy = keypoints.xy
print(xy.shape) # (N, K, 2) where N is the number of person detected
print("Detected person: ", xy.shape[0])
# iter over persons
for idx_person in range(xy.shape[0]):
print("idx_person: ", idx_person)
#iter over keypoints of a fixed person
list_x = []
list_y = []
for i, th in enumerate(xy[idx_person]):
x = int(th[0])
y = int(th[1])
if x !=0.0 and y!=0.0:
list_x.append(x)
list_y.append(y)
print("x: {} - y: {}".format(x, y))
annotated_frame_keypoints = cv2.circle(annotated_frame_keypoints, (x,y), radius=3, color=(0, 0, 255), thickness=-1)
annotated_frame_keypoints = cv2.putText(annotated_frame_keypoints, str(i), (x, y-5), font, 0.7, (0, 0, 255), 2)
if len(list_x) > 0 and len(list_y) > 0:
min_x = min(list_x)
max_x = max(list_x)
min_y = min(list_y)
max_y = max(list_y)
print("min_x: {} - max_x: {} - min_y: {} - max_y: {}".format(min_x, max_x, min_y, max_y))
w = max_x - min_x
h = max_y - min_y
dx = int(w/3)
x0 = min_x - dx
x1 = max_x + dx
y0 = min_y - dx
y1 = max_y + dx
print("x0: {} - x1: {} - y0: {} - y1: {}".format(x0, x1, y0, y1))
coords = (x0, y0 - 10)
text = "person"
color = (0, 255, 0) # colors in BGR
thickness = 3
annotated_frame_bbox = cv2.rectangle(annotated_frame_bbox, (x0, y0), (x1, y1), color, thickness)
annotated_frame_bbox = cv2.putText(annotated_frame_bbox, text, coords, font, 0.7, color, 2)
annotated_frame_path = "/home/enrico/Projects/VideoSurveillance/resources/images/annotated_frame_keypoints_900.png"
cv2.imwrite(annotated_frame_path, annotated_frame_keypoints)
annotated_frame_bbox_path = "/home/enrico/Projects/VideoSurveillance/resources/images/annotated_frame_keypoints_bbox_900.png"
下面圖片顯示了程序應(yīng)用于同一圖像的結(jié)果。每個人的關(guān)鍵點(diǎn)從 0 到 16。如果某些關(guān)鍵點(diǎn)未被檢測到,系統(tǒng)不會產(chǎn)生錯誤,只會將其從輸出圖像中刪除。在邊界框方面,我們可以看到與物體檢測模型相比存在微小差異,這主要是由于關(guān)鍵點(diǎn)位于人物內(nèi)部。
左邊是每個人檢測到的關(guān)鍵點(diǎn)。右邊是根據(jù)關(guān)鍵點(diǎn)坐標(biāo)確定的每個人的框
2. 黃線檢測
Hough變換是個特征提取技術(shù),能幫我們在圖像里檢測線條。這篇文章里,我們會學(xué)習(xí)怎么用Hough變換技術(shù),在圖像里檢測線條和圓形。
為了找出站臺邊的黃線并確定它的線性方程,我們需要這么做:
首先,得把黃線從圖像里分離出來。如果把圖像從BGR顏色空間換成HSV顏色空間,這個任務(wù)會簡單很多。轉(zhuǎn)換用的代碼如下,結(jié)果看【圖4-step1】。
# 把圖像轉(zhuǎn)換成hsv顏色空間
frame_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
第二步,得定義一個包含黃線顏色的黃色范圍。我測試了幾次,找到了合適的值。用這個范圍,我能分離出落在指定黃色譜系內(nèi)的像素,得到一個黃線的掩碼,看【圖4-setp2】。
# 設(shè)置HSV空間中的黃色范圍
yellow_light = np.array([20, 140, 200], np.uint8)
yellow_dark = np.array([35, 255, 255], np.uint8)
# 隔離黃線
mask_yellow = cv2.inRange(frame_hsv, yellow_light, yellow_dark)
kernel = np.ones((4, 4), "uint8")
# 形態(tài)學(xué)閉操作,填充白色區(qū)域的黑色斑點(diǎn)
mask_yellow = cv2.morphologyEx(mask_yellow, cv2.MORPH_CLOSE, kernel)
# 形態(tài)學(xué)開操作,填充黑色區(qū)域的白色斑點(diǎn)
mask_yellow = cv2.morphologyEx(mask_yellow, cv2.MORPH_OPEN, kernel)
第三步,用Canny邊緣檢測算法處理第二步得到的掩碼。這樣就能得出帶有邊緣的圖像??础緢D4-setp3】。
# 找出隔離黃線的邊緣
edges_yellow = cv2.Canny(mask_yellow, 50, 150)
步驟1是HSV顏色空間的圖像。步驟2是檢測到的黃線掩碼。步驟3是用Canny算法得到的邊緣。
接下來準(zhǔn)備好用Probabilistic Hough Transform函數(shù)來提取圖像中所有可能的線條段。這個函數(shù)的語法是這樣的:
lines = cv2.HoughLinesP(image, rho, theta, threshold, minLineLength=None, maxLineGap=None)
參數(shù)包括:
- image:輸入的二值灰度圖像。在我們的例子里,就是Canny算法提取的邊緣圖像。
- rho:累加器在距離維度上的分辨率(像素)。這決定了線到原點(diǎn)的距離的精度。值越小,精度越高。
- theta:累加器在角度維度上的分辨率(弧度)。它定義了線角度的量化精度。
- threshold:認(rèn)為一條線有效所需的最小投票數(shù)(Hough累加器中的交點(diǎn))。值越高,檢測越嚴(yán)格。
- minLineLength:線段的最小長度。比這短的線段會被丟棄。
- maxLineGap:將兩個線段連接成一條線的像素最大間隙。這決定了如何處理同一條線上不連續(xù)的部分。
我在下面的腳本中應(yīng)用了HoughLinesP函數(shù)和斜率與y截距的公式。調(diào)整不同的閾值、minLineLength和maxLineGap值,我找到了能得出黃線單一直線的值。
import cv2
import numpy as np
def find_slope(x1, y1, x2, y2):
if x2 != x1:
return ((y2 - y1) / (x2 - x1))
else:
return np.inf
def find_m_and_q(edges):
lines = cv2.HoughLinesP(
image=edges, # Input edge image
rho=1, # Distance resolution in pixels
theta=np.pi/180, # Angle resolution in radians
threshold=120, # Min number of votes for valid line
minLineLength=80, # Min allowed length of line
maxLineGap=10 # Max allowed gap between line for joining them
)
coefficient_list = []
# Iterate over points
if lines is None:
# if line is None return an empty list of coefficients
return coefficient_list
else:
for points in lines:
x_vertical = None
# Extracted points nested in the list
x1,y1,x2,y2=points[0]
slope = find_slope(x1, y1, x2, y2)
if slope == np.inf:
# if the slope is infinity the intercept is None and set the x vertical
intercept = None
x_vertical = x1
else:
intercept = y1-(x1*(y2-y1)/(x2-x1))
coefficient_list.append((slope, intercept, x_vertical))
print("coefficient_list: ", coefficient_list)
return coefficient_list
def draw_lines(image, list_coefficient):
image_line = image.copy()
h, w = image_line.shape[:2]
for coeff in list_coefficient:
m, q, x_v = coeff
y0 = 0
y1 = h
if m != np.inf:
x0 = -q/m
x1 = (h-q)/m
else:
x0 = x1 = x_v
cv2.line(image_line, (int(x0), int(y0)), (int(x1), int(y1)), (0, 255, 0), 6)
return image_line
綠色線條是Hough變換確定的黃線對應(yīng)的直線
3. 越過黃線檢測
現(xiàn)在,把檢測人和黃線的技術(shù)結(jié)合起來。我找了個在車站錄制的視頻,里面有人靠近軌道,還越過了黃線。因?yàn)橄鄼C(jī)是固定的,黃線在視頻的每一幀里位置都一樣。所以,確定黃線的方程,我只需要看第一幀。確定了黃線對應(yīng)的直線方程后,對于每一幀,我用預(yù)訓(xùn)練的YOLO11模型檢測所有可能的人。接下來,就是要判斷一個人是否越過了黃線。最簡單的辦法是看一個人的框是否和線相交。但是,因?yàn)橄鄼C(jī)和人的位置,可能會出現(xiàn)透視問題:框可能和線相交,但人實(shí)際上并沒有越過線。實(shí)際上,人是用腳越過黃線的,所以得關(guān)注離腳最近的點(diǎn)。下面,我聊聊兩種可能的方法,并分析它們的結(jié)果。在兩種情況下,如果一個人保持在黃線后面,他們的人框會顯示為綠色,一旦系統(tǒng)檢測到未經(jīng)授權(quán)的越過,框就變紅。
兩個腳本都在Google Colab上運(yùn)行,用它提供的免費(fèi)GPU。為了避免內(nèi)存不夠用,我減小了視頻的原始大小。
??【第一種方法】:用框的右下角
一個簡單的辦法是看框的右下角。如果這個點(diǎn)在黃線外,那這個人就跨線了。下面的腳本,就是我解決這個問題的第一次嘗試。
把腳本用在原始視頻上,我跟蹤了靠近站臺的人,并根據(jù)他們是否越過(紅色框)或沒有越過(綠色框)黃線來改變他們的框顏色。
??【第二種方法】:用腳部關(guān)鍵點(diǎn)
之前的解決方案不太準(zhǔn)確。比如,如果一個人伸出手臂,框的右下角可能會更接近黃線——甚至越過它——但實(shí)際上這個人并沒有越過線。一個更精確的解決方案是使用腳部關(guān)鍵點(diǎn)。當(dāng)一個或兩個腳部關(guān)鍵點(diǎn)在黃線外時,就可以確定這個人確實(shí)越過了線。下面的腳本,就是我解決這個問題的更準(zhǔn)確嘗試。一旦檢測到一個人的
結(jié)束語
通過今天的分享,我們不僅揭開了智能視頻監(jiān)控系統(tǒng)的神秘面紗,還深入了解了如何利用前沿的AI技術(shù)來守護(hù)我們的出行安全。從YOLO11的精準(zhǔn)檢測到Hough變換的巧妙應(yīng)用,每一步都是科技與智慧的結(jié)晶。讓我們一起期待,這些技術(shù)在未來能夠更廣泛地應(yīng)用,為我們的日常生活帶來更多的便利和安全保障。感謝您的閱讀,如果您對AI技術(shù)在安全領(lǐng)域的應(yīng)用有更多的想法或建議,歡迎在評論區(qū)留言分享。讓我們攜手科技,共創(chuàng)更加智能、安全的未來!
本文完整的程序可以在https://gist.github.com/enrico310786/deef59964251117652d25f3b1a6ee5de#file-crossing_yellow_line_bbox-py獲取。