Skip to content

Commit aff0dfa

Browse files
committed
ci: Add GitHub Actions workflow for build and release
1 parent 3c7037c commit aff0dfa

29 files changed

+3836
-1052
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto

analytics.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# analytics.py (версия с устранением задержки и оптимизациями)
2+
import cv2
3+
import sys
4+
import json
5+
import time
6+
import base64
7+
import threading # <--- Импортируем модуль для работы с потоками
8+
9+
try:
10+
from ultralytics import YOLO
11+
except ImportError:
12+
print(json.dumps({
13+
"status": "error",
14+
"message": "Ultralytics YOLO library not found. Please run 'pip install ultralytics'."
15+
}), flush=True)
16+
sys.exit(1)
17+
18+
# Класс для чтения видеопотока в отдельном потоке
19+
class FrameGrabber:
20+
"""
21+
Класс для захвата кадров из видеопотока в отдельном потоке,
22+
чтобы избежать накопления задержки в буфере cv2.VideoCapture.
23+
"""
24+
def __init__(self, src=0):
25+
self.stream = cv2.VideoCapture(src)
26+
if not self.stream.isOpened():
27+
raise IOError("Cannot open video stream")
28+
29+
self.ret, self.frame = self.stream.read()
30+
self.stopped = False
31+
self.thread = threading.Thread(target=self.update, args=())
32+
self.thread.daemon = True
33+
34+
def start(self):
35+
self.stopped = False
36+
self.thread.start()
37+
38+
def update(self):
39+
while not self.stopped:
40+
ret, frame = self.stream.read()
41+
if not ret:
42+
self.stop()
43+
break
44+
self.ret = ret
45+
self.frame = frame
46+
47+
def read(self):
48+
return self.ret, self.frame
49+
50+
def stop(self):
51+
self.stopped = True
52+
if self.thread.is_alive():
53+
self.thread.join(timeout=1.0)
54+
self.stream.release()
55+
56+
57+
def run_analytics(rtsp_url, config_str):
58+
try:
59+
model = YOLO("yolov8n.pt")
60+
except Exception as e:
61+
print(json.dumps({"status": "error", "message": f"Failed to load YOLOv8 model: {e}"}), flush=True)
62+
sys.exit(1)
63+
64+
config = {}
65+
if config_str:
66+
try:
67+
config_json = base64.b64decode(config_str).decode('utf-8')
68+
config = json.loads(config_json)
69+
except Exception as e:
70+
print(json.dumps({"status": "error", "message": f"Invalid config provided: {e}"}), flush=True)
71+
72+
roi = config.get('roi')
73+
objects_to_detect = config.get('objects', None)
74+
confidence_threshold = config.get('confidence', 0.5)
75+
frame_skip = int(config.get('frame_skip', 5))
76+
if frame_skip < 1:
77+
frame_skip = 1
78+
resize_width = int(config.get('resize_width', 640))
79+
80+
# Используем наш новый класс FrameGrabber
81+
try:
82+
frame_grabber = FrameGrabber(rtsp_url)
83+
frame_grabber.start()
84+
time.sleep(2) # Даем время на подключение и заполнение первого кадра
85+
except IOError as e:
86+
print(json.dumps({"status": "error", "message": str(e)}), flush=True)
87+
sys.exit(1)
88+
89+
frame_count = 0
90+
91+
# Основной цикл и блок finally для корректного завершения
92+
try:
93+
while not frame_grabber.stopped:
94+
ret, frame = frame_grabber.read()
95+
if not ret or frame is None:
96+
# Поток мог завершиться, даем ему немного времени и проверяем снова
97+
time.sleep(0.5)
98+
if frame_grabber.stopped:
99+
break
100+
continue
101+
102+
frame_count += 1
103+
if frame_count % frame_skip != 0:
104+
# VVV ИЗМЕНЕНИЕ: Замена sleep на continue VVV
105+
# Это более эффективно, так как не вносит искусственную задержку.
106+
# Цикл просто перейдет к следующей итерации.
107+
continue
108+
# ^^^ КОНЕЦ ИЗМЕНЕНИЯ ^^^
109+
110+
original_height, original_width = frame.shape[:2]
111+
112+
scale_x, scale_y = 1.0, 1.0
113+
if resize_width > 0 and original_width > resize_width:
114+
scale_x = original_width / resize_width
115+
new_height = int(original_height / scale_x)
116+
scale_y = original_height / new_height
117+
frame_to_process = cv2.resize(frame, (resize_width, new_height), interpolation=cv2.INTER_AREA)
118+
else:
119+
frame_to_process = frame
120+
121+
results = model(frame_to_process, verbose=False, conf=confidence_threshold)
122+
123+
detected_objects = []
124+
for box in results[0].boxes:
125+
class_id = int(box.cls[0])
126+
label = model.names[class_id]
127+
128+
x1, y1, x2, y2 = box.xyxy[0]
129+
x, y, w, h = int(x1), int(y1), int(x2 - x1), int(y2 - y1)
130+
131+
detected_objects.append({
132+
'label': label,
133+
'confidence': float(box.conf[0]),
134+
'box': {
135+
'x': int(x * scale_x),
136+
'y': int(y * scale_y),
137+
'w': int(w * scale_x),
138+
'h': int(h * scale_y)
139+
}
140+
})
141+
142+
filtered_objects = []
143+
for obj in detected_objects:
144+
if objects_to_detect and obj['label'] not in objects_to_detect:
145+
continue
146+
147+
if roi:
148+
box = obj['box']
149+
obj_center_x = box['x'] + box['w'] / 2
150+
obj_center_y = box['y'] + box['h'] / 2
151+
152+
roi_x1 = roi['x'] * original_width
153+
roi_y1 = roi['y'] * original_height
154+
roi_x2 = (roi['x'] + roi['w']) * original_width
155+
roi_y2 = (roi['y'] + roi['h']) * original_height
156+
157+
if not (roi_x1 < obj_center_x < roi_x2 and roi_y1 < obj_center_y < roi_y2):
158+
continue
159+
160+
filtered_objects.append(obj)
161+
162+
if len(filtered_objects) > 0:
163+
result = {
164+
"status": "objects_detected",
165+
"timestamp": time.time(),
166+
"objects": filtered_objects
167+
}
168+
print(json.dumps(result), flush=True)
169+
170+
finally:
171+
print(json.dumps({"status": "info", "message": "Analytics process stopping."}), flush=True)
172+
frame_grabber.stop()
173+
174+
if __name__ == "__main__":
175+
if len(sys.argv) > 1:
176+
rtsp_stream_url = sys.argv[1]
177+
config_arg = sys.argv[2] if len(sys.argv) > 2 else None
178+
run_analytics(rtsp_stream_url, config_arg)
179+
else:
180+
print(json.dumps({"status": "error", "message": "RTSP URL not provided"}), flush=True)
181+
sys.exit(1)

