#pragma once #include #include #include #include #include #include #include #include #include #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(); p.y = j.at(1).get(); } 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(), j.at(1).get(), j.at(2).get()); } } // 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(); try { double dval = node.as(); int ival = static_cast(dval); if (dval == static_cast(ival)) return ival; return dval; } catch (const YAML::BadConversion&) { std::string val = node.as(); 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()] = 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 classes = {0}; std::vector 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{0}); c.ignored_classes = j.value("ignored_classes", std::vector{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 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 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>(); 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 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{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 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(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 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{"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 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(); } 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(); 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(); preset.roi.points = cam["roi"]["points"].get>(); if (cam.contains("count_anchor")) { preset.count_anchor = cam["count_anchor"].get(); } else if (cam.contains("overlay") && cam["overlay"].contains("count_anchor")) { preset.count_anchor = cam["overlay"]["count_anchor"].get(); } if (cam.contains("gate")) { preset.gate = cam["gate"].get(); preset.has_gate = true; } if (cam.contains("motion")) { preset.motion = cam["motion"].get(); 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(); } } // namespace cc