Skip to content

Commit a70c0a4

Browse files
fix: choose detection box colour by class name rather than index
getDetectionBoxColor() maps a raw class index to a colour using the COCO ordering: 0 is person (blue), 1-8 vehicles (green), 14-23 animals (orange). That is only meaningful for a model that actually is COCO. A custom model - a firearm detector whose class 0 is "gun", say - got its weapons drawn in the blue reserved for people. Resolve the colour through the ObjectClasses instance's own class list instead, so the decision is made on the class name. COCO is unaffected: a test asserts the new lookup agrees with the legacy index mapping across all 80 classes. A custom model now gets red for classes we have no opinion on, and still gets blue for "person" wherever that class happens to sit in its ordering. The static index-based functions are kept for compatibility but documented as COCO-only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThMq92LnrBbU2y9astkCDJ
1 parent 3ec6db6 commit a70c0a4

5 files changed

Lines changed: 183 additions & 2 deletions

File tree

src/zm_netint_yolo.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ int Quadra_Yolo::draw_roi_box_in_place(
436436

437437
// Use provided color, or fall back to default class-based color
438438
if (box_color == 0) {
439-
box_color = ObjectClasses::getDetectionBoxColor(roi_extra.cls);
439+
box_color = object_classes_.boxColorFor(roi_extra.cls);
440440
}
441441
Image in_image(inframe);
442442

@@ -463,7 +463,7 @@ int Quadra_Yolo::draw_roi_box(
463463
snprintf(color_buf, sizeof(color_buf), "0x%06X", box_color);
464464
color = color_buf;
465465
} else {
466-
color = ObjectClasses::getDetectionColorString(roi_extra.cls);
466+
color = object_classes_.colorStringFor(roi_extra.cls);
467467
}
468468

469469
for (int i=0; i<line_width; i++) {

src/zm_object_classes.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "zm_logger.h"
2222

2323
#include <fstream>
24+
#include <set>
2425

2526
// Default COCO dataset class names (80 classes)
2627
const std::vector<std::string> ObjectClasses::kCocoClassNames = {
@@ -94,6 +95,41 @@ const std::string& ObjectClasses::getClassName(int class_id) const {
9495
return kUnknownClass;
9596
}
9697

98+
namespace {
99+
100+
// The COCO groupings the index-based mapping below encodes, by name so they
101+
// survive a model that numbers its classes differently.
102+
const std::set<std::string> kVehicleClasses = {
103+
"bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat"
104+
};
105+
const std::set<std::string> kAnimalClasses = {
106+
"bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe"
107+
};
108+
109+
} // namespace
110+
111+
Rgb ObjectClasses::boxColorForName(const std::string &class_name) {
112+
if (class_name == "person") return kRGBBlue;
113+
if (kVehicleClasses.count(class_name)) return kRGBGreen;
114+
if (kAnimalClasses.count(class_name)) return kRGBOrange;
115+
return kRGBRed;
116+
}
117+
118+
const char *ObjectClasses::colorStringForName(const std::string &class_name) {
119+
if (class_name == "person") return "blue";
120+
if (kVehicleClasses.count(class_name)) return "green";
121+
if (kAnimalClasses.count(class_name)) return "orange";
122+
return "red";
123+
}
124+
125+
Rgb ObjectClasses::boxColorFor(int class_id) const {
126+
return boxColorForName(getClassName(class_id));
127+
}
128+
129+
const char *ObjectClasses::colorStringFor(int class_id) const {
130+
return colorStringForName(getClassName(class_id));
131+
}
132+
97133
Rgb ObjectClasses::getDetectionBoxColor(int class_id) {
98134
if (class_id == 0) {
99135
return kRGBBlue; // Person

src/zm_object_classes.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,28 @@ class ObjectClasses {
4646

4747
// Get detection box color based on class ID.
4848
// Person (class 0) = Blue, Vehicles (1-8) = Green, Animals (14-23) = Orange, Others = Red
49+
//
50+
// These take a raw index and can therefore only assume the COCO ordering. A
51+
// custom model's class 0 is not "person", so prefer the instance methods
52+
// below, which resolve the index through this object's own class list.
4953
static Rgb getDetectionBoxColor(int class_id);
5054

5155
// Get detection color name as string.
5256
static const char* getDetectionColorString(int class_id);
5357

58+
/* Colour for a detection box, chosen from the class *name* rather than its
59+
* index. An index only means anything within the dataset it came from, so a
60+
* firearm model whose class 0 is "gun" would otherwise be drawn in COCO's
61+
* "person" blue. Resolving by name gives identical results for COCO and
62+
* something sensible - red, the default - for classes we have no opinion on.
63+
*/
64+
Rgb boxColorFor(int class_id) const;
65+
const char *colorStringFor(int class_id) const;
66+
67+
// Name-based colour lookup, exposed so it can be tested directly.
68+
static Rgb boxColorForName(const std::string &class_name);
69+
static const char *colorStringForName(const std::string &class_name);
70+
5471
// Access to underlying vector for iteration
5572
const std::vector<std::string>& getClassNames() const { return class_names_; }
5673

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ set(TEST_SOURCES
2222
zm_image.cpp
2323
zm_monitorstream.cpp
2424
zm_netint_lpr.cpp
25+
zm_object_classes.cpp
2526
zm_onvif_renewal.cpp
2627
zm_onvif_wsse.cpp
2728
zm_pixformat.cpp

tests/zm_object_classes.cpp

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
3+
*
4+
* This program is free software; you can redistribute it and/or modify it
5+
* under the terms of the GNU General Public License as published by the
6+
* Free Software Foundation; either version 2 of the License, or (at your
7+
* option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12+
* more details.
13+
*
14+
* You should have received a copy of the GNU General Public License along
15+
* with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#include "zm_catch2.h"
19+
20+
#include <cstdio>
21+
#include <fstream>
22+
#include <string>
23+
24+
#include "zm_object_classes.h"
25+
26+
namespace {
27+
28+
// Write a .names file beside a notional model so loadFromFile() finds it.
29+
std::string WriteNames(const std::string &stem, const std::string &contents) {
30+
const std::string names = stem + ".names";
31+
std::ofstream out(names);
32+
out << contents;
33+
out.close();
34+
return stem + ".nb"; // what loadFromFile is handed
35+
}
36+
37+
} // namespace
38+
39+
TEST_CASE("ObjectClasses::boxColorForName maps the COCO groupings", "[objectclasses]") {
40+
// These are the groupings the old index-based mapping encoded. Resolving them
41+
// by name has to give exactly the same answers for COCO.
42+
SECTION("person is blue") {
43+
REQUIRE(ObjectClasses::boxColorForName("person") == kRGBBlue);
44+
}
45+
46+
SECTION("vehicles are green") {
47+
for (const char *name : {"bicycle", "car", "motorcycle", "airplane",
48+
"bus", "train", "truck", "boat"}) {
49+
REQUIRE(ObjectClasses::boxColorForName(name) == kRGBGreen);
50+
}
51+
}
52+
53+
SECTION("animals are orange") {
54+
for (const char *name : {"bird", "cat", "dog", "horse", "sheep",
55+
"cow", "elephant", "bear", "zebra", "giraffe"}) {
56+
REQUIRE(ObjectClasses::boxColorForName(name) == kRGBOrange);
57+
}
58+
}
59+
60+
SECTION("anything else is red") {
61+
REQUIRE(ObjectClasses::boxColorForName("backpack") == kRGBRed);
62+
REQUIRE(ObjectClasses::boxColorForName("gun") == kRGBRed);
63+
REQUIRE(ObjectClasses::boxColorForName("") == kRGBRed);
64+
}
65+
}
66+
67+
TEST_CASE("ObjectClasses::boxColorFor agrees with the index mapping for COCO",
68+
"[objectclasses]") {
69+
// A default-constructed instance carries COCO, so the new name-based lookup
70+
// must reproduce the legacy index-based one across every class. This is the
71+
// regression guard: the refactor may not change existing behaviour.
72+
ObjectClasses coco;
73+
for (size_t i = 0; i < coco.size(); i++) {
74+
const int id = static_cast<int>(i);
75+
REQUIRE(coco.boxColorFor(id) == ObjectClasses::getDetectionBoxColor(id));
76+
REQUIRE(std::string(coco.colorStringFor(id)) ==
77+
std::string(ObjectClasses::getDetectionColorString(id)));
78+
}
79+
}
80+
81+
TEST_CASE("ObjectClasses::boxColorFor does not give a custom model COCO's colours",
82+
"[objectclasses]") {
83+
/* The bug this fixes: a firearm model's class 0 is "gun", but the index-based
84+
* mapping only knows COCO, where 0 is "person" - so a weapon was drawn in the
85+
* blue reserved for people.
86+
*/
87+
const std::string model = WriteNames("/tmp/zm_objclasses_firearm", "gun\nrifle\nknife\n");
88+
89+
ObjectClasses classes;
90+
REQUIRE(classes.loadFromFile(model));
91+
REQUIRE(classes.size() == 3);
92+
REQUIRE(classes.getClassName(0) == "gun");
93+
94+
// The legacy index lookup still claims blue for class 0 ...
95+
REQUIRE(ObjectClasses::getDetectionBoxColor(0) == kRGBBlue);
96+
// ... but resolving through this model's own names does not.
97+
REQUIRE(classes.boxColorFor(0) == kRGBRed);
98+
REQUIRE(classes.boxColorFor(1) == kRGBRed);
99+
REQUIRE(classes.boxColorFor(2) == kRGBRed);
100+
REQUIRE(std::string(classes.colorStringFor(0)) == "red");
101+
102+
std::remove("/tmp/zm_objclasses_firearm.names");
103+
}
104+
105+
TEST_CASE("ObjectClasses::boxColorFor still honours names a custom model shares with COCO",
106+
"[objectclasses]") {
107+
// A custom model that reorders COCO-ish names must colour by meaning, not by
108+
// position: "person" is blue wherever it lands.
109+
const std::string model = WriteNames("/tmp/zm_objclasses_mixed", "gun\nperson\ncar\n");
110+
111+
ObjectClasses classes;
112+
REQUIRE(classes.loadFromFile(model));
113+
REQUIRE(classes.boxColorFor(0) == kRGBRed); // gun
114+
REQUIRE(classes.boxColorFor(1) == kRGBBlue); // person, at index 1 not 0
115+
REQUIRE(classes.boxColorFor(2) == kRGBGreen); // car, at index 2 not 2-of-COCO
116+
117+
std::remove("/tmp/zm_objclasses_mixed.names");
118+
}
119+
120+
TEST_CASE("ObjectClasses::loadFromFile falls back to COCO when no .names exists",
121+
"[objectclasses]") {
122+
ObjectClasses classes;
123+
REQUIRE_FALSE(classes.loadFromFile("/tmp/zm_objclasses_definitely_absent.nb"));
124+
REQUIRE(classes.size() == 80);
125+
REQUIRE(classes.getClassName(0) == "person");
126+
REQUIRE(classes.boxColorFor(0) == kRGBBlue);
127+
}

0 commit comments

Comments
 (0)