coco.names

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
person
2+
bicycle
3+
car
4+
motorcycle
5+
airplane
6+
bus
7+
train
8+
truck
9+
boat
10+
traffic light
11+
fire hydrant
12+
stop sign
13+
parking meter
14+
bench
15+
bird
16+
cat
17+
dog
18+
horse
19+
sheep
20+
cow
21+
elephant
22+
bear
23+
zebra
24+
giraffe
25+
backpack
26+
umbrella
27+
handbag
28+
tie
29+
suitcase
30+
frisbee
31+
skis
32+
snowboard
33+
sports ball
34+
kite
35+
baseball bat
36+
baseball glove
37+
skateboard
38+
surfboard
39+
tennis racket
40+
bottle
41+
wine glass
42+
cup
43+
fork
44+
knife
45+
spoon
46+
bowl
47+
banana
48+
apple
49+
sandwich
50+
orange
51+
broccoli
52+
carrot
53+
hot dog
54+
pizza
55+
donut
56+
cake
57+
chair
58+
couch
59+
potted plant
60+
bed
61+
dining table
62+
toilet
63+
tv
64+
laptop
65+
mouse
66+
remote
67+
keyboard
68+
cell phone
69+
microwave
70+
oven
71+
toaster
72+
sink
73+
refrigerator
74+
book
75+
clock
76+
vase
77+
scissors
78+
teddy bear
79+
hair drier
80+
toothbrush

