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

+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