forked from zakaria/chicken-counting-sukawarna-det
606 lines
23 KiB
C++
606 lines
23 KiB
C++
#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
|