file-manager.html

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,40 @@
33
<head>
44
<meta charset="UTF-8">
55
<title>Файловый менеджер</title>
6+
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
67
<style>
78
:root {
89
--bg-color: #1e1e1e; --text-color: #d4d4d4; --border-color: #333;
910
--pane-bg: #252526; --header-bg: #3c3c3c; --selected-bg: #094771;
1011
--button-bg: #0e639c; --button-hover-bg: #1177bb;
1112
--progress-bar-bg: #5a5a5a; --progress-bar-fill: #0e639c;
13+
--danger-color: #f85149;
1214
}
1315
body, html { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 0; height: 100%; width: 100%; overflow: hidden; background-color: var(--bg-color); color: var(--text-color); font-size: 14px; }
14-
.container { display: flex; flex-direction: column; height: 100vh; }
16+
.container { display: flex; flex-direction: column; height: 100vh; border: 1px solid var(--border-color); }
17+
18+
/* VVV ИЗМЕНЕНИЕ: Стили для кастомного заголовка VVV */
19+
.header {
20+
height: 32px; background-color: var(--header-bg); display: flex;
21+
align-items: center; padding-left: 12px; flex-shrink: 0;
22+
-webkit-app-region: drag;
23+
}
24+
.header .title { flex-grow: 1; font-weight: 500; }
25+
.window-controls { display: flex; height: 100%; -webkit-app-region: no-drag; }
26+
.window-control-btn {
27+
background: none; border: none; color: var(--text-color);
28+
padding: 0 15px; cursor: pointer; height: 100%;
29+
display: flex; align-items: center; justify-content: center;
30+
transition: background-color 0.2s;
31+
}
32+
.window-control-btn i { font-size: 18px; }
33+
.window-control-btn:hover { background-color: rgba(255,255,255,0.1); }
34+
.window-control-btn.close-btn:hover { background-color: var(--danger-color); color: white; }
35+
/* ^^^ КОНЕЦ ИЗМЕНЕНИЯ ^^^ */
36+
1537
.panes-container { display: flex; flex-grow: 1; overflow: hidden; }
16-
.pane { width: 50%; display: flex; flex-direction: column; background-color: var(--pane-bg); border: 1px solid var(--border-color); }
17-
.pane:first-child { border-right: none; }
38+
.pane { width: 50%; display: flex; flex-direction: column; background-color: var(--pane-bg); border-top: 1px solid var(--border-color); }
39+
.pane:first-child { border-right: 1px solid var(--border-color); }
1840
.pane-header { background-color: var(--header-bg); padding: 5px; display: flex; align-items: center; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
1941
.pane-header h3 { margin: 0; font-size: 1em; flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2042
.pane-header .path-input { flex-grow: 1; background-color: var(--bg-color); border: 1px solid var(--border-color); color: var(--text-color); padding: 4px; font-size: 13px; margin-left: 10px; }
@@ -30,16 +52,24 @@
3052
.controls button:disabled { background-color: #555; cursor: not-allowed; }
3153
.status-bar { padding: 5px 10px; background-color: var(--header-bg); font-size: 12px; height: 22px; line-height: 22px; flex-shrink: 0; display: flex; align-items: center; }
3254
.progress-bar-container { width: 200px; height: 16px; background-color: var(--progress-bar-bg); border-radius: 3px; overflow: hidden; display: none; margin-left: auto;}
33-
.progress-bar-fill { width: 100%; height: 100%; background-color: var(--progress-bar-fill); animation: pulse 2s infinite; }
34-
@keyframes pulse {
35-
0% { opacity: 0.7; }
36-
50% { opacity: 1; }
37-
100% { opacity: 0.7; }
38-
}
55+
.progress-bar-fill { width: 0%; height: 100%; background-color: var(--progress-bar-fill); }
56+
.progress-bar-fill.pulse { width: 100%; animation: pulse 2s infinite; }
57+
@keyframes pulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
3958
</style>
4059
</head>
4160
<body>
4261
<div class="container">
62+
<!-- VVV ИЗМЕНЕНИЕ: Добавлен кастомный заголовок VVV -->
63+
<div class="header">
64+
<div class="title" id="window-title">Файловый менеджер</div>
65+
<div class="window-controls">
66+
<button id="minimize-btn" class="window-control-btn"><i class="material-icons">remove</i></button>
67+
<button id="maximize-btn" class="window-control-btn"><i class="material-icons">crop_square</i></button>
68+
<button id="close-btn" class="window-control-btn close-btn"><i class="material-icons">close</i></button>
69+
</div>
70+
</div>
71+
<!-- ^^^ КОНЕЦ ИЗМЕНЕНИЯ ^^^ -->
72+
4373
<div class="panes-container">
4474
<div class="pane" id="local-pane">
4575
<div class="pane-header">
@@ -70,6 +100,27 @@ <h3>Камера: <span id="camera-name"></span></h3>
70100
</div>
71101
</div>
72102
</div>
103+
104+
<!-- VVV ИЗМЕНЕНИЕ: Добавлен скрипт для кнопок управления VVV -->
105+
<script>
106+
document.addEventListener('DOMContentLoaded', () => {
107+
const cameraName = JSON.parse(new URLSearchParams(window.location.search).get('camera')).name;
108+
document.getElementById('window-title').textContent = `Файловый менеджер: ${cameraName}`;
109+
110+
document.getElementById('minimize-btn').addEventListener('click', () => window.scpApi.minimize());
111+
document.getElementById('maximize-btn').addEventListener('click', () => window.scpApi.maximize());
112+
document.getElementById('close-btn').addEventListener('click', () => window.scpApi.close());
113+
114+
window.scpApi.onWindowMaximized(() => {
115+
document.getElementById('maximize-btn').innerHTML = '<i class="material-icons">filter_none</i>';
116+
});
117+
window.scpApi.onWindowUnmaximized(() => {
118+
document.getElementById('maximize-btn').innerHTML = '<i class="material-icons">crop_square</i>';
119+
});
120+
});
121+
</script>
122+
<!-- ^^^ КОНЕЦ ИЗМЕНЕНИЯ ^^^ -->
123+
73124
<script src="./file-manager.js"></script>
74125
</body>
75126
</html>

0 commit comments

Comments
 (0)