-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathAnalysis.cpp
More file actions
285 lines (253 loc) · 11.3 KB
/
Copy pathAnalysis.cpp
File metadata and controls
285 lines (253 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/*
* Copyright (C) 2025: Arizona Board of Regents on Behalf of the University of Arizona
*/
#include "Analysis.h"
#include <stdexcept>
#include <set>
#include <nlohmann/json.hpp>
using namespace asdp::analysis;
using json = nlohmann::json;
AnalysisReport::AnalysisReport(std::string jsonString)
{
// Convert the incoming string to a JSON object.
json jsonObject;
try {
jsonObject = json::parse(jsonString);
} catch (json::parse_error& e) {
throw std::invalid_argument("Malformed JSON string: " + std::string(e.what()));
}
// Extract required fields
try {
CamID = jsonObject.at("CamID").get<uint32_t>();
json timeJson = jsonObject.at("Time");
Timestamp.seconds = timeJson[0];
Timestamp.microseconds = timeJson[1];
Name = jsonObject.at("Name").get<std::string>();
} catch (json::out_of_range& e) {
throw std::invalid_argument("Missing required field: " + std::string(e.what()));
}
// Extract optional fields
try {
if (jsonObject.contains("Loc")) {
Loc = std::make_shared<std::array<float, 2>>(jsonObject.at("Loc").get<std::array<float, 2>>());
}
} catch (...) { }
try {
if (jsonObject.contains("Rect")) {
Rect = std::make_shared<std::array<float, 2>>(jsonObject.at("Rect").get<std::array<float, 2>>());
}
} catch (...) {}
try {
if (jsonObject.contains("Vel")) {
Vel = std::make_shared< std::array<float, 2> >(jsonObject.at("Vel").get<std::array<float, 2>>());
}
} catch (...) {}
try {
if (jsonObject.contains("Class")) {
for (const auto& classJson : jsonObject.at("Class")) {
Classification c;
if (classJson.contains("Type")) {
c.Type = std::make_shared<std::string>(classJson.at("Type").get<std::string>());
}
if (classJson.contains("Chance")) {
c.Chance = std::make_shared<float>(classJson.at("Chance").get<float>());
}
if (classJson.contains("IFF")) {
c.IFF = std::make_shared<std::string>(classJson.at("IFF").get<std::string>());
}
Class.push_back(c);
}
}
} catch (...) {}
}
asdp::render::CompositeCameras::Annotation AnalysisReport::ConvertToAnnotation(float chanceThreshold, float alpha) const
{
asdp::render::CompositeCameras::Annotation annotation;
// If we have classifications and all of the classifications have Chance fields and all of the filled in
// Chance fields are below threshold, return an empty annotation.
if (!Class.empty()) {
// We have classifications, check their Chance fields.
bool allBelowThreshold = true;
for (const auto& c : Class) {
// If we don't have a chance field, or if the chance is above threshold, we keep the annotation.
if (!c.Chance || *c.Chance >= chanceThreshold) {
allBelowThreshold = false;
break;
}
}
if (allBelowThreshold) {
return annotation; // Return empty annotation
}
}
// Set the camera ID.
annotation.cameraID = CamID;
// Determine the color based on the values in the IFF fields (if there are any). Use a set to determine which
// values are present. If the set has no value or more than one value, use white. If there is a single value,
// then use the corresponding color: red for "foe", green for "friend", yellow for "neutral".
std::set<std::string> iffValues;
for (const auto& c : Class) {
if (c.IFF) {
iffValues.insert(*c.IFF);
}
}
annotation.color = { 1.0f, 1.0f, 1.0f, alpha }; // White
if (iffValues.size() == 1) {
std::string iff = *iffValues.begin();
if (iff == "friend") {
annotation.color = { 0.7f, 1.0f, 0.7f, alpha }; // Green
} else if (iff == "foe") {
annotation.color = { 1.0f, 0.7f, 0.7f, alpha }; // Red
} else if (iff == "neutral") {
annotation.color = { 1.0f, 1.0f, 0.5f, alpha }; // Yellow
}
}
// Set default UV coordinates to the center of the image. If we have a location, use it.
annotation.uv = { 0.5f, 0.5f };
if (Loc) {
annotation.uv = { (*Loc)[0], (*Loc)[1] };
}
// If we have a rectangle, set the bounding box.
// Convert from full-width/height as specified in the Anallysis API to the half-width/half-height
// specified by the Annotation by dividing the values by 2.
if (Rect) {
annotation.bbox = std::make_shared< std::array<float, 2> >(*Rect);
(*annotation.bbox)[0] /= 2.0f;
(*annotation.bbox)[1] /= 2.0f;
}
// Set the label, starting with the base name.
annotation.label = Name;
// Sort the classifications by chance, highest first.
auto sortedClassifications = Class;
std::sort(sortedClassifications.begin(), sortedClassifications.end(),
[](const Classification& a, const Classification& b) {
float chanceA = a.Chance ? *a.Chance : 1.0f;
float chanceB = b.Chance ? *b.Chance : 1.0f;
return chanceA > chanceB;
});
// If there is just one classification, add it on a new line with two spaces before it.
// Otherwise, add each on a new line with two spaces before and a question mark at the end.
if (sortedClassifications.size() == 1) {
const auto& c = sortedClassifications[0];
if (c.Type) {
annotation.label += "\n " + *c.Type;
}
} else {
for (const auto& c : sortedClassifications) {
if (c.Type) {
annotation.label += "\n " + *c.Type + " ?";
}
}
}
return annotation;
}
std::string asdp::analysis::AnalysisReport::Test()
{
try {
// Create a sample JSON string
std::string sampleJson = R"({
"CamID": 1,
"Time": [1625078400, 500000],
"Name": "TestAnalysis",
"Loc": [34.05, -118.25],
"Rect": [100.0, 200.0],
"Vel": [1.0, 0.0],
"Class": [
{"Type": "Car", "Chance": 0.95, "IFF": "friend"},
{"Type": "Person", "Chance": 0.85}
]
})";
// Parse the JSON string
AnalysisReport report(sampleJson);
// Validate parsed data
if (report.CamID != 1) return "CamID mismatch";
if (report.Timestamp.seconds != 1625078400 || report.Timestamp.microseconds != 500000) return "Timestamp mismatch";
if (report.Name != "TestAnalysis") return "Name mismatch";
if (!report.Loc || (*report.Loc)[0] != 34.05f || (*report.Loc)[1] != -118.25f) return "Loc mismatch";
if (!report.Rect || (*report.Rect)[0] != 100.0f || (*report.Rect)[1] != 200.0f) return "Rect mismatch";
if (!report.Vel || report.Vel->size() != 2) return "Vel size mismatch";
if (report.Class.size() != 2) return "Class size mismatch";
if (!report.Class[0].Type || *report.Class[0].Type != "Car") return "Class[0] Type mismatch";
if (!report.Class[0].Chance || *report.Class[0].Chance != 0.95f) return "Class[0] Chance mismatch";
if (!report.Class[0].IFF || *report.Class[0].IFF != "friend") return "Class[0] IFF mismatch";
if (!report.Class[1].Type || *report.Class[1].Type != "Person") return "Class[1] Type mismatch";
if (!report.Class[1].Chance || *report.Class[1].Chance != 0.85f) return "Class[1] Chance mismatch";
if (report.Class[1].IFF != nullptr) return "Class[1] IFF should be nullptr";
// Make another sample JSON with no optional fields.
std::string minimalJson = R"({
"CamID": 2,
"Time": [1625078500, 0],
"Name": "MinimalAnalysis"
})";
// Parse the minimal JSON string
AnalysisReport minimalReport(minimalJson);
// Validate minimal parsed data
if (minimalReport.CamID != 2) return "Minimal CamID mismatch";
if (minimalReport.Timestamp.seconds != 1625078500 || minimalReport.Timestamp.microseconds != 0) return "Minimal Timestamp mismatch";
if (minimalReport.Name != "MinimalAnalysis") return "Minimal Name mismatch";
if (minimalReport.Loc != nullptr) return "Minimal Loc should be nullptr";
if (minimalReport.Rect != nullptr) return "Minimal Rect should be nullptr";
if (minimalReport.Vel != nullptr) return "Minimal Vel should be nullptr";
if (!minimalReport.Class.empty()) return "Minimal Class should be empty";
// Make a sample JSON with some required fields missing to test error handling.
std::string missingFieldsJson = R"({
"CamID": 3,
"Name": "MissingTimestampAnalysis"
})";
try {
AnalysisReport missingFieldsReport(missingFieldsJson);
return "Missing required fields did not throw an exception";
} catch (const std::invalid_argument&) {
// Expected exception
}
// Make a malformed JSON string to test error handling.
std::string malformedJson = R"({
"CamID": 3,
"Time": [1625078600, 0],
"Name": "MalformedAnalysis",
"Loc": [34.05, -118.25
})"; // Missing closing bracket for Loc array
try {
AnalysisReport malformedReport(malformedJson);
return "Malformed JSON did not throw an exception";
} catch (const std::invalid_argument&) {
// Expected exception
}
// Convert the first report to an annotation and validate.
auto annotation = report.ConvertToAnnotation(0.9f, 0.8f);
if (annotation.cameraID != 1) return "Annotation cameraID mismatch";
if (annotation.color[0] != 0.7f || annotation.color[1] != 1.0f || annotation.color[2] != 0.7f ||
annotation.color[3] != 0.8f) {
return "Annotation color mismatch";
}
if (annotation.uv[0] != 34.05f || annotation.uv[1] != -118.25f) return "Annotation uv mismatch";
if (!annotation.bbox || (*annotation.bbox)[0] != 100.0f || (*annotation.bbox)[1] != 200.0f) return "Annotation bbox mismatch";
if (annotation.label.find("TestAnalysis") == std::string::npos) return "Annotation label missing base name";
if (annotation.label.find("Car") == std::string::npos) return "Annotation label missing classification";
if (annotation.label.find("Person") == std::string::npos) return "Annotation label missing classification";
// Retry the conversion with a higher chance threshold to exclude all classifications.
auto annotationFiltered = report.ConvertToAnnotation(1.0f);
if (annotationFiltered.label != "") return "Annotation should be empty due to chance threshold";
// Make a new report whose classifications have no Chance fields and verify that it is converted.
std::string noChanceJson = R"({
"CamID": 4,
"Time": [1625078700, 0],
"Name": "NoChanceAnalysis",
"Class": [
{"Type": "Bicycle", "IFF": "neutral"},
{"Type": "Dog"}
]
})";
AnalysisReport noChanceReport(noChanceJson);
auto annotationNoChance = noChanceReport.ConvertToAnnotation(0.5f);
if (annotationNoChance.label.find("NoChanceAnalysis") == std::string::npos) return "NoChance Annotation label missing base name";
if (annotationNoChance.label.find("Bicycle") == std::string::npos) return "NoChance Annotation label missing classification";
if (annotationNoChance.label.find("Dog") == std::string::npos) return "NoChance Annotation label missing classification";
if (annotationNoChance.color[0] != 1.0f || annotationNoChance.color[1] != 1.0f ||
annotationNoChance.color[2] != 0.5f || annotationNoChance.color[3] != 1.0f) {
return "NoChance Annotation color mismatch";
}
} catch (const std::exception& e) {
return std::string("Exception during test: ") + e.what();
}
return ""; // Success
}