#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 #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; } 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); const char* env_path = argc > 1 ? argv[1] : nullptr; g_config.load_from_env(env_path); std::cout << "Starting ByteTrack Counter (C++)" << 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 << std::endl; std::cout << "DB: " << g_config.db_path << std::endl; int class_ayam = 0; int class_talenan = 1; 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, g_config.track_low_thresh, g_config.track_match_thresh, g_config.track_buffer, g_config.track_min_hits); ByteTracker talenan_tracker(g_config.track_high_thresh, g_config.track_low_thresh, g_config.track_match_thresh, g_config.track_buffer, g_config.track_min_hits); 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; auto session_start = std::chrono::steady_clock::now(); int frame_idx = 0; cv::VideoCapture cap = open_capture(g_config.source); bool is_live = g_config.source.substr(0, 7) == "rtsp://" || g_config.source.substr(0, 7) == "http://"; if (!cap.isOpened()) { std::cerr << "Cannot open source: " << g_config.source << std::endl; store.shutdown(); return 1; } if (is_live && g_config.warmup_frames > 0) warmup_stream(cap, g_config.warmup_frames); int w = static_cast(cap.get(cv::CAP_PROP_FRAME_WIDTH)); int h = static_cast(cap.get(cv::CAP_PROP_FRAME_HEIGHT)); double fps_prop = cap.get(cv::CAP_PROP_FPS); if (fps_prop <= 1) fps_prop = g_config.output_fps; 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)); cap = open_capture(g_config.source); if (!cap.isOpened()) break; w = static_cast(cap.get(cv::CAP_PROP_FRAME_WIDTH)); h = static_cast(cap.get(cv::CAP_PROP_FRAME_HEIGHT)); line_x = resolve_line_x(w, g_config.line_x, g_config.line_x_frac); continue; } auto elapsed_sec = std::chrono::duration_cast( 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; std::vector detections = model(frame); 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()); 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; 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(); double elapsed = static_cast(elapsed_sec); double rate = elapsed > 0 ? (display_total / elapsed * 60.0) : 0.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, rate, g_config.camera_name, now_str()); draw_batch_banner(frame, w, batch_num, batch_pulse); draw_footer(frame, w, h, frame_idx, is_live ? "LIVE-RKNN-BT" : "FILE-RKNN-BT"); 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 << ": " << batch_count << " | Total: " << display_total << " | Uptime " << (elapsed / 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; }