OpenCV 做目标检测不是什么新鲜事了。但大部分人装完跑个 demo 就结束了,真正要集成到自己的项目里,才发现官方示例和实际需求差了不少。
说几个实际能用的。
先装环境
```bash
pip install opencv-python opencv-contrib-python numpy
```
opencv-python 是基础库,opencv-contrib-python 带额外模块(包括我们要用的跟踪器)。如果只需要基础功能,只装第一个就够了。
基于颜色的目标检测
最简单的方法,不需要训练模型,适合颜色特征明显的场景。
```python
import cv2
import numpy as np
def detect_by_color(frame, lower_color, upper_color):
"""按颜色范围检测目标,返回轮廓列表"""
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, lower_color, upper_color)
# 去噪
mask = cv2.erode(mask, None, iterations=2)
mask = cv2.dilate(mask, None, iterations=2)
# 找轮廓
contours, _ = cv2.findContours(
mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
return contours
def draw_detection(frame, contours, min_area=500):
"""在图上画出检测到的目标"""
for cnt in contours:
area = cv2.contourArea(cnt)
if area < min_area:
continue
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.putText(
frame, f"obj {area:.0f}",
(x, y - 5), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (0, 255, 0), 1,
)
return frame
if __name__ == "__main__":
# 蓝色范围 (HSV)
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([130, 255, 255])
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
contours = detect_by_color(frame, lower_blue, upper_blue)
frame = draw_detection(frame, contours)
cv2.imshow("Color Detection", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
```
HSV 颜色范围需要根据实际场景调。蓝色大概在 100-130 度,绿色 40-80,红色有两个区间 0-10 和 160-180。
颜色检测的局限很明显:光照一变,颜色就偏了。室内用还行,室外基本靠不住。
用预训练模型做检测
OpenCV 的 DNN 模块可以直接加载训练好的模型,不需要自己训练。
```python
import cv2
import numpy as np
class ObjectDetector:
def __init__(self, model_path: str, config_path: str,
labels_path: str, confidence_threshold: float = 0.5):
self.net = cv2.dnn.readNet(model_path, config_path)
self.confidence_threshold = confidence_threshold
with open(labels_path) as f:
self.labels = [line.strip() for line in f.readlines()]
def detect(self, frame):
"""对一帧图像做目标检测,返回 (label, confidence, box) 列表"""
h, w = frame.shape[:2]
# 转成模型输入格式
blob = cv2.dnn.blobFromImage(
frame, 1 / 255.0, (416, 416),
swapRB=True, crop=False
)
self.net.setInput(blob)
# 前向传播
layer_names = self.net.getUnconnectedOutLayersNames()
outputs = self.net.forward(layer_names)
results = []
for output in outputs:
for detection in output:
scores = detection[5:]
class_id = np.argmax(scores)
confidence = scores[class_id]
if confidence < self.confidence_threshold:
continue
center_x = int(detection[0] * w)
center_y = int(detection[1] * h)
box_w = int(detection[2] * w)
box_h = int(detection[3] * h)
x = int(center_x - box_w / 2)
y = int(center_y - box_h / 2)
results.append((
self.labels[class_id],
float(confidence),
(x, y, box_w, box_h),
))
return results
def draw_results(frame, results):
"""在图像上绘制检测结果"""
for label, confidence, (x, y, w, h) in results:
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
text = f"{label} {confidence:.2f}"
cv2.putText(
frame, text, (x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1,
)
return frame
if __name__ == "__main__":
detector = ObjectDetector(
model_path="yolov3.weights",
config_path="yolov3.cfg",
labels_path="coco.names",
)
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
results = detector.detect(frame)
frame = draw_results(frame, results)
cv2.imshow("Detection", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
```
YOLOv3 的权重文件和配置文件需要单独下载,大约 240MB。下载地址:
```
wget https://pjreddie.com/media/files/yolov3.weights
wget https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg
wget https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names
```
放到项目目录下就能跑。模型能识别 80 类常见物体——人、车、手机、猫狗这些。
跟踪移动目标
如果摄像头固定不动,检测移动的目标用背景减法更高效。
```python
import cv2
class MotionDetector:
def __init__(self, min_area: int = 2000):
self.bg_subtractor = cv2.createBackgroundSubtractorMOG2()
self.min_area = min_area
def detect(self, frame):
"""检测运动区域,返回轮廓列表"""
mask = self.bg_subtractor.apply(frame)
# 去噪
mask = cv2.erode(mask, None, iterations=2)
mask = cv2.dilate(mask, None, iterations=2)
contours, _ = cv2.findContours(
mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
return [c for c in contours if cv2.contourArea(c) > self.min_area]
if __name__ == "__main__":
detector = MotionDetector(min_area=3000)
cap = cv2.VideoCapture(0)
print("摄像头已开启,按 q 退出")
while True:
ret, frame = cap.read()
if not ret:
break
motions = detector.detect(frame)
for cnt in motions:
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.putText(
frame, "MOTION",
(x, y - 5), cv2.FONT_HERSHEY_SIMPLEX,
0.6, (0, 0, 255), 2,
)
cv2.putText(
frame, f"Moving objects: {len(motions)}",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX,
0.7, (255, 255, 255), 2,
)
cv2.imshow("Motion Detection", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
```
背减减法对光照突变很敏感——开灯关灯会触发大量误报。适合室内光线稳定的监控场景。
目标跟踪
检测到目标之后,如果要对单个目标持续跟踪,用跟踪器比每帧都检测更省资源。
```python
import cv2
def main():
cap = cv2.VideoCapture(0)
tracker = cv2.TrackerCSRT_create()
tracking = False
print("按 s 框选目标开始跟踪,按 q 退出")
while True:
ret, frame = cap.read()
if not ret:
break
if tracking:
success, box = tracker.update(frame)
if success:
x, y, w, h = [int(v) for v in box]
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.putText(
frame, "TRACKING",
(x, y - 5), cv2.FONT_HERSHEY_SIMPLEX,
0.6, (0, 255, 0), 2,
)
else:
cv2.putText(
frame, "LOST",
(50, 50), cv2.FONT_HERSHEY_SIMPLEX,
1, (0, 0, 255), 2,
)
else:
cv2.putText(
frame, "Press 's' to select target",
(50, 50), cv2.FONT_HERSHEY_SIMPLEX,
0.7, (255, 255, 255), 2,
)
cv2.imshow("Tracker", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("s") and not tracking:
# 选择目标区域
box = cv2.selectROI("Tracker", frame, False)
if box[2] > 0 and box[3] > 0:
tracker.init(frame, box)
tracking = True
cv2.destroyWindow("Tracker")
elif key == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
```
OpenCV 自带的 TrackerCSRT 准确率高但慢。如果对速度有要求,换 TrackerKCF,速度快但容易跟丢。按 s 键框选目标,之后由跟踪器自动跟随。
图片批量处理
有时候不是实时检测,而是批量处理文件夹里的图片。
```python
import cv2
from pathlib import Path
def batch_detect(input_dir: str, output_dir: str, detector):
"""批量检测文件夹中的图片"""
Path(output_dir).mkdir(exist_ok=True)
image_exts = {".jpg", ".jpeg", ".png", ".bmp"}
for filepath in Path(input_dir).iterdir():
if filepath.suffix.lower() not in image_exts:
continue
img = cv2.imread(str(filepath))
if img is None:
print(f"无法读取: {filepath.name}")
continue
results = detector.detect(img)
img = draw_results(img, results)
output_path = Path(output_dir) / filepath.name
cv2.imwrite(str(output_path), img)
print(f"{filepath.name}: 检测到 {len(results)} 个目标")
if __name__ == "__main__":
detector = ObjectDetector("yolov3.weights", "yolov3.cfg", "coco.names")
batch_detect("./input_images", "./output_images", detector)
```
几个踩过的坑
1. 摄像头打不开。cap = cv2.VideoCapture(0) 里的 0 是摄像头编号。笔记本内置摄像头一般用 0,外接 USB 摄像头可能用 1 或 2。有时候被其他程序占用了也会打不开,先关掉微信/QQ 的视频功能再试。
2. DNN 模型加载慢。YOLOv3 的权重文件 240MB,每次启动加载要几秒。频繁重启的话考虑把 detector 对象做成全局的,只加载一次。
3. 实时检测很吃 CPU。笔记本上 YOLOv3 跑实时视频流大概 5-10 帧,换 YOLOv3-tiny(更小的模型)能到 20 帧以上,准确率下降一些但够用。
4. 颜色检测受光照影响大。室内可以用,室外基本不行。如果场景固定,可以在不同时段多采样几个颜色范围取平均。
5. 中文路径问题。OpenCV 的 imread 不支持中文路径。路径里有中文,读出来是 None。要么全用英文路径,要么用 numpy 绕一下:
```python
import numpy as np
def imread_chinese(path):
"""支持中文路径的 cv2.imread"""
return cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)
```
更多推荐




所有评论(0)