4 Commits
Author SHA1 Message Date
proitlab be204b91cc To move to main 2026-07-22 11:58:27 +07:00
proitlab f34eaa3af4 Create C++ version 2026-07-22 00:03:02 +07:00
dsutanto 1a3242f611 merge upstream 2026-07-21 16:34:12 +07:00
proitlab 6af7dc1de0 Update with requirements.txt 2026-07-21 16:33:16 +07:00
29 changed files with 3603 additions and 26 deletions

No files matched your search

+10 -7
View File
@@ -11,11 +11,11 @@ defaults:
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
classes: [0]
ignored_classes: [1, 2]
conf: 0.35
conf: 0.55
iou: 0.55
imgsz: 640
device: "0"
min_box_area_px: 3000
min_box_area_px: 5000
validate_while_inside: true
detection_zone:
enabled: true
@@ -25,7 +25,7 @@ defaults:
tracker:
tracker_config_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/trackers/botsort_chicken.yaml
persist: true
track_buffer: 75
track_buffer: 45
gate:
mode: two_line
lines_y: [420, 730]
@@ -40,7 +40,7 @@ defaults:
debounce_frames: 12
min_features: 60
max_corners: 80
stride_frames: 2
stride_frames: 3
flow_scale: 0.5
quality_level: 0.01
min_distance: 8
@@ -49,10 +49,11 @@ defaults:
show_boxes: true
show_track_trails: false
trail_length: 20
show_center_marker: true
show_center_marker: false
show_track_ring: false
count_anchor: [780, 120]
inside_box_only: true
validated_only: true
pending_blink: true
pending_colors:
- [255, 255, 0]
@@ -60,7 +61,7 @@ defaults:
display:
show_window: false
encoder: auto
output_bitrate_kbps: 4000
output_bitrate_kbps: 2000
codec_preference: [avc1, mp4v, H264]
performance:
half: false
@@ -80,7 +81,7 @@ defaults:
inset_right_px: 60
inset_top_px: 0
inset_bottom_px: 0
min_overlap_ratio: 0.30
min_overlap_ratio: 0.35
cameras:
CC1:
@@ -95,6 +96,8 @@ cameras:
CC2:
camera_num: 2
count_anchor: [900, 120]
detection:
min_box_area_px: 3000
roi:
points:
- [20, 380]
+121
View File
@@ -0,0 +1,121 @@
batch:
root_dir: /media/jetson/DATA/.Codes/VIDEOS/cycle7/kandang-atas
camera_glob: "kandang_*_camera_{num}_*.mp4"
output_subdir: output
compress_max_mb: 200
delete_intermediate: false
checkpoint_every_n_frames: 3000
defaults:
detection:
model_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/chicken-detection-model-v26n-300e-best-2026-05-02-NEW.engine
classes: [0]
ignored_classes: [1, 2]
conf: 0.35
iou: 0.55
imgsz: 640
device: "0"
min_box_area_px: 3000
validate_while_inside: true
detection_zone:
enabled: true
buffer_above_px: 250
buffer_below_px: 250
show_in_overlay: true
tracker:
tracker_config_path: /media/jetson/DATA/.Codes/chicken-counting-sukawarna-det/configs/trackers/botsort_chicken.yaml
persist: true
track_buffer: 75
gate:
mode: two_line
lines_y: [420, 730]
direction: bottom_to_up
motion:
enabled: true
axis: vertical
forward_sign: 1.0
ema_alpha: 0.2
reverse_enter_threshold: -1.5
reverse_exit_threshold: -0.5
debounce_frames: 12
min_features: 60
max_corners: 80
stride_frames: 2
flow_scale: 0.5
quality_level: 0.01
min_distance: 8
block_radius: 6
overlay:
show_boxes: true
show_track_trails: false
trail_length: 20
show_center_marker: true
show_track_ring: false
count_anchor: [780, 120]
inside_box_only: true
pending_blink: true
pending_colors:
- [255, 255, 0]
- [0, 255, 255]
display:
show_window: false
encoder: auto
output_bitrate_kbps: 4000
codec_preference: [avc1, mp4v, H264]
performance:
half: false
overlay_buffer_reuse: true
inference_stride: 2
stream:
enabled: true
shm_dir: /dev/shm
interval_frames: 5
feedback:
enabled: true
every_n_frames: 3000
save_images: true
log_to_terminal: true
roi:
inset_left_px: 60
inset_right_px: 60
inset_top_px: 0
inset_bottom_px: 0
min_overlap_ratio: 0.30
cameras:
CC1:
camera_num: 1
count_anchor: [780, 120]
roi:
points:
- [250, 330]
- [1650, 330]
- [1650, 720]
- [250, 720]
CC2:
camera_num: 2
count_anchor: [900, 120]
roi:
points:
- [20, 380]
- [1880, 380]
- [1880, 720]
- [20, 720]
CC3:
camera_num: 3
count_anchor: [900, 120]
roi:
points:
- [20, 330]
- [1880, 330]
- [1880, 720]
- [20, 720]
CC4:
camera_num: 4
count_anchor: [700, 120]
roi:
points:
- [50, 330]
- [1450, 330]
- [1450, 720]
- [50, 720]
+4 -4
View File
@@ -1,9 +1,9 @@
tracker_type: botsort
track_high_thresh: 0.5
track_low_thresh: 0.1
new_track_thresh: 0.6
track_high_thresh: 0.65
track_low_thresh: 0.3
new_track_thresh: 0.7
track_buffer: 75
match_thresh: 0.8
match_thresh: 0.9
fuse_score: true
gmc_method: none
proximity_thresh: 0.5
+64
View File
@@ -0,0 +1,64 @@
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)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
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})
target_compile_options(chicken_counter_lib PRIVATE -O3 -march=armv8.2-a+fp16+dotprod -flto -DNDEBUG)
add_executable(chicken_counter_cli
src/main.cpp
)
target_link_libraries(chicken_counter_cli PRIVATE chicken_counter_lib)
target_link_options(chicken_counter_cli PRIVATE -Wl,--strip-all)
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
+617
View File
@@ -0,0 +1,617 @@
#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 validated_only = false;
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}, {"validated_only", c.validated_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.validated_only = j.value("validated_only", false);
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);
if (j.contains("output_path") && !j["output_path"].is_null())
c.output_path = j["output_path"].get<std::string>();
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};
GateConfig gate;
MotionConfig motion;
nlohmann::json detection_overrides;
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;
}
if (cam.contains("detection")) {
preset.detection_overrides = cam["detection"];
}
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 (!preset.detection_overrides.empty()) {
if (!raw.contains("detection")) raw["detection"] = nlohmann::json::object();
raw["detection"].update(preset.detection_overrides);
}
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();
if (!output_path.empty()) raw["display"]["output_path"] = 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
+162
View File
@@ -0,0 +1,162 @@
#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);
if (config.overlay.validated_only && !validated) continue;
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
+390
View File
@@ -0,0 +1,390 @@
#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;
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;
}
}
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)\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: model outputs (1, 300, 6) = [x1,y1,x2,y2,conf,cls] in letterbox space ---
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);
const float* d = output_buf;
int stride = 6; // (x1,y1,x2,y2,conf,cls) per detection
for (int i = 0; i < output_buf_size / stride; ++i) {
const float* row = d + i * stride;
float conf = row[4];
if (conf < conf_thresh) continue;
// boxes are in letterbox coordinates, scale back
float x1 = (row[0] - pad_x) / scale;
float y1 = (row[1] - pad_y) / scale;
float x2 = (row[2] - pad_x) / scale;
float y2 = (row[3] - pad_y) / scale;
int ix1 = std::max(0, std::min(static_cast<int>(x1), ow));
int iy1 = std::max(0, std::min(static_cast<int>(y1), oh));
int ix2 = std::max(0, std::min(static_cast<int>(x2), ow));
int iy2 = std::max(0, std::min(static_cast<int>(y2), oh));
if (ix2 > ix1 && iy2 > iy1) {
iboxes.push_back({ix1, iy1, ix2 - ix1, iy2 - iy1});
scores.push_back(conf);
}
}
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;
}
+124
View File
@@ -0,0 +1,124 @@
apturl==0.5.2
bcrypt==3.2.0
beniget==0.4.1
blinker==1.9.0
Brlapi==0.8.3
certifi==2020.6.20
chardet==4.0.0
click==8.4.2
colorama==0.4.4
contourpy==1.3.2
cpuset==1.6
cryptography==3.4.8
cupshelpers==1.0
cycler==0.12.1
dbus-python==1.2.18
decorator==4.4.2
defer==1.0.6
distro==1.7.0
distro-info==1.1+ubuntu0.2
dnspython==2.1.0
duplicity==0.8.21
et_xmlfile==2.0.0
fasteners==0.14.1
filelock==3.29.7
Flask==3.1.3
fonttools==4.63.0
fsspec==2026.6.0
future==0.18.2
gast==0.5.2
gpg==1.16.0
httplib2==0.20.2
idna==3.3
importlib-metadata==4.6.4
itsdangerous==2.2.0
jeepney==0.7.1
jetson-stats==4.3.2
Jetson.GPIO==2.1.7
Jinja2==3.1.6
keyring==23.5.0
kiwisolver==1.5.0
language-selector==0.1
lap==0.5.13
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
lockfile==0.12.2
louis==3.20.0
lxml==4.8.0
macaroonbakery==1.3.1
Mako==1.1.3
Markdown==3.3.6
MarkupSafe==3.0.3
matplotlib==3.10.9
ml_dtypes==0.5.4
monotonic==1.6
more-itertools==8.10.0
mpmath==1.3.0
networkx==3.4.2
numpy==1.26.4
nvidia-ml-py==13.610.43
oauthlib==3.2.0
olefile==0.46
onboard==1.4.1
onnx==1.22.0
opencv-python==4.11.0.86
openpyxl==3.1.5
packaging==26.2
paho-mqtt==2.1.0
paramiko==2.9.3
pexpect==4.8.0
Pillow==9.0.1
ply==3.11
polars==1.42.1
polars-runtime-32==1.42.1
protobuf==7.35.1
psutil==7.2.2
ptyprocess==0.7.0
pycairo==1.20.1
pycups==2.0.1
Pygments==2.11.2
PyGObject==3.42.1
PyJWT==2.3.0
pymacaroons==0.13.0
PyNaCl==1.5.0
PyOpenGL==3.1.5
pyparsing==3.3.2
pyRFC3339==1.1
pyservicemaker @ file:///opt/nvidia/deepstream/deepstream-7.1/service-maker/python/pyservicemaker-0.0.1-py3-none-linux_aarch64.whl
python-apt==2.4.0+ubuntu4.1
python-dateutil==2.8.1
python-dbusmock==0.27.5
python-debian==0.1.43+ubuntu1.1
python-dotenv==1.2.2
pythran==0.10.0
pytz==2022.1
pyxdg==0.27
PyYAML==6.0.3
ranger-fm==1.9.3
requests==2.25.1
requests-toolbelt==0.9.1
scipy==1.8.0
SecretStorage==3.3.1
six==1.16.0
smbus2==0.5.0
sympy==1.13.1
systemd-python==234
tensorrt==10.3.0
tensorrt_dispatch==10.3.0
tensorrt_lean==10.3.0
torch @ https://developer.download.nvidia.com/compute/redist/jp/v61/pytorch/torch-2.5.0a0+872d972e41.nv24.08.17622132-cp310-cp310-linux_aarch64.whl
torchvision @ file:///home/jetson/Downloads/.torch-2.5.0-nv24.08/torchvision-0.20.1a0%2B3ac97aa-cp310-cp310-linux_aarch64.whl#sha256=06658c32ef66451be301d52f2310c64689af299ea247dc262d5563f2a557ee3d
typing_extensions==4.16.0
ubuntu-drivers-common==0.0.0
ubuntu-pro-client==8001
ufw==0.36.1
ultralytics==8.4.90
ultralytics-thop==2.0.20
urllib3==1.26.5
urwid==2.1.2
wadllib==1.3.6
Werkzeug==3.1.8
xdg==5
xkit==0.0.0
zipp==1.0.0
+8
View File
@@ -145,6 +145,7 @@ class OverlayConfig:
show_track_ring: bool = False
count_anchor: Point = (900, 120)
inside_box_only: bool = True
validated_only: bool = False
pending_blink: bool = True
pending_colors: list[Color] = field(
default_factory=lambda: [(255, 255, 0), (0, 255, 255)]
@@ -222,6 +223,7 @@ class CameraPreset:
count_anchor: Point | None = None
gate: GateConfig | None = None
motion: MotionConfig | None = None
detection_overrides: dict[str, Any] | None = None
@dataclass
@@ -330,6 +332,7 @@ def load_batch_config(path: str | Path) -> BatchSettings:
gate = GateConfig(**camera_raw["gate"]) if "gate" in camera_raw else None
motion = MotionConfig(**camera_raw["motion"]) if "motion" in camera_raw else None
detection_overrides = camera_raw.get("detection", None)
cameras[camera_id] = CameraPreset(
camera_id=camera_id,
@@ -338,6 +341,7 @@ def load_batch_config(path: str | Path) -> BatchSettings:
count_anchor=count_anchor,
gate=gate,
motion=motion,
detection_overrides=detection_overrides,
)
return BatchSettings(batch=batch, defaults=defaults, cameras=cameras)
@@ -387,6 +391,10 @@ def build_camera_config_from_batch(
raw.setdefault("overlay", {})
raw["overlay"]["count_anchor"] = list(preset.count_anchor)
if preset.detection_overrides is not None:
raw.setdefault("detection", {})
raw["detection"].update(preset.detection_overrides)
raw.setdefault("display", {})
raw["display"]["output_path"] = str(output_path) if output_path is not None else None
raw["display"]["show_window"] = False
+7 -10
View File
@@ -52,6 +52,8 @@ def draw_overlay(
continue
validated = counting_zone.is_validated(track.track_id)
if config.overlay.validated_only and not validated:
continue
x1, y1, x2, y2 = track.bbox_xyxy
cx, cy = track.centroid
sequence_number = counting_zone.sequence_number_for(track.track_id)
@@ -66,16 +68,11 @@ def draw_overlay(
cv2.rectangle(annotated, (x1, y1), (x2, y2), box_color, 2)
if validated and sequence_number is not None:
_draw_outlined_text(
annotated,
str(sequence_number),
(x1, max(24, y1 - 8)),
font_scale=0.8,
fill_color=LIME,
outline_color=BLACK,
thickness=2,
outline_thickness=4,
)
label = str(sequence_number)
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 3)
ox, oy = x1, max(24, y1 - 8)
cv2.rectangle(annotated, (ox - 2, oy - th - 2), (ox + tw + 2, oy + 2), BLACK, -1)
cv2.putText(annotated, label, (ox, oy), cv2.FONT_HERSHEY_SIMPLEX, 0.8, LIME, 2, cv2.LINE_AA)
if config.overlay.show_center_marker:
marker_color = ORANGE if validated else (pending_colors[0 if blink_on else 1 % len(pending_colors)])
+44 -5
View File
@@ -6,8 +6,10 @@ import json
import shutil
import sys
import time
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from queue import Queue
from threading import Thread
import cv2
import numpy as np
@@ -78,6 +80,34 @@ class _ProgressBar:
sys.stderr.flush()
class _VideoWriterThread:
"""Background thread that encodes frames without blocking the main loop."""
def __init__(self, writer: cv2.VideoWriter, max_queue: int = 60) -> None:
self._writer = writer
self._queue: Queue = Queue(maxsize=max_queue)
self._thread = Thread(target=self._run, daemon=True)
self._thread.start()
def _run(self) -> None:
while True:
frame = self._queue.get()
if frame is None:
break
self._writer.write(frame)
def write(self, frame: np.ndarray) -> None:
self._queue.put(frame.copy())
def stop(self) -> None:
self._queue.put(None)
self._thread.join(timeout=5.0)
@property
def queue_size(self) -> int:
return self._queue.qsize()
@dataclass
class PipelineArtifacts:
capture: cv2.VideoCapture
@@ -90,6 +120,7 @@ class PipelineArtifacts:
total_source_frames: int | None
owns_tracker: bool
detection_zone_rect: tuple[int, int, int, int] | None = None
writer_thread: _VideoWriterThread | None = None
def build_pipeline(
@@ -129,6 +160,7 @@ def build_pipeline(
overlay_buffer = np.empty((height, width, 3), dtype=np.uint8)
writer = None
writer_thread = None
if config.display.output_path:
fps = config.display.write_fps or capture.get(cv2.CAP_PROP_FPS) or 30.0
writer = make_video_writer(
@@ -139,6 +171,8 @@ def build_pipeline(
output_bitrate_kbps=config.display.output_bitrate_kbps,
codec_preference=config.display.codec_preference,
)
writer_thread = _VideoWriterThread(writer)
print("[video] writer thread started")
if config.stream.enabled:
cam_dir = Path(config.stream.shm_dir) / f"chicken_counter_{config.camera_id}"
@@ -157,6 +191,7 @@ def build_pipeline(
total_source_frames=total_source_frames,
owns_tracker=owns_tracker,
detection_zone_rect=detection_zone_rect,
writer_thread=writer_thread,
)
@@ -310,12 +345,16 @@ def run_pipeline(
user_quit = True
break
if artifacts.writer is not None and last_annotated is not None:
if artifacts.writer_thread is not None and last_annotated is not None:
freeze_frame_count = int((config.display.write_fps or 30.0) * 2)
for _ in range(max(1, freeze_frame_count)):
artifacts.writer.write(last_annotated)
artifacts.writer_thread.write(last_annotated)
finally:
artifacts.capture.release()
if artifacts.writer_thread is not None:
print(f"[video] flushing {artifacts.writer_thread.queue_size} queued frames...")
artifacts.writer_thread.stop()
print("[video] writer thread stopped")
if artifacts.writer is not None:
artifacts.writer.release()
if config.display.show_window:
@@ -348,8 +387,8 @@ def _consume_result(
) -> None:
if config.display.show_window:
cv2.imshow(config.display.window_name, annotated)
if artifacts.writer is not None:
artifacts.writer.write(annotated)
if artifacts.writer_thread is not None:
artifacts.writer_thread.write(annotated)
for event in result.count_events:
if config.performance.verbose: