diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c8f6a7a4..b02dfd96 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,10 +34,7 @@ jobs: - name: Run unittests env: API_URL: ${{ secrets.API_URL }} - API_LOGIN: ${{ secrets.API_LOGIN }} - API_PWD: ${{ secrets.API_PWD }} - LAT: 48.88 - LON: 2.38 + API_TOKEN: ${{ secrets.API_TOKEN }} run: | coverage run -m pytest tests/ coverage xml diff --git a/pyproject.toml b/pyproject.toml index ff9e5057..210c6374 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dynamic = ["version"] dependencies = [ "onnxruntime==1.18.1", "ncnn==1.0.20240410", - "pyroclient @ git+https://github.com/pyronear/pyro-api.git@767be30a781b52b29d68579d543e3f45ac8c4713#egg=pyroclient&subdirectory=client", + "pyroclient @ git+https://github.com/pyronear/pyro-api.git@main#egg=pyroclient&subdirectory=client", "requests>=2.20.0,<3.0.0", "tqdm>=4.62.0", "huggingface_hub==0.23.1", diff --git a/pyroengine/engine.py b/pyroengine/engine.py index ff9b0730..b1734b36 100644 --- a/pyroengine/engine.py +++ b/pyroengine/engine.py @@ -56,8 +56,6 @@ class Engine: conf_thresh: confidence threshold to send an alert api_url: url of the pyronear API cam_creds: api credectials for each camera, the dictionary should be as the one in the example - latitude: device latitude - longitude: device longitude alert_relaxation: number of consecutive positive detections required to send the first alert, and also the number of consecutive negative detections before stopping the alert frame_size: Resize frame to frame_size before sending it to the api in order to save bandwidth (H, W) @@ -84,8 +82,6 @@ def __init__( max_bbox_size: float = 0.4, api_url: Optional[str] = None, cam_creds: Optional[Dict[str, Dict[str, str]]] = None, - latitude: Optional[float] = None, - longitude: Optional[float] = None, nb_consecutive_frames: int = 4, frame_size: Optional[Tuple[int, int]] = None, cache_backup_period: int = 60, @@ -105,15 +101,13 @@ def __init__( self.conf_thresh = conf_thresh # API Setup - if isinstance(api_url, str): - assert isinstance(latitude, float) and isinstance(longitude, float) and isinstance(cam_creds, dict) - self.latitude = latitude - self.longitude = longitude - self.api_client = {} + self.api_client: dict[str, Any] = {} if isinstance(api_url, str) and isinstance(cam_creds, dict): # Instantiate clients for each camera - for _id, vals in cam_creds.items(): - self.api_client[_id] = client.Client(api_url, vals["login"], vals["password"]) + for _id, (camera_token, _) in cam_creds.items(): + ip = _id.split("_")[0] + if ip not in self.api_client.keys(): + self.api_client[ip] = client.Client(camera_token, api_url) # Cache & relaxation self.frame_saving_period = frame_saving_period @@ -123,6 +117,7 @@ def __init__( self.cache_backup_period = cache_backup_period self.day_time_strategy = day_time_strategy self.save_captured_frames = save_captured_frames + self.cam_creds = cam_creds # Local backup self._backup_size = backup_size @@ -181,7 +176,7 @@ def _dump_cache(self) -> None: "frame_path": str(self._cache.joinpath(f"pending_frame{idx}.jpg")), "cam_id": info["cam_id"], "ts": info["ts"], - "localization": info["localization"], + "bboxes": info["bboxes"], } ) @@ -204,7 +199,8 @@ def _load_cache(self) -> None: def heartbeat(self, cam_id: str) -> Response: """Updates last ping of device""" - return self.api_client[cam_id].heartbeat() + ip = cam_id.split("_")[0] + return self.api_client[ip].heartbeat() def _update_states(self, frame: Image.Image, preds: np.ndarray, cam_key: str) -> int: """Updates the detection states""" @@ -244,10 +240,27 @@ def _update_states(self, frame: Image.Image, preds: np.ndarray, cam_key: str) -> iou_match = [np.max(iou) > 0 for iou in ious] output_predictions = preds[iou_match, :] + if len(output_predictions) == 0: + missing_bbox = combine_predictions + missing_bbox[:, -1] = 0 + + else: + # Add missing bboxes + ious = box_iou(combine_predictions[:, :4], output_predictions[:, :4]) + missing_bbox = combine_predictions[ious[0] == 0, :] + if len(missing_bbox): + missing_bbox[:, -1] = 0 + output_predictions = np.concatenate([output_predictions, missing_bbox]) + # Limit bbox size for api output_predictions = np.round(output_predictions, 3) # max 3 digit output_predictions = output_predictions[:5, :] # max 5 bbox + # Add default bbox + if len(output_predictions) == 0: + output_predictions = np.zeros((1, 5)) + output_predictions[:, 2:4] += 0.0001 + self._states[cam_key]["last_predictions"].append( (frame, preds, output_predictions.tolist(), datetime.now(timezone.utc).isoformat(), False) ) @@ -295,12 +308,10 @@ def predict(self, frame: Image.Image, cam_id: Optional[str] = None) -> float: # Alert if conf > self.conf_thresh and len(self.api_client) > 0 and isinstance(cam_id, str): # Save the alert in cache to avoid connection issues - for idx, (frame, preds, localization, ts, is_staged) in enumerate( - self._states[cam_key]["last_predictions"] - ): + for idx, (frame, preds, bboxes, ts, is_staged) in enumerate(self._states[cam_key]["last_predictions"]): if not is_staged: - self._stage_alert(frame, cam_id, ts, localization) - self._states[cam_key]["last_predictions"][idx] = frame, preds, localization, ts, True + self._stage_alert(frame, cam_id, ts, bboxes) + self._states[cam_key]["last_predictions"][idx] = frame, preds, bboxes, ts, True # Check if it's time to backup pending alerts ts = datetime.now(timezone.utc) @@ -310,7 +321,7 @@ def predict(self, frame: Image.Image, cam_id: Optional[str] = None) -> float: return float(conf) - def _stage_alert(self, frame: Image.Image, cam_id: str, ts: int, localization: list) -> None: + def _stage_alert(self, frame: Image.Image, cam_id: str, ts: int, bboxes: list) -> None: # Store information in the queue self._alerts.append( { @@ -319,53 +330,40 @@ def _stage_alert(self, frame: Image.Image, cam_id: str, ts: int, localization: l "ts": ts, "media_id": None, "alert_id": None, - "localization": localization, + "bboxes": bboxes, } ) def _process_alerts(self) -> None: - for _ in range(len(self._alerts)): - # try to upload the oldest element - frame_info = self._alerts[0] - cam_id = frame_info["cam_id"] - logging.info(f"Camera '{cam_id}' - Sending alert from {frame_info['ts']}...") - - # Save alert on device - self._local_backup(frame_info["frame"], cam_id) - - try: - # Media creation - if not isinstance(self._alerts[0]["media_id"], int): - self._alerts[0]["media_id"] = self.api_client[cam_id].create_media_from_device().json()["id"] - # Alert creation - if not isinstance(self._alerts[0]["alert_id"], int): - self._alerts[0]["alert_id"] = ( - self.api_client[cam_id] - .send_alert_from_device( - lat=self.latitude, - lon=self.longitude, - media_id=self._alerts[0]["media_id"], - localization=self._alerts[0]["localization"], - ) - .json()["id"] - ) - # Media upload - stream = io.BytesIO() - frame_info["frame"].save(stream, format="JPEG", quality=self.jpeg_quality) - response = self.api_client[cam_id].upload_media( - self._alerts[0]["media_id"], - media_data=stream.getvalue(), - ) - # Force a KeyError if the request failed - response.json()["id"] - # Clear - self._alerts.popleft() - logging.info(f"Camera '{cam_id}' - alert sent") - stream.seek(0) # "Rewind" the stream to the beginning so we can read its content - except (KeyError, ConnectionError) as e: - logging.warning(f"Camera '{cam_id}' - unable to upload cache") - logging.warning(e) - break + if self.cam_creds is not None: + for _ in range(len(self._alerts)): + # try to upload the oldest element + frame_info = self._alerts[0] + cam_id = frame_info["cam_id"] + logging.info(f"Camera '{cam_id}' - Sending alert from {frame_info['ts']}...") + + # Save alert on device + self._local_backup(frame_info["frame"], cam_id) + + try: + # Detection creation + 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] + ip = cam_id.split("_")[0] + response = self.api_client[ip].create_detection(stream.getvalue(), cam_azimuth, bboxes) + # Force a KeyError if the request failed + response.json()["id"] + # Clear + self._alerts.popleft() + logging.info(f"Camera '{cam_id}' - alert sent") + stream.seek(0) # "Rewind" the stream to the beginning so we can read its content + except (KeyError, ConnectionError) as e: + logging.warning(f"Camera '{cam_id}' - unable to upload cache") + logging.warning(e) + break def _local_backup(self, img: Image.Image, cam_id: Optional[str], is_alert: bool = True) -> None: """Save image on device diff --git a/pyroengine/sensors.py b/pyroengine/sensors.py index fc2ba9c8..bf8b1b85 100644 --- a/pyroengine/sensors.py +++ b/pyroengine/sensors.py @@ -46,6 +46,7 @@ def __init__( password: str, cam_type: str = "ptz", cam_poses: Optional[List[int]] = None, + cam_azimuths: Optional[List[int]] = None, protocol: str = "https", ): self.ip_address = ip_address @@ -53,6 +54,7 @@ def __init__( self.password = password self.cam_type = cam_type self.cam_poses = cam_poses if cam_poses is not None else [] + self.cam_azimuths = cam_azimuths if cam_azimuths is not None else [] self.protocol = protocol if len(self.cam_poses): diff --git a/src/poetry.lock b/src/poetry.lock index b607d425..bc913ab0 100644 --- a/src/poetry.lock +++ b/src/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "certifi" @@ -147,31 +147,31 @@ cron = ["capturer (>=2.4)"] [[package]] name = "filelock" -version = "3.16.1" +version = "3.17.0" description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, - {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "flatbuffers" -version = "24.12.23" +version = "25.1.24" description = "The FlatBuffers serialization format for Python" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "flatbuffers-24.12.23-py2.py3-none-any.whl", hash = "sha256:c418e0d48890f4142b92fd3e343e73a48f194e1f80075ddcc5793779b3585444"}, - {file = "flatbuffers-24.12.23.tar.gz", hash = "sha256:2910b0bc6ae9b6db78dd2b18d0b7a0709ba240fb5585f286a3a2b30785c22dac"}, + {file = "flatbuffers-25.1.24-py2.py3-none-any.whl", hash = "sha256:1abfebaf4083117225d0723087ea909896a34e3fec933beedb490d595ba24145"}, + {file = "flatbuffers-25.1.24.tar.gz", hash = "sha256:e0f7b7d806c0abdf166275492663130af40c11f89445045fbef0aa3c9a8643ad"}, ] [[package]] @@ -216,14 +216,14 @@ tqdm = ["tqdm"] [[package]] name = "huggingface-hub" -version = "0.27.1" +version = "0.28.1" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "huggingface_hub-0.27.1-py3-none-any.whl", hash = "sha256:1c5155ca7d60b60c2e2fc38cbb3ffb7f7c3adf48f824015b219af9061771daec"}, - {file = "huggingface_hub-0.27.1.tar.gz", hash = "sha256:c004463ca870283909d715d20f066ebd6968c2207dae9393fdffb3c1d4d8f98b"}, + {file = "huggingface_hub-0.28.1-py3-none-any.whl", hash = "sha256:aa6b9a3ffdae939b72c464dbb0d7f99f56e649b55c3d52406f49e0a5a620c0a7"}, + {file = "huggingface_hub-0.28.1.tar.gz", hash = "sha256:893471090c98e3b6efbdfdacafe4052b20b84d59866fb6f54c33d9af18c303ae"}, ] [package.dependencies] @@ -236,13 +236,13 @@ tqdm = ">=4.42.1" typing-extensions = ">=3.7.4.3" [package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] hf-transfer = ["hf-transfer (>=0.1.4)"] inference = ["aiohttp"] -quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.5.0)"] +quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.9.0)"] tensorflow = ["graphviz", "pydot", "tensorflow"] tensorflow-testing = ["keras (<3.0)", "tensorflow"] testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] @@ -526,19 +526,19 @@ sympy = "*" [[package]] name = "opencv-python" -version = "4.10.0.84" +version = "4.11.0.86" description = "Wrapper package for OpenCV python bindings." optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"}, + {file = "opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec"}, ] [package.dependencies] @@ -723,16 +723,15 @@ develop = false requests = ">=2.31.0,<3.0.0" [package.extras] -dev = ["Jinja2 (<3.1)", "docutils (<0.18)", "furo (>=2022.3.4)", "mypy (==1.4.1)", "pytest (>=5.3.2)", "pytest-cov (>=3.0.0,<5.0.0)", "pytest-pretty (>=1.0.0,<2.0.0)", "sphinx (>=3.0.0,!=3.5.0)", "sphinx-copybutton (>=0.3.1)", "sphinxemoji (>=0.1.8)"] docs = ["Jinja2 (<3.1)", "docutils (<0.18)", "furo (>=2022.3.4)", "sphinx (>=3.0.0,!=3.5.0)", "sphinx-copybutton (>=0.3.1)", "sphinxemoji (>=0.1.8)"] -quality = ["mypy (==1.4.1)"] -test = ["pytest (>=5.3.2)", "pytest-cov (>=3.0.0,<5.0.0)", "pytest-pretty (>=1.0.0,<2.0.0)"] +quality = ["mypy (==1.10.0)"] +test = ["pytest (==8.3.4)", "pytest-cov (>=4.0.0,<5.0.0)", "pytest-pretty (>=1.0.0,<2.0.0)", "types-requests (>=2.0.0)"] [package.source] type = "git" url = "https://github.com/pyronear/pyro-api.git" -reference = "767be30a781b52b29d68579d543e3f45ac8c4713" -resolved_reference = "767be30a781b52b29d68579d543e3f45ac8c4713" +reference = "main" +resolved_reference = "d70b389382648730bf0822a471e3a808703c4175" subdirectory = "client" [[package]] @@ -962,4 +961,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "62f3b73d023ff58d510ec3edb8963498f110a2d578a74fa744deaae59ab11aef" +content-hash = "d3aaa187b6ff1a4294fc5a26bc317f04681e163eff405d611f89a42c5f744a1f" diff --git a/src/pyproject.toml b/src/pyproject.toml index 1bdda731..93e2b8ac 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" [tool.poetry.dependencies] python = "^3.9" -pyroclient = { git = "https://github.com/pyronear/pyro-api.git", rev = "767be30a781b52b29d68579d543e3f45ac8c4713", subdirectory = "client" } +pyroclient = { git = "https://github.com/pyronear/pyro-api.git", branch = "main", subdirectory = "client" } pyroengine = "^0.2.0" python-dotenv = ">=0.15.0" onnxruntime = "1.18.1" diff --git a/src/requirements.txt b/src/requirements.txt index 39d621a6..0b5dee34 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -2,23 +2,23 @@ certifi==2024.12.14 ; python_version >= "3.9" and python_version < "4" charset-normalizer==3.4.1 ; python_version >= "3.9" and python_version < "4" colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and platform_system == "Windows" coloredlogs==15.0.1 ; python_version >= "3.9" and python_version < "4.0" -filelock==3.16.1 ; python_version >= "3.9" and python_version < "4" -flatbuffers==24.12.23 ; python_version >= "3.9" and python_version < "4.0" +filelock==3.17.0 ; python_version >= "3.9" and python_version < "4" +flatbuffers==25.1.24 ; python_version >= "3.9" and python_version < "4.0" fsspec==2024.12.0 ; python_version >= "3.9" and python_version < "4" -huggingface-hub==0.27.1 ; python_version >= "3.9" and python_version < "4" +huggingface-hub==0.28.1 ; python_version >= "3.9" and python_version < "4" humanfriendly==10.0 ; python_version >= "3.9" and python_version < "4.0" idna==3.10 ; python_version >= "3.9" and python_version < "4" mpmath==1.3.0 ; python_version >= "3.9" and python_version < "4.0" ncnn==1.0.20240410 ; python_version >= "3.9" and python_version < "4.0" numpy==1.26.4 ; python_version >= "3.9" and python_version < "4" onnxruntime==1.18.1 ; python_version >= "3.9" and python_version < "4.0" -opencv-python==4.10.0.84 ; python_version >= "3.9" and python_version < "4.0" +opencv-python==4.11.0.86 ; python_version >= "3.9" and python_version < "4.0" packaging==24.2 ; python_version >= "3.9" and python_version < "4.0" pillow==11.1.0 ; python_version >= "3.9" and python_version < "4" portalocker==3.1.1 ; python_version >= "3.9" and python_version < "4.0" protobuf==5.29.3 ; python_version >= "3.9" and python_version < "4.0" pyreadline3==3.5.4 ; sys_platform == "win32" and python_version >= "3.9" and python_version < "4.0" -pyroclient @ git+https://github.com/pyronear/pyro-api.git@767be30a781b52b29d68579d543e3f45ac8c4713#subdirectory=client ; python_version >= "3.9" and python_version < "4" +pyroclient @ git+https://github.com/pyronear/pyro-api.git@d70b389382648730bf0822a471e3a808703c4175#subdirectory=client ; python_version >= "3.9" and python_version < "4" pyroengine==0.2.0 ; python_version >= "3.9" and python_version < "4" python-dotenv==1.0.1 ; python_version >= "3.9" and python_version < "4.0" pywin32==308 ; python_version >= "3.9" and python_version < "4.0" and platform_system == "Windows" diff --git a/src/run.py b/src/run.py index 6e33bf97..f11dddef 100644 --- a/src/run.py +++ b/src/run.py @@ -41,24 +41,24 @@ def main(args): splitted_cam_creds = {} cameras = [] for _ip, cam_data in cameras_credentials.items(): - cam_poses = [] - for creds in cam_data["credentials"]: - if cam_data["type"] == "ptz": - splitted_cam_creds[_ip + "_" + str(creds["posid"])] = creds - cam_poses.append(creds["posid"]) - else: - splitted_cam_creds[_ip] = creds - - cameras.append(ReolinkCamera(_ip, CAM_USER, CAM_PWD, cam_data["type"], cam_poses, args.protocol)) + 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): + splitted_cam_creds[_ip + "_" + str(pos_id)] = cam_data["token"], cam_azimuth + else: + cam_poses = [] + cam_azimuths = [cam_data["azimuth"]] + splitted_cam_creds[_ip] = cam_data["token"], cam_data["azimuth"] + + cameras.append(ReolinkCamera(_ip, CAM_USER, CAM_PWD, cam_data["type"], cam_poses, cam_azimuths, args.protocol)) engine = Engine( - args.model_path, - args.thresh, - args.max_bbox_size, - API_URL, - splitted_cam_creds, - LAT, - LON, + model_path=args.model_path, + conf_thresh=args.thresh, + max_bbox_size=args.max_bbox_size, + api_url=API_URL, + cam_creds=splitted_cam_creds, cache_folder=args.cache, backup_size=args.backup_size, nb_consecutive_frames=args.nb_consecutive_frames, @@ -117,7 +117,7 @@ def main(args): parser.add_argument("--send_alerts", type=bool, default=True, help="Save all captured frames locally") # Time config - parser.add_argument("--period", type=int, default=30, help="Number of seconds between each camera stream analysis") + parser.add_argument("--period", type=int, default=5, help="Number of seconds between each camera stream analysis") args = parser.parse_args() main(args) diff --git a/tests/test_engine.py b/tests/test_engine.py index 9df30146..a5749b2c 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,5 +1,6 @@ import json import os +import time from datetime import datetime, timezone from pathlib import Path @@ -17,7 +18,7 @@ def test_engine_offline(tmpdir_factory, mock_wildfire_image, mock_forest_image): # Cache saving _ts = datetime.now().isoformat() - engine._stage_alert(mock_wildfire_image, 0, datetime.now().isoformat(), localization="dummy") + engine._stage_alert(mock_wildfire_image, 0, datetime.now().isoformat(), bboxes="dummy") assert len(engine._alerts) == 1 assert engine._alerts[0]["ts"] < datetime.now().isoformat() and _ts < engine._alerts[0]["ts"] assert engine._alerts[0]["media_id"] is None @@ -33,7 +34,7 @@ def test_engine_offline(tmpdir_factory, mock_wildfire_image, mock_forest_image): "frame_path": str(engine._cache.joinpath("pending_frame0.jpg")), "cam_id": 0, "ts": engine._alerts[0]["ts"], - "localization": "dummy", + "bboxes": "dummy", } # Overrites cache files engine._dump_cache() @@ -52,7 +53,6 @@ def test_engine_offline(tmpdir_factory, mock_wildfire_image, mock_forest_image): assert isinstance(engine._states["-1"]["last_predictions"][0][0], Image.Image) assert engine._states["-1"]["last_predictions"][0][1].shape[0] == 0 assert engine._states["-1"]["last_predictions"][0][1].shape[1] == 5 - assert engine._states["-1"]["last_predictions"][0][2] == [] assert engine._states["-1"]["last_predictions"][0][3] < datetime.now().isoformat() assert engine._states["-1"]["last_predictions"][0][4] is False @@ -63,7 +63,6 @@ def test_engine_offline(tmpdir_factory, mock_wildfire_image, mock_forest_image): assert isinstance(engine._states["-1"]["last_predictions"][0][0], Image.Image) assert engine._states["-1"]["last_predictions"][1][1].shape[0] > 0 assert engine._states["-1"]["last_predictions"][1][1].shape[1] == 5 - assert engine._states["-1"]["last_predictions"][1][2] == [] assert engine._states["-1"]["last_predictions"][1][3] < datetime.now().isoformat() assert engine._states["-1"]["last_predictions"][1][4] is False @@ -85,17 +84,14 @@ 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") - lat = os.environ.get("LAT") - lon = os.environ.get("LON") - cam_creds = {"dummy_cam": {"login": os.environ.get("API_LOGIN"), "password": os.environ.get("API_PWD")}} + cam_creds = {"dummy_cam": (os.environ.get("API_TOKEN"), 0)} # Skip the API-related tests if the URL is not specified if isinstance(api_url, str): engine = Engine( api_url=api_url, + conf_thresh=0.01, cam_creds=cam_creds, - latitude=float(lat), - longitude=float(lon), nb_consecutive_frames=4, frame_saving_period=3, cache_folder=folder, @@ -105,9 +101,11 @@ def test_engine_online(tmpdir_factory, mock_wildfire_stream, mock_wildfire_image start_ts = datetime.now(timezone.utc).isoformat() response = engine.heartbeat("dummy_cam") assert response.status_code // 100 == 2 - ts = datetime.now(timezone.utc).isoformat() json_respone = response.json() - assert start_ts < json_respone["last_ping"] < ts + time.sleep(0.1) + ts = datetime.now(timezone.utc).isoformat() + + assert start_ts < json_respone["last_active_at"] < ts # Send an alert engine.predict(mock_wildfire_image, "dummy_cam") assert len(engine._states["dummy_cam"]["last_predictions"]) == 1