Add pause-on-first-frame for file sources (.continue resume)
This commit is contained in:
1 parent
139cff7474
commit
1d1199fa04
8 files changed
+78
-18
No files matched your search
@@ -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.
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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++;
|
||||
|
||||
Reference in new issue
Block a user