Compare commits
3 Commits
5b3d251803
...
e28c2a538b
| Author | SHA1 | Date |
|---|---|---|
|
|
e28c2a538b | 1 month ago |
|
|
23bcf0567e | 1 month ago |
|
|
b5372e74ea | 1 month ago |
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13 virtualenv at C:\Users\Legion\PycharmProjects\VideoStream\.venv" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="34">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="tensorflow" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="traitlets" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="decorator" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="matplotlib-inline" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="PyYAML" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="python-dateutil" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="wcwidth" />
|
||||||
|
<item index="7" class="java.lang.String" itemvalue="cycler" />
|
||||||
|
<item index="8" class="java.lang.String" itemvalue="numpy" />
|
||||||
|
<item index="9" class="java.lang.String" itemvalue="requests" />
|
||||||
|
<item index="10" class="java.lang.String" itemvalue="seaborn" />
|
||||||
|
<item index="11" class="java.lang.String" itemvalue="Pygments" />
|
||||||
|
<item index="12" class="java.lang.String" itemvalue="certifi" />
|
||||||
|
<item index="13" class="java.lang.String" itemvalue="prompt-toolkit" />
|
||||||
|
<item index="14" class="java.lang.String" itemvalue="urllib3" />
|
||||||
|
<item index="15" class="java.lang.String" itemvalue="pyparsing" />
|
||||||
|
<item index="16" class="java.lang.String" itemvalue="scipy" />
|
||||||
|
<item index="17" class="java.lang.String" itemvalue="six" />
|
||||||
|
<item index="18" class="java.lang.String" itemvalue="opencv-python" />
|
||||||
|
<item index="19" class="java.lang.String" itemvalue="parso" />
|
||||||
|
<item index="20" class="java.lang.String" itemvalue="ipython" />
|
||||||
|
<item index="21" class="java.lang.String" itemvalue="kiwisolver" />
|
||||||
|
<item index="22" class="java.lang.String" itemvalue="packaging" />
|
||||||
|
<item index="23" class="java.lang.String" itemvalue="typing-extensions" />
|
||||||
|
<item index="24" class="java.lang.String" itemvalue="psutil" />
|
||||||
|
<item index="25" class="java.lang.String" itemvalue="pandas" />
|
||||||
|
<item index="26" class="java.lang.String" itemvalue="tqdm" />
|
||||||
|
<item index="27" class="java.lang.String" itemvalue="fonttools" />
|
||||||
|
<item index="28" class="java.lang.String" itemvalue="jedi" />
|
||||||
|
<item index="29" class="java.lang.String" itemvalue="matplotlib" />
|
||||||
|
<item index="30" class="java.lang.String" itemvalue="charset-normalizer" />
|
||||||
|
<item index="31" class="java.lang.String" itemvalue="pytz" />
|
||||||
|
<item index="32" class="java.lang.String" itemvalue="idna" />
|
||||||
|
<item index="33" class="java.lang.String" itemvalue="Pillow" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 virtualenv at C:\Users\Legion\PycharmProjects\VideoStream\.venv" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/CHAMELEON.iml" filepath="$PROJECT_DIR$/.idea/CHAMELEON.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
# PTZ Tracker — Modular Version
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
```
|
||||||
|
ptz_tracker_modular/
|
||||||
|
├── app.py # entrypoint (async main)
|
||||||
|
├── calibration.py # preset/sector and pan-sign calibration
|
||||||
|
├── config.py # constants + camera config
|
||||||
|
├── events.py # alarms, detection event plumbing
|
||||||
|
├── geom.py # angular math and sector helpers
|
||||||
|
├── heartbeat.py # UDP heartbeat
|
||||||
|
├── model.py # YOLO loading + forward
|
||||||
|
├── patrol.py # patrol logic (bounded/smooth + sync)
|
||||||
|
├── postprocess.py # PTZ control + zoom + notify_detected
|
||||||
|
├── preview.py # JPEG encode and preview publishing
|
||||||
|
├── ptz_io.py # PTZ HTTP session + worker + bounded move
|
||||||
|
├── sector.py # sector init and autocalculation
|
||||||
|
├── state.py # runtime shared state
|
||||||
|
├── status.py # PTZ status poller
|
||||||
|
├── streaming.py # WS servers, MicroBatcher, CPP bridges
|
||||||
|
└── __init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
```bash
|
||||||
|
python -m ptz_tracker_modular.app
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional env:
|
||||||
|
- `YOLO_WEIGHTS=/path/to/weights.pt`
|
||||||
|
- `CAM_<ID>_PASSWORD=...` for every camera id
|
||||||
|
|
||||||
|
Dependencies: `opencv-python`, `torch`, `ultralytics`, `websockets`, `requests`, `numpy`.
|
||||||
@ -0,0 +1 @@
|
|||||||
|
# package
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,179 @@
|
|||||||
|
[[camera]]
|
||||||
|
id = 0
|
||||||
|
ip = "192.168.54.8"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 1
|
||||||
|
wiper_channel = 1
|
||||||
|
preset1 = "1"
|
||||||
|
preset2 = "2"
|
||||||
|
sweep_sign = 1
|
||||||
|
is_ptz = true
|
||||||
|
mac = "80:BE:AF:E5:48:01"
|
||||||
|
preset1_deg = 343.000000
|
||||||
|
preset2_deg = 66.000000
|
||||||
|
preset1_tilt_deg = -138.000000
|
||||||
|
preset2_tilt_deg = -138.000000
|
||||||
|
sector_min_deg = 345.000000
|
||||||
|
sector_max_deg = 64.000000
|
||||||
|
geo_last_source = "sector_autocal"
|
||||||
|
geo_last_at = 1770695205
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 1
|
||||||
|
ip = "192.168.54.8"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 2
|
||||||
|
wiper_channel = 1
|
||||||
|
is_ptz = false
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 2
|
||||||
|
ip = "192.168.54.9"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 1
|
||||||
|
wiper_channel = 1
|
||||||
|
preset1 = "1"
|
||||||
|
preset2 = "2"
|
||||||
|
sweep_sign = 1
|
||||||
|
is_ptz = true
|
||||||
|
mac = "80:BE:AF:E5:48:09"
|
||||||
|
preset1_deg = 342.000000
|
||||||
|
preset2_deg = 134.000000
|
||||||
|
preset1_tilt_deg = -140.000000
|
||||||
|
preset2_tilt_deg = -140.000000
|
||||||
|
sector_min_deg = 344.000000
|
||||||
|
sector_max_deg = 132.000000
|
||||||
|
geo_last_source = "sector_autocal"
|
||||||
|
geo_last_at = 1770695207
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 3
|
||||||
|
ip = "192.168.54.9"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 2
|
||||||
|
wiper_channel = 1
|
||||||
|
is_ptz = false
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 4
|
||||||
|
ip = "192.168.54.10"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 1
|
||||||
|
wiper_channel = 1
|
||||||
|
preset1 = "1"
|
||||||
|
preset2 = "2"
|
||||||
|
sweep_sign = 1
|
||||||
|
is_ptz = true
|
||||||
|
mac = "80:BE:AF:E5:47:DB"
|
||||||
|
preset1_deg = 343.600000
|
||||||
|
preset2_deg = 343.600000
|
||||||
|
preset1_tilt_deg = -12.000000
|
||||||
|
preset2_tilt_deg = -12.000000
|
||||||
|
sector_min_deg = 343.600000
|
||||||
|
sector_max_deg = 343.600000
|
||||||
|
geo_last_source = "sector_autocal"
|
||||||
|
geo_last_at = 1770695209
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 5
|
||||||
|
ip = "192.168.54.10"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 2
|
||||||
|
wiper_channel = 1
|
||||||
|
is_ptz = false
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 6
|
||||||
|
ip = "192.168.54.11"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 1
|
||||||
|
wiper_channel = 1
|
||||||
|
preset1 = "1"
|
||||||
|
preset2 = "2"
|
||||||
|
sweep_sign = 1
|
||||||
|
is_ptz = true
|
||||||
|
mac = "80:BE:AF:E5:47:D5"
|
||||||
|
preset1_deg = 340.200000
|
||||||
|
preset2_deg = 129.000000
|
||||||
|
preset1_tilt_deg = -150.000000
|
||||||
|
preset2_tilt_deg = -150.000000
|
||||||
|
sector_min_deg = 342.200000
|
||||||
|
sector_max_deg = 127.000000
|
||||||
|
geo_last_source = "sector_autocal"
|
||||||
|
geo_last_at = 1770695210
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 7
|
||||||
|
ip = "192.168.54.11"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 2
|
||||||
|
wiper_channel = 1
|
||||||
|
is_ptz = false
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 8
|
||||||
|
ip = "192.168.54.12"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 1
|
||||||
|
wiper_channel = 1
|
||||||
|
preset1 = "1"
|
||||||
|
preset2 = "2"
|
||||||
|
sweep_sign = 1
|
||||||
|
is_ptz = true
|
||||||
|
mac = "80:BE:AF:E5:48:07"
|
||||||
|
preset1_deg = 346.300000
|
||||||
|
preset2_deg = 33.000000
|
||||||
|
preset1_tilt_deg = -118.000000
|
||||||
|
preset2_tilt_deg = -118.000000
|
||||||
|
sector_min_deg = 348.300000
|
||||||
|
sector_max_deg = 31.000000
|
||||||
|
geo_last_source = "sector_autocal"
|
||||||
|
geo_last_at = 1770695212
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 9
|
||||||
|
ip = "192.168.54.12"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 2
|
||||||
|
wiper_channel = 1
|
||||||
|
is_ptz = false
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 10
|
||||||
|
ip = "192.168.54.13"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 1
|
||||||
|
wiper_channel = 1
|
||||||
|
preset1 = "1"
|
||||||
|
preset2 = "2"
|
||||||
|
sweep_sign = 1
|
||||||
|
is_ptz = true
|
||||||
|
mac = "80:BE:AF:E5:47:C1"
|
||||||
|
preset1_deg = 340.700000
|
||||||
|
preset2_deg = 41.000000
|
||||||
|
preset1_tilt_deg = -120.000000
|
||||||
|
preset2_tilt_deg = -120.000000
|
||||||
|
sector_min_deg = 342.700000
|
||||||
|
sector_max_deg = 39.000000
|
||||||
|
geo_last_source = "sector_autocal"
|
||||||
|
geo_last_at = 1770695214
|
||||||
|
|
||||||
|
[[camera]]
|
||||||
|
id = 11
|
||||||
|
ip = "192.168.54.13"
|
||||||
|
username = "admin"
|
||||||
|
password = "Vika_1975"
|
||||||
|
ptz_channel = 2
|
||||||
|
wiper_channel = 1
|
||||||
|
is_ptz = false
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
from config import CPP_WS_URI, CPP_CTRL_WS_URI, DROP_DECODE_WHEN_BUSY, PUBLISH_RAW_BEFORE_INFER, PREVIEW_TARGET_W
|
||||||
|
from utils import json_loads, json_dumps
|
||||||
|
from state import detected_cameras, video_clients, ptz_states, detection_queue
|
||||||
|
from media import publish_preview, maybe_downscale
|
||||||
|
from detection import batcher
|
||||||
|
|
||||||
|
logger = logging.getLogger("PTZTracker")
|
||||||
|
|
||||||
|
async def cpp_ws_loop() -> None:
|
||||||
|
frames_total = 0
|
||||||
|
skipped_total = 0
|
||||||
|
last_log = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logger.info("[CPP WS] connecting to %s ...", CPP_WS_URI)
|
||||||
|
async with websockets.connect(
|
||||||
|
CPP_WS_URI, max_size=None, compression=None, ping_interval=None, close_timeout=1.0
|
||||||
|
) as ws:
|
||||||
|
logger.info("[CPP WS] connected")
|
||||||
|
async for raw in ws:
|
||||||
|
if isinstance(raw, (bytes, bytearray)):
|
||||||
|
buf = memoryview(raw)
|
||||||
|
if len(buf) < 3 or buf[0] != 1:
|
||||||
|
continue
|
||||||
|
idx = int.from_bytes(buf[1:3], "big")
|
||||||
|
from config import CAMERA_CONFIG
|
||||||
|
if idx not in CAMERA_CONFIG:
|
||||||
|
continue
|
||||||
|
if DROP_DECODE_WHEN_BUSY and ptz_states[idx]["proc_busy"] and (not video_clients):
|
||||||
|
skipped_total += 1
|
||||||
|
continue
|
||||||
|
arr = np.frombuffer(buf[3:], dtype=np.uint8)
|
||||||
|
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
||||||
|
if img is None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
msg = json_loads(raw)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if msg.get("type") != "image":
|
||||||
|
continue
|
||||||
|
idx_str, b64 = msg["data"].split("|", 1)
|
||||||
|
idx = int(idx_str)
|
||||||
|
from config import CAMERA_CONFIG
|
||||||
|
if idx not in CAMERA_CONFIG:
|
||||||
|
continue
|
||||||
|
if DROP_DECODE_WHEN_BUSY and ptz_states[idx]["proc_busy"] and (not video_clients):
|
||||||
|
skipped_total += 1
|
||||||
|
continue
|
||||||
|
arr = np.frombuffer(base64.b64decode(b64), dtype=np.uint8)
|
||||||
|
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
||||||
|
if img is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
frames_total += 1
|
||||||
|
h, w = img.shape[:2]
|
||||||
|
now = time.time()
|
||||||
|
if now - last_log >= 1.0:
|
||||||
|
logger.info("[CPP WS] recv: %d fps, skipped=%d | last cam=%s | %dx%d | clients=%d",
|
||||||
|
frames_total, skipped_total, idx, w, h, len(video_clients))
|
||||||
|
frames_total = 0; skipped_total = 0; last_log = now
|
||||||
|
|
||||||
|
detected_cameras.add(idx)
|
||||||
|
|
||||||
|
if PUBLISH_RAW_BEFORE_INFER and video_clients:
|
||||||
|
down, _, _ = maybe_downscale(img, PREVIEW_TARGET_W)
|
||||||
|
publish_preview(idx, down)
|
||||||
|
|
||||||
|
pst = ptz_states[idx]
|
||||||
|
if pst["proc_busy"]:
|
||||||
|
continue
|
||||||
|
pst["proc_busy"] = True
|
||||||
|
|
||||||
|
async def process_frame(cam_idx: int, frame):
|
||||||
|
try:
|
||||||
|
assert batcher is not None, "batcher is not initialized"
|
||||||
|
await batcher.submit(cam_idx, frame, cam_idx in from_state_ptz_ids())
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Error processing frame for camera %s: %s", cam_idx, exc)
|
||||||
|
finally:
|
||||||
|
ptz_states[cam_idx]["proc_busy"] = False
|
||||||
|
|
||||||
|
# helper to avoid circular import at top-level
|
||||||
|
def from_state_ptz_ids():
|
||||||
|
from state import PTZ_CAM_IDS
|
||||||
|
return PTZ_CAM_IDS
|
||||||
|
|
||||||
|
asyncio.create_task(process_frame(idx, img))
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("[CPP WS] error: %s", exc)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
async def cpp_detection_loop() -> None:
|
||||||
|
assert detection_queue is not None
|
||||||
|
RESYNC_EVERY_SEC = 10.0
|
||||||
|
|
||||||
|
def _snapshot_states():
|
||||||
|
from config import CAMERA_CONFIG
|
||||||
|
snap = []
|
||||||
|
for cid in CAMERA_CONFIG.keys():
|
||||||
|
st = ptz_states.get(cid, {})
|
||||||
|
snap.append({"IdCamera": int(cid), "detection": bool(st.get("rec_active", False))})
|
||||||
|
return snap
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logger.info("[CPP CTRL] connecting to %s ...", CPP_CTRL_WS_URI)
|
||||||
|
async with websockets.connect(
|
||||||
|
CPP_CTRL_WS_URI, max_size=None, compression=None, ping_interval=None, close_timeout=1.0
|
||||||
|
) as ws:
|
||||||
|
logger.info("[CPP CTRL] connected")
|
||||||
|
try:
|
||||||
|
for item in _snapshot_states():
|
||||||
|
await ws.send(json_dumps({"type": "detection", "data": item}))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("[CPP CTRL] initial resync failed: %s", e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
last_resync = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = time.time()
|
||||||
|
if now - last_resync >= RESYNC_EVERY_SEC and detection_queue.empty():
|
||||||
|
try:
|
||||||
|
for item in _snapshot_states():
|
||||||
|
await ws.send(json_dumps({"type": "detection", "data": item}))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("[CPP CTRL] periodic resync failed: %s", e)
|
||||||
|
break
|
||||||
|
last_resync = now
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = await asyncio.wait_for(detection_queue.get(), timeout=1.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ws.send(json_dumps(payload))
|
||||||
|
last_resync = time.time()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("[CPP CTRL] send failed: %s", exc)
|
||||||
|
try:
|
||||||
|
detection_queue.put_nowait(payload)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
detection_queue.task_done()
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("[CPP CTRL] error: %s", exc)
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
# ptz_tracker_modular/detection.py
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
import os as _os
|
||||||
|
|
||||||
|
from .config import (
|
||||||
|
INFER_IMGSZ, MODEL_CONF, TARGET_CLASSES,
|
||||||
|
W_EMA_ALPHA, DEFAULT_CLIENT_FPS, DRAW_ALL_ON_PANO,
|
||||||
|
DETECT_CONF, ALARM_CONF, RECOVER_ZOOM_OUT,
|
||||||
|
CENTER_GATE_IN, AIM_SMOOTH, AIM_P, AIM_I, AIM_D,
|
||||||
|
AUTO_STRATEGY, SHORT_LOSS_SEC, LONG_LOSS_SEC,
|
||||||
|
ABS_MIN_FALLBACK, ABS_MAX_FALLBACK,
|
||||||
|
)
|
||||||
|
# Сохранение старого имени для веса модели
|
||||||
|
YOLO_WEIGHTS = _os.getenv("YOLO_WEIGHTS", "C:/Users/Legion/Desktop/weight/electro-pribory-new.pt")
|
||||||
|
|
||||||
|
from .utils import clipf
|
||||||
|
from .state import ptz_states, detect_stats_global, detect_stats_cam, video_clients, batcher as _batcher
|
||||||
|
from .preview import publish_preview, maybe_downscale
|
||||||
|
from .postprocess import notify_detected, postprocess_control
|
||||||
|
from .events import queue_alarm
|
||||||
|
from .ptz_io import move_ptz_bounded, stop_ptz, call_autofocus
|
||||||
|
from .patrol import return_to_preset
|
||||||
|
|
||||||
|
from .streaming import MicroBatcher
|
||||||
|
from .model import yolo_forward
|
||||||
|
|
||||||
|
# Старый код иногда ждал detection.batcher — даём псевдоним на state.batcher
|
||||||
|
batcher = _batcher
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'INFER_IMGSZ', 'MODEL_CONF', 'TARGET_CLASSES',
|
||||||
|
'W_EMA_ALPHA', 'DEFAULT_CLIENT_FPS', 'DRAW_ALL_ON_PANO',
|
||||||
|
'DETECT_CONF','ALARM_CONF','RECOVER_ZOOM_OUT',
|
||||||
|
'CENTER_GATE_IN','AIM_SMOOTH','AIM_P','AIM_I','AIM_D',
|
||||||
|
'AUTO_STRATEGY','SHORT_LOSS_SEC','LONG_LOSS_SEC','ABS_MIN_FALLBACK','ABS_MAX_FALLBACK',
|
||||||
|
'YOLO_WEIGHTS',
|
||||||
|
'clipf', 'ptz_states','detect_stats_global','detect_stats_cam','video_clients',
|
||||||
|
'publish_preview','maybe_downscale','notify_detected','postprocess_control','queue_alarm',
|
||||||
|
'move_ptz_bounded','stop_ptz','call_autofocus','return_to_preset',
|
||||||
|
'MicroBatcher','yolo_forward','batcher'
|
||||||
|
]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import asyncio, socket, logging
|
||||||
|
from . import config
|
||||||
|
logger = logging.getLogger("PTZTracker")
|
||||||
|
|
||||||
|
async def heartbeat_pinger() -> None:
|
||||||
|
addr = (config.HEARTBEAT_HOST, config.HEARTBEAT_PORT)
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
try:
|
||||||
|
s.setblocking(False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
s.sendto(b"hb", addr)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("heartbeat send failed: %s", e)
|
||||||
|
await asyncio.sleep(config.HEARTBEAT_INTERVAL)
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging() -> logging.Logger:
|
||||||
|
"""Configure application logging and return the PTZTracker logger."""
|
||||||
|
os.makedirs("logs", exist_ok=True)
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler("logs/ptz_tracker.log", encoding="utf-8"),
|
||||||
|
logging.StreamHandler(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return logging.getLogger("PTZTracker")
|
||||||
|
|
||||||
|
|
||||||
|
logger = setup_logging()
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import logging, os, numpy as np, torch
|
||||||
|
from ultralytics import YOLO
|
||||||
|
from typing import List
|
||||||
|
from . import config
|
||||||
|
logger = logging.getLogger("PTZTracker")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from torch.amp import autocast # type: ignore
|
||||||
|
except Exception:
|
||||||
|
autocast = None # type: ignore
|
||||||
|
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
WEIGHT_PATH = os.getenv("YOLO_WEIGHTS", "/home/electro-pribory/Desktop/weight/electro-pribory.pt")
|
||||||
|
|
||||||
|
model_shared = YOLO(WEIGHT_PATH).to(device)
|
||||||
|
try:
|
||||||
|
model_shared.fuse()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
model_shared.model.to(memory_format=torch.channels_last) # type: ignore
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if device == "cuda":
|
||||||
|
try:
|
||||||
|
model_shared.model.half() # type: ignore
|
||||||
|
logger.info("Model set to FP16")
|
||||||
|
except Exception:
|
||||||
|
logger.info("FP16 not supported, using FP32")
|
||||||
|
try:
|
||||||
|
if hasattr(torch, "compile"):
|
||||||
|
model_shared.model = torch.compile(model_shared.model, mode="reduce-overhead") # type: ignore
|
||||||
|
logger.info("torch.compile enabled")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("torch.compile not used: %s", e)
|
||||||
|
|
||||||
|
model_shared.model.eval()
|
||||||
|
logger.info("Single shared model loaded")
|
||||||
|
|
||||||
|
try:
|
||||||
|
_warm = np.zeros((config.INFER_IMGSZ, config.INFER_IMGSZ, 3), dtype=np.uint8)
|
||||||
|
_ = model_shared([_warm], imgsz=config.INFER_IMGSZ, conf=0.1, iou=0.45, verbose=False)
|
||||||
|
if device == "cuda":
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
del _warm
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
INFER_ARGS = dict(imgsz=config.INFER_IMGSZ, conf=config.MODEL_CONF, iou=0.45, verbose=False)
|
||||||
|
|
||||||
|
@torch.inference_mode()
|
||||||
|
def yolo_forward(frames: List[np.ndarray]):
|
||||||
|
if device == "cuda" and autocast is not None:
|
||||||
|
with autocast("cuda", dtype=torch.float16): # type: ignore[misc]
|
||||||
|
return model_shared(frames, **INFER_ARGS)
|
||||||
|
return model_shared(frames, **INFER_ARGS)
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,201 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import time
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from typing import Any, Dict, Optional, Tuple, List, Set
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import config # нормальный пакетный импорт
|
||||||
|
except Exception: # если файл импортнули как топ-левел
|
||||||
|
import config as config # type: ignore
|
||||||
|
|
||||||
|
# ---------------- Clients / WS ----------------
|
||||||
|
vue_control_clients: Set[Any] = set()
|
||||||
|
video_clients: Set[Any] = set()
|
||||||
|
detected_cameras: Set[int] = set()
|
||||||
|
|
||||||
|
# Порядок и маппинг «плиток» (обновляется из streaming._broadcast_init_to_vue)
|
||||||
|
last_broadcast_ids: List[int] = []
|
||||||
|
index_to_cam_id: Dict[int, int] = {} # 1-based: 1 -> cam_id_1, 2 -> cam_id_2, ...
|
||||||
|
|
||||||
|
# ---------------- Queues/loop (init in main) ----------------
|
||||||
|
alarm_queue: Optional[Any] = None
|
||||||
|
detection_queue: Optional[Any] = None
|
||||||
|
MAIN_LOOP: Optional[Any] = None
|
||||||
|
|
||||||
|
# ---------------- PTZ thread + queue (init in main) ----------------
|
||||||
|
ptz_cmd_q: Optional[Any] = None
|
||||||
|
_ptz_thread: Optional[Any] = None
|
||||||
|
|
||||||
|
# ---------------- MicroBatcher (init in main) ----------------
|
||||||
|
batcher: Optional[Any] = None
|
||||||
|
|
||||||
|
# ---------------- Preview frames ----------------
|
||||||
|
latest_jpeg_by_cam: Dict[int, Tuple[int, bytes]] = {}
|
||||||
|
frame_id_by_cam: Dict[int, int] = {}
|
||||||
|
|
||||||
|
# ---------------- Detect stats ----------------
|
||||||
|
detect_stats_global = {"last_ts": None, "ema_interval": None, "times": deque(maxlen=512)}
|
||||||
|
detect_stats_cam = defaultdict(lambda: {"last_ts": None, "ema_interval": None, "times": deque(maxlen=256)})
|
||||||
|
|
||||||
|
# ---------------- Sync runtime (cohorts) ----------------
|
||||||
|
sync_state: Dict[str, Dict[str, Any]] = {
|
||||||
|
g: {"dir": +1, "leg_idx": 0, "leg_start": 0.0, "leg_end": 0.0, "next_start_at": 0.0}
|
||||||
|
for g in config.SYNC_COHORTS
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------- Default PTZ state factory ----------------
|
||||||
|
def _default_ptz_state(cam_id: int) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Полный набор ключей, чтобы все модули (postprocess/patrol/ptz_io/…)
|
||||||
|
не ловили KeyError при «горячем» добавлении камеры.
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
is_ptz = bool(cam_id in config.PTZ_CAM_IDS)
|
||||||
|
|
||||||
|
return {
|
||||||
|
# Tracking / kinematics
|
||||||
|
"last_seen": now,
|
||||||
|
"returning": False,
|
||||||
|
"last_dx": 0.0, "last_dy": 0.0,
|
||||||
|
"ix": 0.0, "iy": 0.0,
|
||||||
|
"vx": 0.0, "vy": 0.0,
|
||||||
|
"prev_t": None, "prev_cx": None, "prev_cy": None,
|
||||||
|
"prev_w_frac": None, "w_growth": 0.0, "w_frac_ema": None,
|
||||||
|
"mode": "IDLE",
|
||||||
|
|
||||||
|
# Image history
|
||||||
|
"prev_gray": None, "of_pts": None, "last_bbox": None,
|
||||||
|
|
||||||
|
# Zoom/aim dynamics
|
||||||
|
"zoom_state": 0,
|
||||||
|
"zoom_int": 0.0,
|
||||||
|
"zoom_prev_cmd": 0.0,
|
||||||
|
"zoom_ramp_start": 0.0,
|
||||||
|
"zoom_last_change": 0.0,
|
||||||
|
"zoom_lock": False,
|
||||||
|
"lock_candidate_since": None,
|
||||||
|
"was_zoomed_in": False,
|
||||||
|
"zoom_reset_timer": None, # asyncio.TimerHandle | None
|
||||||
|
|
||||||
|
# Preset timers
|
||||||
|
"preset_timer": None, # asyncio.TimerHandle | None
|
||||||
|
"preset_target": None, # str | int | None
|
||||||
|
|
||||||
|
# Command/throttle bookkeeping
|
||||||
|
"proc_busy": False,
|
||||||
|
"last_focus_time": 0.0,
|
||||||
|
"last_ptz_send": 0.0,
|
||||||
|
"last_cmd": (0.0, 0.0, 0.0), # (pan, tilt, zoom) -1..+1
|
||||||
|
"last_cmd_ts": 0.0,
|
||||||
|
"last_pan": 0.0,
|
||||||
|
"last_tilt": 0.0,
|
||||||
|
"last_zoom": 0.0,
|
||||||
|
"ptz_busy_until": 0.0,
|
||||||
|
"pan_sign": 1,
|
||||||
|
|
||||||
|
# Modes / loss recovery
|
||||||
|
"mode": "IDLE",
|
||||||
|
"loss_since": None,
|
||||||
|
"recover_until": 0.0,
|
||||||
|
"fallback_done": False,
|
||||||
|
"center_lock": 0,
|
||||||
|
"scan_phase": 0, "next_scan_at": 0.0,
|
||||||
|
"zoom_hold_until": 0.0,
|
||||||
|
"az_none_since": None,
|
||||||
|
"last_return_at": 0.0,
|
||||||
|
|
||||||
|
# Stats / UI
|
||||||
|
"track_history": deque(maxlen=12),
|
||||||
|
"stable_frames": 0,
|
||||||
|
"alarm_sent": False,
|
||||||
|
"miss_frames": 0,
|
||||||
|
|
||||||
|
# Patrol
|
||||||
|
"patrol_dir": +1,
|
||||||
|
"patrol_active": False,
|
||||||
|
"patrol_idx": 0, # <- пригодится управлялке патруля
|
||||||
|
"patrol_pause_until": 0.0, # <- мягкие паузы между ногами
|
||||||
|
"leg_start": 0.0,
|
||||||
|
"leg_end": 0.0,
|
||||||
|
"endpoint_latch": 0,
|
||||||
|
|
||||||
|
# Sweep
|
||||||
|
"sweep_active": False,
|
||||||
|
"sweep_pan": 0.0,
|
||||||
|
"sweep_start_at": 0.0,
|
||||||
|
|
||||||
|
# Anti-smear / shots
|
||||||
|
"shot_next_at": 0.0,
|
||||||
|
"shot_pause_until": 0.0,
|
||||||
|
|
||||||
|
# Absolute commands
|
||||||
|
"last_abs_send": 0.0,
|
||||||
|
"last_abs_deg": None,
|
||||||
|
|
||||||
|
# PTZ status (telemetry)
|
||||||
|
# общие поля, которые раньше были:
|
||||||
|
"az_deg": None, # относительный пан от драйвера (если такой есть)
|
||||||
|
"tilt_deg": None, # относительный тилт от драйвера (если такой есть)
|
||||||
|
# добавленные поля для абсолютной геометрии/оптики:
|
||||||
|
"pan_deg_abs": None, # абсолютный пан 0..360, привязанный к северу
|
||||||
|
"tilt_deg_abs": None, # абсолютный наклон (если доступен)
|
||||||
|
"hfov_deg": None, # точный HFOV PTZ на текущем зуме (если знаем)
|
||||||
|
"abs_zoom": 0.0, # абсолютное значение зума (масштаб зависит от твоего статус-пуллера)
|
||||||
|
|
||||||
|
# Sector (soft bounds)
|
||||||
|
"sector_left_deg": None,
|
||||||
|
"sector_right_deg": None,
|
||||||
|
"sector_center_deg": None,
|
||||||
|
|
||||||
|
# Sync mode
|
||||||
|
"sync_wait": False,
|
||||||
|
|
||||||
|
# Recording debounce (CPP CTRL channel)
|
||||||
|
"rec_active": False,
|
||||||
|
"rec_first_seen": None,
|
||||||
|
"rec_started_at": 0.0,
|
||||||
|
"rec_last_seen": 0.0,
|
||||||
|
"rec_last_off_at": 0.0,
|
||||||
|
"rec_last_sent": None,
|
||||||
|
|
||||||
|
# Meta
|
||||||
|
"is_ptz": is_ptz,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------- Runtime PTZ state map ----------------
|
||||||
|
ptz_states: Dict[int, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
# Инициализация для камер, которые уже были в конфиге на момент импорта
|
||||||
|
_now = time.time()
|
||||||
|
for _cid in config.CAMERA_CONFIG:
|
||||||
|
# Берём дефолты и при необходимости можно дополнять из существующего (если будет перезагрузка модуля)
|
||||||
|
ptz_states[_cid] = _default_ptz_state(_cid)
|
||||||
|
|
||||||
|
# ---------------- Public helpers ----------------
|
||||||
|
def ensure_cam_state(cam_id: int, *, is_ptz: Optional[bool] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Гарантирует наличие и полноту структуры состояния для cam_id.
|
||||||
|
Если камера уже есть — дольём недостающие ключи без перетирания.
|
||||||
|
is_ptz можно подсказать явно; если None — вычислим по config.PTZ_CAM_IDS.
|
||||||
|
"""
|
||||||
|
st = ptz_states.get(cam_id)
|
||||||
|
if st is None:
|
||||||
|
st = _default_ptz_state(cam_id)
|
||||||
|
if is_ptz is not None:
|
||||||
|
st["is_ptz"] = bool(is_ptz)
|
||||||
|
ptz_states[cam_id] = st
|
||||||
|
return st
|
||||||
|
|
||||||
|
# Дополним недостающие ключи (на случай старого состояния)
|
||||||
|
defaults = _default_ptz_state(cam_id)
|
||||||
|
for k, v in defaults.items():
|
||||||
|
if k not in st:
|
||||||
|
st[k] = v
|
||||||
|
|
||||||
|
# Актуализируем флаг is_ptz, если указан явно
|
||||||
|
if is_ptz is not None:
|
||||||
|
st["is_ptz"] = bool(is_ptz)
|
||||||
|
else:
|
||||||
|
st["is_ptz"] = bool(cam_id in config.PTZ_CAM_IDS)
|
||||||
|
|
||||||
|
return st
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue