diff --git a/README.md b/README.md index fb93d577..a810e1be 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,9 @@ The important field for the camera API is: * `adapter` which selects the camera implementation -Other fields such as `type`, `azimuth`, `poses`, or `bbox_mask_url` are used by the engine and the API. +Other fields such as `type`, `pose_ids`, `poses`, or `bbox_mask_url` are used by the engine and the API. + +`pose_ids` contains the pose IDs from the pyro-api database. For static cameras it is a list with a single element; for PTZ cameras each entry corresponds positionally to the matching physical preset in `poses`. If `bbox_mask_url` is set, occlusion mask files are fetched at `{bbox_mask_url}_{pose_id}.json`. Below is one generic example for each adapter: `url`, `rtsp`, `reolink` static, `reolink` PTZ and `mock`. ```json @@ -139,7 +141,7 @@ Below is one generic example for each adapter: `url`, `rtsp`, `reolink` static, "name": "url_camera_1", "adapter": "url", "url": "http://user:password@camera-host:1234/cgi-bin/snapshot.cgi", - "azimuth": 0, + "pose_ids": [10], "id": "10", "bbox_mask_url": "", "poses": [], @@ -151,7 +153,7 @@ Below is one generic example for each adapter: `url`, `rtsp`, `reolink` static, "name": "rtsp_camera_1", "adapter": "rtsp", "rtsp_url": "rtsp://user:password@camera-host:554/live/STREAM_ID", - "azimuth": 0, + "pose_ids": [11], "id": "11", "bbox_mask_url": "https://example.com/occlusion-masks/rtsp_camera_1", "poses": [], @@ -163,7 +165,7 @@ Below is one generic example for each adapter: `url`, `rtsp`, `reolink` static, "name": "reolink_static_1", "adapter": "reolink", "type": "static", - "azimuth": 45, + "pose_ids": [12], "id": "12", "poses": [], "bbox_mask_url": "https://example.com/occlusion-masks/reolink_static_1", @@ -176,7 +178,7 @@ Below is one generic example for each adapter: `url`, `rtsp`, `reolink` static, "type": "ptz", "id": "13", "poses": [0, 1, 2, 3], - "azimuths": [0, 90, 180, 270], + "pose_ids": [20, 21, 22, 23], "bbox_mask_url": "https://example.com/occlusion-masks/reolink_ptz_1", "token": "JWT_TOKEN_HERE" }, @@ -186,7 +188,7 @@ Below is one generic example for each adapter: `url`, `rtsp`, `reolink` static, "adapter": "linovision", "type": "ptz", "poses": [0, 1, 2, 3], - "azimuths": [0, 90, 180, 270], + "pose_ids": [30, 31, 32, 33], "azimuth_offset_deg": 90, "bbox_mask_url": "https://example.com/occlusion-masks/linovision_ptz_1", "token": "JWT_TOKEN_HERE" @@ -196,7 +198,7 @@ Below is one generic example for each adapter: `url`, `rtsp`, `reolink` static, "name": "mock_camera_1", "adapter": "mock", "type": "static", - "azimuth": 0, + "pose_ids": [0], "id": "14", "poses": [], "bbox_mask_url": "", diff --git a/pyroengine/engine.py b/pyroengine/engine.py index a0b009a4..d0811443 100644 --- a/pyroengine/engine.py +++ b/pyroengine/engine.py @@ -155,8 +155,8 @@ def __init__( self.occlusion_masks: Dict[str, Tuple[Optional[str], Dict[Any, Any], int]] = {"-1": (None, {}, 0)} if isinstance(cam_creds, dict): - for cam_id, (_, azimuth, bbox_mask_url) in cam_creds.items(): - self.occlusion_masks[cam_id] = (bbox_mask_url, {}, int(azimuth)) + for cam_id, (_, pose_id, bbox_mask_url) in cam_creds.items(): + self.occlusion_masks[cam_id] = (bbox_mask_url, {}, pose_id) # Restore pending alerts cache self._alerts: deque = deque(maxlen=cache_size) @@ -300,16 +300,17 @@ def predict( ): logging.info(f"Update occlusion masks for cam {cam_key}") self._states[cam_key]["last_bbox_mask_fetch"] = time.time() - bbox_mask_url, bbox_mask_dict, azimuth = self.occlusion_masks[cam_key] + bbox_mask_url, bbox_mask_dict, pose_id = self.occlusion_masks[cam_key] if bbox_mask_url is not None: - full_url = f"{bbox_mask_url}_{azimuth}.json" + full_url = f"{bbox_mask_url}_{pose_id}.json" try: response = requests.get(full_url) + response.raise_for_status() bbox_mask_dict = response.json() - self.occlusion_masks[cam_key] = (bbox_mask_url, bbox_mask_dict, azimuth) + self.occlusion_masks[cam_key] = (bbox_mask_url, bbox_mask_dict, pose_id) logging.info(f"Downloaded occlusion masks for cam {cam_key} at {bbox_mask_url} :{bbox_mask_dict}") except requests.exceptions.RequestException: - logging.info(f"No occluson available for: {cam_key}") + logging.info(f"No occlusion masks available for: {cam_key}") # Inference with ONNX if fake_pred is None: @@ -396,20 +397,26 @@ def _process_alerts(self) -> None: try: # Detection creation + bboxes = self._alerts[0]["bboxes"] + if not bboxes: + logging.warning(f"Camera '{cam_id}' - skipping alert with empty bboxes") + self._alerts.popleft() + continue stream = io.BytesIO() frame_info["frame"].save(stream, format="JPEG", quality=self.jpeg_quality) - bboxes = self._alerts[0]["bboxes"] bboxes = [tuple(bboxe) for bboxe in bboxes] - _, cam_azimuth, _ = self.cam_creds[cam_id] + _, pose_id, _ = self.cam_creds[cam_id] ip = cam_id.split("_")[0] - response = self.api_client[ip].create_detection(stream.getvalue(), cam_azimuth, bboxes) + response = self.api_client[ip].create_detection(stream.getvalue(), bboxes, pose_id) try: - # Force a KeyError if the request failed response.json()["id"] except ValueError: logging.error(f"Camera '{cam_id}' - non-JSON response body: {response.text}") raise + except KeyError: + logging.error(f"Camera '{cam_id}' - unexpected API response: {response.text}") + raise # Clear self._alerts.popleft() diff --git a/requirements.txt b/requirements.txt index f665c286..f21d96d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ portalocker==3.2.0 ; python_version >= "3.11" and python_version < "4.0" protobuf==6.33.1 ; python_version >= "3.11" and python_version < "4.0" pyreadline3==3.5.4 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32" pyro-camera-api-client @ git+https://github.com/pyronear/pyro-engine.git@0f3ff6836d226334847af63e365e8849c2bced22#subdirectory=pyro_camera_api/client ; python_version >= "3.11" and python_version < "4.0" -pyroclient @ git+https://github.com/pyronear/pyro-api.git@9cba4afdf1d096436ca875bfde104f1a9bc1df24#subdirectory=client ; python_version >= "3.11" and python_version < "4.0" +pyroclient @ git+https://github.com/pyronear/pyro-api.git@119ff76266eee72ffeb06141a85d420506322fa8#subdirectory=client ; python_version >= "3.11" and python_version < "4.0" python-dotenv==1.1.0 ; python_version >= "3.11" and python_version < "4.0" pywin32==311 ; python_version >= "3.11" and python_version < "4.0" and platform_system == "Windows" pyyaml==6.0.3 ; python_version >= "3.11" and python_version < "4.0" diff --git a/src/run.py b/src/run.py index f7dacf16..fdd5a3ab 100644 --- a/src/run.py +++ b/src/run.py @@ -42,13 +42,11 @@ def main(args): if cam_data["type"] == "ptz": cam_poses = cam_data["poses"] - cam_azimuths = cam_data["azimuths"] - for pos_id, cam_azimuth in zip(cam_poses, cam_azimuths, strict=False): - splitted_cam_creds[_ip + "_" + str(pos_id)] = (cam_data["token"], cam_azimuth, bbox_mask_url) + cam_pose_ids = cam_data["pose_ids"] + for pos_id, pose_id in zip(cam_poses, cam_pose_ids, strict=False): + splitted_cam_creds[_ip + "_" + str(pos_id)] = (cam_data["token"], pose_id, bbox_mask_url) else: - cam_poses = [] - cam_azimuths = [cam_data["azimuth"]] - splitted_cam_creds[_ip] = cam_data["token"], cam_data["azimuth"], bbox_mask_url + splitted_cam_creds[_ip] = cam_data["token"], cam_data["pose_ids"][0], bbox_mask_url engine = Engine( model_path=args.model_path, diff --git a/tests/test_engine.py b/tests/test_engine.py index c4b8bfff..b1938690 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -131,7 +131,7 @@ def test_engine_online(tmpdir_factory, mock_wildfire_stream, mock_wildfire_image # With API load_dotenv(Path(__file__).parent.parent.joinpath(".env").absolute()) api_url = os.environ.get("API_URL") - cam_creds = {"dummy_cam": (os.environ.get("API_TOKEN"), 0, None)} + cam_creds = {"dummy_cam": (os.environ.get("API_TOKEN"), 1, None)} # Skip the API-related tests if the URL is not specified if isinstance(api_url, str):