Create C++ version

This commit is contained in:
proitlab committed 2026-07-22 00:03:02 +07:00
1 parent 1a3242f611
commit f34eaa3af4
25 files changed
+3353 -24

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
cmake_minimum_required(VERSION 3.16)
project(chicken_counter VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(OpenCV 4.0 REQUIRED COMPONENTS core imgproc video videoio highgui imgcodecs dnn)
find_package(nlohmann_json 3.0 REQUIRED)
find_package(yaml-cpp REQUIRED)
find_package(CUDAToolkit REQUIRED)
find_library(NVINFER_LIB nvinfer PATHS /usr/lib/aarch64-linux-gnu REQUIRED)
find_library(NVONNX_LIB nvonnxparser PATHS /usr/lib/aarch64-linux-gnu REQUIRED)
set(COMMON_LIBS
opencv_core opencv_imgproc opencv_video opencv_videoio opencv_highgui opencv_imgcodecs opencv_dnn
nlohmann_json::nlohmann_json
yaml-cpp
CUDA::cudart
${NVINFER_LIB}
${NVONNX_LIB}
)
add_library(chicken_counter_lib STATIC
src/pipeline.cpp
src/batch_runner.cpp
)
target_include_directories(chicken_counter_lib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
/usr/include/aarch64-linux-gnu
)
target_link_libraries(chicken_counter_lib PUBLIC ${COMMON_LIBS})
add_executable(chicken_counter_cli
src/main.cpp
)
target_link_libraries(chicken_counter_cli PRIVATE chicken_counter_lib)
add_executable(test_config
tests/test_config.cpp
)
target_link_libraries(test_config PRIVATE chicken_counter_lib)
add_executable(test_modules
tests/test_modules.cpp
)
target_link_libraries(test_modules PRIVATE chicken_counter_lib)
add_executable(chicken_counter_dashboard
src/dashboard.cpp
)
target_link_libraries(chicken_counter_dashboard PRIVATE pthread)
enable_testing()
add_test(NAME config COMMAND test_config)
add_test(NAME modules COMMAND test_modules)
@@ -0,0 +1,102 @@
#pragma once
#include <filesystem>
#include <iostream>
#include <regex>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include "chicken_counter/config.hpp"
namespace cc {
struct CameraDiscoveryResult {
std::unordered_map<std::string, std::string> found;
std::unordered_map<std::string, std::string> skipped;
};
inline std::string replace_glob_placeholder(const std::string& pattern, int num) {
std::string s = pattern;
std::string token = "{num}";
size_t pos = s.find(token);
if (pos != std::string::npos) {
s.replace(pos, token.size(), std::to_string(num));
}
return s;
}
inline std::vector<std::string> glob_filenames(const std::string& dir, const std::string& pattern) {
namespace fs = std::filesystem;
std::vector<std::string> matches;
std::string regex_str = "^";
for (char c : pattern) {
if (c == '*') regex_str += ".*";
else if (c == '?') regex_str += ".";
else if (c == '.' || c == '[' || c == ']' || c == '(' || c == ')' || c == '{' || c == '}')
regex_str += std::string("\\") + c;
else regex_str += c;
}
regex_str += "$";
std::regex re(regex_str);
for (auto& entry : fs::directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
std::string fname = entry.path().filename().string();
if (std::regex_match(fname, re))
matches.push_back(entry.path().string());
}
std::sort(matches.begin(), matches.end());
return matches;
}
inline CameraDiscoveryResult discover_camera_videos(
const std::string& day_dir,
const BatchSettings& settings)
{
namespace fs = std::filesystem;
if (!fs::is_directory(day_dir))
throw std::runtime_error("Daily input folder does not exist: " + day_dir);
CameraDiscoveryResult result;
int total = static_cast<int>(settings.cameras.size());
using pair_t = std::pair<std::string, CameraPreset>;
std::vector<pair_t> sorted_cams(settings.cameras.begin(), settings.cameras.end());
std::sort(sorted_cams.begin(), sorted_cams.end(),
[](const pair_t& a, const pair_t& b) { return a.second.camera_num < b.second.camera_num; });
for (const auto& [camera_id, preset] : sorted_cams) {
std::string pattern = replace_glob_placeholder(settings.batch.camera_glob, preset.camera_num);
auto matches = glob_filenames(day_dir, pattern);
if (matches.empty()) {
result.skipped[camera_id] = "video_not_found";
continue;
}
if (matches.size() > 1) {
result.skipped[camera_id] = "multiple_matches";
continue;
}
result.found[camera_id] = matches[0];
}
if (result.found.empty()) {
std::string summary;
for (const auto& [id, reason] : result.skipped) summary += id + " (" + reason + "), ";
throw std::runtime_error("No camera videos found in " + day_dir + ". Skipped: " + summary);
}
int found_count = static_cast<int>(result.found.size());
if (!result.skipped.empty()) {
std::string summary;
for (const auto& [id, reason] : result.skipped) summary += id + " (" + reason + "), ";
std::cerr << "[batch] discovered " << found_count << "/" << total
<< " cameras; skipped: " << summary << "\n";
} else {
std::cerr << "[batch] discovered " << found_count << "/" << total << " cameras\n";
}
return result;
}
} // namespace cc
@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include "chicken_counter/config.hpp"
namespace cc {
std::string run_daily_batch(const BatchSettings& settings,
const std::string& date = "",
bool verbose = false,
bool no_video = false,
bool show_progress = false);
} // namespace cc
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <stdexcept>
#include <string>
#include <opencv2/videoio.hpp>
namespace cc {
inline cv::VideoCapture open_capture(const std::string& source) {
cv::VideoCapture cap;
if (source.size() == 1 && std::isdigit(source[0])) {
cap.open(std::stoi(source));
} else {
cap.open(source);
}
if (!cap.isOpened())
throw std::runtime_error("Unable to open video source: " + source);
return cap;
}
} // namespace cc
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <string>
#include <opencv2/videoio.hpp>
namespace cc {
inline double video_duration_seconds(const std::string& path) {
cv::VideoCapture cap(path);
if (!cap.isOpened())
throw std::runtime_error("Unable to open video for duration probe: " + path);
double fc = cap.get(cv::CAP_PROP_FRAME_COUNT);
double fps = cap.get(cv::CAP_PROP_FPS);
cap.release();
if (fps > 0 && fc > 0) return fc / fps;
throw std::runtime_error("Unable to determine duration for video: " + path);
}
inline double file_size_mb(const std::string& path) {
return static_cast<double>(std::filesystem::file_size(path)) / (1024.0 * 1024.0);
}
inline void run_ffmpeg(const std::vector<std::string>& command) {
std::string cmd;
for (const auto& arg : command) cmd += arg + " ";
cmd = cmd.substr(0, cmd.size() - 1) + " 2>&1";
int ret = std::system(cmd.c_str());
if (ret != 0)
throw std::runtime_error("ffmpeg failed with code " + std::to_string(ret));
}
inline double compress_video_to_target(
const std::string& input_path,
const std::string& output_path,
int max_mb = 200,
int max_attempts = 3)
{
namespace fs = std::filesystem;
if (!fs::is_regular_file(input_path))
throw std::runtime_error("Input video not found: " + input_path);
fs::create_directories(fs::path(output_path).parent_path());
double duration = video_duration_seconds(input_path);
if (duration <= 0)
throw std::runtime_error("Invalid video duration for " + input_path);
int target_kbps = std::max(300, static_cast<int>((max_mb * 8192) / duration * 0.92));
for (int attempt = 0; attempt < max_attempts; ++attempt) {
int attempt_kbps = std::max(300,
static_cast<int>(target_kbps * std::pow(0.85, attempt)));
if (fs::exists(output_path)) fs::remove(output_path);
std::vector<std::vector<std::string>> codec_attempts = {
{"-c:v", "h264_nvmpi", "-b:v", std::to_string(attempt_kbps) + "k",
"-maxrate", std::to_string(attempt_kbps) + "k",
"-bufsize", std::to_string(attempt_kbps * 2) + "k"},
{"-c:v", "libx264", "-preset", "fast",
"-b:v", std::to_string(attempt_kbps) + "k",
"-maxrate", std::to_string(attempt_kbps) + "k",
"-bufsize", std::to_string(attempt_kbps * 2) + "k"}
};
bool succeeded = false;
for (const auto& cargs : codec_attempts) {
std::vector<std::string> cmd = {"ffmpeg", "-y", "-i", input_path};
cmd.insert(cmd.end(), cargs.begin(), cargs.end());
cmd.push_back("-c:a");
cmd.push_back("copy");
cmd.push_back(output_path);
try {
run_ffmpeg(cmd);
succeeded = true;
break;
} catch (const std::runtime_error&) {
if (fs::exists(output_path)) fs::remove(output_path);
}
}
if (!succeeded)
throw std::runtime_error("Unable to compress video: " + input_path);
double size_mb = file_size_mb(output_path);
std::cerr << "[compress] " << fs::path(output_path).filename().string()
<< ": " << size_mb << " MB (attempt " << (attempt + 1)
<< ", target " << attempt_kbps << " kbps)\n";
if (size_mb <= max_mb) return size_mb;
}
double final_size = file_size_mb(output_path);
if (final_size > max_mb)
throw std::runtime_error("Compressed video exceeds " + std::to_string(max_mb)
+ " MB: " + output_path);
return final_size;
}
} // namespace cc
+605
View File
@@ -0,0 +1,605 @@
#pragma once
#include <cstdint>
#include <fstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
#include <nlohmann/json.hpp>
#include <opencv2/core/types.hpp>
#include <yaml-cpp/yaml.h>
#include "chicken_counter/types.hpp"
// ---------------------------------------------------------------------------
// cv::Point2i ↔ nlohmann::json (serialised as [x, y])
// ---------------------------------------------------------------------------
namespace cv {
inline void to_json(nlohmann::json& j, const Point2i& p) { j = {p.x, p.y}; }
inline void from_json(const nlohmann::json& j, Point2i& p) {
p.x = j.at(0).get<int>();
p.y = j.at(1).get<int>();
}
inline void to_json(nlohmann::json& j, const Scalar& s) { j = {s[0], s[1], s[2]}; }
inline void from_json(const nlohmann::json& j, Scalar& s) {
s = Scalar(j.at(0).get<double>(), j.at(1).get<double>(), j.at(2).get<double>());
}
} // namespace cv
namespace cc {
// ---------------------------------------------------------------------------
// YAML::Node → nlohmann::json
// ---------------------------------------------------------------------------
inline nlohmann::json yaml_to_json(const YAML::Node& node) {
if (node.IsNull()) return nullptr;
if (node.IsScalar()) {
std::string tag = node.Tag();
if (tag == "!") return node.as<std::string>();
try {
double dval = node.as<double>();
int ival = static_cast<int>(dval);
if (dval == static_cast<double>(ival)) return ival;
return dval;
} catch (const YAML::BadConversion&) {
std::string val = node.as<std::string>();
if (val == "true" || val == "True" || val == "yes" || val == "Yes")
return true;
if (val == "false" || val == "False" || val == "no" || val == "No")
return false;
if (val == "null" || val == "Null" || val == "NULL" || val == "~")
return nullptr;
return val;
}
}
if (node.IsSequence()) {
nlohmann::json arr = nlohmann::json::array();
for (const auto& item : node) arr.push_back(yaml_to_json(item));
return arr;
}
if (node.IsMap()) {
nlohmann::json obj = nlohmann::json::object();
for (const auto& kv : node) obj[kv.first.as<std::string>()] = yaml_to_json(kv.second);
return obj;
}
return nullptr;
}
// ---------------------------------------------------------------------------
// Config structs (no std::optional – nlohmann 3.10 compatibility)
// ---------------------------------------------------------------------------
struct DetectionConfig {
std::string model_path;
std::vector<int> classes = {0};
std::vector<int> ignored_classes = {1, 2};
float conf = 0.35f;
float iou = 0.55f;
int imgsz = 640;
std::string device; // empty = auto
int min_box_area_px = 0;
bool validate_while_inside = true;
};
inline void to_json(nlohmann::json& j, const DetectionConfig& c) {
j = {
{"model_path", c.model_path},
{"classes", c.classes},
{"ignored_classes", c.ignored_classes},
{"conf", c.conf}, {"iou", c.iou},
{"imgsz", c.imgsz},
{"min_box_area_px", c.min_box_area_px},
{"validate_while_inside", c.validate_while_inside}
};
if (!c.device.empty()) j["device"] = c.device;
}
inline void from_json(const nlohmann::json& j, DetectionConfig& c) {
j.at("model_path").get_to(c.model_path);
c.classes = j.value("classes", std::vector<int>{0});
c.ignored_classes = j.value("ignored_classes", std::vector<int>{1, 2});
c.conf = j.value("conf", 0.35f);
c.iou = j.value("iou", 0.55f);
c.imgsz = j.value("imgsz", 640);
if (j.contains("device")) {
if (j["device"].is_string()) c.device = j["device"];
else c.device = j["device"].dump();
}
c.min_box_area_px = j.value("min_box_area_px", 0);
c.validate_while_inside = j.value("validate_while_inside", true);
}
struct RoiConfig {
std::vector<cv::Point2i> points;
int inset_left_px = 0;
int inset_right_px = 0;
int inset_top_px = 0;
int inset_bottom_px = 0;
float min_overlap_ratio = 0.0f;
bool is_polygon() const { return points.size() > 2; }
cv::Rect bounding_rect() const {
if (points.empty()) return {};
int x1 = points[0].x, y1 = points[0].y, x2 = x1, y2 = y1;
for (const auto& p : points) {
if (p.x < x1) x1 = p.x; if (p.y < y1) y1 = p.y;
if (p.x > x2) x2 = p.x; if (p.y > y2) y2 = p.y;
}
return cv::Rect(x1, y1, x2 - x1, y2 - y1);
}
std::vector<cv::Point2i> counting_polygon() const {
auto br = bounding_rect();
int x_min = br.x + inset_left_px;
int x_max = br.x + br.width - inset_right_px;
int y_min = br.y + inset_top_px;
int y_max = br.y + br.height - inset_bottom_px;
const int min_w = 20, min_h = 20;
if (x_max - x_min < min_w) {
int cx = (x_min + x_max) / 2;
x_min = cx - min_w / 2; x_max = cx + min_w / 2;
}
if (y_max - y_min < min_h) {
int cy = (y_min + y_max) / 2;
y_min = cy - min_h / 2; y_max = cy + min_h / 2;
}
return {{x_min, y_min}, {x_max, y_min}, {x_max, y_max}, {x_min, y_max}};
}
cv::Rect counting_rect() const {
auto poly = counting_polygon();
if (poly.empty()) return {};
int x1 = poly[0].x, y1 = poly[0].y, x2 = x1, y2 = y1;
for (const auto& p : poly) {
if (p.x < x1) x1 = p.x; if (p.y < y1) y1 = p.y;
if (p.x > x2) x2 = p.x; if (p.y > y2) y2 = p.y;
}
return cv::Rect(x1, y1, x2 - x1, y2 - y1);
}
};
inline void to_json(nlohmann::json& j, const RoiConfig& c) {
j = {
{"points", c.points},
{"inset_left_px", c.inset_left_px},
{"inset_right_px", c.inset_right_px},
{"inset_top_px", c.inset_top_px},
{"inset_bottom_px", c.inset_bottom_px},
{"min_overlap_ratio", c.min_overlap_ratio}
};
}
inline void from_json(const nlohmann::json& j, RoiConfig& c) {
c.points = j.at("points").get<std::vector<cv::Point2i>>();
c.inset_left_px = j.value("inset_left_px", 0);
c.inset_right_px = j.value("inset_right_px", 0);
c.inset_top_px = j.value("inset_top_px", 0);
c.inset_bottom_px = j.value("inset_bottom_px", 0);
c.min_overlap_ratio = j.value("min_overlap_ratio", 0.0f);
}
struct DetectionZoneConfig {
bool enabled = false;
int buffer_above_px = 250;
int buffer_below_px = 250;
bool show_in_overlay = false;
cv::Rect compute_rect(const RoiConfig& roi, int frame_w, int frame_h) const {
auto br = roi.bounding_rect();
int x1 = std::max(0, br.x);
int x2 = std::min(frame_w, br.x + br.width);
int y1 = std::max(0, br.y - buffer_above_px);
int y2 = std::min(frame_h, br.y + br.height + buffer_below_px);
return cv::Rect(x1, y1, x2 - x1, y2 - y1);
}
};
inline void to_json(nlohmann::json& j, const DetectionZoneConfig& c) {
j = {{"enabled", c.enabled}, {"buffer_above_px", c.buffer_above_px},
{"buffer_below_px", c.buffer_below_px}, {"show_in_overlay", c.show_in_overlay}};
}
inline void from_json(const nlohmann::json& j, DetectionZoneConfig& c) {
c.enabled = j.value("enabled", false);
c.buffer_above_px = j.value("buffer_above_px", 250);
c.buffer_below_px = j.value("buffer_below_px", 250);
c.show_in_overlay = j.value("show_in_overlay", false);
}
struct TrackerConfig {
std::string tracker_config_path;
bool persist = true;
int track_buffer = 75;
};
inline void to_json(nlohmann::json& j, const TrackerConfig& c) {
j = {{"tracker_config_path", c.tracker_config_path},
{"persist", c.persist}, {"track_buffer", c.track_buffer}};
}
inline void from_json(const nlohmann::json& j, TrackerConfig& c) {
j.at("tracker_config_path").get_to(c.tracker_config_path);
c.persist = j.value("persist", true);
c.track_buffer = j.value("track_buffer", 75);
}
struct GateConfig {
std::string mode = "two_line";
std::vector<int> lines_y = {320, 600};
std::string direction = "bottom_to_up";
};
inline void to_json(nlohmann::json& j, const GateConfig& c) {
j = {{"mode", c.mode}, {"lines_y", c.lines_y}, {"direction", c.direction}};
}
inline void from_json(const nlohmann::json& j, GateConfig& c) {
c.mode = j.value("mode", "two_line");
c.lines_y = j.value("lines_y", std::vector<int>{320, 600});
c.direction = j.value("direction", "bottom_to_up");
}
struct MotionConfig {
bool enabled = true;
std::string axis = "vertical";
float forward_sign = 1.0f;
float ema_alpha = 0.2f;
float reverse_enter_threshold = -1.5f;
float reverse_exit_threshold = -0.5f;
int debounce_frames = 12;
int min_features = 60;
int max_corners = 300;
float quality_level = 0.01f;
int min_distance = 8;
int block_radius = 6;
int stride_frames = 1;
float flow_scale = 1.0f;
};
inline void to_json(nlohmann::json& j, const MotionConfig& c) {
j = {{"enabled", c.enabled}, {"axis", c.axis}, {"forward_sign", c.forward_sign},
{"ema_alpha", c.ema_alpha}, {"reverse_enter_threshold", c.reverse_enter_threshold},
{"reverse_exit_threshold", c.reverse_exit_threshold}, {"debounce_frames", c.debounce_frames},
{"min_features", c.min_features}, {"max_corners", c.max_corners},
{"quality_level", c.quality_level}, {"min_distance", c.min_distance},
{"block_radius", c.block_radius}, {"stride_frames", c.stride_frames},
{"flow_scale", c.flow_scale}};
}
inline void from_json(const nlohmann::json& j, MotionConfig& c) {
c.enabled = j.value("enabled", true);
c.axis = j.value("axis", "vertical");
c.forward_sign = j.value("forward_sign", 1.0f);
c.ema_alpha = j.value("ema_alpha", 0.2f);
c.reverse_enter_threshold = j.value("reverse_enter_threshold", -1.5f);
c.reverse_exit_threshold = j.value("reverse_exit_threshold", -0.5f);
c.debounce_frames = j.value("debounce_frames", 12);
c.min_features = j.value("min_features", 60);
c.max_corners = j.value("max_corners", 300);
c.quality_level = j.value("quality_level", 0.01f);
c.min_distance = j.value("min_distance", 8);
c.block_radius = j.value("block_radius", 6);
c.stride_frames = j.value("stride_frames", 1);
c.flow_scale = j.value("flow_scale", 1.0f);
}
struct OverlayConfig {
bool show_boxes = true;
bool show_track_trails = true;
int trail_length = 20;
bool show_center_marker = true;
bool show_track_ring = false;
cv::Point2i count_anchor = {900, 120};
bool inside_box_only = true;
bool pending_blink = true;
std::vector<cv::Scalar> pending_colors = {cv::Scalar(255, 255, 0), cv::Scalar(0, 255, 255)};
};
inline void to_json(nlohmann::json& j, const OverlayConfig& c) {
j = {{"show_boxes", c.show_boxes}, {"show_track_trails", c.show_track_trails},
{"trail_length", c.trail_length}, {"show_center_marker", c.show_center_marker},
{"show_track_ring", c.show_track_ring}, {"count_anchor", c.count_anchor},
{"inside_box_only", c.inside_box_only}, {"pending_blink", c.pending_blink},
{"pending_colors", c.pending_colors}};
}
inline void from_json(const nlohmann::json& j, OverlayConfig& c) {
c.show_boxes = j.value("show_boxes", true);
c.show_track_trails = j.value("show_track_trails", true);
c.trail_length = j.value("trail_length", 20);
c.show_center_marker = j.value("show_center_marker", true);
c.show_track_ring = j.value("show_track_ring", false);
c.count_anchor = j.value("count_anchor", cv::Point2i{900, 120});
c.inside_box_only = j.value("inside_box_only", true);
c.pending_blink = j.value("pending_blink", true);
c.pending_colors = j.value("pending_colors",
std::vector<cv::Scalar>{cv::Scalar(255, 255, 0), cv::Scalar(0, 255, 255)});
}
struct DisplayConfig {
std::string window_name = "Chicken Counter";
bool show_window = true;
std::string output_path; // empty = no output
float write_fps = -1.0f; // -1 = auto
int max_frames = -1; // -1 = unlimited
std::string encoder = "auto";
int output_bitrate_kbps = 4000;
std::vector<std::string> codec_preference = {"avc1", "mp4v", "H264"};
};
inline void to_json(nlohmann::json& j, const DisplayConfig& c) {
j = {{"window_name", c.window_name}, {"show_window", c.show_window},
{"encoder", c.encoder}, {"output_bitrate_kbps", c.output_bitrate_kbps},
{"codec_preference", c.codec_preference}};
if (!c.output_path.empty()) j["output_path"] = c.output_path;
if (c.write_fps >= 0) j["write_fps"] = c.write_fps;
if (c.max_frames >= 0) j["max_frames"] = c.max_frames;
}
inline void from_json(const nlohmann::json& j, DisplayConfig& c) {
c.window_name = j.value("window_name", "Chicken Counter");
c.show_window = j.value("show_window", true);
c.output_path = j.value("output_path", "");
c.write_fps = j.value("write_fps", -1.0f);
c.max_frames = j.value("max_frames", -1);
c.encoder = j.value("encoder", "auto");
c.output_bitrate_kbps = j.value("output_bitrate_kbps", 4000);
c.codec_preference = j.value("codec_preference",
std::vector<std::string>{"avc1", "mp4v", "H264"});
}
struct PerformanceConfig {
bool half = false;
bool overlay_buffer_reuse = true;
int inference_stride = 1;
bool verbose = false;
};
inline void to_json(nlohmann::json& j, const PerformanceConfig& c) {
j = {{"half", c.half}, {"overlay_buffer_reuse", c.overlay_buffer_reuse},
{"inference_stride", c.inference_stride}, {"verbose", c.verbose}};
}
inline void from_json(const nlohmann::json& j, PerformanceConfig& c) {
c.half = j.value("half", false);
c.overlay_buffer_reuse = j.value("overlay_buffer_reuse", true);
c.inference_stride = j.value("inference_stride", 1);
c.verbose = j.value("verbose", false);
}
struct StreamConfig {
bool enabled = false;
std::string shm_dir = "/dev/shm";
int interval_frames = 5;
};
inline void to_json(nlohmann::json& j, const StreamConfig& c) {
j = {{"enabled", c.enabled}, {"shm_dir", c.shm_dir},
{"interval_frames", c.interval_frames}};
}
inline void from_json(const nlohmann::json& j, StreamConfig& c) {
c.enabled = j.value("enabled", false);
c.shm_dir = j.value("shm_dir", "/dev/shm");
c.interval_frames = j.value("interval_frames", 5);
}
struct FeedbackConfig {
bool enabled = false;
int every_n_frames = 300;
bool save_images = true;
std::string image_output_dir = "output/checkpoints";
bool log_to_terminal = true;
};
inline void to_json(nlohmann::json& j, const FeedbackConfig& c) {
j = {{"enabled", c.enabled}, {"every_n_frames", c.every_n_frames},
{"save_images", c.save_images}, {"image_output_dir", c.image_output_dir},
{"log_to_terminal", c.log_to_terminal}};
}
inline void from_json(const nlohmann::json& j, FeedbackConfig& c) {
c.enabled = j.value("enabled", false);
c.every_n_frames = j.value("every_n_frames", 300);
c.save_images = j.value("save_images", true);
c.image_output_dir = j.value("image_output_dir", "output/checkpoints");
c.log_to_terminal = j.value("log_to_terminal", true);
}
struct CameraConfig {
std::string camera_id;
std::string source;
DetectionConfig detection;
TrackerConfig tracker;
RoiConfig roi;
GateConfig gate;
MotionConfig motion;
OverlayConfig overlay;
DisplayConfig display;
PerformanceConfig performance;
FeedbackConfig feedback;
DetectionZoneConfig detection_zone;
StreamConfig stream;
};
inline void to_json(nlohmann::json& j, const CameraConfig& c) {
j = {{"camera_id", c.camera_id}, {"source", c.source},
{"detection", c.detection}, {"tracker", c.tracker},
{"roi", c.roi}, {"gate", c.gate}, {"motion", c.motion},
{"overlay", c.overlay}, {"display", c.display},
{"performance", c.performance}, {"feedback", c.feedback},
{"detection_zone", c.detection_zone}, {"stream", c.stream}};
}
inline void from_json(const nlohmann::json& j, CameraConfig& c) {
j.at("camera_id").get_to(c.camera_id);
j.at("source").get_to(c.source);
c.detection = j.value("detection", DetectionConfig{});
c.tracker = j.value("tracker", TrackerConfig{});
c.roi = j.value("roi", RoiConfig{});
c.gate = j.value("gate", GateConfig{});
c.motion = j.value("motion", MotionConfig{});
c.overlay = j.value("overlay", OverlayConfig{});
c.display = j.value("display", DisplayConfig{});
c.performance = j.value("performance", PerformanceConfig{});
c.feedback = j.value("feedback", FeedbackConfig{});
c.detection_zone = j.value("detection_zone", DetectionZoneConfig{});
c.stream = j.value("stream", StreamConfig{});
}
struct BatchConfig {
std::string root_dir;
std::string camera_glob = "kandang_*_camera_{num}_*.mp4";
std::string output_subdir = "output";
int compress_max_mb = 200;
bool delete_intermediate = false;
int checkpoint_every_n_frames = 3000;
};
inline void to_json(nlohmann::json& j, const BatchConfig& c) {
j = {{"root_dir", c.root_dir}, {"camera_glob", c.camera_glob},
{"output_subdir", c.output_subdir}, {"compress_max_mb", c.compress_max_mb},
{"delete_intermediate", c.delete_intermediate},
{"checkpoint_every_n_frames", c.checkpoint_every_n_frames}};
}
inline void from_json(const nlohmann::json& j, BatchConfig& c) {
j.at("root_dir").get_to(c.root_dir);
c.camera_glob = j.value("camera_glob", "kandang_*_camera_{num}_*.mp4");
c.output_subdir = j.value("output_subdir", "output");
c.compress_max_mb = j.value("compress_max_mb", 200);
c.delete_intermediate = j.value("delete_intermediate", false);
c.checkpoint_every_n_frames = j.value("checkpoint_every_n_frames", 3000);
}
struct CameraPreset {
std::string camera_id;
int camera_num;
RoiConfig roi;
cv::Point2i count_anchor = {-1, -1}; // (-1,-1) = not set
GateConfig gate;
MotionConfig motion;
bool has_gate = false;
bool has_motion = false;
};
struct BatchSettings {
BatchConfig batch;
nlohmann::json defaults = nlohmann::json::object();
std::unordered_map<std::string, CameraPreset> cameras;
};
// ---------------------------------------------------------------------------
// Config-loading functions
// ---------------------------------------------------------------------------
inline nlohmann::json load_data(const std::string& path) {
if (path.size() >= 5 && path.compare(path.size() - 5, 5, ".json") == 0) {
std::ifstream f(path);
return nlohmann::json::parse(f);
}
YAML::Node yaml = YAML::LoadFile(path);
return yaml_to_json(yaml);
}
inline nlohmann::json deep_merge(nlohmann::json base, const nlohmann::json& override) {
for (auto it = override.begin(); it != override.end(); ++it) {
if (it.value().is_object() && base.contains(it.key()) && base[it.key()].is_object()) {
base[it.key()] = deep_merge(base[it.key()], it.value());
} else {
base[it.key()] = it.value();
}
}
return base;
}
inline CameraConfig load_camera_config(const std::string& path, const std::string& camera_id = "") {
auto raw = load_data(path);
if (raw.contains("batch")) {
throw std::runtime_error(
"This is a batch config file. Use 'chicken-counter batch --config ...' instead.");
}
if (raw.contains("cameras") && !raw.contains("defaults")) {
if (camera_id.empty())
throw std::runtime_error("camera_id is required when config contains multiple cameras");
raw = raw["cameras"][camera_id];
}
return raw.get<CameraConfig>();
}
inline BatchSettings load_batch_config(const std::string& path) {
auto raw = load_data(path);
if (!raw.contains("batch"))
throw std::runtime_error("Batch config must contain a top-level 'batch' section");
BatchSettings settings;
settings.batch = raw["batch"].get<BatchConfig>();
settings.defaults = raw.value("defaults", nlohmann::json::object());
if (raw.contains("cameras")) {
for (auto& [id, cam] : raw["cameras"].items()) {
CameraPreset preset;
preset.camera_id = id;
preset.camera_num = cam["camera_num"].get<int>();
preset.roi.points = cam["roi"]["points"].get<std::vector<cv::Point2i>>();
if (cam.contains("count_anchor")) {
preset.count_anchor = cam["count_anchor"].get<cv::Point2i>();
} else if (cam.contains("overlay") && cam["overlay"].contains("count_anchor")) {
preset.count_anchor = cam["overlay"]["count_anchor"].get<cv::Point2i>();
}
if (cam.contains("gate")) {
preset.gate = cam["gate"].get<GateConfig>();
preset.has_gate = true;
}
if (cam.contains("motion")) {
preset.motion = cam["motion"].get<MotionConfig>();
preset.has_motion = true;
}
settings.cameras[id] = std::move(preset);
}
}
return settings;
}
inline CameraConfig build_camera_config_from_batch(
const BatchSettings& settings,
const std::string& camera_id,
const std::string& source,
const std::string& output_path,
const std::string& checkpoint_dir)
{
auto it = settings.cameras.find(camera_id);
if (it == settings.cameras.end())
throw std::runtime_error("Unknown camera_id in batch config: " + camera_id);
const auto& preset = it->second;
auto raw = deep_merge(settings.defaults, {{"camera_id", camera_id}, {"source", source}});
if (preset.count_anchor.x >= 0) {
if (!raw.contains("overlay")) raw["overlay"] = nlohmann::json::object();
raw["overlay"]["count_anchor"] = preset.count_anchor;
}
if (!raw.contains("roi")) raw["roi"] = nlohmann::json::object();
raw["roi"]["points"] = preset.roi.points;
if (preset.has_gate) {
raw["gate"] = {{"mode", preset.gate.mode},
{"lines_y", preset.gate.lines_y},
{"direction", preset.gate.direction}};
}
if (preset.has_motion) {
raw["motion"] = {
{"enabled", preset.motion.enabled},
{"axis", preset.motion.axis},
{"forward_sign", preset.motion.forward_sign},
{"ema_alpha", preset.motion.ema_alpha},
{"reverse_enter_threshold", preset.motion.reverse_enter_threshold},
{"reverse_exit_threshold", preset.motion.reverse_exit_threshold},
{"debounce_frames", preset.motion.debounce_frames},
{"min_features", preset.motion.min_features},
{"max_corners", preset.motion.max_corners},
{"quality_level", preset.motion.quality_level},
{"min_distance", preset.motion.min_distance},
{"block_radius", preset.motion.block_radius},
{"stride_frames", preset.motion.stride_frames},
{"flow_scale", preset.motion.flow_scale}
};
}
if (!raw.contains("display")) raw["display"] = nlohmann::json::object();
raw["display"]["output_path"] = output_path.empty() ? nlohmann::json(nullptr) : nlohmann::json(output_path);
raw["display"]["show_window"] = false;
if (!raw.contains("feedback")) raw["feedback"] = nlohmann::json::object();
raw["feedback"]["enabled"] = true;
raw["feedback"]["every_n_frames"] = settings.batch.checkpoint_every_n_frames;
raw["feedback"]["save_images"] = !output_path.empty();
raw["feedback"]["image_output_dir"] = checkpoint_dir;
raw["feedback"]["log_to_terminal"] = true;
return raw.get<CameraConfig>();
}
} // namespace cc
+199
View File
@@ -0,0 +1,199 @@
#pragma once
#include <cstdint>
#include <deque>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <opencv2/imgproc.hpp>
#include <opencv2/core/types.hpp>
#include "chicken_counter/config.hpp"
#include "chicken_counter/types.hpp"
namespace cc {
class CountingZone {
public:
CountingZone() {}
CountingZone(const RoiConfig& roi,
const GateConfig& gate,
int trail_length,
int track_buffer,
int min_box_area_px = 0,
bool validate_while_inside = true,
bool verbose = false)
: roi(roi), gate(gate), trail_length(trail_length),
track_buffer(track_buffer), min_box_area_px(min_box_area_px),
min_overlap_ratio(roi.min_overlap_ratio),
validate_while_inside(validate_while_inside),
verbose(verbose)
{
for (auto& p : roi.counting_polygon())
counting_polygon.push_back(p);
auto r = roi.counting_rect();
counting_rect = r;
}
std::vector<CountEvent> update(
const std::vector<TrackObservation>& tracks,
int frame_index,
bool counting_paused = false)
{
std::vector<CountEvent> events;
std::unordered_set<int> active_ids, inside_ids;
for (const auto& track : tracks) {
active_ids.insert(track.track_id);
last_seen_frame[track.track_id] = frame_index;
auto& hist = histories[track.track_id];
if (hist.size() >= static_cast<size_t>(trail_length))
hist.pop_front();
hist.push_back({track.centroid_x, track.centroid_y});
if (inside_roi({track.centroid_x, track.centroid_y}))
inside_ids.insert(track.track_id);
if (counting_paused) continue;
if (inside_ids.find(track.track_id) == inside_ids.end()) continue;
if (counted_ids.find(track.track_id) != counted_ids.end()) continue;
bool should_validate = false;
if (validate_while_inside) {
should_validate = meets_validation_thresholds(track);
} else {
bool just_entered = inside_ids.find(track.track_id) != inside_ids.end()
&& prev_inside_ids.find(track.track_id) == prev_inside_ids.end();
should_validate = just_entered && meets_validation_thresholds(track);
}
if (should_validate) {
counted_ids.insert(track.track_id);
++total_entered_count;
sequence_numbers[track.track_id] = total_entered_count;
latest_validated_track_id = track.track_id;
CountEvent ev;
ev.track_id = track.track_id;
ev.frame_index = frame_index;
ev.total_entered_after_event = total_entered_count;
ev.sequence_number = total_entered_count;
events.push_back(ev);
if (verbose) {
int bbox_area = std::max(0, track.bbox_x2 - track.bbox_x1)
* std::max(0, track.bbox_y2 - track.bbox_y1);
double overlap = bbox_overlap_ratio(track);
std::cerr << "[count] track=" << track.track_id
<< " seq=#" << total_entered_count
<< " frame=" << frame_index
<< " area=" << bbox_area
<< " overlap=" << overlap
<< " conf=" << track.confidence
<< " centroid=" << track.centroid_x << "," << track.centroid_y << "\n";
}
}
}
inside_box_count = static_cast<int>(inside_ids.size());
current_inside_ids = inside_ids;
prev_inside_ids = inside_ids;
purge_stale(frame_index, active_ids);
return events;
}
std::vector<cv::Point2i> trail_for(int track_id) const {
auto it = histories.find(track_id);
if (it == histories.end()) return {};
return {it->second.begin(), it->second.end()};
}
size_t sequence_number_for(int track_id) const {
auto it = sequence_numbers.find(track_id);
return (it != sequence_numbers.end()) ? it->second : 0;
}
bool is_inside(int track_id) const {
return current_inside_ids.find(track_id) != current_inside_ids.end();
}
bool is_validated(int track_id) const {
return counted_ids.find(track_id) != counted_ids.end();
}
int inside_box_count = 0;
int total_entered_count = 0;
std::optional<int> latest_validated_track_id;
private:
RoiConfig roi;
GateConfig gate;
int trail_length;
int track_buffer;
int min_box_area_px;
float min_overlap_ratio;
bool validate_while_inside;
bool verbose;
std::vector<cv::Point2i> counting_polygon;
cv::Rect counting_rect;
std::unordered_map<int, std::deque<cv::Point2i>> histories;
std::unordered_map<int, int> last_seen_frame;
std::unordered_set<int> counted_ids;
std::unordered_set<int> prev_inside_ids;
std::unordered_set<int> current_inside_ids;
std::unordered_map<int, int> sequence_numbers;
bool inside_roi(cv::Point2i p) const {
return cv::pointPolygonTest(counting_polygon, p, false) > 0;
}
bool meets_size_threshold(const TrackObservation& track) const {
int area = std::max(0, track.bbox_x2 - track.bbox_x1)
* std::max(0, track.bbox_y2 - track.bbox_y1);
return area >= min_box_area_px;
}
double bbox_overlap_ratio(const TrackObservation& track) const {
int bbox_area = std::max(0, track.bbox_x2 - track.bbox_x1)
* std::max(0, track.bbox_y2 - track.bbox_y1);
if (bbox_area <= 0) return 0.0;
int ix1 = std::max(track.bbox_x1, counting_rect.x);
int iy1 = std::max(track.bbox_y1, counting_rect.y);
int ix2 = std::min(track.bbox_x2, counting_rect.x + counting_rect.width);
int iy2 = std::min(track.bbox_y2, counting_rect.y + counting_rect.height);
if (ix2 <= ix1 || iy2 <= iy1) return 0.0;
double intersection = (ix2 - ix1) * (iy2 - iy1);
return intersection / bbox_area;
}
bool meets_overlap_threshold(const TrackObservation& track) const {
if (min_overlap_ratio <= 0) return true;
return bbox_overlap_ratio(track) >= min_overlap_ratio;
}
bool meets_validation_thresholds(const TrackObservation& track) const {
return meets_size_threshold(track) && meets_overlap_threshold(track);
}
void purge_stale(int frame_index, const std::unordered_set<int>& active_ids) {
std::vector<int> stale;
for (const auto& [id, last] : last_seen_frame) {
if (active_ids.find(id) == active_ids.end()
&& frame_index - last > track_buffer)
stale.push_back(id);
}
for (int id : stale) {
last_seen_frame.erase(id);
histories.erase(id);
prev_inside_ids.erase(id);
current_inside_ids.erase(id);
}
}
};
} // namespace cc
+146
View File
@@ -0,0 +1,146 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <iostream>
#include <vector>
#include <opencv2/core/types.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/video.hpp>
#include "chicken_counter/config.hpp"
#include "chicken_counter/types.hpp"
namespace cc {
class BackwardMotionDetector {
public:
BackwardMotionDetector() {}
BackwardMotionDetector(const MotionConfig& config,
const RoiConfig& roi,
bool verbose = false)
: config(config), roi(roi), verbose(verbose)
{
int x1 = roi.points[0].x, y1 = roi.points[0].y;
int x2 = x1, y2 = y1;
for (const auto& p : roi.points) {
if (p.x < x1) x1 = p.x; if (p.y < y1) y1 = p.y;
if (p.x > x2) x2 = p.x; if (p.y > y2) y2 = p.y;
}
roi_bounds = cv::Rect(x1, y1, x2 - x1, y2 - y1);
}
MotionState update(const cv::Mat& frame,
const std::vector<TrackObservation>& tracks,
int frame_index)
{
if (!config.enabled) return state;
int stride = std::max(1, config.stride_frames);
if (frame_index % stride != 0) return state;
cv::Mat gray;
cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
cv::Mat gray_roi = gray(roi_bounds);
double scale = config.flow_scale;
if (scale < 1.0) {
int tw = std::max(1, static_cast<int>(gray_roi.cols * scale));
int th = std::max(1, static_cast<int>(gray_roi.rows * scale));
cv::resize(gray_roi, gray_roi, {tw, th}, 0, 0, cv::INTER_AREA);
} else {
scale = 1.0;
}
cv::Mat mask(gray_roi.size(), CV_8UC1, cv::Scalar(255));
int r = config.block_radius;
for (const auto& track : tracks) {
int lx1 = static_cast<int>((std::max(0, track.bbox_x1 - r) - roi_bounds.x) * scale);
int ly1 = static_cast<int>((std::max(0, track.bbox_y1 - r) - roi_bounds.y) * scale);
int lx2 = static_cast<int>((std::min(roi_bounds.x + roi_bounds.width, track.bbox_x2 + r) - roi_bounds.x) * scale);
int ly2 = static_cast<int>((std::min(roi_bounds.y + roi_bounds.height, track.bbox_y2 + r) - roi_bounds.y) * scale);
if (lx2 <= lx1 || ly2 <= ly1) continue;
cv::rectangle(mask, {lx1, ly1}, {lx2, ly2}, 0, -1);
}
std::vector<cv::Point2f> points;
cv::goodFeaturesToTrack(gray_roi, points, config.max_corners,
config.quality_level, config.min_distance, mask, config.block_radius);
if (previous_gray.empty() || static_cast<int>(points.size()) < config.min_features) {
previous_gray = gray_roi.clone();
return state;
}
std::vector<cv::Point2f> next_points;
std::vector<uint8_t> status;
std::vector<float> err;
cv::calcOpticalFlowPyrLK(previous_gray, gray_roi, points, next_points, status, err);
previous_gray = gray_roi.clone();
if (next_points.empty() || status.empty()) return state;
int valid_count = 0;
double flow_sum = 0.0;
for (size_t i = 0; i < status.size(); ++i) {
if (!status[i]) continue;
double v = (config.axis == "vertical")
? (next_points[i].y - points[i].y)
: (next_points[i].x - points[i].x);
flow_sum += v;
valid_count++;
}
if (valid_count < config.min_features) return state;
double median_speed = flow_sum / valid_count * config.forward_sign;
double alpha = config.ema_alpha;
state.smoothed_speed = static_cast<float>(
alpha * median_speed + (1.0 - alpha) * state.smoothed_speed);
if (state.smoothed_speed <= config.reverse_enter_threshold) {
++state.consecutive_reverse_frames;
} else if (state.smoothed_speed > config.reverse_exit_threshold) {
state.consecutive_reverse_frames = 0;
state.backward_active = false;
}
if (state.consecutive_reverse_frames >= config.debounce_frames) {
bool was_active = state.backward_active;
state.backward_active = true;
if (verbose && !was_active) {
std::cerr << "[motion] BACKWARD TRIGGERED! smoothed="
<< state.smoothed_speed
<< " consecutive=" << state.consecutive_reverse_frames << "\n";
}
}
if (verbose) {
++update_count;
std::cerr << "[motion #" << update_count
<< "] features=" << valid_count
<< " median_speed=" << median_speed
<< " smoothed=" << state.smoothed_speed
<< " consecutive_rev=" << state.consecutive_reverse_frames
<< " backward=" << state.backward_active << "\n";
}
return state;
}
MotionState state;
private:
MotionConfig config;
RoiConfig roi;
bool verbose;
cv::Rect roi_bounds;
cv::Mat previous_gray;
int update_count = 0;
};
} // namespace cc
+161
View File
@@ -0,0 +1,161 @@
#pragma once
#include <cstdint>
#include <string>
#include <opencv2/imgproc.hpp>
#include <opencv2/core/types.hpp>
#include "chicken_counter/config.hpp"
#include "chicken_counter/counting.hpp"
#include "chicken_counter/types.hpp"
namespace cc {
const cv::Scalar WHITE(255, 255, 255);
const cv::Scalar BLACK(0, 0, 0);
const cv::Scalar CYAN(255, 255, 0);
const cv::Scalar RED(0, 0, 255);
const cv::Scalar YELLOW(0, 255, 255);
const cv::Scalar ORANGE(0, 165, 255);
const cv::Scalar BLUE(255, 120, 0);
const cv::Scalar LIME(80, 220, 80);
const cv::Scalar GRAY(160, 160, 160);
inline void draw_outlined_text(
cv::Mat& frame,
const std::string& text,
cv::Point origin,
double font_scale,
const cv::Scalar& fill_color,
const cv::Scalar& outline_color,
int thickness,
int outline_thickness)
{
int font = cv::FONT_HERSHEY_SIMPLEX;
cv::putText(frame, text, origin, font, font_scale, outline_color, outline_thickness, cv::LINE_AA);
cv::putText(frame, text, origin, font, font_scale, fill_color, thickness, cv::LINE_AA);
}
inline void draw_trail(cv::Mat& frame, const std::vector<cv::Point2i>& trail) {
if (trail.size() < 2) return;
for (size_t i = 1; i < trail.size(); ++i)
cv::line(frame, trail[i - 1], trail[i], YELLOW, 2);
}
inline void draw_dashed_rectangle(
cv::Mat& frame,
cv::Point pt1,
cv::Point pt2,
const cv::Scalar& color,
int thickness = 1,
int dash_length = 12)
{
int x1 = pt1.x, y1 = pt1.y, x2 = pt2.x, y2 = pt2.y;
for (int x = x1; x < x2; x += dash_length * 2)
cv::line(frame, {x, y1}, {std::min(x + dash_length, x2), y1}, color, thickness);
for (int x = x1; x < x2; x += dash_length * 2)
cv::line(frame, {x, y2}, {std::min(x + dash_length, x2), y2}, color, thickness);
for (int y = y1; y < y2; y += dash_length * 2)
cv::line(frame, {x1, y}, {x1, std::min(y + dash_length, y2)}, color, thickness);
for (int y = y1; y < y2; y += dash_length * 2)
cv::line(frame, {x2, y}, {x2, std::min(y + dash_length, y2)}, color, thickness);
}
inline void draw_detection_zone(cv::Mat& frame, const CameraConfig& config) {
int h = frame.rows, w = frame.cols;
auto r = config.detection_zone.compute_rect(config.roi, w, h);
draw_dashed_rectangle(frame, {r.x, r.y}, {r.x + r.width, r.y + r.height}, GRAY, 1);
}
inline void draw_roi_and_gates(cv::Mat& frame, const CameraConfig& config) {
std::vector<cv::Point2i> pts = config.roi.counting_polygon();
std::vector<std::vector<cv::Point>> contours(1);
for (const auto& p : pts) contours[0].emplace_back(p);
cv::polylines(frame, contours, true, BLUE, 3);
}
inline cv::Mat draw_overlay(
cv::Mat& frame,
const CameraConfig& config,
CountingZone& counting_zone,
const std::vector<TrackObservation>& tracks,
const MotionState& motion_state,
int frame_index = 0,
cv::Mat* buffer = nullptr)
{
cv::Mat annotated;
if (buffer) {
frame.copyTo(*buffer);
annotated = *buffer;
} else {
annotated = frame.clone();
}
if (config.detection_zone.enabled && config.detection_zone.show_in_overlay)
draw_detection_zone(annotated, config);
draw_roi_and_gates(annotated, config);
bool blink_on = (frame_index / 8) % 2 == 0;
const auto& pc = config.overlay.pending_colors;
auto pending_colors = pc.empty()
? std::vector<cv::Scalar>{CYAN, YELLOW}
: pc;
for (const auto& track : tracks) {
bool inside_box = counting_zone.is_inside(track.track_id);
if (config.overlay.inside_box_only && !inside_box) continue;
bool validated = counting_zone.is_validated(track.track_id);
int x1 = track.bbox_x1, y1 = track.bbox_y1, x2 = track.bbox_x2, y2 = track.bbox_y2;
int cx = track.centroid_x, cy = track.centroid_y;
int seq = static_cast<int>(counting_zone.sequence_number_for(track.track_id));
if (config.overlay.show_boxes) {
cv::Scalar box_color;
if (validated) {
box_color = ORANGE;
} else if (config.overlay.pending_blink) {
box_color = pending_colors[blink_on ? 0 : 1 % pending_colors.size()];
} else {
box_color = pending_colors[0];
}
cv::rectangle(annotated, {x1, y1}, {x2, y2}, box_color, 2);
}
if (validated && seq > 0) {
draw_outlined_text(annotated, std::to_string(seq),
{x1, std::max(24, y1 - 8)}, 0.8, LIME, BLACK, 2, 4);
}
if (config.overlay.show_center_marker) {
cv::Scalar marker_color = validated ? ORANGE
: pending_colors[blink_on ? 0 : 1 % pending_colors.size()];
cv::circle(annotated, {cx, cy}, 4, marker_color, -1);
if (config.overlay.show_track_ring) {
int radius = std::max(20, static_cast<int>(std::max(x2 - x1, y2 - y1) * 0.6));
cv::circle(annotated, {cx, cy}, radius, WHITE, 1);
}
}
if (config.overlay.show_track_trails) {
auto trail = counting_zone.trail_for(track.track_id);
draw_trail(annotated, trail);
}
}
auto& anchor = config.overlay.count_anchor;
draw_outlined_text(annotated,
"TOTAL ENTERED: " + std::to_string(counting_zone.total_entered_count),
anchor, 1.35, BLUE, BLACK, 4, 6);
const char* motion_label = motion_state.backward_active ? "BACKWARD STOP" : "FORWARD";
cv::Scalar motion_color = motion_state.backward_active ? RED : YELLOW;
cv::putText(annotated, motion_label, {anchor.x, anchor.y + 42},
cv::FONT_HERSHEY_SIMPLEX, 0.8, motion_color, 2, cv::LINE_AA);
return annotated;
}
} // namespace cc
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <chrono>
#include <optional>
#include <opencv2/core.hpp>
#include <opencv2/videoio.hpp>
#include "chicken_counter/config.hpp"
#include "chicken_counter/counting.hpp"
#include "chicken_counter/motion.hpp"
#include "chicken_counter/tracking.hpp"
#include "chicken_counter/types.hpp"
namespace cc {
struct PipelineArtifacts {
cv::VideoCapture capture;
DetectionTracker* tracker;
CountingZone counting_zone;
BackwardMotionDetector motion_detector;
cv::VideoWriter writer;
cv::Mat overlay_buffer;
double run_start_time;
int total_source_frames;
bool owns_tracker;
cv::Rect detection_zone_rect;
bool has_writer;
bool has_detection_zone;
};
PipelineArtifacts build_pipeline(const CameraConfig& config,
DetectionTracker* tracker = nullptr);
PipelineResult run_pipeline(const CameraConfig& config,
DetectionTracker* tracker = nullptr,
bool show_progress = false);
} // namespace cc
+139
View File
@@ -0,0 +1,139 @@
#pragma once
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include "chicken_counter/types.hpp"
namespace cc {
inline std::string now_iso() {
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
std::ostringstream oss;
oss << std::put_time(std::gmtime(&t), "%FT%TZ");
return oss.str();
}
inline std::string relative_output_path(const std::string& path,
const std::string& base_dir) {
if (path.empty()) return "";
if (base_dir.empty()) return std::filesystem::path(path).filename().string();
try {
auto rel = std::filesystem::relative(path, base_dir);
return rel.string();
} catch (...) {
return std::filesystem::path(path).filename().string();
}
}
inline nlohmann::json build_camera_report_entry(
const CameraBatchResult& item,
const std::string& output_dir = "")
{
if (item.skipped) {
return {{"skipped", true},
{"skip_reason", item.skip_reason},
{"total_entered", 0}};
}
return {
{"total_entered", item.pipeline.total_entered_count},
{"source_video", std::filesystem::path(item.pipeline.source_video).filename().string()},
{"vis_video", relative_output_path(item.pipeline.vis_video_path, output_dir)},
{"compressed_video", relative_output_path(item.compressed_video_path, output_dir)},
{"compressed_size_mb", item.compressed_size_mb},
{"frames_processed", item.pipeline.frames_processed},
{"stopped_reason", item.pipeline.stopped_reason},
{"elapsed_seconds", std::round(item.pipeline.elapsed_seconds * 10.0) / 10.0}
};
}
struct BatchReport {
std::string date;
std::string generated_at;
nlohmann::json cameras;
int total_entered_sum;
};
inline BatchReport build_batch_report(
const std::string& date,
const std::vector<CameraBatchResult>& results,
const std::string& output_dir = "")
{
BatchReport report;
report.date = date;
report.generated_at = now_iso();
report.total_entered_sum = 0;
for (const auto& item : results) {
auto entry = build_camera_report_entry(item, output_dir);
if (entry.empty()) continue;
report.cameras[item.camera_id] = entry;
if (!item.skipped)
report.total_entered_sum += entry.value("total_entered", 0);
}
return report;
}
inline std::string write_json_file(const std::string& path, const nlohmann::json& j) {
namespace fs = std::filesystem;
fs::create_directories(fs::path(path).parent_path());
std::ofstream f(path);
f << j.dump(2);
std::cerr << "[report] wrote " << path << "\n";
return path;
}
inline std::string write_batch_report(const BatchReport& report,
const std::string& output_path) {
nlohmann::json j = {
{"date", report.date},
{"generated_at", report.generated_at},
{"cameras", report.cameras},
{"total_entered_sum", report.total_entered_sum}
};
return write_json_file(output_path, j);
}
inline std::string write_camera_report(
const std::string& date,
const CameraBatchResult& item,
const std::string& output_dir)
{
namespace fs = std::filesystem;
fs::create_directories(output_dir);
auto entry = build_camera_report_entry(item, output_dir);
nlohmann::json payload = {
{"date", date},
{"camera_id", item.camera_id},
{"generated_at", now_iso()}
};
for (auto& [k, v] : entry.items()) payload[k] = v;
std::string path = output_dir + "/" + item.camera_id + "_counts_" + date + ".json";
return write_json_file(path, payload);
}
inline std::string persist_batch_reports(
const std::string& date,
const std::vector<CameraBatchResult>& results,
const std::string& output_dir)
{
const auto& latest = results.back();
write_camera_report(date, latest, output_dir);
std::string aggregate_path = output_dir + "/counts_" + date + ".json";
auto report = build_batch_report(date, results, output_dir);
write_batch_report(report, aggregate_path);
return aggregate_path;
}
} // namespace cc
+420
View File
@@ -0,0 +1,420 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <fstream>
#include <iostream>
#include <memory>
#include <vector>
#include <opencv2/core.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/video/tracking.hpp>
#include <NvInfer.h>
#include <NvOnnxParser.h>
#include <cuda_runtime.h>
#include "chicken_counter/config.hpp"
#include "chicken_counter/types.hpp"
namespace cc {
template <typename T> struct TRTDeleter { void operator()(T* p) const { delete p; } };
template <typename T> using TRT_ptr = std::unique_ptr<T, TRTDeleter<T>>;
struct CudaStreamDeleter { void operator()(cudaStream_t* s) const { cudaStreamDestroy(*s); delete s; } };
using Cuda_stream_ptr = std::unique_ptr<cudaStream_t, CudaStreamDeleter>;
struct CudaHostDeleter { template <typename T> void operator()(T* p) const { cudaFreeHost(p); } };
template <typename T> using Cuda_host_ptr = std::unique_ptr<T, CudaHostDeleter>;
inline void trt_check(cudaError_t e, const char* m = "") {
if (e != cudaSuccess) throw std::runtime_error(std::string("CUDA:") + cudaGetErrorString(e) + " " + m);
}
// ---------------------------------------------------------------------------
// TensorRT engine (build from ONNX, cache to disk)
// ---------------------------------------------------------------------------
class TensorRTEngine {
public:
TensorRTEngine(const std::string& onnx_path, const std::string& cache_path = "") {
if (!cache_path.empty()) {
std::ifstream fc(cache_path, std::ios::binary | std::ios::ate);
if (fc) {
size_t sz = fc.tellg(); fc.seekg(0);
std::vector<char> data(sz);
fc.read(data.data(), sz);
runtime.reset(nvinfer1::createInferRuntime(logger));
engine.reset(runtime->deserializeCudaEngine(data.data(), sz));
if (engine) {
std::cerr << "[trt] loaded cache: " << cache_path << "\n";
init_io();
return;
}
}
}
std::cerr << "[trt] building from " << onnx_path << " ...\n";
auto builder = TRT_ptr<nvinfer1::IBuilder>(nvinfer1::createInferBuilder(logger));
auto network = TRT_ptr<nvinfer1::INetworkDefinition>(
builder->createNetworkV2(1U << static_cast<uint32_t>(
nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH)));
auto parser = TRT_ptr<nvonnxparser::IParser>(nvonnxparser::createParser(*network, logger));
if (!parser->parseFromFile(onnx_path.c_str(),
static_cast<int>(nvinfer1::ILogger::Severity::kWARNING)))
throw std::runtime_error("ONNX parse failed");
auto config = TRT_ptr<nvinfer1::IBuilderConfig>(builder->createBuilderConfig());
config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 256ULL << 20);
if (builder->platformHasFastFp16()) config->setFlag(nvinfer1::BuilderFlag::kFP16);
// set optimisation profile if input has dynamic dims
int nb = network->getNbInputs();
if (nb > 0) {
auto in = network->getInput(0);
auto prof = builder->createOptimizationProfile();
nvinfer1::Dims minD = in->getDimensions(), optD = minD, maxD = minD;
for (int d = 0; d < minD.nbDims; ++d) {
if (minD.d[d] < 0) {
minD.d[d] = 1; optD.d[d] = 1; maxD.d[d] = 1;
if (d == 2) { optD.d[d] = 640; maxD.d[d] = 640; }
if (d == 3) { optD.d[d] = 640; maxD.d[d] = 640; }
}
}
prof->setDimensions(in->getName(), nvinfer1::OptProfileSelector::kMIN, minD);
prof->setDimensions(in->getName(), nvinfer1::OptProfileSelector::kOPT, optD);
prof->setDimensions(in->getName(), nvinfer1::OptProfileSelector::kMAX, maxD);
config->addOptimizationProfile(prof);
}
auto plan = TRT_ptr<nvinfer1::IHostMemory>(
builder->buildSerializedNetwork(*network, *config));
if (!plan) throw std::runtime_error("buildSerializedNetwork failed");
runtime.reset(nvinfer1::createInferRuntime(logger));
engine.reset(runtime->deserializeCudaEngine(plan->data(), plan->size()));
if (!cache_path.empty()) {
std::ofstream out(cache_path, std::ios::binary);
out.write(static_cast<const char*>(plan->data()), plan->size());
std::cerr << "[trt] cached: " << cache_path << " (" << plan->size() << " B)\n";
}
init_io();
}
~TensorRTEngine() {
for (auto& kv : buffers) cudaFree(kv.second);
}
void run(const float* input, float* output) {
trt_check(cudaMemcpyAsync(buffers[input_name], input, input_bytes,
cudaMemcpyHostToDevice, *stream));
context->enqueueV3(*stream);
trt_check(cudaMemcpyAsync(output, buffers[output_name], output_bytes,
cudaMemcpyDeviceToHost, *stream));
cudaStreamSynchronize(*stream);
}
std::string input_name, output_name;
size_t input_bytes = 0, output_bytes = 0;
int output_num_classes = 1, output_num_cells = 8400;
bool output_layout_packed = true; // true = [1,N,8400], false = [1,8400,N]
private:
struct Logger : nvinfer1::ILogger {
void log(Severity sev, const char* msg) noexcept override {
if (sev <= Severity::kWARNING) std::cerr << "[trt] " << msg << std::endl;
}
};
Logger logger;
TRT_ptr<nvinfer1::IRuntime> runtime;
TRT_ptr<nvinfer1::ICudaEngine> engine;
TRT_ptr<nvinfer1::IExecutionContext> context;
std::unordered_map<std::string, void*> buffers;
Cuda_stream_ptr stream;
void init_io() {
context.reset(engine->createExecutionContext());
if (!context) throw std::runtime_error("createExecutionContext failed");
int nb = engine->getNbIOTensors();
for (int i = 0; i < nb; ++i) {
auto name = engine->getIOTensorName(i);
auto mode = engine->getTensorIOMode(name);
auto shape = engine->getTensorShape(name);
size_t bytes = 1;
for (int d = 0; d < shape.nbDims; ++d) bytes *= shape.d[d];
bytes *= sizeof(float);
void* ptr = nullptr;
cudaError_t e = cudaMalloc(&ptr, bytes);
if (e != cudaSuccess) throw std::runtime_error(std::string("cudaMalloc:") + cudaGetErrorString(e));
if (!context->setTensorAddress(name, ptr))
throw std::runtime_error(std::string("setTensorAddress: ") + name);
buffers[name] = ptr;
if (mode == nvinfer1::TensorIOMode::kINPUT) {
input_name = name; input_bytes = bytes;
} else {
output_name = name; output_bytes = bytes;
// cache output layout once
int d1 = (shape.nbDims >= 2) ? shape.d[1] : 1;
int d2 = (shape.nbDims >= 3) ? shape.d[2] : 1;
if (d2 > d1) {
output_layout_packed = true; // [1, N, cells]
output_num_classes = std::max(1, d1 - 4);
output_num_cells = d2;
} else {
output_layout_packed = false; // [1, cells, N]
output_num_classes = std::max(1, d2 - 4);
output_num_cells = d1;
}
}
}
stream.reset(new cudaStream_t{});
trt_check(cudaStreamCreate(stream.get()));
std::cerr << "[trt] ready: in=" << input_name << " (" << input_bytes
<< "B) out=" << output_name << " (" << output_bytes
<< "B) cls=" << output_num_classes << " cells=" << output_num_cells << "\n";
}
};
// ---------------------------------------------------------------------------
// DetectionTracker (TensorRT inference + SORT tracking)
// ---------------------------------------------------------------------------
class DetectionTracker {
public:
DetectionTracker(const CameraConfig& config)
: config(config), imgsz(config.detection.imgsz),
conf_thresh(config.detection.conf),
iou_thresh(config.detection.iou),
verbose(config.performance.verbose),
track_buffer(config.tracker.track_buffer)
{
std::string path = config.detection.model_path;
size_t dot = path.rfind('.');
std::string kind = (dot != std::string::npos) ? path.substr(dot) : "";
if (kind == ".engine" || kind == ".onnx") {
std::string onnx = (kind == ".engine")
? path.substr(0, path.size() - 7) + ".onnx" : path;
use_trt = true;
trt = std::make_unique<TensorRTEngine>(onnx, path + ".cache");
} else {
throw std::runtime_error("Unsupported model format: " + kind);
}
// alloc pinned output buffer (reused every inference)
int out_floats = static_cast<int>(trt->output_bytes / sizeof(float));
cudaMallocHost(&output_buf, trt->output_bytes);
output_buf_size = out_floats;
std::cerr << "[model] ready\n";
}
~DetectionTracker() {
if (output_buf) cudaFreeHost(output_buf);
}
void reset_tracking() {
active_tracks.clear();
last_frame_id = 0;
}
std::vector<TrackObservation> infer(const cv::Mat& frame,
const cv::Rect& crop_rect = {}) {
++last_frame_id;
cv::Mat source;
int off_x = 0, off_y = 0;
if (!crop_rect.empty() && crop_rect.width > 0 && crop_rect.height > 0) {
source = frame(crop_rect);
off_x = crop_rect.x; off_y = crop_rect.y;
} else {
source = frame;
}
float scale; int pad_x, pad_y;
cv::Mat blob = preprocess(source, scale, pad_x, pad_y);
// inference (blob.data is already NCHW float, use directly)
trt->run(reinterpret_cast<float*>(blob.data), output_buf);
// decode
auto dets = decode(scale, pad_x, pad_y, source.cols, source.rows);
auto tracks = associate(dets, off_x, off_y);
if (verbose && last_frame_id % 30 == 0)
std::cerr << "[track] f=" << last_frame_id << " det=" << dets.size()
<< " trk=" << tracks.size() << "\n";
return tracks;
}
CameraConfig config;
private:
bool use_trt = false;
std::unique_ptr<TensorRTEngine> trt;
int imgsz;
float conf_thresh, iou_thresh;
bool verbose;
int track_buffer, last_frame_id = 0;
float* output_buf = nullptr;
int output_buf_size = 0;
// --- Kalman track ---
struct KalmanTrack {
int id; cv::KalmanFilter kf; cv::Rect2f bbox;
int hits = 0, time_since_update = 0;
KalmanTrack(int tid, const cv::Rect2f& b) : id(tid), bbox(b) {
kf.init(7, 4, 0, CV_32F);
kf.transitionMatrix = (cv::Mat_<float>(7, 7) <<
1,0,0,0,1,0,0, 0,1,0,0,0,1,0, 0,0,1,0,0,0,1, 0,0,0,1,0,0,0,
0,0,0,0,1,0,0, 0,0,0,0,0,1,0, 0,0,0,0,0,0,1);
cv::setIdentity(kf.measurementMatrix);
cv::setIdentity(kf.processNoiseCov, cv::Scalar::all(1e-2));
cv::setIdentity(kf.measurementNoiseCov, cv::Scalar::all(1e-1));
cv::setIdentity(kf.errorCovPost, cv::Scalar::all(1));
kf.statePost.at<float>(0) = b.x + b.width/2;
kf.statePost.at<float>(1) = b.y + b.height/2;
kf.statePost.at<float>(2) = b.area();
kf.statePost.at<float>(3) = b.width / b.height;
}
cv::Rect2f predict() {
cv::Mat p = kf.predict();
float w = std::sqrt(std::max(1.0f, p.at<float>(2) * p.at<float>(3)));
float h = std::max(1.0f, p.at<float>(2) / w);
bbox = cv::Rect2f(p.at<float>(0) - w/2, p.at<float>(1) - h/2, w, h);
return bbox;
}
void update(const cv::Rect2f& b) {
float cx = b.x + b.width/2, cy = b.y + b.height/2;
kf.correct((cv::Mat_<float>(4, 1) << cx, cy, b.area(), b.width / b.height));
bbox = b; hits++; time_since_update = 0;
}
};
std::vector<KalmanTrack> active_tracks;
int next_track_id = 1;
// --- preprocess ---
cv::Mat preprocess(const cv::Mat& img, float& scale, int& pad_x, int& pad_y) {
int w = img.cols, h = img.rows;
scale = static_cast<float>(imgsz) / std::max(w, h);
int nw = static_cast<int>(w * scale), nh = static_cast<int>(h * scale);
pad_x = (imgsz - nw) / 2;
pad_y = (imgsz - nh) / 2;
cv::Mat r, p;
cv::resize(img, r, {nw, nh});
cv::copyMakeBorder(r, p, pad_y, imgsz - nh - pad_y, pad_x, imgsz - nw - pad_x,
cv::BORDER_CONSTANT, {114, 114, 114});
return cv::dnn::blobFromImage(p, 1.0/255.0, {imgsz, imgsz}, cv::Scalar(), true, false);
}
// --- decode (uses cache layout) ---
std::vector<cv::Rect2f> decode(float scale, int pad_x, int pad_y, int ow, int oh) {
std::vector<cv::Rect> iboxes; iboxes.reserve(32);
std::vector<float> scores; scores.reserve(32);
int nc = trt->output_num_classes;
int cells = trt->output_num_cells;
int stride = nc + 4; // total channels per cell
const float* d = output_buf;
if (trt->output_layout_packed) {
// layout [1, N, cells] — each channel is contiguous stride apart
for (int i = 0; i < cells; ++i) {
float maxc = 0;
for (int c = 0; c < nc; ++c) {
float v = d[(4 + c) * cells + i];
if (v > maxc) maxc = v;
}
if (maxc < conf_thresh) continue;
float cx = d[i], cy = d[cells + i], w = d[2*cells + i], h = d[3*cells + i];
float x = (cx - pad_x) / scale, y = (cy - pad_y) / scale;
float bw = w / scale, bh = h / scale;
int x1 = std::max(0, std::min(static_cast<int>(x - bw/2), ow));
int y1 = std::max(0, std::min(static_cast<int>(y - bh/2), oh));
int x2 = std::max(0, std::min(static_cast<int>(x + bw/2), ow));
int y2 = std::max(0, std::min(static_cast<int>(y + bh/2), oh));
if (x2 > x1 && y2 > y1) { iboxes.push_back({x1, y1, x2 - x1, y2 - y1}); scores.push_back(maxc); }
}
} else {
// layout [1, cells, N] — contiguous per row
for (int i = 0; i < cells; ++i) {
const float* row = d + i * stride;
float maxc = 0;
for (int c = 0; c < nc; ++c) { float v = row[4 + c]; if (v > maxc) maxc = v; }
if (maxc < conf_thresh) continue;
float cx = row[0], cy = row[1], w = row[2], h = row[3];
float x = (cx - pad_x) / scale, y = (cy - pad_y) / scale;
int x1 = std::max(0, std::min(static_cast<int>(x - w/scale/2), ow));
int y1 = std::max(0, std::min(static_cast<int>(y - h/scale/2), oh));
int x2 = std::max(0, std::min(static_cast<int>(x + w/scale/2), ow));
int y2 = std::max(0, std::min(static_cast<int>(y + h/scale/2), oh));
if (x2 > x1 && y2 > y1) { iboxes.push_back({x1, y1, x2 - x1, y2 - y1}); scores.push_back(maxc); }
}
}
std::vector<int> idx; cv::dnn::NMSBoxes(iboxes, scores, conf_thresh, iou_thresh, idx);
std::vector<cv::Rect2f> out; out.reserve(idx.size());
for (int i : idx) out.push_back(iboxes[i]);
return out;
}
// --- SORT association ---
std::vector<TrackObservation> associate(const std::vector<cv::Rect2f>& dets, int ox, int oy) {
for (auto& t : active_tracks) { t.predict(); t.time_since_update++; }
int nd = static_cast<int>(dets.size()), nt = static_cast<int>(active_tracks.size());
if (nd == 0) goto cleanup;
{
std::vector<std::vector<double>> iou(nt, std::vector<double>(nd));
for (int t = 0; t < nt; ++t)
for (int d = 0; d < nd; ++d)
iou[t][d] = 1.0 - _iou(active_tracks[t].bbox, dets[d]);
std::vector<int> order(nd); for (int i = 0; i < nd; ++i) order[i] = i;
std::sort(order.begin(), order.end(), [&](int a, int b){ return dets[a].area() > dets[b].area(); });
std::vector<bool> used(nd, false); std::vector<int> match(nt, -1);
for (int d : order) {
int best = -1; double best_cost = 0.3;
for (int t = 0; t < nt; ++t) {
if (match[t] >= 0) continue;
if (iou[t][d] < best_cost) { best_cost = iou[t][d]; best = t; }
}
if (best >= 0) { match[best] = d; used[d] = true; }
}
for (int t = 0; t < nt; ++t) if (match[t] >= 0) active_tracks[t].update(dets[match[t]]);
for (int d = 0; d < nd; ++d) if (!used[d]) {
KalmanTrack tk(++next_track_id, dets[d]); tk.hits = 1; active_tracks.push_back(tk);
}
}
cleanup:
active_tracks.erase(std::remove_if(active_tracks.begin(), active_tracks.end(),
[this](const KalmanTrack& t){ return t.time_since_update > track_buffer; }),
active_tracks.end());
std::vector<TrackObservation> out;
for (const auto& t : active_tracks) {
if (t.hits < 3) continue;
auto& b = t.bbox;
int x1 = static_cast<int>(b.x) + ox, y1 = static_cast<int>(b.y) + oy;
int x2 = static_cast<int>(b.x + b.width) + ox, y2 = static_cast<int>(b.y + b.height) + oy;
out.push_back({t.id, 0, 0.9f, x1, y1, x2, y2, (x1+x2)/2, (y1+y2)/2, {}});
}
return out;
}
static double _iou(const cv::Rect2f& a, const cv::Rect2f& b) {
float ix1 = std::max(a.x, b.x), iy1 = std::max(a.y, b.y);
float ix2 = std::min(a.x + a.width, b.x + b.width);
float iy2 = std::min(a.y + a.height, b.y + b.height);
if (ix2 <= ix1 || iy2 <= iy1) return 0.0;
float I = (ix2 - ix1) * (iy2 - iy1);
float U = a.area() + b.area() - I;
return U > 0 ? I / U : 0.0;
}
};
} // namespace cc
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include <opencv2/core/types.hpp>
namespace cc {
struct TrackObservation {
int track_id;
int class_id;
float confidence;
int bbox_x1, bbox_y1, bbox_x2, bbox_y2;
int centroid_x, centroid_y;
std::vector<cv::Point2i> mask_polygon_xy;
};
struct CountEvent {
int track_id;
int frame_index;
int total_entered_after_event;
int sequence_number;
};
struct MotionState {
float smoothed_speed = 0.0f;
int consecutive_reverse_frames = 0;
bool backward_active = false;
};
struct FrameResult {
int frame_index = 0;
std::vector<TrackObservation> tracks;
int inside_box_count = 0;
int total_entered_count = 0;
std::optional<int> latest_validated_track_id;
MotionState motion_state;
std::vector<CountEvent> count_events;
};
struct PipelineResult {
std::string camera_id;
int total_entered_count;
int frames_processed;
std::string stopped_reason;
std::string vis_video_path;
std::string source_video;
double elapsed_seconds;
};
struct CameraBatchResult {
std::string camera_id;
PipelineResult pipeline;
bool skipped = false;
std::string skip_reason;
std::string compressed_video_path;
double compressed_size_mb = 0.0;
};
} // namespace cc
@@ -0,0 +1,66 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
#include <opencv2/videoio.hpp>
namespace cc {
inline cv::VideoWriter make_video_writer(
const std::string& path,
const cv::Size& frame_size,
double fps,
const std::string& encoder = "auto",
int output_bitrate_kbps = 4000,
const std::vector<std::string>& codec_preference = {})
{
namespace fs = std::filesystem;
fs::create_directories(fs::path(path).parent_path());
int bitrate_bps = std::max(1, output_bitrate_kbps) * 1000;
auto codecs = codec_preference.empty()
? std::vector<std::string>{"avc1", "mp4v", "H264"}
: codec_preference;
if (encoder == "auto" || encoder == "gstreamer") {
int fps_int = std::max(1, static_cast<int>(std::round(fps)));
std::string pipeline =
"appsrc ! video/x-raw, format=BGR ! "
"video/x-raw,width=" + std::to_string(frame_size.width) +
",height=" + std::to_string(frame_size.height) +
",framerate=" + std::to_string(fps_int) + "/1 ! "
"videoconvert ! nvvidconv ! "
"video/x-raw(memory:NVMM),format=NV12 ! "
"nvv4l2h264enc bitrate=" + std::to_string(bitrate_bps) +
" insert-sps-pps=true ! "
"h264parse ! mp4mux ! filesink location=" + path;
cv::VideoWriter writer(pipeline, cv::CAP_GSTREAMER, 0, fps, frame_size, true);
if (writer.isOpened()) {
std::cerr << "[video] opened GStreamer hardware encoder (bitrate="
<< output_bitrate_kbps << " kbps)\n";
return writer;
}
writer.release();
if (encoder == "gstreamer")
throw std::runtime_error("GStreamer video writer failed for: " + path);
}
for (const auto& codec : codecs) {
int fourcc = cv::VideoWriter::fourcc(codec[0], codec[1], codec[2], codec[3]);
cv::VideoWriter writer(path, fourcc, fps, frame_size);
if (writer.isOpened()) {
std::cerr << "[video] opened OpenCV encoder (codec=" << codec << ")\n";
return writer;
}
writer.release();
}
throw std::runtime_error("Unable to open video writer: " + path);
}
} // namespace cc
+140
View File
@@ -0,0 +1,140 @@
#include "chicken_counter/batch_runner.hpp"
#include <chrono>
#include <cstdio>
#include <ctime>
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
#include "chicken_counter/batch_discovery.hpp"
#include "chicken_counter/compress.hpp"
#include "chicken_counter/pipeline.hpp"
#include "chicken_counter/report.hpp"
#include "chicken_counter/tracking.hpp"
#include "chicken_counter/types.hpp"
namespace cc {
static std::string today_iso() {
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
char buf[16];
std::strftime(buf, sizeof(buf), "%Y-%m-%d", std::localtime(&t));
return buf;
}
std::string run_daily_batch(const BatchSettings& settings,
const std::string& date,
bool verbose,
bool no_video,
bool show_progress) {
namespace fs = std::filesystem;
std::string run_date = date.empty() ? today_iso() : date;
auto day_dir = fs::path(settings.batch.root_dir) / run_date;
auto output_dir = day_dir / settings.batch.output_subdir;
fs::create_directories(output_dir);
std::fprintf(stderr, "[batch] starting daily run for %s\n", run_date.c_str());
std::fprintf(stderr, "[batch] input folder: %s\n", day_dir.c_str());
std::fprintf(stderr, "[batch] output folder: %s\n", output_dir.c_str());
if (no_video)
std::fprintf(stderr, "[batch] --no-video: skipping video output, overlay, and compression\n");
auto discovery = discover_camera_videos(day_dir.string(), settings);
using pair_t = std::pair<std::string, CameraPreset>;
std::vector<pair_t> sorted_cams(settings.cameras.begin(), settings.cameras.end());
std::sort(sorted_cams.begin(), sorted_cams.end(),
[](const pair_t& a, const pair_t& b) {
return a.second.camera_num < b.second.camera_num;
});
std::string first_id;
for (const auto& [id, _] : sorted_cams) {
if (discovery.found.count(id)) { first_id = id; break; }
}
auto first_source = discovery.found[first_id];
auto init_out = no_video ? "" : (output_dir / (first_id + "_vis.mp4")).string();
auto init_ckpt = (output_dir / "checkpoints" / first_id).string();
auto init_cfg = build_camera_config_from_batch(
settings, first_id, first_source, init_out, init_ckpt);
DetectionTracker shared_tracker(init_cfg);
std::vector<CameraBatchResult> camera_results;
auto report_path = output_dir / ("counts_" + run_date + ".json");
for (const auto& [camera_id, _] : sorted_cams) {
if (discovery.skipped.count(camera_id)) {
auto reason = discovery.skipped.at(camera_id);
std::fprintf(stderr, "[batch] skipping %s: %s\n", camera_id.c_str(), reason.c_str());
CameraBatchResult cr;
cr.camera_id = camera_id;
cr.skipped = true;
cr.skip_reason = reason;
camera_results.push_back(cr);
persist_batch_reports(run_date, camera_results, output_dir.string());
continue;
}
auto source_path = discovery.found.at(camera_id);
auto vis_path = no_video ? "" : (output_dir / (camera_id + "_vis.mp4")).string();
auto checkpoint_dir = (output_dir / "checkpoints" / camera_id).string();
std::fprintf(stderr, "[batch] processing %s from %s\n",
camera_id.c_str(),
fs::path(source_path).filename().c_str());
auto cam_cfg = build_camera_config_from_batch(
settings, camera_id, source_path, vis_path, checkpoint_dir);
cam_cfg.performance.verbose = verbose;
auto result = run_pipeline(cam_cfg, &shared_tracker, show_progress);
CameraBatchResult cr;
cr.camera_id = camera_id;
cr.pipeline = result;
camera_results.push_back(cr);
std::fprintf(stderr, "[batch] finished %s: total_entered=%d frames=%d reason=%s\n",
camera_id.c_str(), result.total_entered_count,
result.frames_processed, result.stopped_reason.c_str());
persist_batch_reports(run_date, camera_results, output_dir.string());
}
if (no_video) {
auto report = build_batch_report(run_date, camera_results, output_dir.string());
std::fprintf(stderr, "[batch] complete for %s: total_entered_sum=%d report=%s\n",
run_date.c_str(), report.total_entered_sum, report_path.c_str());
return report_path.string();
}
std::fprintf(stderr, "[batch] all cameras complete; starting compression\n");
for (auto& item : camera_results) {
if (item.skipped) continue;
auto vis_path = item.pipeline.vis_video_path;
if (vis_path.empty()) continue;
auto compressed_path = (output_dir / (item.camera_id + "_compressed.mp4")).string();
double size_mb = compress_video_to_target(
vis_path, compressed_path,
settings.batch.compress_max_mb);
item.compressed_video_path = compressed_path;
item.compressed_size_mb = size_mb;
if (settings.batch.delete_intermediate)
fs::remove(vis_path);
persist_batch_reports(run_date, camera_results, output_dir.string());
}
auto report = build_batch_report(run_date, camera_results, output_dir.string());
std::fprintf(stderr, "[batch] complete for %s: total_entered_sum=%d report=%s\n",
run_date.c_str(), report.total_entered_sum, report_path.c_str());
return report_path.string();
}
} // namespace cc
+284
View File
@@ -0,0 +1,284 @@
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
static const int DEFAULT_PORT = 8080;
static const char* DEFAULT_SHM = "/dev/shm";
static const int DEFAULT_POLL_MS = 500;
static const char* HTML = R"~(<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Chicken Counter - Live Dashboard</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:hidden}
#app{display:flex;height:100vh}
#sidebar{width:260px;background:#16161e;padding:16px;overflow-y:auto;flex-shrink:0}
#sidebar h1{font-size:18px;color:#80dc5a;margin-bottom:16px}
#sidebar .stat{margin-bottom:12px}
#sidebar .stat label{display:block;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px}
#sidebar .stat .value{font-size:22px;font-weight:700;color:#e0e0e0}
#sidebar .stat .value.warn{color:#ff9f43}
#sidebar .stat .value.good{color:#80dc5a}
#cam-list{list-style:none;margin-top:16px}
#cam-list li{padding:8px 10px;margin:2px 0;border-radius:6px;cursor:pointer;font-size:13px;transition:background .2s}
#cam-list li:hover{background:#222}
#cam-list li.active{background:#1a3a2a;color:#80dc5a;font-weight:700}
#cam-list li .cam-badge{float:right;font-size:10px;padding:1px 6px;border-radius:8px;background:#222;color:#888}
#cam-list li.active .cam-badge{background:#2a5a3a;color:#80dc5a}
#main{flex:1;display:flex;flex-direction:column}
#frame-container{flex:1;display:flex;align-items:center;justify-content:center;background:#000;position:relative}
#frame-img{max-width:100%;max-height:100%;object-fit:contain}
#no-frame{color:#555;font-size:18px}
#top-bar{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;background:#16161e;font-size:12px}
#top-bar .refresh{color:#888}
#top-bar .status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
#top-bar .status-dot.online{background:#80dc5a;box-shadow:0 0 6px #80dc5a}
#top-bar .status-dot.offline{background:#555}
.refresh-btn{padding:4px 12px;border-radius:4px;background:#222;border:1px solid #444;color:#ccc;cursor:pointer;font-size:11px}
.refresh-btn:hover{background:#333}
</style>
</head>
<body>
<div id="app">
<div id="sidebar">
<h1>Chicken Counter</h1>
<div class="stat"><label>Total Entered</label><div class="value good" id="stat-total">--</div></div>
<div class="stat"><label>Inside Box</label><div class="value" id="stat-inside">--</div></div>
<div class="stat"><label>Tracks</label><div class="value" id="stat-tracks">--</div></div>
<div class="stat"><label>Frame</label><div class="value" id="stat-frame">--</div></div>
<div class="stat"><label>Motion Speed</label><div class="value" id="stat-speed">--</div></div>
<div class="stat"><label>Status</label><div class="value" id="stat-status">--</div></div>
<ul id="cam-list"></ul>
</div>
<div id="main">
<div id="top-bar">
<span><span class="status-dot" id="status-dot"></span><span id="status-text">waiting for pipeline...</span></span>
<span><span class="refresh" id="refresh-counter"></span> ago &nbsp;
<button class="refresh-btn" onclick="load()">Refresh</button></span>
</div>
<div id="frame-container">
<img id="frame-img" alt="live stream">
<div id="no-frame"></div>
</div>
</div>
</div>
<script>
var POLL_MS=%%POLL%%;
var SHM="%%SHM%%";
var cameras=[],activeCam=null,lastUpdate=0;
var img=document.getElementById("frame-img");
var noFrame=document.getElementById("no-frame");
function loadCameras(){fetch("/api/cameras").then(r=>r.json()).then(data=>{cameras=data.cameras||[];renderCamList();if(cameras.length&&!activeCam)selectCam(cameras[0]);if(!cameras.length){noFrame.textContent="No cameras in "+SHM;img.style.display="none";}});}
function renderCamList(){var ul=document.getElementById("cam-list");ul.innerHTML=cameras.map(function(c){return'<li class="'+(c===activeCam?"active":"")+'" onclick="selectCam(\''+c+'\')">'+c+'<span class="cam-badge">&#9654;</span></li>';}).join("");}
function selectCam(id){activeCam=id;renderCamList();load();}
function load(){if(!activeCam)return;var t=Date.now();img.src="/shm/"+activeCam+"/frame.jpg?t="+t;fetch("/shm/"+activeCam+"/stats.json?t="+t).then(function(r){if(!r.ok){setOffline();return;}return r.json();}).then(function(s){if(!s)return;lastUpdate=Date.now();document.getElementById("stat-total").textContent=s.total_entered_count;document.getElementById("stat-inside").textContent=s.inside_box_count;document.getElementById("stat-tracks").textContent=s.track_count;document.getElementById("stat-frame").textContent=s.frame_index;document.getElementById("stat-speed").textContent=s.smoothed_speed;document.getElementById("stat-status").textContent=s.backward_active?"BACKWARD STOP":"RUNNING";var el=document.getElementById("stat-status");el.className="value"+(s.backward_active?" warn":" good");document.getElementById("status-dot").className="status-dot online";document.getElementById("status-text").textContent=activeCam+" - frame "+s.frame_index;});}
function setOffline(){document.getElementById("status-dot").className="status-dot offline";document.getElementById("status-text").textContent=activeCam+" - offline";}
function updateRefresh(){var ago=Math.round((Date.now()-lastUpdate)/1000);document.getElementById("refresh-counter").textContent=ago+"s";}
img.onerror=function(){img.style.display="none";noFrame.style.display="block";noFrame.textContent="Waiting for frame...";};
img.onload=function(){img.style.display="block";noFrame.style.display="none";};
setInterval(function(){load();},POLL_MS);
setInterval(loadCameras,3000);
setInterval(updateRefresh,1000);
loadCameras();
</script>
</body>
</html>)~";
static std::string url_decode(const std::string& s) {
std::string r;
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] == '%' && i + 2 < s.size()) {
int v;
sscanf(s.c_str() + i + 1, "%2x", &v);
r += static_cast<char>(v);
i += 2;
} else {
r += s[i];
}
}
return r;
}
static std::string read_file(const std::string& path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return "";
auto sz = f.tellg();
f.seekg(0);
std::string data(sz, 0);
f.read(data.data(), sz);
return data;
}
static bool ends_with(const std::string& s, const std::string& suffix) {
return s.size() >= suffix.size() && s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
}
static bool starts_with(const std::string& s, const std::string& prefix) {
return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0;
}
static std::string get_mime(const std::string& path) {
if (ends_with(path, ".jpg") || ends_with(path, ".jpeg")) return "image/jpeg";
if (ends_with(path, ".json")) return "application/json";
if (ends_with(path, ".html")) return "text/html; charset=utf-8";
return "application/octet-stream";
}
static std::string http_response(int code, const std::string& ct,
const std::string& body) {
std::ostringstream r;
r << "HTTP/1.0 " << code << " OK\r\n";
r << "Content-Type: " << ct << "\r\n";
r << "Content-Length: " << body.size() << "\r\n";
r << "Cache-Control: no-cache, no-store, must-revalidate\r\n";
r << "Connection: close\r\n";
r << "\r\n" << body;
return r.str();
}
static std::string str_replace(std::string s, const std::string& from,
const std::string& to) {
size_t pos = s.find(from);
if (pos != std::string::npos) s.replace(pos, from.size(), to);
return s;
}
static std::string json_escape(const std::string& s) {
std::ostringstream r;
r << '"';
for (char c : s) {
if (c == '"') r << "\\\"";
else if (c == '\\') r << "\\\\";
else r << c;
}
r << '"';
return r.str();
}
static void handle_client(int fd, const std::string& shm_dir, int poll_ms) {
char buf[8192];
ssize_t n = recv(fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) { close(fd); return; }
buf[n] = 0;
std::string req(buf);
if (req.find("GET ") != 0) { close(fd); return; }
// parse path
size_t p1 = req.find(' ');
size_t p2 = req.find(' ', p1 + 1);
std::string path = url_decode(req.substr(p1 + 1, p2 - p1 - 1));
// strip query string
size_t q = path.find('?');
if (q != std::string::npos) path = path.substr(0, q);
std::string resp;
if (path == "/" || path == "/index.html") {
std::string html = HTML;
html = str_replace(html, "%%POLL%%", std::to_string(poll_ms));
html = str_replace(html, "%%SHM%%", shm_dir);
resp = http_response(200, "text/html; charset=utf-8", html);
} else if (path == "/api/cameras") {
std::string cams = "[]";
if (std::filesystem::is_directory(shm_dir)) {
std::ostringstream arr;
arr << "[";
bool first = true;
for (auto& entry : std::filesystem::directory_iterator(shm_dir)) {
if (!entry.is_directory()) continue;
std::string name = entry.path().filename().string();
if (!starts_with(name, "chicken_counter_")) continue;
if (!first) arr << ","; first = false;
arr << json_escape(name.substr(17)); // strip "chicken_counter_"
}
arr << "]";
cams = arr.str();
}
resp = http_response(200, "application/json", "{\"cameras\":" + cams + "}");
} else if (starts_with(path, "/shm/")) {
std::string rel = path.substr(5);
size_t slash = rel.find('/');
if (slash != std::string::npos) {
std::string cam = "chicken_counter_" + rel.substr(0, slash);
std::string file = rel.substr(slash + 1);
std::string fpath = shm_dir + "/" + cam + "/" + file;
// security: avoid path traversal
auto resolved = std::filesystem::weakly_canonical(fpath);
auto base = std::filesystem::weakly_canonical(shm_dir);
if (starts_with(resolved.string(), base.string())) {
auto data = read_file(resolved.string());
if (!data.empty()) {
resp = http_response(200, get_mime(file), data);
}
}
}
}
if (resp.empty())
resp = "HTTP/1.0 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
send(fd, resp.data(), resp.size(), 0);
close(fd);
}
int main(int argc, char** argv) {
int port = DEFAULT_PORT;
std::string shm_dir = DEFAULT_SHM;
int poll_ms = DEFAULT_POLL_MS;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--port" && i + 1 < argc) port = std::stoi(argv[++i]);
else if (arg == "--shm-dir" && i + 1 < argc) shm_dir = argv[++i];
else if (arg == "--poll-ms" && i + 1 < argc) poll_ms = std::stoi(argv[++i]);
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return 1; }
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(sock, (sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); return 1;
}
listen(sock, 16);
std::cerr << "[dashboard] serving at http://0.0.0.0:" << port << "\n";
std::cerr << "[dashboard] shm_dir=" << shm_dir << " poll=" << poll_ms << "ms\n";
while (true) {
sockaddr_in client{};
socklen_t len = sizeof(client);
int client_fd = accept(sock, (sockaddr*)&client, &len);
if (client_fd < 0) continue;
std::thread(handle_client, client_fd, shm_dir, poll_ms).detach();
}
close(sock);
return 0;
}
+59
View File
@@ -0,0 +1,59 @@
#include <cstring>
#include <iostream>
#include <string>
#include "chicken_counter/batch_runner.hpp"
#include "chicken_counter/config.hpp"
#include "chicken_counter/pipeline.hpp"
static void print_usage() {
std::cerr <<
"Usage: chicken_counter run --config PATH [--camera-id ID] [--verbose] [--progress-bar]\n"
" chicken_counter batch --config PATH [--date YYYY-MM-DD] [--verbose] [--no-video] [--progress-bar]\n";
}
int main(int argc, char** argv) {
if (argc < 2) { print_usage(); return 1; }
std::string command = argv[1];
// parse optional args
std::string config_path, camera_id, date;
bool verbose = false, no_video = false, progress_bar = false;
for (int i = 2; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--config" && i + 1 < argc) config_path = argv[++i];
else if (arg == "--camera-id" && i + 1 < argc) camera_id = argv[++i];
else if (arg == "--date" && i + 1 < argc) date = argv[++i];
else if (arg == "--verbose") verbose = true;
else if (arg == "--no-video") no_video = true;
else if (arg == "--progress-bar") progress_bar = true;
}
if (config_path.empty()) {
std::cerr << "error: --config is required\n";
return 1;
}
if (command == "batch") {
auto settings = cc::load_batch_config(config_path);
cc::run_daily_batch(settings, date, verbose, no_video, progress_bar);
return 0;
}
if (command == "run") {
auto cfg = cc::load_camera_config(config_path, camera_id);
cfg.performance.verbose = verbose;
auto result = cc::run_pipeline(cfg, nullptr, progress_bar);
std::cout << "[done] camera=" << result.camera_id
<< " total_entered=" << result.total_entered_count
<< " frames=" << result.frames_processed
<< " reason=" << result.stopped_reason << "\n";
return 0;
}
std::cerr << "error: unknown command '" << command << "'\n";
print_usage();
return 1;
}
+480
View File
@@ -0,0 +1,480 @@
#include "chicken_counter/pipeline.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
#include <opencv2/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/videoio.hpp>
#include "chicken_counter/capture.hpp"
#include "chicken_counter/overlay.hpp"
#include "chicken_counter/video_writer.hpp"
namespace cc {
// ---------------------------------------------------------------------------
// Progress bar (same as Python _ProgressBar, stderr inline)
// ---------------------------------------------------------------------------
static std::string format_duration(double seconds) {
if (seconds < 60.0) return std::to_string(static_cast<int>(seconds)) + "s";
int total = static_cast<int>(seconds);
int minutes = total / 60;
int secs = total % 60;
if (minutes < 60) return std::to_string(minutes) + "m" + (secs < 10 ? "0" : "") + std::to_string(secs) + "s";
int hours = minutes / 60;
minutes %= 60;
return std::to_string(hours) + "h" + (minutes < 10 ? "0" : "") + std::to_string(minutes) + "m";
}
class ProgressBar {
public:
ProgressBar(int total, int width = 30) : _total(total), _width(width) {}
void render(int frame_idx, double elapsed, double fps,
int inside, int total_entered, bool backward) {
double now = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch()).count();
if (now - _last_render < 0.2 && frame_idx > 1 && _checkpoint_msg.empty()) return;
_last_render = now;
std::string cp = build_checkpoint_suffix();
std::string status = backward ? "backward" : "running";
std::string elapsed_s = format_duration(elapsed);
std::string line;
if (_total > 0) {
int pct = std::min(100, frame_idx * 100 / _total);
int filled = _width * pct / 100;
std::string bar = "[" + std::string(filled, '=') + ">" + std::string(_width - filled, ' ') + "]";
double eta_s = fps > 0 ? (_total - frame_idx) / fps : 0.0;
char buf[256];
snprintf(buf, sizeof(buf), "\r%s %3d%% %d/%d %s eta=%s %.1ffps count=%d/%d %s%s",
bar.c_str(), pct, frame_idx, _total,
elapsed_s.c_str(), format_duration(eta_s).c_str(),
fps, inside, total_entered, status.c_str(), cp.c_str());
line = buf;
} else {
char buf[256];
snprintf(buf, sizeof(buf), "\rframe=%d %s %.1ffps count=%d/%d %s%s",
frame_idx, elapsed_s.c_str(), fps,
inside, total_entered, status.c_str(), cp.c_str());
line = buf;
}
int pad = std::max(0, _last_line_len - static_cast<int>(line.size()));
_last_line_len = static_cast<int>(line.size());
std::fprintf(stderr, "%s%s", line.c_str(), std::string(pad, ' ').c_str());
std::fflush(stderr);
}
void emit(const std::string& msg) {
_checkpoint_msg = msg;
_last_render = 0.0;
}
void finish() {
std::fprintf(stderr, "\n");
std::fflush(stderr);
}
bool has_pending() const { return !_checkpoint_msg.empty(); }
private:
int _total, _width;
double _last_render = 0.0;
int _last_line_len = 0;
std::string _checkpoint_msg;
std::string build_checkpoint_suffix() {
if (_checkpoint_msg.empty()) return "";
std::string m = _checkpoint_msg;
_checkpoint_msg.clear();
return " [" + m + "]";
}
};
// ---------------------------------------------------------------------------
// build_pipeline
// ---------------------------------------------------------------------------
PipelineArtifacts build_pipeline(const CameraConfig& config,
DetectionTracker* tracker) {
PipelineArtifacts art{};
art.capture = open_capture(config.source);
art.owns_tracker = (tracker == nullptr);
art.tracker = tracker ? tracker : new DetectionTracker(config);
art.counting_zone = CountingZone(
config.roi, config.gate,
config.overlay.trail_length,
config.tracker.track_buffer,
config.detection.min_box_area_px,
config.detection.validate_while_inside,
config.performance.verbose);
art.motion_detector = BackwardMotionDetector(
config.motion, config.roi, config.performance.verbose);
int width = static_cast<int>(art.capture.get(cv::CAP_PROP_FRAME_WIDTH));
int height = static_cast<int>(art.capture.get(cv::CAP_PROP_FRAME_HEIGHT));
int fc = static_cast<int>(art.capture.get(cv::CAP_PROP_FRAME_COUNT));
art.total_source_frames = (fc > 0) ? fc : 0;
if (config.detection_zone.enabled && width > 0 && height > 0) {
art.detection_zone_rect = config.detection_zone.compute_rect(config.roi, width, height);
art.has_detection_zone = true;
std::fprintf(stderr, "[detection_zone] enabled crop=(%d,%d)-(%d,%d)\n",
art.detection_zone_rect.x, art.detection_zone_rect.y,
art.detection_zone_rect.x + art.detection_zone_rect.width,
art.detection_zone_rect.y + art.detection_zone_rect.height);
}
if (config.performance.overlay_buffer_reuse && width > 0 && height > 0)
art.overlay_buffer = cv::Mat(height, width, CV_8UC3);
art.has_writer = !config.display.output_path.empty();
if (art.has_writer) {
double fps = config.display.write_fps > 0
? static_cast<double>(config.display.write_fps)
: art.capture.get(cv::CAP_PROP_FPS);
if (fps <= 0) fps = 30.0;
art.writer = make_video_writer(
config.display.output_path, {width, height}, fps,
config.display.encoder, config.display.output_bitrate_kbps,
config.display.codec_preference);
}
if (config.stream.enabled) {
namespace fs = std::filesystem;
auto cam_dir = fs::path(config.stream.shm_dir) / ("chicken_counter_" + config.camera_id);
if (fs::exists(cam_dir)) {
fs::remove_all(cam_dir);
std::fprintf(stderr, "[stream] cleaned %s\n", cam_dir.c_str());
}
}
art.run_start_time = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch()).count();
return art;
}
// ---------------------------------------------------------------------------
// Helper: should_emit_feedback
// ---------------------------------------------------------------------------
static bool should_emit_feedback(const CameraConfig& cfg, int frame_idx) {
if (!cfg.feedback.enabled || cfg.feedback.every_n_frames <= 0) return false;
return frame_idx % cfg.feedback.every_n_frames == 0;
}
// ---------------------------------------------------------------------------
// Helper: emit_periodic_feedback
// ---------------------------------------------------------------------------
static void emit_periodic_feedback(const CameraConfig& cfg,
const PipelineArtifacts& art,
const cv::Mat& annotated,
const FrameResult& result,
ProgressBar* progress) {
if (cfg.feedback.log_to_terminal) {
double elapsed = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch()).count()
- art.run_start_time;
double fps = result.frame_index / elapsed;
std::string status = result.motion_state.backward_active ? "backward_stop" : "running";
char buf[512];
if (art.total_source_frames > 0) {
double eta_s = fps > 0 ? (art.total_source_frames - result.frame_index) / fps : 0.0;
snprintf(buf, sizeof(buf),
"[checkpoint] frame=%d/%d elapsed=%s fps=%.1f inside_box=%d "
"total_entered=%d backward_active=%d status=%s eta=%s",
result.frame_index, art.total_source_frames,
format_duration(elapsed).c_str(), fps,
result.inside_box_count, result.total_entered_count,
result.motion_state.backward_active, status.c_str(),
format_duration(eta_s).c_str());
} else {
snprintf(buf, sizeof(buf),
"[checkpoint] frame=%d elapsed=%s fps=%.1f inside_box=%d "
"total_entered=%d backward_active=%d status=%s",
result.frame_index, format_duration(elapsed).c_str(), fps,
result.inside_box_count, result.total_entered_count,
result.motion_state.backward_active, status.c_str());
}
if (progress) progress->emit(buf);
else std::fprintf(stderr, "\r\033[K%s\n", buf);
}
if (cfg.feedback.save_images) {
namespace fs = std::filesystem;
fs::create_directories(cfg.feedback.image_output_dir);
char fname[512];
snprintf(fname, sizeof(fname), "%s/frame_%06d.jpg",
cfg.feedback.image_output_dir.c_str(), result.frame_index);
cv::imwrite(fname, annotated);
}
}
// ---------------------------------------------------------------------------
// Helper: write_stream_frame
// ---------------------------------------------------------------------------
static void write_stream_frame(const std::string& shm_dir,
const std::string& camera_id,
const cv::Mat& frame,
const FrameResult& result) {
namespace fs = std::filesystem;
auto cam_dir = fs::path(shm_dir) / ("chicken_counter_" + camera_id);
fs::create_directories(cam_dir);
auto jpg_path = cam_dir / "frame.jpg";
auto tmp_jpg = cam_dir / ".frame_tmp.jpg";
cv::imwrite(tmp_jpg.string(), frame, {cv::IMWRITE_JPEG_QUALITY, 75});
fs::rename(tmp_jpg, jpg_path);
// Write stats.json atomically
auto stats_path = cam_dir / "stats.json";
auto tmp_stats = cam_dir / ".stats_tmp.json";
{
std::ofstream f(tmp_stats);
f << "{\"frame_index\":" << result.frame_index
<< ",\"inside_box_count\":" << result.inside_box_count
<< ",\"total_entered_count\":" << result.total_entered_count
<< ",\"track_count\":" << result.tracks.size()
<< ",\"backward_active\":" << (result.motion_state.backward_active ? "true" : "false")
<< ",\"smoothed_speed\":" << result.motion_state.smoothed_speed
<< ",\"count_events\":" << result.count_events.size() << "}";
}
fs::rename(tmp_stats, stats_path);
}
// ---------------------------------------------------------------------------
// run_pipeline (main loop)
// ---------------------------------------------------------------------------
PipelineResult run_pipeline(const CameraConfig& config,
DetectionTracker* tracker,
bool show_progress) {
if (tracker) {
tracker->config = config;
tracker->reset_tracking();
}
auto art = build_pipeline(config, tracker);
std::unique_ptr<DetectionTracker> owned_tracker;
if (art.owns_tracker) owned_tracker.reset(art.tracker);
int inference_stride = std::max(1, config.performance.inference_stride);
std::fprintf(stderr, "[perf] inference_stride=%d motion.stride_frames=%d motion.flow_scale=%.1f\n",
inference_stride, std::max(1, config.motion.stride_frames),
config.motion.flow_scale);
int frame_index = 0;
cv::Mat last_annotated;
std::vector<TrackObservation> last_tracks;
std::string stopped_reason = "eof";
bool user_quit = false;
bool verbose = config.performance.verbose;
int total_source_frames = art.total_source_frames;
int verbose_interval = std::max(1, inference_stride * 30);
ProgressBar* progress = nullptr;
if (show_progress) progress = new ProgressBar(total_source_frames);
// verbose timing accumulators
double cum_read = 0, cum_infer = 0, cum_motion = 0, cum_count = 0, cum_overlay = 0, cum_write = 0;
int timed_frames = 0;
auto t_loop_start = std::chrono::steady_clock::now();
try {
while (true) {
auto t0 = verbose ? std::chrono::steady_clock::now() : t_loop_start;
cv::Mat frame;
if (!art.capture.read(frame)) break;
++frame_index;
auto t_read = std::chrono::steady_clock::now();
if (frame_index % inference_stride == 0 || last_tracks.empty()) {
cv::Rect crop = art.has_detection_zone ? art.detection_zone_rect : cv::Rect{};
last_tracks = art.tracker->infer(frame, crop);
}
auto t_infer = std::chrono::steady_clock::now();
auto& tracks = last_tracks;
auto motion_state = art.motion_detector.update(frame, tracks, frame_index);
auto t_motion = std::chrono::steady_clock::now();
auto count_events = art.counting_zone.update(
tracks, frame_index, motion_state.backward_active);
auto t_count = std::chrono::steady_clock::now();
bool needs_overlay = config.display.show_window
|| art.has_writer
|| config.stream.enabled;
cv::Mat annotated;
if (needs_overlay) {
annotated = draw_overlay(frame, config, art.counting_zone,
tracks, motion_state, frame_index,
art.overlay_buffer.empty() ? nullptr : &art.overlay_buffer);
} else {
annotated = frame;
}
auto t_overlay = std::chrono::steady_clock::now();
FrameResult result;
result.frame_index = frame_index;
result.tracks = tracks;
result.inside_box_count = art.counting_zone.inside_box_count;
result.total_entered_count = art.counting_zone.total_entered_count;
result.motion_state = motion_state;
result.count_events = count_events;
// consume result
if (config.display.show_window) {
cv::imshow(config.display.window_name, annotated);
}
if (art.has_writer && art.writer.isOpened()) {
art.writer.write(annotated);
}
for (const auto& ev : count_events) {
if (config.performance.verbose) {
char buf[256];
snprintf(buf, sizeof(buf),
"[frame %d] counted track=%d inside_box=%d total_entered=%d",
ev.frame_index, ev.track_id,
result.inside_box_count, ev.total_entered_after_event);
if (progress) progress->emit(buf);
else std::fprintf(stderr, "%s\n", buf);
}
}
if (should_emit_feedback(config, frame_index)) {
emit_periodic_feedback(config, art, annotated, result, progress);
}
last_annotated = annotated;
if (config.stream.enabled
&& frame_index % std::max(1, config.stream.interval_frames) == 0) {
write_stream_frame(config.stream.shm_dir, config.camera_id,
annotated, result);
}
auto t_write = std::chrono::steady_clock::now();
if (verbose) {
auto to_ms = [](auto start, auto end) {
return std::chrono::duration<double, std::milli>(end - start).count();
};
if (frame_index % inference_stride == 0) {
cum_read += to_ms(t0, t_read);
cum_infer += to_ms(t_read, t_infer);
cum_motion += to_ms(t_infer, t_motion);
cum_count += to_ms(t_motion, t_count);
cum_overlay += to_ms(t_count, t_overlay);
cum_write += to_ms(t_overlay, t_write);
++timed_frames;
}
if (frame_index % verbose_interval == 0 && timed_frames > 0) {
double n = timed_frames;
std::fprintf(stderr,
"[debug ~%df avg ms] read=%.1f infer=%.1f motion=%.1f "
"count=%.1f overlay=%.1f write=%.1f tracks=%zu inside=%d total=%d "
"motion_speed=%.1f backward=%d\n",
verbose_interval,
cum_read / n, cum_infer / n, cum_motion / n,
cum_count / n, cum_overlay / n, cum_write / n,
tracks.size(),
art.counting_zone.inside_box_count,
art.counting_zone.total_entered_count,
motion_state.smoothed_speed,
motion_state.backward_active);
cum_read = cum_infer = cum_motion = cum_count = cum_overlay = cum_write = 0;
timed_frames = 0;
}
}
if (progress) {
auto elapsed = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch()).count()
- art.run_start_time;
double fps = frame_index / elapsed;
progress->render(frame_index, elapsed, fps,
art.counting_zone.inside_box_count,
art.counting_zone.total_entered_count,
motion_state.backward_active);
}
if (motion_state.backward_active) {
stopped_reason = "backward";
char buf[128];
snprintf(buf, sizeof(buf),
"[stop] backward detection confirmed at frame=%d; ending pipeline",
frame_index);
if (progress) progress->emit(buf);
else std::fprintf(stderr, "%s\n", buf);
break;
}
if (config.display.max_frames > 0 && frame_index >= config.display.max_frames) {
stopped_reason = "max_frames";
break;
}
if (config.display.show_window && (cv::waitKey(1) & 0xFF) == 'q') {
stopped_reason = "user_quit";
user_quit = true;
break;
}
}
// freeze frame
if (art.has_writer && art.writer.isOpened() && !last_annotated.empty()) {
int freeze_count = static_cast<int>(((config.display.write_fps > 0
? config.display.write_fps : 30.0f) * 2));
freeze_count = std::max(1, freeze_count);
for (int i = 0; i < freeze_count; ++i)
art.writer.write(last_annotated);
}
} catch (...) {
art.capture.release();
if (art.has_writer && art.writer.isOpened()) art.writer.release();
if (config.display.show_window) cv::destroyAllWindows();
if (progress) { progress->finish(); delete progress; }
throw;
}
art.capture.release();
if (art.has_writer && art.writer.isOpened()) art.writer.release();
if (config.display.show_window) cv::destroyAllWindows();
double elapsed_seconds = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch()).count()
- art.run_start_time;
if (user_quit) stopped_reason = "user_quit";
if (progress) { progress->finish(); delete progress; }
PipelineResult pr;
pr.camera_id = config.camera_id;
pr.total_entered_count = art.counting_zone.total_entered_count;
pr.frames_processed = frame_index;
pr.stopped_reason = stopped_reason;
pr.vis_video_path = config.display.output_path;
pr.source_video = config.source;
pr.elapsed_seconds = elapsed_seconds;
return pr;
}
} // namespace cc
+56
View File
@@ -0,0 +1,56 @@
#include <cassert>
#include <iostream>
#include "chicken_counter/config.hpp"
int main() {
std::cout << "=== test_config ===" << std::endl;
auto raw = cc::load_data(
"/media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/cameras/example_camera.yaml");
// print detection device type for debugging
std::cout << "device type: " << raw["detection"]["device"].type_name()
<< " value: " << raw["detection"]["device"] << std::endl;
auto cam = raw.get<cc::CameraConfig>();
assert(cam.camera_id == "coop_cam_03");
assert(cam.detection.conf > 0.0f);
assert(cam.roi.points.size() >= 2);
assert(cam.roi.is_polygon());
auto cpoly = cam.roi.counting_polygon();
assert(cpoly.size() == 4);
auto crect = cam.roi.counting_rect();
assert(crect.width >= 20 && crect.height >= 20);
std::cout << " camera_config: " << cam.camera_id << " OK" << std::endl;
std::cout << " detection model: " << cam.detection.model_path << std::endl;
std::cout << " device: " << cam.detection.device << std::endl;
std::cout << " counting rect: "
<< crect.x << "," << crect.y << " "
<< crect.width << "x" << crect.height << std::endl;
auto batch = cc::load_batch_config(
"/media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/cycle7_batch.yaml");
assert(!batch.cameras.empty());
assert(batch.batch.compress_max_mb > 0);
std::cout << " batch_config: " << batch.cameras.size() << " cameras OK" << std::endl;
auto first_id = batch.cameras.begin()->first;
auto built = cc::build_camera_config_from_batch(
batch, first_id,
"/tmp/test.mp4",
"/tmp/output/test.mp4",
"/tmp/checkpoints/" + first_id);
assert(built.camera_id == first_id);
assert(built.source == "/tmp/test.mp4");
assert(built.display.output_path == "/tmp/output/test.mp4");
assert(!built.display.show_window);
assert(built.feedback.enabled);
std::cout << " build_from_batch: " << built.camera_id << " OK" << std::endl;
std::cout << "=== all tests passed ===" << std::endl;
return 0;
}
+136
View File
@@ -0,0 +1,136 @@
#include <cassert>
#include <iostream>
#include <opencv2/core.hpp>
#include "chicken_counter/capture.hpp"
#include "chicken_counter/video_writer.hpp"
#include "chicken_counter/counting.hpp"
#include "chicken_counter/motion.hpp"
#include "chicken_counter/overlay.hpp"
#include "chicken_counter/batch_discovery.hpp"
#include "chicken_counter/report.hpp"
#include "chicken_counter/compress.hpp"
int main() {
std::cout << "=== test_modules ===" << std::endl;
// test CountingZone
{
cc::RoiConfig roi;
roi.points = {{100, 200}, {1600, 200}, {1600, 800}, {100, 800}};
roi.min_overlap_ratio = 0.3f;
cc::GateConfig gate;
gate.mode = "two_line";
gate.lines_y = {320, 600};
cc::CountingZone zone(roi, gate, 20, 75, 500, false, false);
cc::TrackObservation t;
t.track_id = 1;
t.bbox_x1 = 200; t.bbox_y1 = 300;
t.bbox_x2 = 350; t.bbox_y2 = 500;
t.centroid_x = 275; t.centroid_y = 400;
t.confidence = 0.9f;
auto events = zone.update({t}, 100, false);
assert(zone.total_entered_count == 1);
assert(events.size() == 1);
assert(events[0].track_id == 1);
assert(zone.is_inside(1));
assert(zone.is_validated(1));
auto trail = zone.trail_for(1);
assert(trail.size() == 1);
std::cout << " CountingZone OK" << std::endl;
}
// test BackwardMotionDetector (disabled)
{
cc::MotionConfig mc;
mc.enabled = false;
cc::RoiConfig roi;
roi.points = {{0, 0}, {100, 0}, {100, 100}, {0, 100}};
cc::BackwardMotionDetector detector(mc, roi, false);
cv::Mat frame(100, 100, CV_8UC3, cv::Scalar(0, 0, 0));
std::vector<cc::TrackObservation> tracks;
auto state = detector.update(frame, tracks, 0);
assert(!state.backward_active);
std::cout << " BackwardMotionDetector OK" << std::endl;
}
// test overlay
{
cc::RoiConfig roi;
roi.points = {{50, 50}, {200, 50}, {200, 200}, {50, 200}};
cc::GateConfig gate;
cc::CountingZone zone(roi, gate, 20, 75);
cc::CameraConfig cfg;
cfg.camera_id = "test";
cfg.roi = roi;
cfg.overlay.trail_length = 20;
cv::Mat frame(300, 400, CV_8UC3, cv::Scalar(60, 60, 60));
cc::MotionState ms;
cc::TrackObservation t;
t.track_id = 99;
t.bbox_x1 = 100; t.bbox_y1 = 80;
t.bbox_x2 = 150; t.bbox_y2 = 130;
t.centroid_x = 125; t.centroid_y = 105;
t.confidence = 0.9f;
zone.update({t}, 1, false);
auto annotated = cc::draw_overlay(frame, cfg, zone, {t}, ms, 1);
assert(annotated.rows == 300 && annotated.cols == 400);
std::cout << " Overlay OK" << std::endl;
}
// test report
{
cc::PipelineResult pr;
pr.camera_id = "CC1";
pr.total_entered_count = 42;
pr.frames_processed = 1000;
pr.stopped_reason = "eof";
pr.source_video = "/tmp/CC1.mp4";
pr.elapsed_seconds = 10.5;
cc::CameraBatchResult cr;
cr.camera_id = "CC1";
cr.pipeline = pr;
auto entry = cc::build_camera_report_entry(cr, "/tmp/output");
assert(entry["total_entered"] == 42);
std::cout << " Report OK" << std::endl;
}
// test batch_discovery
{
cc::CameraPreset preset;
preset.camera_id = "CC1";
preset.camera_num = 1;
preset.roi.points = {{0, 0}, {100, 100}};
cc::BatchSettings settings;
settings.batch.root_dir = "/tmp/batch";
settings.batch.camera_glob = "kandang_*_camera_{num}_*.mp4";
settings.cameras["CC1"] = preset;
// test pattern replacement (not actual filesystem)
auto pattern = cc::replace_glob_placeholder(settings.batch.camera_glob, 1);
assert(pattern == "kandang_*_camera_1_*.mp4");
std::cout << " BatchDiscovery OK" << std::endl;
}
std::cout << "=== all tests passed ===" << std::endl;
return 0;
}