Skip to content

Commit 84589af

Browse files
committed
[add]web获取实时视频流
1 parent c891bf8 commit 84589af

9 files changed

Lines changed: 514 additions & 47 deletions

File tree

README.md

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -46,50 +46,38 @@ sudo usermod -aG docker $USER
4646
# 注意:执行完上一条命令后需要注销并重新登录才能生效
4747
```
4848

49-
### 2. 运行项目 (以 RK3576 为例)
49+
### 2. 运行项目
5050

51-
首先开启 X11 访问权限(用于预览窗口显示):
51+
本项目提供两种预览方式:**本地 GUI 窗口预览**(需连接显示器)和 **Web 浏览器远程预览**(推荐)。
5252

53-
```bash
54-
xhost +local:docker
55-
```
56-
57-
拉取最新镜像
58-
59-
```bash
60-
sudo docker pull ghcr.io/litxaohu/rk3588_yolo/rk3588-yolo:latest
61-
sudo docker pull ghcr.io/litxaohu/rk3588_yolo/rk3576-yolo:latest
62-
```
63-
64-
运行 Docker 容器:
65-
66-
rk3588:
53+
#### 方式 A: Web 浏览器远程预览 (推荐)
54+
无需配置 X11,直接通过浏览器访问。
6755

6856
```bash
69-
sudo docker run --rm --privileged --net=host --env DISPLAY=$DISPLAY \
70-
-v /tmp/.X11-unix:/tmp/.X11-unix \
71-
-v /dev/bus/usb:/dev/bus/usb \
72-
--device /dev/video0:/dev/video0 \
73-
--device /dev/dri/renderD128:/dev/dri/renderD129 \
57+
# 以 RK3588 为例
58+
sudo docker run --rm --privileged --net=host \
59+
--device /dev/dri/renderD129:/dev/dri/renderD129 \
7460
-v /proc/device-tree/compatible:/proc/device-tree/compatible \
75-
ghcr.io/litxaohu/rk3588_yolo/rk3588-yolo:latest
76-
python realtime_detection.py --model_path model/yolo11n.rknn --camera_id 0
61+
ghcr.io/litxaohu/recomputer-rk-cv/rk3588-yolo:latest
7762
```
63+
运行后,在同局域网的浏览器访问:`http://开发板IP:8000`
7864

79-
rk3576:
65+
#### 方式 B: 本地 GUI 窗口预览
66+
需要开启 X11 访问权限:
8067

8168
```bash
69+
xhost +local:docker
70+
71+
# 以 RK3588 为例
8272
sudo docker run --rm --privileged --net=host --env DISPLAY=$DISPLAY \
8373
-v /tmp/.X11-unix:/tmp/.X11-unix \
84-
-v /dev/bus/usb:/dev/bus/usb \
85-
--device /dev/video0:/dev/video0 \
86-
--device /dev/dri/renderD128:/dev/dri/renderD128 \
74+
--device /dev/dri/renderD129:/dev/dri/renderD129 \
8775
-v /proc/device-tree/compatible:/proc/device-tree/compatible \
88-
ghcr.io/litxaohu/rk3588_yolo/rk3576-yolo:latest
76+
ghcr.io/litxaohu/recomputer-rk-cv/rk3588-yolo:latest \
8977
python realtime_detection.py --model_path model/yolo11n.rknn --camera_id 0
9078
```
9179

92-
> **注意**对于 RK3576,设备路径改为 `/dev/dri/renderD128`
80+
> **提示**RK3588 的 NPU 设备通常为 `/dev/dri/renderD129`,RK3576 通常为 `/dev/dri/renderD128`
9381
9482
## 平台详细文档
9583

docker/rk3576/Yolo11.dockerfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,12 @@ RUN chmod 755 /usr/lib/librknnrt.so
4545
# Copy the rest of the application code
4646
COPY . .
4747

48+
# Expose port for Web Preview
49+
EXPOSE 8000
50+
4851
# Set the default command to run the detection script
49-
CMD ["python", "realtime_detection.py", "--model_path", "model/yolo11n.rknn", "--video_path", "video/test.mp4"]
52+
# GUI version (requires DISPLAY)
53+
# CMD ["python", "realtime_detection.py", "--model_path", "model/yolo11n.rknn", "--video_path", "video/test.mp4"]
54+
55+
# Web version (accessible via browser at http://localhost:8000)
56+
CMD ["python", "web_detection.py", "--model_path", "model/yolo11n.rknn", "--source", "video/test.mp4"]

docker/rk3588/Yolo11.dockerfile

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ RUN chmod 755 /usr/lib/librknnrt.so
5151
# Copy the rest of the application code
5252
COPY . .
5353

