Skip to content

fix(engine): decide day or night per frame and skip frozen frames - #459

Closed
MateoLostanlen wants to merge 5 commits into
developfrom
fix/engine-day-night-per-frame
Closed

MateoLostanlen wants to merge 5 commits into
developfrom
fix/engine-day-night-per-frame

Conversation

@MateoLostanlen

Copy link
Copy Markdown
Member

Part of pyronear/roadmap#16, engine side of #422.

  • Night patrols were only stopped when already stopped (inverted condition), so they ran all night. They are now stopped when running.
  • Day or night was decided from the last frame of the loop, so one stale or infrared frame could put the whole site to sleep in daylight. Each frame is now checked: night frames are not analyzed, and the site sleeps only when every frame of the pass is a night frame. A night frame also drops the camera's detection history, so evening frames are not combined with the next morning's.
  • A frame identical to the previous one for the same pose is skipped. A frozen camera at Epinay re-uploaded the same false positive every 30 s for 9 days.

@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.24%. Comparing base (a2604d4) to head (8443ba6).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
pyroengine/core.py 93.33% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #459      +/-   ##
===========================================
+ Coverage    80.51%   84.24%   +3.72%     
===========================================
  Files            6        6              
  Lines          847      882      +35     
===========================================
+ Hits           682      743      +61     
+ Misses         165      139      -26     
Flag Coverage Δ
unittests 84.24% <93.33%> (+3.72%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@MateoLostanlen

Copy link
Copy Markdown
Member Author

Tested on pi-test (Pi 5, Debian 13, Docker 29), branch develop + #419 + #459 + #461, with the fake Reolink cameras of pyronear/pyro-envdev#65: camera 1 serves a colour frame (never-smoke scenario), camera 2 the same frame converted to grayscale (max(R-G)=0, read as IR by is_day_time).

One camera at night: OK

  • Night frame for 172.30.0.12_5, skipping inference for every pose of camera 2.
  • Camera 1 analysed on the first pass (No wildfire), then Frame identical to the previous one for 172.30.0.11_5, skipping inference on every later pass (the fake camera holds its last frame).
  • No site sleep.

Both cameras at night: OK

  • Camera 1 switched to grayscale at 11:48:26. Next pass: all 8 frames Night frame. At 11:49:43: Stopped patrol for camera ... due to night for both, Nighttime detected by every camera, sleeping for 1 hour. The camera API got POST /patrol/stop_patrol for both and reports patrol_running: false.

Not tested: the morning wake-up after the 1 h sleep, and the detection history drop on a night frame.

… camera

- The pass only collected the frames it received, so one night frame next to
  a camera answering 204 (no fresh image, PR #458) read as "every camera
  reports night" and put the site to sleep for an hour. The site now sleeps
  only when every expected frame came back and all of them are night frames;
  one day frame still wakes it.
- After the night sleep, only the first camera was probed: in infrared or down,
  it kept the whole site asleep in daylight. Every camera is probed until one
  shows daylight, and the site stays asleep only when all of them answer night.
@Adelagric

Copy link
Copy Markdown

I looked at the evening and morning side of this change, for pyronear/vision-rd#157 (false alerts at the day/night transition).

This PR also fixes evening alerts sent at dawn

On develop, alerts staged during the last day pass are not sent before the night sleep. The night branch of main_loop never calls _process_alerts, and the day branch calls it before inference_loop. When the last frame of that pass is infrared, the site sleeps with the alerts still queued and sends them on wake-up. The API receives them in one burst: its "3 detections in 5 minutes" window is keyed on created_at, so the burst opens a sequence at once, notified at dawn with an evening started_at.

With this PR, a pass with at least one day frame keeps the site awake, so the queue is sent at the start of the next pass. The test below checks it: one PTZ camera, pose 1 still in colour and staging an alert, pose 2 already in infrared, then every pose in infrared.

Code Result
develop fails, 1 alert still queued at the night sleep
develop + _process_alerts before the night sleep passes
this PR passes

Could you add it to the PR so this does not come back?

Test for tests/test_core.py (needs from collections import deque)
class _NightSleepError(Exception):
    """Raised by the patched sleep to leave ``main_loop`` when the site goes to sleep for the night."""


@patch("pyroengine.core.PyroCameraAPIClient")
def test_main_loop_sends_pending_alerts_before_night_sleep(mock_client_class, mock_engine, monkeypatch):
    """Alerts staged during the last day pass must leave the device before the night sleep.

    Otherwise they wait in the cache until the morning: the API then receives a burst of evening
    detections, which fills its "3 detections in 5 minutes" window (keyed on reception time) at once
    and opens a sequence that is notified at dawn.
    """
    day_img = Image.new("RGB", (100, 100), (255, 200, 200))
    night_img = Image.new("RGB", (100, 100), (128, 128, 128))
    # Pass 1: pose 1 is still in colour (and stages an alert), pose 2 has switched to infrared.
    # Later passes: every pose is in infrared.
    frames = {"pass": 0}

    def latest_image(_ip, pose):
        if frames["pass"] == 1 and pose == 1:
            return day_img
        return night_img

    mock_client = mock_client_class.return_value
    mock_client.get_stream_status.return_value = {"active_pipelines": [], "active_ffmpeg": []}
    mock_client.get_latest_image.side_effect = latest_image
    mock_client.capture_image.return_value = night_img

    mock_engine._alerts = deque()

    def predict(frame, cam_id):
        # Only the colour frame carries a detection worth staging.
        if frame is day_img:
            mock_engine._alerts.append({"cam_id": cam_id})

    mock_engine.predict.side_effect = predict
    mock_engine._process_alerts.side_effect = mock_engine._alerts.clear

    pending_at_night_sleep = []

    def fake_sleep(seconds, *_args, **_kwargs):
        if seconds == 3600:
            pending_at_night_sleep.append(len(mock_engine._alerts))
            raise _NightSleepError

    camera_data = {"192.168.1.1": {"name": "cam1", "type": "ptz", "poses": [1, 2]}}
    controller = SystemController(mock_engine, camera_data, "http://fake.url")
    mock_engine.predict.reset_mock()
    mock_engine._alerts.clear()
    monkeypatch.setattr("pyroengine.core.time.sleep", fake_sleep)

    def inference_pass():
        frames["pass"] += 1
        SystemController.inference_loop(controller)

    controller.inference_loop = inference_pass
    controller.is_day = True

    with pytest.raises(_NightSleepError):
        controller.main_loop(period=30)

    mock_engine.predict.assert_any_call(day_img, "192.168.1.1_1")
    assert pending_at_night_sleep == [0], "alerts staged before nightfall were left in the cache overnight"

Two cases left, as questions

  1. The flush fails at dusk. _process_alerts logs the error and keeps the queue. If the next pass is all night, the site sleeps and the queue goes out in the morning, with the same effect as above. Should stale staged alerts be dropped on wake-up, or should the API ignore detections with an old recorded_at when it opens a sequence?
  2. A site mixes IR cameras with a camera that never switches to IR (Add configurable night detection strategy for cameras without IR support #387). That camera always gives a day frame, so the site never sleeps and the camera is analysed all night on dark colour frames. On develop this depended on camera order, since the last frame decided. Do such sites exist today? If so, this PR and Add configurable night detection strategy for cameras without IR support #387 may need to land together.

Checked locally on macOS with pytest tests/test_core.py: 14 passed on this branch with the test above added. ruff check and ruff format --check with the pinned ruff 0.14 are clean.

@MateoLostanlen
MateoLostanlen requested a review from fe51 October 29, 2026 08:56
@MateoLostanlen
MateoLostanlen force-pushed the fix/engine-day-night-per-frame branch from 41ce4cf to 8443ba6 Compare October 29, 2026 09:12
@MateoLostanlen
MateoLostanlen deleted the fix/engine-day-night-per-frame branch October 29, 2026 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants