Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be204b91cc | ||
|
|
f34eaa3af4 | ||
|
|
1a3242f611 | ||
|
|
6af7dc1de0 |
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
<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">▶</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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user