54+
# Expose port for Web Preview
55+
EXPOSE 8000
56+
5457
# Set the default command to run the detection script
55-
# Users can override this command
56-
# Using a default model path (assuming one exists or user mounts it)
57-
# Since we don't know which model the user wants, we can just set python as entrypoint or give a help message
58-
# But the user asked for "one line docker command", so a default CMD is good.
59-
# Let's assume yolo11n.rknn is a good default if it exists.
60-
CMD ["python", "realtime_detection.py", "--model_path", "model/yolo11n.rknn", "--video_path", "video/test.mp4"]
58+
# GUI version (requires DISPLAY)
59+
# CMD ["python", "realtime_detection.py", "--model_path", "model/yolo11n.rknn", "--video_path", "video/test.mp4"]
60+
61+
# Web version (accessible via browser at http://localhost:8000)
62+
CMD ["python", "web_detection.py", "--model_path", "model/yolo11n.rknn", "--source", "video/test.mp4"]

src/rk3576/README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@
1212
- `model/`:存放针对 RK3576 转换的 `.rknn` 模型。
1313
- `realtime_detection.py`:主程序。
1414

15-
## 运行命令
15+
## 运行方式
16+
17+
### 1. Web 浏览器预览 (推荐)
18+
```bash
19+
sudo docker run --rm --privileged --net=host \
20+
--device /dev/dri/renderD128:/dev/dri/renderD128 \
21+
-v /proc/device-tree/compatible:/proc/device-tree/compatible \
22+
ghcr.io/litxaohu/recomputer-rk-cv/rk3576-yolo:latest
23+
```
24+
访问:`http://localhost:8000`
25+
26+
### 2. 本地 GUI 预览
1627
```bash
28+
xhost +local:docker
1729
sudo docker run --rm --privileged --net=host --env DISPLAY=$DISPLAY \
1830
-v /tmp/.X11-unix:/tmp/.X11-unix \
19-
-v /dev/bus/usb:/dev/bus/usb \
20-
--device /dev/video0:/dev/video0 \
2131
--device /dev/dri/renderD128:/dev/dri/renderD128 \
2232
-v /proc/device-tree/compatible:/proc/device-tree/compatible \
23-
ghcr.io/litxaohu/rk3588_yolo/rk3576-yolo:latest
33+
ghcr.io/litxaohu/recomputer-rk-cv/rk3576-yolo:latest \
2434
python realtime_detection.py --model_path model/yolo11n.rknn --camera_id 0
2535
```

src/rk3576/requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
numpy
2-
opencv-python
2+
opencv-python
3+
fastapi
4+
uvicorn
5+
python-multipart

