Add face alarm reentry suppression
This commit is contained in:
parent
da36345a90
commit
68d7c9edcf
@ -358,6 +358,18 @@
|
||||
"per_track_cooldown_ms": 0
|
||||
}
|
||||
],
|
||||
"face_track_aggregation": {
|
||||
"known": {
|
||||
"min_hits": 3,
|
||||
"hit_window_ms": 3000,
|
||||
"reentry_cooldown_ms": 300000
|
||||
},
|
||||
"unknown": {
|
||||
"min_track_age_ms": 2000,
|
||||
"min_quality_hits": 4,
|
||||
"reentry_cooldown_ms": 300000
|
||||
}
|
||||
},
|
||||
"face_rules": [
|
||||
{
|
||||
"name": "unknown_face",
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "utils/logger.h"
|
||||
@ -32,8 +33,10 @@ namespace {
|
||||
struct FaceTrackAggregationConfig {
|
||||
int known_min_hits = 3;
|
||||
int known_hit_window_ms = 3000;
|
||||
int known_reentry_cooldown_ms = 0;
|
||||
int unknown_min_track_age_ms = 2000;
|
||||
int unknown_min_quality_hits = 4;
|
||||
int unknown_reentry_cooldown_ms = 0;
|
||||
int state_expire_ms = 5000;
|
||||
};
|
||||
|
||||
@ -90,17 +93,21 @@ FaceTrackAggregationConfig ParseFaceTrackAggregationConfig(const SimpleJson& con
|
||||
|
||||
cfg.known_min_hits = std::max(1, agg->ValueOr<int>("known_min_hits", cfg.known_min_hits));
|
||||
cfg.known_hit_window_ms = std::max(0, agg->ValueOr<int>("known_hit_window_ms", cfg.known_hit_window_ms));
|
||||
cfg.known_reentry_cooldown_ms = std::max(0, agg->ValueOr<int>("known_reentry_cooldown_ms", cfg.known_reentry_cooldown_ms));
|
||||
cfg.unknown_min_track_age_ms = std::max(0, agg->ValueOr<int>("unknown_min_track_age_ms", cfg.unknown_min_track_age_ms));
|
||||
cfg.unknown_min_quality_hits = std::max(1, agg->ValueOr<int>("unknown_min_quality_hits", cfg.unknown_min_quality_hits));
|
||||
cfg.unknown_reentry_cooldown_ms = std::max(0, agg->ValueOr<int>("unknown_reentry_cooldown_ms", cfg.unknown_reentry_cooldown_ms));
|
||||
cfg.state_expire_ms = std::max(0, agg->ValueOr<int>("state_expire_ms", cfg.state_expire_ms));
|
||||
|
||||
if (const SimpleJson* known = agg->Find("known"); known && known->IsObject()) {
|
||||
cfg.known_min_hits = std::max(1, known->ValueOr<int>("min_hits", cfg.known_min_hits));
|
||||
cfg.known_hit_window_ms = std::max(0, known->ValueOr<int>("hit_window_ms", cfg.known_hit_window_ms));
|
||||
cfg.known_reentry_cooldown_ms = std::max(0, known->ValueOr<int>("reentry_cooldown_ms", cfg.known_reentry_cooldown_ms));
|
||||
}
|
||||
if (const SimpleJson* unknown = agg->Find("unknown"); unknown && unknown->IsObject()) {
|
||||
cfg.unknown_min_track_age_ms = std::max(0, unknown->ValueOr<int>("min_track_age_ms", cfg.unknown_min_track_age_ms));
|
||||
cfg.unknown_min_quality_hits = std::max(1, unknown->ValueOr<int>("min_quality_hits", cfg.unknown_min_quality_hits));
|
||||
cfg.unknown_reentry_cooldown_ms = std::max(0, unknown->ValueOr<int>("reentry_cooldown_ms", cfg.unknown_reentry_cooldown_ms));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
@ -109,6 +116,7 @@ FaceTrackAggregationConfig ParseFaceTrackAggregationConfig(const SimpleJson& con
|
||||
FaceTrackDecision UpdateFaceTrackState(
|
||||
const FaceTrackAggregationConfig& cfg,
|
||||
FaceTrackState& state,
|
||||
std::unordered_map<int, uint64_t>& known_identity_last_alarm_ms,
|
||||
const FaceRecogItem& item,
|
||||
uint64_t now_ms) {
|
||||
FaceTrackDecision decision;
|
||||
@ -145,8 +153,21 @@ FaceTrackDecision UpdateFaceTrackState(
|
||||
|
||||
if (!state.reported_known &&
|
||||
static_cast<int>(state.known_hit_times.size()) >= std::max(1, cfg.known_min_hits)) {
|
||||
if (cfg.known_reentry_cooldown_ms > 0 && state.best_known_person_id >= 0) {
|
||||
const auto it_last = known_identity_last_alarm_ms.find(state.best_known_person_id);
|
||||
if (it_last != known_identity_last_alarm_ms.end() &&
|
||||
now_ms >= it_last->second &&
|
||||
(now_ms - it_last->second) < static_cast<uint64_t>(cfg.known_reentry_cooldown_ms)) {
|
||||
state.reported_known = true;
|
||||
state.reported_unknown = false;
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
state.reported_known = true;
|
||||
state.reported_unknown = false;
|
||||
if (state.best_known_person_id >= 0) {
|
||||
known_identity_last_alarm_ms[state.best_known_person_id] = now_ms;
|
||||
}
|
||||
decision.trigger_known = true;
|
||||
}
|
||||
return decision;
|
||||
@ -256,6 +277,7 @@ public:
|
||||
ParseFaceRules(config);
|
||||
track_agg_cfg_ = ParseFaceTrackAggregationConfig(config);
|
||||
face_track_states_.clear();
|
||||
known_identity_last_alarm_ms_.clear();
|
||||
|
||||
// Optional evaluation throttle (for alarm side). 0 = disabled.
|
||||
eval_interval_ms_ = std::max<int64_t>(0, static_cast<int64_t>(config.ValueOr<int>("eval_interval_ms", 0)));
|
||||
@ -528,6 +550,7 @@ public:
|
||||
face_last_trigger_.clear();
|
||||
face_person_last_trigger_.clear();
|
||||
face_track_states_.clear();
|
||||
known_identity_last_alarm_ms_.clear();
|
||||
packet_buffer_ = std::move(new_packet_buffer);
|
||||
old_actions = std::move(actions_);
|
||||
actions_ = std::move(new_actions);
|
||||
@ -644,6 +667,7 @@ private:
|
||||
face_last_trigger_.clear();
|
||||
face_person_last_trigger_.clear();
|
||||
face_track_states_.clear();
|
||||
known_identity_last_alarm_ms_.clear();
|
||||
}
|
||||
|
||||
static bool FaceItemMatchesRule(
|
||||
@ -745,7 +769,8 @@ private:
|
||||
if (!qualifying_observations[i]) continue;
|
||||
const auto& it = frame->face_recog->items[i];
|
||||
auto& state = face_track_states_[it.person_track_id];
|
||||
track_decisions[i] = UpdateFaceTrackState(track_agg_cfg_, state, it, now_epoch_ms);
|
||||
track_decisions[i] = UpdateFaceTrackState(
|
||||
track_agg_cfg_, state, known_identity_last_alarm_ms_, it, now_epoch_ms);
|
||||
have_track_decision[i] = true;
|
||||
}
|
||||
|
||||
@ -888,6 +913,7 @@ private:
|
||||
std::map<std::string, std::chrono::steady_clock::time_point> face_person_last_trigger_;
|
||||
FaceTrackAggregationConfig track_agg_cfg_;
|
||||
std::map<int, FaceTrackState> face_track_states_;
|
||||
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms_;
|
||||
std::shared_ptr<PacketRingBuffer> packet_buffer_;
|
||||
std::vector<std::unique_ptr<IAlarmAction>> actions_;
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "node.h"
|
||||
@ -72,10 +73,11 @@ AlarmNode::FaceRule MakeKnownRule() {
|
||||
TEST(FaceTrackAlarmTest, IgnoresLowQualityUntrackedFace) {
|
||||
FaceTrackAggregationConfig cfg;
|
||||
FaceTrackState state;
|
||||
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
|
||||
|
||||
FaceRecogItem item = MakeUnknownFace(-1, -1, "", 0.40f);
|
||||
|
||||
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, item, 1000);
|
||||
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1000);
|
||||
EXPECT_FALSE(decision.trigger_known);
|
||||
EXPECT_FALSE(decision.trigger_unknown);
|
||||
}
|
||||
@ -86,16 +88,42 @@ TEST(FaceTrackAlarmTest, EmitsKnownAfterStableHitsOnSameTrack) {
|
||||
cfg.known_hit_window_ms = 3000;
|
||||
|
||||
FaceTrackState state;
|
||||
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
|
||||
const FaceRecogItem item = MakeKnownFace(5, 1, "reg_001", 0.62f);
|
||||
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 1000).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 1500).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1000).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1500).trigger_known);
|
||||
|
||||
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, item, 2000);
|
||||
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 2000);
|
||||
EXPECT_TRUE(decision.trigger_known);
|
||||
EXPECT_FALSE(decision.trigger_unknown);
|
||||
}
|
||||
|
||||
TEST(FaceTrackAlarmTest, SuppressesKnownReentryInsideConfiguredCooldown) {
|
||||
FaceTrackAggregationConfig cfg;
|
||||
cfg.known_min_hits = 2;
|
||||
cfg.known_hit_window_ms = 3000;
|
||||
cfg.known_reentry_cooldown_ms = 5000;
|
||||
|
||||
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
|
||||
const FaceRecogItem initial_track = MakeKnownFace(8, 1, "alice", 0.66f);
|
||||
|
||||
FaceTrackState first_track_state;
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, first_track_state, known_identity_last_alarm_ms, initial_track, 1000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, first_track_state, known_identity_last_alarm_ms, initial_track, 1500).trigger_known);
|
||||
|
||||
FaceTrackState reentry_state;
|
||||
FaceRecogItem reentry_track = initial_track;
|
||||
reentry_track.person_track_id = 18;
|
||||
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, reentry_state, known_identity_last_alarm_ms, reentry_track, 4000).trigger_known);
|
||||
const FaceTrackDecision suppressed = UpdateFaceTrackState(
|
||||
cfg, reentry_state, known_identity_last_alarm_ms, reentry_track, 4500);
|
||||
EXPECT_FALSE(suppressed.trigger_known);
|
||||
EXPECT_TRUE(reentry_state.reported_known);
|
||||
EXPECT_EQ(known_identity_last_alarm_ms.at(1), 1500u);
|
||||
}
|
||||
|
||||
TEST(FaceTrackAlarmTest, DoesNotEmitUnknownForKnownPersonScoreWobble) {
|
||||
FaceTrackAggregationConfig cfg;
|
||||
cfg.known_min_hits = 3;
|
||||
@ -104,14 +132,16 @@ TEST(FaceTrackAlarmTest, DoesNotEmitUnknownForKnownPersonScoreWobble) {
|
||||
cfg.unknown_min_quality_hits = 1;
|
||||
|
||||
FaceTrackState state;
|
||||
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
|
||||
const FaceRecogItem known = MakeKnownFace(9, 1, "reg_001", 0.63f);
|
||||
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known, 1000).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known, 1500).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known, 2000).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 1000).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 1500).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 2000).trigger_known);
|
||||
|
||||
const FaceRecogItem wobble = MakeUnknownFace(9, 1, "reg_001", 0.42f);
|
||||
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, wobble, 2600);
|
||||
const FaceTrackDecision decision = UpdateFaceTrackState(
|
||||
cfg, state, known_identity_last_alarm_ms, wobble, 2600);
|
||||
EXPECT_FALSE(decision.trigger_known);
|
||||
EXPECT_FALSE(decision.trigger_unknown);
|
||||
}
|
||||
@ -124,16 +154,18 @@ TEST(FaceTrackAlarmTest, DoesNotEmitUnknownWhileTrackIsKnownLeaning) {
|
||||
cfg.unknown_min_quality_hits = 1;
|
||||
|
||||
FaceTrackState state;
|
||||
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
|
||||
const FaceRecogItem known = MakeKnownFace(10, 1, "alice", 0.63f);
|
||||
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known, 1000).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known, 1500).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 1000).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 1500).trigger_known);
|
||||
EXPECT_EQ(state.best_known_person_id, 1);
|
||||
EXPECT_EQ(state.best_known_name, "alice");
|
||||
EXPECT_EQ(state.known_hit_times.size(), 2u);
|
||||
|
||||
const FaceRecogItem wobble = MakeUnknownFace(10, 1, "alice", 0.42f);
|
||||
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, wobble, 2600);
|
||||
const FaceTrackDecision decision = UpdateFaceTrackState(
|
||||
cfg, state, known_identity_last_alarm_ms, wobble, 2600);
|
||||
EXPECT_FALSE(decision.trigger_known);
|
||||
EXPECT_FALSE(decision.trigger_unknown);
|
||||
}
|
||||
@ -144,14 +176,15 @@ TEST(FaceTrackAlarmTest, ResetsKnownStateWhenIdentityChangesOnSameTrack) {
|
||||
cfg.known_hit_window_ms = 3000;
|
||||
|
||||
FaceTrackState state;
|
||||
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
|
||||
const FaceRecogItem alice = MakeKnownFace(7, 1, "alice", 0.80f);
|
||||
const FaceRecogItem bob = MakeKnownFace(7, 2, "bob", 0.82f);
|
||||
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, alice, 1000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, alice, 1500).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, alice, 1000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, alice, 1500).trigger_known);
|
||||
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, bob, 2000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, bob, 2500).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, bob, 2000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, bob, 2500).trigger_known);
|
||||
}
|
||||
|
||||
TEST(FaceTrackAlarmTest, ExpiresStateAfterTrackInactivity) {
|
||||
@ -161,13 +194,14 @@ TEST(FaceTrackAlarmTest, ExpiresStateAfterTrackInactivity) {
|
||||
cfg.state_expire_ms = 1000;
|
||||
|
||||
FaceTrackState state;
|
||||
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
|
||||
const FaceRecogItem item = MakeKnownFace(8, 1, "alice", 0.80f);
|
||||
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 1000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, item, 1500).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1500).trigger_known);
|
||||
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 4000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, item, 4500).trigger_known);
|
||||
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 4000).trigger_known);
|
||||
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 4500).trigger_known);
|
||||
}
|
||||
|
||||
TEST(FaceTrackAlarmTest, DisqualifiedFramesDoNotAdvanceTrackAggregation) {
|
||||
@ -277,6 +311,63 @@ TEST(FaceTrackAlarmTest, OverlappingRulesDoNotDoubleCountSingleFrameEvidence) {
|
||||
EXPECT_TRUE(it_second->second.reported_known);
|
||||
}
|
||||
|
||||
TEST(FaceTrackAlarmTest, KnownPersonDoesNotReAlarmWhileTrackStaysActive) {
|
||||
AlarmNode node;
|
||||
node.track_agg_cfg_.known_min_hits = 2;
|
||||
node.track_agg_cfg_.known_hit_window_ms = 5000;
|
||||
|
||||
auto rule = MakeKnownRule();
|
||||
rule.cooldown_ms = 0;
|
||||
node.face_rules_.push_back(rule);
|
||||
|
||||
FaceRecogItem item = MakeKnownFace(61, 5, "alice", 0.88f);
|
||||
item.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f};
|
||||
|
||||
EXPECT_EQ(node.Process(MakeFaceFrame(1, item)), NodeStatus::OK);
|
||||
EXPECT_EQ(node.alarm_count_, 0u);
|
||||
|
||||
EXPECT_EQ(node.Process(MakeFaceFrame(2, item)), NodeStatus::OK);
|
||||
EXPECT_EQ(node.alarm_count_, 1u);
|
||||
|
||||
EXPECT_EQ(node.Process(MakeFaceFrame(3, item)), NodeStatus::OK);
|
||||
EXPECT_EQ(node.alarm_count_, 1u);
|
||||
|
||||
EXPECT_EQ(node.Process(MakeFaceFrame(4, item)), NodeStatus::OK);
|
||||
EXPECT_EQ(node.alarm_count_, 1u);
|
||||
}
|
||||
|
||||
TEST(FaceTrackAlarmTest, KnownPersonReEntryInsideCooldownOnNewTrackDoesNotReAlarm) {
|
||||
AlarmNode node;
|
||||
node.track_agg_cfg_.known_min_hits = 2;
|
||||
node.track_agg_cfg_.known_hit_window_ms = 5000;
|
||||
node.track_agg_cfg_.known_reentry_cooldown_ms = 1000;
|
||||
|
||||
auto rule = MakeKnownRule();
|
||||
rule.cooldown_ms = 0;
|
||||
node.face_rules_.push_back(rule);
|
||||
|
||||
FaceRecogItem first_track = MakeKnownFace(71, 5, "alice", 0.88f);
|
||||
first_track.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f};
|
||||
FaceRecogItem second_track = MakeKnownFace(72, 5, "alice", 0.88f);
|
||||
second_track.bbox = first_track.bbox;
|
||||
|
||||
EXPECT_EQ(node.Process(MakeFaceFrame(1, first_track)), NodeStatus::OK);
|
||||
EXPECT_EQ(node.alarm_count_, 0u);
|
||||
|
||||
EXPECT_EQ(node.Process(MakeFaceFrame(2, first_track)), NodeStatus::OK);
|
||||
EXPECT_EQ(node.alarm_count_, 1u);
|
||||
ASSERT_EQ(node.known_identity_last_alarm_ms_.size(), 1u);
|
||||
const auto first_alarm_it = node.known_identity_last_alarm_ms_.find(5);
|
||||
ASSERT_NE(first_alarm_it, node.known_identity_last_alarm_ms_.end());
|
||||
|
||||
EXPECT_EQ(node.Process(MakeFaceFrame(3, second_track)), NodeStatus::OK);
|
||||
EXPECT_EQ(node.alarm_count_, 1u);
|
||||
|
||||
EXPECT_EQ(node.Process(MakeFaceFrame(4, second_track)), NodeStatus::OK);
|
||||
EXPECT_EQ(node.alarm_count_, 1u);
|
||||
EXPECT_EQ(node.known_identity_last_alarm_ms_.at(5), first_alarm_it->second);
|
||||
}
|
||||
|
||||
TEST(FaceTrackAlarmTest, IgnoresLegacyDisableFlagAndStillUsesTrackAggregation) {
|
||||
AlarmNode node;
|
||||
NodeContext ctx;
|
||||
@ -296,7 +387,8 @@ TEST(FaceTrackAlarmTest, IgnoresLegacyDisableFlagAndStillUsesTrackAggregation) {
|
||||
"enable": false,
|
||||
"known": {
|
||||
"min_hits": 2,
|
||||
"hit_window_ms": 5000
|
||||
"hit_window_ms": 5000,
|
||||
"reentry_cooldown_ms": 60000
|
||||
}
|
||||
}
|
||||
})");
|
||||
@ -304,6 +396,7 @@ TEST(FaceTrackAlarmTest, IgnoresLegacyDisableFlagAndStillUsesTrackAggregation) {
|
||||
ASSERT_TRUE(node.Init(config, ctx));
|
||||
EXPECT_EQ(node.track_agg_cfg_.known_min_hits, 2);
|
||||
EXPECT_EQ(node.track_agg_cfg_.known_hit_window_ms, 5000);
|
||||
EXPECT_EQ(node.track_agg_cfg_.known_reentry_cooldown_ms, 60000);
|
||||
|
||||
FaceRecogItem item = MakeKnownFace(31, 5, "alice", 0.88f);
|
||||
item.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user