#include "config.hpp" #include "bytetrack.hpp" #include "batch_store.hpp" #include "csv_logger.hpp" #include "rknn_yolo.hpp" #include "drawing.hpp" #include "nms.hpp" #include "version.hpp" #include #include #include #include #include #include #include #include #include #include #include static std::atomic shutdown_requested{false}; static void signal_handler(int) { shutdown_requested = true; std::cout << "\nShutdown requested — finishing current frame..." << std::endl; } static std::string now_str() { auto now = std::chrono::system_clock::now(); auto t = std::chrono::system_clock::to_time_t(now); std::ostringstream ss; ss << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S"); return ss.str(); } static cv::VideoCapture open_capture(const std::string& source) { bool is_live = source.substr(0, 7) == "rtsp://" || source.substr(0, 7) == "http://"; if (is_live) { setenv("OPENCV_FFMPEG_CAPTURE_OPTIONS", g_config.rtsp_ffmpeg_options.c_str(), 1); } cv::VideoCapture cap(source, cv::CAP_FFMPEG); cap.set(cv::CAP_PROP_BUFFERSIZE, 1); return cap; } static void warmup_stream(cv::VideoCapture& cap, int n) { std::cout << "Warming up stream..." << std::endl; cv::Mat frame; for (int i = 0; i < n; ++i) cap.read(frame); std::cout << "Stream ready!" << std::endl; } static bool connect_stream(cv::VideoCapture& cap, int& w, int& h, double& fps) { bool is_live = g_config.source.substr(0, 7) == "rtsp://" || g_config.source.substr(0, 7) == "http://"; int attempts = 0; while (!shutdown_requested) { cap = open_capture(g_config.source); if (cap.isOpened()) break; attempts++; if (g_config.max_reconnect_attempts > 0 && attempts >= g_config.max_reconnect_attempts) { std::cerr << "Cannot open source after " << attempts << " attempts: " << g_config.source << std::endl; return false; } std::cout << "Cannot open source, retry in " << g_config.reconnect_delay_sec << "s..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(g_config.reconnect_delay_sec)); } if (shutdown_requested) return false; if (is_live && g_config.warmup_frames > 0) warmup_stream(cap, g_config.warmup_frames); w = static_cast(cap.get(cv::CAP_PROP_FRAME_WIDTH)); h = static_cast(cap.get(cv::CAP_PROP_FRAME_HEIGHT)); fps = cap.get(cv::CAP_PROP_FPS); if (fps <= 1) fps = g_config.output_fps; return true; } struct TrackedInfo { float cx; std::chrono::steady_clock::time_point ts; }; static void prune_stale_tracks(std::unordered_map& tracked, double prune_sec) { auto now = std::chrono::steady_clock::now(); std::vector stale; for (auto& [tid, info] : tracked) { auto age = std::chrono::duration_cast(now - info.ts).count(); if (age > prune_sec) stale.push_back(tid); } for (int tid : stale) tracked.erase(tid); } int main(int argc, char* argv[]) { signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); if (argc < 2) { std::cout << "ByteTrack Counter (C++)" << std::endl; std::cout << "Version: " << version::GIT_COMMIT << " | branch: " << version::GIT_BRANCH << " | built: " << version::BUILD_DATE << " " << version::BUILD_TIME << " | rknn: " << (version::BUILD_RKNN ? "ON" : "OFF"); if (!version::GIT_TAG.empty()) std::cout << " | tag: " << version::GIT_TAG; std::cout << std::endl; std::cout << "Usage: bytetrack-counter " << std::endl; std::cout << " config.env path to environment configuration file (required)" << std::endl; return 1; } g_config.load_from_env(argv[1]); std::cout << "Starting ByteTrack Counter (C++)" << std::endl; std::cout << "Version: " << version::GIT_COMMIT << " | branch: " << version::GIT_BRANCH << " | built: " << version::BUILD_DATE << " " << version::BUILD_TIME << " | rknn: " << (version::BUILD_RKNN ? "ON" : "OFF"); if (!version::GIT_TAG.empty()) std::cout << " | tag: " << version::GIT_TAG; std::cout << std::endl; std::cout << "Camera: " << g_config.camera_name << std::endl; std::cout << "Source: " << g_config.source << std::endl; std::cout << "Model: " << g_config.model_path << " | imgsz=" << g_config.imgsz << " | core_mask=" << g_config.core_mask << std::endl; std::cout << "DB: " << g_config.db_path << std::endl; std::cout << "State: " << g_config.state_file << std::endl; std::cout << "ByteTrack Index 0: high_thresh=" << g_config.track_high_thresh_0 << " low_thresh=" << g_config.track_low_thresh_0 << " match_thresh=" << g_config.track_match_thresh_0 << " buffer=" << g_config.track_buffer_0 << " min_hits=" << g_config.track_min_hits_0 << std::endl; std::cout << "ByteTrack Index 1: high_thresh=" << g_config.track_high_thresh_1 << " low_thresh=" << g_config.track_low_thresh_1 << " match_thresh=" << g_config.track_match_thresh_1 << " buffer=" << g_config.track_buffer_1 << " min_hits=" << g_config.track_min_hits_1 << std::endl; int class_ayam = g_config.ayam_class_id; int class_talenan = g_config.talenan_class_id; auto logger = [](const std::string& msg) { std::cout << "[" << now_str() << "] " << msg << std::endl; }; BatchStore store(g_config.db_path, g_config.state_file, g_config.camera_name, g_config.object_label, g_config.daily_cutoff_time, g_config.batch_timeout_seconds, g_config.ignore_batch_label_timeout, g_config.min_object_per_batch, g_config.min_duration_per_batch, logger); store.start_cutoff_watcher(); std::unique_ptr cross_logger; if (g_config.export_csv) { cross_logger = std::make_unique(g_config.cross_csv, std::vector{"batch", "frame", "timestamp", "chicken_id"}); } RKNNYOLO model(g_config.model_path, g_config.core_mask, g_config.imgsz, g_config.conf, 0.45f, g_config.num_classes, 0, g_config.score_sigmoid); ByteTracker ayam_tracker(g_config.track_high_thresh_0, g_config.track_low_thresh_0, g_config.track_match_thresh_0, g_config.track_buffer_0, g_config.track_min_hits_0); ByteTracker talenan_tracker(g_config.track_high_thresh_1, g_config.track_low_thresh_1, g_config.track_match_thresh_1, g_config.track_buffer_1, g_config.track_min_hits_1); std::unordered_set ayam_line_crossed; std::unordered_set talenan_line_crossed; std::unordered_map ayam_cross_flash; std::unordered_map talenan_cross_flash; int line_pulse = 0, count_pulse = 0, batch_pulse = 0; std::vector popups; std::deque crossing_times; auto session_start = std::chrono::steady_clock::now(); int frame_idx = 0; float inf_ema = 0.0f; std::string model_name = g_config.model_path; auto slash = model_name.find_last_of("/\\"); if (slash != std::string::npos) model_name = model_name.substr(slash + 1); cv::VideoCapture cap; int w, h; double fps_prop; if (!connect_stream(cap, w, h, fps_prop)) { 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 << " @ " << fps_prop << "fps | line x=" << line_x << " | cross=" << g_config.cross_direction << std::endl; int reconnect_count = 0; std::unordered_map ayam_tracked; std::unordered_map talenan_tracked; while (!shutdown_requested) { cv::Mat frame; cap >> frame; if (frame.empty()) { if (!is_live) break; reconnect_count++; std::cout << "Stream dropped (attempt " << reconnect_count << "), reconnecting..." << std::endl; cap.release(); std::this_thread::sleep_for(std::chrono::seconds(g_config.reconnect_delay_sec)); if (!connect_stream(cap, w, h, fps_prop)) break; line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac); continue; } auto elapsed_sec = std::chrono::duration( std::chrono::steady_clock::now() - session_start).count(); auto mono = std::chrono::steady_clock::now(); bool ayam_crossed_frame = false, batch_closed_frame = false, batch_started_frame = false; auto inf_start = std::chrono::steady_clock::now(); std::vector detections = model(frame); float inf_ms = std::chrono::duration( std::chrono::steady_clock::now() - inf_start).count(); inf_ema = inf_ema * 0.9f + inf_ms * 0.1f; if (!detections.empty()) { std::vector ayam_boxes, talenan_boxes; std::vector ayam_scores, talenan_scores; std::vector ayam_cx_list, talenan_cx_list; std::vector>> ayam_kpts_list; for (const auto& det : detections) { const auto& bbox = det.bbox; float cx = (bbox(0) + bbox(2)) / 2.0f; if (det.cls == class_talenan) { talenan_boxes.push_back(bbox); talenan_scores.push_back(det.score); talenan_cx_list.push_back(cx); } else if (det.cls == class_ayam) { ayam_boxes.push_back(bbox); ayam_scores.push_back(det.score); ayam_cx_list.push_back(cx); ayam_kpts_list.push_back(det.keypoints); } } int na = static_cast(ayam_boxes.size()); int nt = static_cast(talenan_boxes.size()); if (na > 0) store.ping_activity(); Eigen::MatrixXf ayam_mat(na, 4), talenan_mat(nt, 4); Eigen::VectorXf ayam_scores_vec(na), talenan_scores_vec(nt); for (int i = 0; i < na; ++i) { ayam_mat.row(i) = ayam_boxes[i]; ayam_scores_vec(i) = ayam_scores[i]; } for (int i = 0; i < nt; ++i) { talenan_mat.row(i) = talenan_boxes[i]; talenan_scores_vec(i) = talenan_scores[i]; } // Apply NMS per-class to suppress duplicate boxes if (na > 0) { auto keep = nms(ayam_mat, ayam_scores_vec); int nk = static_cast(keep.size()); Eigen::MatrixXf fm(nk, 4); Eigen::VectorXf fs(nk); std::vector fb; std::vector fcx; std::vector>> fkpts; for (int ki = 0; ki < nk; ++ki) { int idx = keep[ki]; fm.row(ki) = ayam_mat.row(idx); fs(ki) = ayam_scores_vec(idx); fb.push_back(ayam_boxes[idx]); fcx.push_back(ayam_cx_list[idx]); if (idx < (int)ayam_kpts_list.size()) fkpts.push_back(ayam_kpts_list[idx]); } na = nk; ayam_mat = std::move(fm); ayam_scores_vec = std::move(fs); ayam_boxes = std::move(fb); ayam_cx_list = std::move(fcx); ayam_kpts_list = std::move(fkpts); } if (nt > 0) { auto keep = nms(talenan_mat, talenan_scores_vec); int nk = static_cast(keep.size()); Eigen::MatrixXf fm(nk, 4); Eigen::VectorXf fs(nk); std::vector fb; std::vector fcx; for (int ki = 0; ki < nk; ++ki) { int idx = keep[ki]; fm.row(ki) = talenan_mat.row(idx); fs(ki) = talenan_scores_vec(idx); fb.push_back(talenan_boxes[idx]); fcx.push_back(talenan_cx_list[idx]); } nt = nk; talenan_mat = std::move(fm); talenan_scores_vec = std::move(fs); talenan_boxes = std::move(fb); talenan_cx_list = std::move(fcx); } TrackResult ayam_tr = ayam_tracker.update(ayam_mat, ayam_scores_vec); TrackResult talenan_tr = talenan_tracker.update(talenan_mat, talenan_scores_vec); // Process talenan crossings for (int di = 0; di < nt; ++di) { auto it = talenan_tr.det_to_track.find(di); if (it == talenan_tr.det_to_track.end()) continue; int tid = it->second; float cx = talenan_cx_list[di]; auto tit = talenan_tracked.find(tid); if (tit != talenan_tracked.end()) { float prev_cx = tit->second.cx; if (crossed_line(prev_cx, cx, line_x, g_config.cross_direction) && talenan_line_crossed.find(tid) == talenan_line_crossed.end()) { talenan_line_crossed.insert(tid); if (store.record_talenan_crossing(tid)) { batch_closed_frame = true; talenan_cross_flash[tid] = g_config.cross_flash_frames; popups.push_back({ static_cast(cx) - 20, static_cast((talenan_boxes[di](1) + talenan_boxes[di](3)) / 2), frame_idx, "BATCH CLOSED" }); } } } talenan_tracked[tid] = {cx, mono}; } // Process ayam crossings for (int di = 0; di < na; ++di) { auto it = ayam_tr.det_to_track.find(di); if (it == ayam_tr.det_to_track.end()) continue; int tid = it->second; float cx = ayam_cx_list[di]; auto tit = ayam_tracked.find(tid); if (tit != ayam_tracked.end()) { float prev_cx = tit->second.cx; if (crossed_line(prev_cx, cx, line_x, g_config.cross_direction) && ayam_line_crossed.find(tid) == ayam_line_crossed.end()) { ayam_line_crossed.insert(tid); auto [count, started_new] = store.record_ayam_crossing(tid); if (cross_logger) { std::ostringstream frame_str, tid_str; frame_str << frame_idx; tid_str << tid; cross_logger->write_row({ std::to_string(store.current_batch_number()), frame_str.str(), now_str(), tid_str.str() }); } ayam_crossed_frame = true; if (started_new) batch_started_frame = true; crossing_times.push_back(mono); ayam_cross_flash[tid] = g_config.cross_flash_frames; popups.push_back({ static_cast(cx) - 12, static_cast((ayam_boxes[di](1) + ayam_boxes[di](3)) / 2), frame_idx, "+1" }); } } ayam_tracked[tid] = {cx, mono}; } // Track lost tracks for continuity for (auto& [tid, cx] : ayam_tr.lost_map) { if (ayam_tracked.find(tid) == ayam_tracked.end()) ayam_tracked[tid] = {cx, mono}; } // Draw talenan for (int di = 0; di < nt; ++di) { auto it = talenan_tr.det_to_track.find(di); if (it == talenan_tr.det_to_track.end()) continue; int tid = it->second; const auto& bbox = talenan_boxes[di]; int x1 = static_cast(bbox(0)), y1 = static_cast(bbox(1)); int x2 = static_cast(bbox(2)), y2 = static_cast(bbox(3)); int flash = talenan_cross_flash.count(tid) ? talenan_cross_flash[tid] : 0; cv::Scalar color = flash > 0 ? C_GREEN : C_TALENAN_BOX; cv::rectangle(frame, cv::Point(x1, y1), cv::Point(x2, y2), color, flash > 0 ? 3 : 2); draw_pill(frame, "TALENAN " + std::to_string(tid), x1, y1 - 4, color); } // Draw ayam for (int di = 0; di < na; ++di) { auto it = ayam_tr.det_to_track.find(di); if (it == ayam_tr.det_to_track.end()) continue; int tid = it->second; const auto& bbox = ayam_boxes[di]; int x1 = static_cast(bbox(0)), y1 = static_cast(bbox(1)); int x2 = static_cast(bbox(2)), y2 = static_cast(bbox(3)); int flash = ayam_cross_flash.count(tid) ? ayam_cross_flash[tid] : 0; cv::Scalar color = flash > 0 ? C_GREEN : C_AYAM_BOX; cv::rectangle(frame, cv::Point(x1, y1), cv::Point(x2, y2), color, flash > 0 ? 3 : 2); draw_pill(frame, "ID " + std::to_string(tid), x1, y1 - 4, color); if (di < (int)ayam_kpts_list.size() && !ayam_kpts_list[di].empty()) draw_skeleton_bold(frame, ayam_kpts_list[di]); } } if (ayam_crossed_frame) { line_pulse = g_config.line_pulse_frames; count_pulse = g_config.count_pulse_frames; } if (batch_closed_frame) { line_pulse = g_config.line_pulse_frames; } if (batch_started_frame) { batch_pulse = g_config.batch_pulse_frames; } int batch_num = store.current_batch_number(); int batch_count = store.current_batch_count(); int display_total = store.display_total(); while (!crossing_times.empty() && std::chrono::duration(mono - crossing_times.front()).count() > g_config.rate_window_sec) crossing_times.pop_front(); double rate = crossing_times.empty() ? 0.0 : (crossing_times.size() / static_cast(g_config.rate_window_sec) * 60.0); draw_elegant_counting_line(frame, line_x, h, line_pulse); draw_hero_count(frame, line_x, h, batch_count, count_pulse); draw_hud(frame, w, batch_num, batch_count, display_total, elapsed_sec, rate); draw_batch_banner(frame, w, batch_num, batch_pulse); draw_footer(frame, w, h, frame_idx, is_live ? "LIVE" : "FILE", inf_ema, model_name); popups = draw_popups(frame, popups, frame_idx); // Flash decay for (auto& flash_map : {&ayam_cross_flash, &talenan_cross_flash}) { for (auto it = flash_map->begin(); it != flash_map->end(); ) { it->second--; if (it->second <= 0) it = flash_map->erase(it); else ++it; } } line_pulse = std::max(0, line_pulse - 1); count_pulse = std::max(0, count_pulse - 1); 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()); std::vector jpeg; cv::imencode(".jpg", 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(jpeg.data()), jpeg.size()); } catch (...) {} } frame_idx++; if (frame_idx % g_config.flush_every_n_frames == 0) { std::cout << "[" << now_str() << "] Frame " << frame_idx << " | Batch " << (batch_num ? std::to_string(batch_num) : "-") << ": " << (batch_num ? std::to_string(batch_count) : "-") << " | Total: " << display_total << " | Uptime " << (elapsed_sec / 3600.0) << "h" << std::endl; } prune_stale_tracks(ayam_tracked, g_config.tracked_prune_sec); prune_stale_tracks(talenan_tracked, g_config.tracked_prune_sec); } cap.release(); store.shutdown(); std::cout << "\n=== Batch Summary (SQLite) ===" << std::endl; std::cout << "Database: " << g_config.db_path << std::endl; return 0; }