src/rk3576/web_detection.py

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import os
2+
import cv2
3+
import sys
4+
import argparse
5+
import time
6+
import numpy as np
7+
from fastapi import FastAPI, Response
8+
from fastapi.responses import StreamingResponse
9+
import uvicorn
10+
import threading
11+
12+
# 导入共享工具
13+
from py_utils.coco_utils import COCO_test_helper
14+
15+
# 尝试导入RKNN-Toolkit-Lite2
16+
try:
17+
from rknnlite.api import RKNNLite
18+
RKNN_LITE_AVAILABLE = True
19+
except ImportError:
20+
RKNN_LITE_AVAILABLE = False
21+
print("Warning: RKNN-Toolkit-Lite2 not available, using fallback")
22+
23+
# 常量定义
24+
OBJ_THRESH = 0.25
25+
NMS_THRESH = 0.45
26+
IMG_SIZE = (640, 640)
27+
CLASSES = ("person", "bicycle", "car","motorbike ","aeroplane ","bus ","train","truck ","boat","traffic light",
28+
"fire hydrant","stop sign ","parking meter","bench","bird","cat","dog ","horse ","sheep","cow","elephant",
29+
"bear","zebra ","giraffe","backpack","umbrella","handbag","tie","suitcase","frisbee","skis","snowboard","sports ball","kite",
30+
"baseball bat","baseball glove","skateboard","surfboard","tennis racket","bottle","wine glass","cup","fork","knife ",
31+
"spoon","bowl","banana","apple","sandwich","orange","broccoli","carrot","hot dog","pizza ","donut","cake","chair","sofa",
32+
"pottedplant","bed","diningtable","toilet ","tvmonitor","laptop ","mouse ","remote ","keyboard ","cell phone","microwave ",
33+
"oven ","toaster","sink","refrigerator ","book","clock","vase","scissors ","teddy bear ","hair drier", "toothbrush ")
34+
35+
app = FastAPI(title="reComputer RK3576 Web Preview")
36+
37+
def filter_boxes(boxes, box_confidences, box_class_probs):
38+
"""Filter boxes with object threshold."""
39+
box_confidences = box_confidences.reshape(-1)
40+
class_max_score = np.max(box_class_probs, axis=-1)
41+
classes = np.argmax(box_class_probs, axis=-1)
42+
_class_pos = np.where(class_max_score * box_confidences >= OBJ_THRESH)
43+
scores = (class_max_score * box_confidences)[_class_pos]
44+
boxes = boxes[_class_pos]
45+
classes = classes[_class_pos]
46+
return boxes, classes, scores
47+
48+
def nms_boxes(boxes, scores):
49+
"""Suppress non-maximal boxes."""
50+
x = boxes[:, 0]
51+
y = boxes[:, 1]
52+
w = boxes[:, 2] - boxes[:, 0]
53+
h = boxes[:, 3] - boxes[:, 1]
54+
areas = w * h
55+
order = scores.argsort()[::-1]
56+
keep = []
57+
while order.size > 0:
58+
i = order[0]
59+
keep.append(i)
60+
xx1 = np.maximum(x[i], x[order[1:]])
61+
yy1 = np.maximum(y[i], y[order[1:]])
62+
xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
63+
yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
64+
w1 = np.maximum(0.0, xx2 - xx1 + 0.00001)
65+
h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
66+
inter = w1 * h1
67+
ovr = inter / (areas[i] + areas[order[1:]] - inter)
68+
inds = np.where(ovr <= NMS_THRESH)[0]
69+
order = order[inds + 1]
70+
return keep
71+
72+
def dfl(position):
73+
# Distribution Focal Loss (DFL)
74+
n, c, h, w = position.shape
75+
p_num = 4
76+
mc = c // p_num
77+
y = position.reshape(n, p_num, mc, h, w)
78+
y_exp = np.exp(y - np.max(y, axis=2, keepdims=True))
79+
y_softmax = y_exp / np.sum(y_exp, axis=2, keepdims=True)
80+
acc_metrix = np.arange(mc).reshape(1, 1, mc, 1, 1).astype(np.float32)
81+
y = (y_softmax * acc_metrix).sum(2)
82+
return y
83+
84+
def box_process(position):
85+
grid_h, grid_w = position.shape[2:4]
86+
col, row = np.meshgrid(np.arange(0, grid_w), np.arange(0, grid_h))
87+
col = col.reshape(1, 1, grid_h, grid_w)
88+
row = row.reshape(1, 1, grid_h, grid_w)
89+
grid = np.concatenate((col, row), axis=1)
90+
stride = np.array([IMG_SIZE[1]//grid_h, IMG_SIZE[0]//grid_w]).reshape(1,2,1,1)
91+
position = dfl(position)
92+
box_xy = grid + 0.5 - position[:,0:2,:,:]
93+
box_xy2 = grid + 0.5 + position[:,2:4,:,:]
94+
xyxy = np.concatenate((box_xy*stride, box_xy2*stride), axis=1)
95+
return xyxy
96+
97+
def post_process(input_data):
98+
if input_data is None:
99+
return None, None, None
100+
boxes, scores, classes_conf = [], [], []
101+
defualt_branch = 3
102+
pair_per_branch = len(input_data) // defualt_branch
103+
for i in range(defualt_branch):
104+
boxes.append(box_process(input_data[pair_per_branch*i]))
105+
classes_conf.append(input_data[pair_per_branch*i+1])
106+
scores.append(np.ones_like(input_data[pair_per_branch*i+1][:,:1,:,:], dtype=np.float32))
107+
108+
def sp_flatten(_in):
109+
ch = _in.shape[1]
110+
_in = _in.transpose(0, 2, 3, 1)
111+
return _in.reshape(-1, ch)
112+
113+
boxes = [sp_flatten(_v) for _v in boxes]
114+
classes_conf = [sp_flatten(_v) for _v in classes_conf]
115+
scores = [sp_flatten(_v) for _v in scores]
116+
boxes = np.concatenate(boxes)
117+
classes_conf = np.concatenate(classes_conf)
118+
scores = np.concatenate(scores)
119+
boxes, classes, scores = filter_boxes(boxes, scores, classes_conf)
120+
nboxes, nclasses, nscores = [], [], []
121+
for c in set(classes):
122+
inds = np.where(classes == c)
123+
b = boxes[inds]
124+
c = classes[inds]
125+
s = scores[inds]
126+
keep = nms_boxes(b, s)
127+
if len(keep) != 0:
128+
nboxes.append(b[keep])
129+
nclasses.append(c[keep])
130+
nscores.append(s[keep])
131+
if not nclasses and not nscores:
132+
return None, None, None
133+
boxes = np.concatenate(nboxes)
134+
classes = np.concatenate(nclasses)
135+
scores = np.concatenate(nscores)
136+
return boxes, classes, scores
137+
138+
def draw(image, boxes, scores, classes):
139+
for box, score, cl in zip(boxes, scores, classes):
140+
top, left, right, bottom = [int(_b) for _b in box]
141+
cv2.rectangle(image, (top, left), (right, bottom), (255, 0, 0), 2)
142+
cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score),
143+
(top, left - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
144+
145+
class RKNNLiteModel:
146+
def __init__(self, model_path):
147+
if not RKNN_LITE_AVAILABLE:
148+
raise ImportError("RKNN-Toolkit-Lite2 is not available")
149+
if not os.path.exists(model_path):
150+
raise FileNotFoundError(f"RKNN model file not found: {model_path}")
151+
self.rknn_lite = RKNNLite()
152+
print('Loading RKNN model...')
153+
ret = self.rknn_lite.load_rknn(model_path)
154+
if ret != 0:
155+
raise Exception(f"Load RKNN model failed with error code: {ret}")
156+
print('Initializing runtime...')
157+
# RK3576 使用默认初始化
158+
ret = self.rknn_lite.init_runtime()
159+
if ret != 0:
160+
raise Exception(f"Init runtime failed with error code: {ret}")
161+
162+
def run(self, input_data):
163+
return self.rknn_lite.inference(inputs=[input_data])
164+
165+
class VideoStreamer:
166+
def __init__(self, model_path, source):
167+
self.model = RKNNLiteModel(model_path)
168+
self.cap = cv2.VideoCapture(source)
169+
self.co_helper = COCO_test_helper(enable_letter_box=True)
170+
self.fps_counter = 0
171+
self.inference_time = 0
172+
self.lock = threading.Lock()
173+
174+
def generate_frames(self):
175+
while True:
176+
success, frame = self.cap.read()
177+
if not success:
178+
if isinstance(self.cap, cv2.VideoCapture):
179+
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
180+
continue
181+
break
182+
183+
# 预处理
184+
img = self.co_helper.letter_box(im=frame.copy(), new_shape=(IMG_SIZE[1], IMG_SIZE[0]), pad_color=(0,0,0))
185+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
186+
input_data = np.expand_dims(img, axis=0)
187+
188+
# 推理
189+
t1 = time.time()
190+
outputs = self.model.run(input_data)
191+
self.inference_time = time.time() - t1
192+
193+
if self.inference_time > 0:
194+
inf_fps = 1.0 / self.inference_time
195+
self.fps_counter = 0.9 * self.fps_counter + 0.1 * inf_fps if self.fps_counter > 0 else inf_fps
196+
197+
# 后处理
198+
boxes, classes, scores = post_process(outputs)
199+
200+
if boxes is not None:
201+
draw(frame, self.co_helper.get_real_box(boxes), scores, classes)
202+
203+
# 绘制信息
204+
cv2.putText(frame, f'NPU FPS: {self.fps_counter:.1f}', (20, 40),
205+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
206+
cv2.putText(frame, f'Inference: {self.inference_time*1000:.1f}ms', (20, 80),
207+
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
208+
209+
ret, buffer = cv2.imencode('.jpg', frame)
210+
frame_bytes = buffer.tobytes()
211+
yield (b'--frame\r\n'
212+
b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
213+
214+
streamer = None
215+
216+
@app.get("/api/video_feed")
217+
async def video_feed():
218+
return StreamingResponse(streamer.generate_frames(),
219+
media_type="multipart/x-mixed-replace; boundary=frame")
220+
221+
@app.get("/")
222+
async def index():
223+
return Response(content="""
224+
<html>
225+
<head><title>reComputer RK3576 Web Preview</title></head>
226+
<body style="background-color: #1a1a1a; color: white; text-align: center; font-family: sans-serif;">
227+
<h1>reComputer RK3576 Real-time Detection</h1>
228+
<div style="margin: 20px auto; display: inline-block; border: 5px solid #333; border-radius: 10px; overflow: hidden;">
229+
<img src="/api/video_feed" style="max-width: 100%; height: auto;">
230+
</div>
231+
<p>Streaming via FastAPI + MJPEG</p>
232+
</body>
233+
</html>
234+
""", media_type="text/html")
235+
236+
if __name__ == "__main__":
237+
parser = argparse.ArgumentParser(description="RK3576 YOLO Web Detection")
238+
parser.add_argument('--model_path', type=str, default='model/yolo11n.rknn', help='path to rknn model')
239+
parser.add_argument('--source', type=str, default='1', help='camera id or video path')
240+
parser.add_argument('--host', type=str, default='0.0.0.0', help='host address')
241+
parser.add_argument('--port', type=int, default=8000, help='port number')
242+
args = parser.parse_args()
243+
244+
if args.source.isdigit():
245+
source = int(args.source)
246+
else:
247+
source = args.source
248+
249+
streamer = VideoStreamer(args.model_path, source)
250+
uvicorn.run(app, host=args.host, port=args.port)

0 commit comments

Comments
 (0)