#pragma once #include #include #include #include #include #include #include #include #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 update( const std::vector& tracks, int frame_index, bool counting_paused = false) { std::vector events; std::unordered_set 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(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(inside_ids.size()); current_inside_ids = inside_ids; prev_inside_ids = inside_ids; purge_stale(frame_index, active_ids); return events; } std::vector 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 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 counting_polygon; cv::Rect counting_rect; std::unordered_map> histories; std::unordered_map last_seen_frame; std::unordered_set counted_ids; std::unordered_set prev_inside_ids; std::unordered_set current_inside_ids; std::unordered_map 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& active_ids) { std::vector 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