Add pause-on-first-frame for file sources (.continue resume)

This commit is contained in:
proitlab committed 2026-08-10 10:16:07 +07:00
1 parent 139cff7474
commit 1d1199fa04
8 files changed
+78 -18

No files matched your search

+10
View File
@@ -68,3 +68,13 @@ bytetrack-counter <config.env> --source /tmp/test.mp4 # local file (single
bytetrack-counter <config.env> --source rtsp://cam:554/x # live stream
bytetrack-counter <config.env> -s path # or --source=path
```
## Pause on first frame (file sources)
For local file input, hold the first frame as the live stream view until signalled:
- Enable via `--pause-on-first-frame` (CLI, overrides env) or `PAUSE_ON_FIRST_FRAME=true`.
- Frame 0 is fully processed (inference, tracking, HUD) then the loop idles: the annotated
frame is re-written to `LIVE_STREAM_FRAME_PATH` while polling for `SHM_DIR/.continue`.
- Resume by `touch /dev/shm/bytetrack-counter/.continue` (file is deleted on consume).
- Ignored for live (rtsp/http) sources with a warning. A stale `.continue` is cleared at startup.
+4 -1
View File
@@ -23,12 +23,13 @@ env: .env KEY=VALUE file — all config per config.env.example
env: SHM_DIR → shared memory directory (/dev/shm/bytetrack-counter)
env: LIVE_STREAM_FRAME_PATH → JPEG snapshot path
env: RECORD_VIDEO, RECORD_DELAY_SECONDS, RECORD_END_DELAY, RECORD_RETENTION_DAYS, RECORD_CRF, RECORD_PRESET
cmd: bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>]
cmd: bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>] [--pause-on-first-frame]
db: SQLite batches(id,counting_date,batch_number,camera_name,object_label,count,start_time,end_time)
db: SQLite daily_summaries(id,counting_date,camera_name,object_label,total_count,total_batches)
file: JSON state — batch_number,count,start_time,last_detection_time,counted_event_ids[]
file: CSV crossing log — batch,frame,timestamp,chicken_id
file: SHM_DIR/.reset → trigger full state reset
file: SHM_DIR/.continue → resume processing after pause-on-first-frame
file: SHM_DIR/batch_pending_*.mp4 → confirmed when ayam detected, moved to OUTPUT_DIR/<batch counting_date>/batch_<num>_<YYYYMMDD>_<HHMMSS>.mp4 (num=batch, YYYYMMDD=counting_date, HHMMSS=move time)
sig: SIGTERM|SIGINT → shutdown_requested → graceful exit
```
@@ -51,6 +52,7 @@ V12: SHM_DIR/.reset → BatchStore::reset() — end active batch, clear memory,
V13: video recording: motion → SPECULATING (≤RECORD_DELAY_SECONDS), ayam → CONFIRMED → talenan/batch-end → STOPPING (+RECORD_END_DELAY) → move to OUTPUT_DIR/<batch counting_date>/ via fork
V14: video retention: day-dirs > RECORD_RETENTION_DAYS old → removed @ startup & @ each CUTOFF_TIME (localtime cutoff matches folder names)
V15: RECORD_CRF (lower = better, -1 unset) & RECORD_PRESET → OPENCV_FFMPEG_WRITE_OPTIONS before writer_.open() (crf;<n>|preset;<p>)
V16: pause_on_first_frame (env/--pause-on-first-frame, file source only; live ignored w/ warn): process frame 0 → idle-loop, re-write annotated frame to LIVE_STREAM_FRAME_PATH, wait for SHM_DIR/.continue → delete → resume
```
## §T — Tasks
@@ -75,6 +77,7 @@ T15|x|Graceful shutdown: SIGTERM → close batch → save state|I.sig
T16|x|Video per-batch recording: motion-triggered → speculating in shm, confirmed when ayam detected ≤ RECORD_DELAY_SECONDS, stopped RECORD_END_DELAY after batch close, moved to OUTPUT_DIR/<batch counting_date>/ via fork, retention cleanup at startup & cutoff, H.264 quality via RECORD_CRF/RECORD_PRESET (OPENCV_FFMPEG_WRITE_OPTIONS). Unconfirmed recordings deleted.|V13,V14,V15
T17|~?|Python DEBUG_TRACKING equivalent (per-frame score/track dump)|-
T18|x|CLI --source/-s/--source= override of SOURCE (rtsp|http|local mp4; file = single pass)|I.cmd
T19|x|Pause-on-first-frame for file source: process frame 0, hold annotated frame as live view, wait for SHM_DIR/.continue, resume. Env PAUSE_ON_FIRST_FRAME + --pause-on-first-frame; stale .continue cleared @ startup; live source ignored w/ warning.|V16
```
## §B — Bugs
Binary file not shown.
+3 -3
View File
@@ -4,11 +4,11 @@
namespace version {
const std::string GIT_COMMIT = "ef17a910";
const std::string GIT_COMMIT = "139cff74";
const std::string GIT_BRANCH = "main";
const std::string GIT_TAG = "";
const std::string BUILD_DATE = "2026-08-07";
const std::string BUILD_TIME = "23:10:08";
const std::string BUILD_DATE = "2026-08-10";
const std::string BUILD_TIME = "10:11:04";
const int BUILD_YEAR = 2026;
const bool BUILD_RKNN = 1;
+4
View File
@@ -121,6 +121,10 @@ MOTION_THRESHOLD=5.0
RATE_WINDOW_SEC=60
# Number of frames to discard at startup to let the stream buffer stabilise
WARMUP_FRAMES=30
# Pause after the first frame of a FILE source, holding it as the live stream view,
# until a '.continue' file is placed in SHM_DIR (true/false, default: false).
# Ignored for live (rtsp/http) sources. CLI: --pause-on-first-frame (overrides env).
PAUSE_ON_FIRST_FRAME=false
# Delay in seconds between stream reconnection attempts
RECONNECT_DELAY_SEC=3
# Maximum reconnection attempts (0 = infinite)
+3
View File
@@ -124,6 +124,9 @@ void AppConfig::load_from_env(const char* env_path) {
auto mt_s = get_env("MOTION_THRESHOLD");
if (!mt_s.empty()) motion_threshold = std::stof(mt_s);
auto poff_s = get_env("PAUSE_ON_FIRST_FRAME");
pause_on_first_frame = (poff_s == "true");
auto wf_s = get_env("WARMUP_FRAMES");
if (!wf_s.empty()) warmup_frames = std::stoi(wf_s);
auto rd_s = get_env("RECONNECT_DELAY_SEC");
+1
View File
@@ -53,6 +53,7 @@ struct AppConfig {
bool motion_detection_enabled = false;
float motion_threshold = 5.0f;
bool pause_on_first_frame = false;
int warmup_frames = 30;
int reconnect_delay_sec = 3;
int max_reconnect_attempts = 0;
+53 -14
View File
@@ -101,6 +101,7 @@ int main(int argc, char* argv[]) {
signal(SIGTERM, signal_handler);
std::string cli_source;
bool cli_pause = false;
for (int i = 2; i < argc; ++i) {
std::string a = argv[i];
if (a == "--source" || a == "-s") {
@@ -112,6 +113,8 @@ int main(int argc, char* argv[]) {
}
} else if (a.rfind("--source=", 0) == 0) {
cli_source = a.substr(9);
} else if (a == "--pause-on-first-frame") {
cli_pause = true;
}
}
@@ -124,19 +127,33 @@ int main(int argc, char* argv[]) {
if (!version::GIT_TAG.empty())
std::cout << " | tag: " << version::GIT_TAG;
std::cout << std::endl;
std::cout << "Usage: bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>]" << std::endl;
std::cout << "Usage: bytetrack-counter <config.env> [--source <rtsp://|http://|/path/to/file.mp4>] [--pause-on-first-frame]" << std::endl;
std::cout << " config.env path to environment configuration file (required)" << std::endl;
std::cout << " --source, -s override video source (rtsp://, http://, or local file)" << std::endl;
std::cout << " --pause-on-first-frame for file input: show first frame, pause until .continue in SHM_DIR" << std::endl;
return 1;
}
g_config.load_from_env(argv[1]);
if (cli_pause) {
g_config.pause_on_first_frame = true;
std::cout << "Pause-on-first-frame enabled by CLI" << std::endl;
}
if (!cli_source.empty()) {
g_config.source = cli_source;
std::cout << "Source overridden by CLI: " << g_config.source << std::endl;
}
bool is_live = g_config.source.substr(0, 7) == "rtsp://" ||
g_config.source.substr(0, 7) == "http://";
if (g_config.pause_on_first_frame && is_live) {
std::cerr << "Warning: --pause-on-first-frame ignored (source is live, not a file)" << std::endl;
g_config.pause_on_first_frame = false;
}
std::cout << "Starting ByteTrack Counter (C++)" << std::endl;
std::cout << "Version: " << version::GIT_COMMIT
<< " | branch: " << version::GIT_BRANCH
@@ -181,6 +198,8 @@ int main(int argc, char* argv[]) {
std::filesystem::remove(g_config.live_stream_frame_path, ec);
std::filesystem::remove(reset_file, ec);
}
std::error_code ce;
std::filesystem::remove(g_config.shm_dir + "/.continue", ce);
}
int class_ayam = g_config.ayam_class_id;
@@ -248,9 +267,6 @@ int main(int argc, char* argv[]) {
store.shutdown();
return shutdown_requested ? 0 : 1;
}
bool is_live = g_config.source.substr(0, 7) == "rtsp://" ||
g_config.source.substr(0, 7) == "http://";
int line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac);
std::cout << "RKNN+ByteTrack counter | " << w << "x" << h << " @ "
@@ -273,6 +289,19 @@ int main(int argc, char* argv[]) {
int reconnect_count = 0;
bool blank_live_frame = false;
bool paused = false;
auto write_live_jpeg = [](const cv::Mat& out_frame, int quality) {
try {
std::filesystem::path p(g_config.live_stream_frame_path);
if (p.has_parent_path())
std::filesystem::create_directories(p.parent_path());
std::vector<uint8_t> jpeg;
cv::imencode(".jpg", out_frame, jpeg, {cv::IMWRITE_JPEG_QUALITY, quality});
std::ofstream f(g_config.live_stream_frame_path, std::ios::binary);
f.write(reinterpret_cast<const char*>(jpeg.data()), jpeg.size());
} catch (...) {}
};
std::unordered_map<int, TrackedInfo> ayam_tracked;
std::unordered_map<int, TrackedInfo> talenan_tracked;
@@ -595,21 +624,31 @@ int main(int argc, char* argv[]) {
batch_pulse = std::max(0, batch_pulse - 1);
if (g_config.live_stream_enabled && frame_idx % g_config.live_stream_every_n == 0) {
try {
std::filesystem::path p(g_config.live_stream_frame_path);
if (p.has_parent_path())
std::filesystem::create_directories(p.parent_path());
cv::Mat out_frame = frame;
if (blank_live_frame) {
out_frame = cv::Mat::zeros(frame.size(), frame.type());
blank_live_frame = false;
}
std::vector<uint8_t> jpeg;
cv::imencode(".jpg", out_frame, jpeg,
{cv::IMWRITE_JPEG_QUALITY, g_config.live_stream_quality});
std::ofstream f(g_config.live_stream_frame_path, std::ios::binary);
f.write(reinterpret_cast<const char*>(jpeg.data()), jpeg.size());
} catch (...) {}
write_live_jpeg(out_frame, g_config.live_stream_quality);
}
if (g_config.pause_on_first_frame && !paused && frame_idx == 0) {
paused = true;
std::cout << "[" << now_str() << "] [PAUSE] First frame processed — "
<< "waiting for .continue in " << g_config.shm_dir << std::endl;
cv::Mat pause_frame = frame.clone();
while (!shutdown_requested && paused) {
write_live_jpeg(pause_frame, g_config.live_stream_quality);
std::string cont_file = g_config.shm_dir + "/.continue";
if (std::filesystem::exists(cont_file)) {
std::error_code ec;
std::filesystem::remove(cont_file, ec);
std::cout << "[" << now_str() << "] [PAUSE] .continue detected — resuming" << std::endl;
paused = false;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
}
frame_idx++;