second
This commit is contained in:
parent
336ed3dadc
commit
2a26c0b973
0
__init__.py
Normal file
0
__init__.py
Normal file
BIN
__pycache__/__init__.cpython-39.pyc
Normal file
BIN
__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
@ -17,6 +17,11 @@
|
|||||||
│
|
│
|
||||||
├── utils/
|
├── utils/
|
||||||
│ └── 006_utils.py # 工具函数
|
│ └── 006_utils.py # 工具函数
|
||||||
|
├── tests/ # 测试各个程序
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── 008_test_person_detector.py
|
||||||
|
│ ├── 009_test_distance_estimator.py
|
||||||
|
│ └── 010_test_api_server.py
|
||||||
│
|
│
|
||||||
├── 007_main.py # 主程序
|
├── 007_main.py # 主程序
|
||||||
└── requirements.txt # 项目依赖
|
└── requirements.txt # 项目依赖
|
||||||
47
main.py
Normal file
47
main.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import yaml
|
||||||
|
import uvicorn
|
||||||
|
from src. import RTSPCamera
|
||||||
|
from src.person_detector import PersonDetector
|
||||||
|
from src.distance_estimator import DistanceEstimator
|
||||||
|
from src.api_server import DistanceAPI, app
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
with open('config/config.yaml', 'r') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = load_config()
|
||||||
|
|
||||||
|
# 初始化摄像头
|
||||||
|
camera = RTSPCamera(
|
||||||
|
config['camera']['rtsp_url'],
|
||||||
|
config['camera']['fps']
|
||||||
|
)
|
||||||
|
camera.start()
|
||||||
|
|
||||||
|
# 初始化检测器
|
||||||
|
detector = PersonDetector(
|
||||||
|
config['model']['person_detection']['model_name'],
|
||||||
|
config['model']['person_detection']['confidence_threshold']
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化距离估算器
|
||||||
|
estimator = DistanceEstimator(
|
||||||
|
config['model']['distance_estimation']['focal_length'],
|
||||||
|
config['model']['distance_estimation']['sensor_height'],
|
||||||
|
config['model']['distance_estimation']['average_person_height']
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化API
|
||||||
|
distance_api = DistanceAPI(camera, detector, estimator)
|
||||||
|
app.distance_api = distance_api
|
||||||
|
|
||||||
|
# 启动API服务器
|
||||||
|
uvicorn.run(
|
||||||
|
app,
|
||||||
|
host=config['api']['host'],
|
||||||
|
port=config['api']['port']
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
output/detection_20250113_110502_763716.jpg
Normal file
BIN
output/detection_20250113_110502_763716.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
@ -1,23 +0,0 @@
|
|||||||
from ultralytics import YOLO
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
class PersonDetector:
|
|
||||||
def __init__(self, model_path, conf_threshold=0.5):
|
|
||||||
self.model = YOLO(model_path)
|
|
||||||
self.conf_threshold = conf_threshold
|
|
||||||
|
|
||||||
def detect(self, frame):
|
|
||||||
results = self.model(frame, classes=0) # 只检测人类
|
|
||||||
persons = []
|
|
||||||
|
|
||||||
for result in results:
|
|
||||||
boxes = result.boxes
|
|
||||||
for box in boxes:
|
|
||||||
if box.conf > self.conf_threshold:
|
|
||||||
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
|
||||||
persons.append({
|
|
||||||
'bbox': (int(x1), int(y1), int(x2), int(y2)),
|
|
||||||
'confidence': float(box.conf)
|
|
||||||
})
|
|
||||||
|
|
||||||
return persons
|
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
BIN
src/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
src/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/distance_estimator.cpython-39.pyc
Normal file
BIN
src/__pycache__/distance_estimator.cpython-39.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/person_detector.cpython-39.pyc
Normal file
BIN
src/__pycache__/person_detector.cpython-39.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/ttt.cpython-39.pyc
Normal file
BIN
src/__pycache__/ttt.cpython-39.pyc
Normal file
Binary file not shown.
51
src/person_detector.py
Normal file
51
src/person_detector.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from ultralytics import YOLO
|
||||||
|
import numpy as np
|
||||||
|
from ..utils.utils import VisualizationUtils
|
||||||
|
|
||||||
|
class PersonDetector:
|
||||||
|
def __init__(self, model_path, conf_threshold=0.5):
|
||||||
|
self.model = YOLO(model_path)
|
||||||
|
self.conf_threshold = conf_threshold
|
||||||
|
self.vis_utils = VisualizationUtils()
|
||||||
|
|
||||||
|
def detect(self, frame, save_visualization=False, save_path="output"):
|
||||||
|
"""
|
||||||
|
检测图像中的人物并可选择保存可视化结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: 输入图像
|
||||||
|
save_visualization: 是否保存可视化结果
|
||||||
|
save_path: 可视化结果保存路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
persons: 检测到的人物列表
|
||||||
|
vis_path: 如果save_visualization为True,返回保存的文件路径,否则为None
|
||||||
|
"""
|
||||||
|
results = self.model(frame, classes=0) # 只检测人类
|
||||||
|
persons = []
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
boxes = result.boxes
|
||||||
|
for box in boxes:
|
||||||
|
if box.conf > self.conf_threshold:
|
||||||
|
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
||||||
|
persons.append({
|
||||||
|
'bbox': (int(x1), int(y1), int(x2), int(y2)),
|
||||||
|
'confidence': float(box.conf)
|
||||||
|
})
|
||||||
|
|
||||||
|
vis_path = None
|
||||||
|
if save_visualization and persons:
|
||||||
|
# 绘制检测结果
|
||||||
|
vis_image = self.vis_utils.draw_detection_results(
|
||||||
|
frame,
|
||||||
|
persons,
|
||||||
|
show_confidence=True
|
||||||
|
)
|
||||||
|
# 保存结果
|
||||||
|
vis_path = self.vis_utils.save_detection_image(
|
||||||
|
vis_image,
|
||||||
|
save_path
|
||||||
|
)
|
||||||
|
|
||||||
|
return persons, vis_path
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
Binary file not shown.
BIN
tests/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
tests/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
40
tests/test_api_server.py
Normal file
40
tests/test_api_server.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
class Distance(BaseModel):
|
||||||
|
distance_mm: float
|
||||||
|
confidence: float
|
||||||
|
detection_image_path: Optional[str] = None
|
||||||
|
|
||||||
|
class DistanceAPI:
|
||||||
|
def __init__(self, camera, detector, estimator):
|
||||||
|
self.camera = camera
|
||||||
|
self.detector = detector
|
||||||
|
self.estimator = estimator
|
||||||
|
|
||||||
|
async def get_distances(self) -> List[Distance]:
|
||||||
|
frame = self.camera.get_frame()
|
||||||
|
if frame is None:
|
||||||
|
raise HTTPException(status_code=500, detail="No frame available")
|
||||||
|
|
||||||
|
# 检测人物并保存可视化结果
|
||||||
|
persons, vis_path = self.detector.detect(
|
||||||
|
frame,
|
||||||
|
save_visualization=True,
|
||||||
|
save_path="output/detections"
|
||||||
|
)
|
||||||
|
|
||||||
|
distances = []
|
||||||
|
for person in persons:
|
||||||
|
bbox = person['bbox']
|
||||||
|
person_height = bbox[3] - bbox[1] # y2 - y1
|
||||||
|
distance = self.estimator.estimate_distance(person_height, frame.shape[0])
|
||||||
|
|
||||||
|
distances.append(Distance(
|
||||||
|
distance_mm=distance,
|
||||||
|
confidence=person['confidence'],
|
||||||
|
detection_image_path=vis_path
|
||||||
|
))
|
||||||
|
|
||||||
|
return distances
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
tests/test_data/person.jpg
Normal file
BIN
tests/test_data/person.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
54
tests/test_distance_estimator.py
Normal file
54
tests/test_distance_estimator.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import pytest
|
||||||
|
import numpy as np
|
||||||
|
from ..src.distance_estimator import DistanceEstimator
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def estimator():
|
||||||
|
return DistanceEstimator(
|
||||||
|
focal_length=1000, # mm
|
||||||
|
sensor_height=5.6, # mm
|
||||||
|
avg_person_height=1700 # mm
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_distance_estimator_initialization(estimator):
|
||||||
|
assert estimator.focal_length == 1000
|
||||||
|
assert estimator.sensor_height == 5.6
|
||||||
|
assert estimator.avg_person_height == 1700
|
||||||
|
|
||||||
|
def test_distance_estimation(estimator):
|
||||||
|
# 测试不同像素高度的情况
|
||||||
|
test_cases = [
|
||||||
|
# (person_height_pixels, frame_height, expected_distance_range)
|
||||||
|
(400, 720, (2000, 5000)), # 近距离
|
||||||
|
(200, 720, (4000, 10000)), # 中等距离
|
||||||
|
(100, 720, (8000, 20000)) # 远距离
|
||||||
|
]
|
||||||
|
|
||||||
|
for height_px, frame_height, (min_dist, max_dist) in test_cases:
|
||||||
|
distance = estimator.estimate_distance(height_px, frame_height)
|
||||||
|
assert min_dist <= distance <= max_dist, \
|
||||||
|
f"Distance {distance}mm not in expected range [{min_dist}, {max_dist}]"
|
||||||
|
|
||||||
|
def test_distance_estimation_edge_cases(estimator):
|
||||||
|
# 测试边界情况
|
||||||
|
with pytest.raises(ZeroDivisionError):
|
||||||
|
estimator.estimate_distance(0, 720)
|
||||||
|
|
||||||
|
# 测试极小值
|
||||||
|
distance = estimator.estimate_distance(1, 720)
|
||||||
|
assert distance > 0
|
||||||
|
|
||||||
|
# 测试当人物高度等于图像高度的情况
|
||||||
|
distance = estimator.estimate_distance(720, 720)
|
||||||
|
assert distance > 0
|
||||||
|
|
||||||
|
def test_distance_estimation_linearity(estimator):
|
||||||
|
# 测试距离估算的线性关系
|
||||||
|
height1, height2 = 200, 100
|
||||||
|
frame_height = 720
|
||||||
|
|
||||||
|
distance1 = estimator.estimate_distance(height1, frame_height)
|
||||||
|
distance2 = estimator.estimate_distance(height2, frame_height)
|
||||||
|
|
||||||
|
# 当像素高度减半时,距离应该翻倍
|
||||||
|
assert abs(distance2 / distance1 - 2.0) < 0.1
|
||||||
51
tests/test_person_detector.py
Normal file
51
tests/test_person_detector.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import pytest
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
# import os
|
||||||
|
# import sys
|
||||||
|
|
||||||
|
from ..src.person_detector import PersonDetector
|
||||||
|
# from src.person_detector import PersonDetector
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def detector():
|
||||||
|
# 使用YOLOv8n模型进行测试
|
||||||
|
return PersonDetector("yolov8n.pt", conf_threshold=0.5)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_image():
|
||||||
|
# 创建一个测试用的图像,包含一个人的图像
|
||||||
|
img_path = "tests/test_data/person.jpg" # 请准备一张包含人物的测试图片
|
||||||
|
return cv2.imread(img_path)
|
||||||
|
|
||||||
|
def test_person_detector_initialization(detector):
|
||||||
|
assert detector.model is not None
|
||||||
|
assert detector.conf_threshold == 0.5
|
||||||
|
|
||||||
|
def test_person_detection(detector, sample_image):
|
||||||
|
persons = detector.detect(sample_image, True)
|
||||||
|
|
||||||
|
# 验证检测结果
|
||||||
|
assert isinstance(persons, list)
|
||||||
|
if len(persons) > 0:
|
||||||
|
person = persons[0]
|
||||||
|
assert 'bbox' in person
|
||||||
|
assert 'confidence' in person
|
||||||
|
|
||||||
|
# 验证边界框格式
|
||||||
|
bbox = person['bbox']
|
||||||
|
assert len(bbox) == 4
|
||||||
|
assert all(isinstance(x, int) for x in bbox)
|
||||||
|
assert bbox[0] < bbox[2] # x1 < x2
|
||||||
|
assert bbox[1] < bbox[3] # y1 < y2
|
||||||
|
|
||||||
|
# 验证置信度
|
||||||
|
print("人物置信度", person['confidence'])
|
||||||
|
assert 0 <= person['confidence'] <= 1
|
||||||
|
|
||||||
|
def test_person_detector_empty_image(detector):
|
||||||
|
# 测试空图像
|
||||||
|
empty_image = np.zeros((100, 100, 3), dtype=np.uint8)
|
||||||
|
persons = detector.detect(empty_image)
|
||||||
|
assert isinstance(persons, list)
|
||||||
|
assert len(persons) == 0
|
||||||
BIN
utils/__pycache__/utils.cpython-39.pyc
Normal file
BIN
utils/__pycache__/utils.cpython-39.pyc
Normal file
Binary file not shown.
117
utils/utils.py
Normal file
117
utils/utils.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
class VisualizationUtils:
|
||||||
|
@staticmethod
|
||||||
|
def draw_detection_results(
|
||||||
|
image: np.ndarray,
|
||||||
|
persons: list,
|
||||||
|
distances: list = None,
|
||||||
|
save_path: str = "output",
|
||||||
|
show_confidence: bool = True
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
在图像上绘制检测结果和距离信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: 原始图像
|
||||||
|
persons: 检测到的人物列表,每个元素包含bbox和confidence
|
||||||
|
distances: 对应的距离列表(可选)
|
||||||
|
save_path: 保存路径
|
||||||
|
show_confidence: 是否显示置信度
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
绘制了检测结果的图像
|
||||||
|
"""
|
||||||
|
# 创建图像副本
|
||||||
|
vis_image = image.copy()
|
||||||
|
|
||||||
|
# 定义颜色和字体
|
||||||
|
BOX_COLOR = (0, 255, 0) # 绿色边框
|
||||||
|
TEXT_COLOR = (255, 255, 255) # 白色文字
|
||||||
|
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
||||||
|
FONT_SCALE = 0.6
|
||||||
|
THICKNESS = 2
|
||||||
|
|
||||||
|
# 遍历每个检测结果
|
||||||
|
for idx, person in enumerate(persons):
|
||||||
|
bbox = person['bbox']
|
||||||
|
conf = person['confidence']
|
||||||
|
|
||||||
|
# 绘制边界框
|
||||||
|
cv2.rectangle(
|
||||||
|
vis_image,
|
||||||
|
(bbox[0], bbox[1]),
|
||||||
|
(bbox[2], bbox[3]),
|
||||||
|
BOX_COLOR,
|
||||||
|
THICKNESS
|
||||||
|
)
|
||||||
|
|
||||||
|
# 准备显示文本
|
||||||
|
text_items = []
|
||||||
|
if show_confidence:
|
||||||
|
text_items.append(f"Conf: {conf:.2f}")
|
||||||
|
|
||||||
|
if distances and idx < len(distances):
|
||||||
|
dist_meters = distances[idx] / 1000 # 转换为米
|
||||||
|
text_items.append(f"Dist: {dist_meters:.2f}m")
|
||||||
|
|
||||||
|
# 绘制文本
|
||||||
|
text = " | ".join(text_items)
|
||||||
|
if text:
|
||||||
|
# 获取文本大小
|
||||||
|
(text_width, text_height), _ = cv2.getTextSize(
|
||||||
|
text, FONT, FONT_SCALE, THICKNESS
|
||||||
|
)
|
||||||
|
|
||||||
|
# 绘制文本背景
|
||||||
|
cv2.rectangle(
|
||||||
|
vis_image,
|
||||||
|
(bbox[0], bbox[1] - text_height - 10),
|
||||||
|
(bbox[0] + text_width + 10, bbox[1]),
|
||||||
|
BOX_COLOR,
|
||||||
|
-1 # 填充矩形
|
||||||
|
)
|
||||||
|
|
||||||
|
# 绘制文本
|
||||||
|
cv2.putText(
|
||||||
|
vis_image,
|
||||||
|
text,
|
||||||
|
(bbox[0] + 5, bbox[1] - 5),
|
||||||
|
FONT,
|
||||||
|
FONT_SCALE,
|
||||||
|
TEXT_COLOR,
|
||||||
|
THICKNESS
|
||||||
|
)
|
||||||
|
|
||||||
|
return vis_image
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_detection_image(
|
||||||
|
image: np.ndarray,
|
||||||
|
save_path: str = "output"
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
保存检测结果图像
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: 要保存的图像
|
||||||
|
save_path: 保存路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
保存的文件路径
|
||||||
|
"""
|
||||||
|
# 确保输出目录存在
|
||||||
|
os.makedirs(save_path, exist_ok=True)
|
||||||
|
|
||||||
|
# 生成文件名(使用时间戳)
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||||
|
filename = f"detection_{timestamp}.jpg"
|
||||||
|
filepath = os.path.join(save_path, filename)
|
||||||
|
|
||||||
|
# 保存图像
|
||||||
|
cv2.imwrite(filepath, image)
|
||||||
|
|
||||||
|
return filepath
|
||||||
BIN
yolov8n.pt
Normal file
BIN
yolov8n.pt
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user