1.3.64
 
Loading...
Searching...
No Matches
RadiationCamera.cpp
Go to the documentation of this file.
1
17#include "RadiationModel.h"
18#include "LensFlare.h"
19
20#include <queue>
21#include <set>
22#include <stack>
23#include <sstream>
24#include <filesystem>
25
26#include "global.h"
27
28using namespace helios;
29
30void RadiationModel::addRadiationCamera(const std::string &camera_label, const std::vector<std::string> &band_label, const helios::vec3 &position, const helios::vec3 &lookat, const CameraProperties &camera_properties, uint antialiasing_samples) {
31
32 if (antialiasing_samples == 0) {
33 helios_runtime_error("ERROR (RadiationModel::addRadiationCamera): The model requires at least 1 antialiasing sample to run.");
34 } else if (camera_properties.camera_resolution.x <= 0 || camera_properties.camera_resolution.y <= 0) {
35 helios_runtime_error("ERROR (RadiationModel::addRadiationCamera): Camera resolution must be at least 1x1.");
36 } else if (camera_properties.HFOV < 0 || camera_properties.HFOV > 180.f) {
37 helios_runtime_error("ERROR (RadiationModel::addRadiationCamera): Camera horizontal field of view must be between 0 and 180 degrees.");
38 }
39
40 // Auto-calculate FOV_aspect_ratio from camera resolution to ensure square pixels
41 CameraProperties modified_properties = camera_properties;
42 if (camera_properties.FOV_aspect_ratio != 0.f) {
43 std::cerr << "WARNING (RadiationModel::addRadiationCamera): FOV_aspect_ratio is deprecated and will be ignored. The value is auto-calculated from camera_resolution to ensure square pixels." << std::endl;
44 }
45 modified_properties.FOV_aspect_ratio = float(camera_properties.camera_resolution.x) / float(camera_properties.camera_resolution.y);
46
47 RadiationCamera camera(camera_label, band_label, position, lookat, modified_properties, antialiasing_samples);
48 if (cameras.find(camera_label) == cameras.end()) {
49 cameras.emplace(camera_label, camera);
50 } else {
51 if (message_flag) {
52 std::cout << "Camera with label " << camera_label << "already exists. Existing properties will be replaced by new inputs." << std::endl;
53 }
54 cameras.erase(camera_label);
55 cameras.emplace(camera_label, camera);
56 }
57
58 if (iscameravisualizationenabled) {
59 buildCameraModelGeometry(camera_label);
60 }
61
62 // Auto-populate camera metadata (does not enable JSON writing)
63 CameraMetadata metadata;
64 populateCameraMetadata(camera_label, metadata);
65 camera_metadata[camera_label] = metadata;
66
67 radiativepropertiesneedupdate = true;
68}
69
70void RadiationModel::addRadiationCamera(const std::string &camera_label, const std::vector<std::string> &band_label, const helios::vec3 &position, const helios::SphericalCoord &viewing_direction, const CameraProperties &camera_properties,
71 uint antialiasing_samples) {
72
73 vec3 lookat = position + sphere2cart(viewing_direction);
74 addRadiationCamera(camera_label, band_label, position, lookat, camera_properties, antialiasing_samples);
75}
76
77void RadiationModel::setCameraSpectralResponse(const std::string &camera_label, const std::string &band_label, const std::string &global_data) {
78 if (cameras.find(camera_label) == cameras.end()) {
79 helios_runtime_error("ERROR (setCameraSpectralResponse): Camera '" + camera_label + "' does not exist.");
80 } else if (!doesBandExist(band_label)) {
81 helios_runtime_error("ERROR (setCameraSpectralResponse): Band '" + band_label + "' does not exist.");
82 }
83
84 cameras.at(camera_label).band_spectral_response[band_label] = global_data;
85
86 radiativepropertiesneedupdate = true;
87}
88
89void RadiationModel::setCameraSpectralResponseFromLibrary(const std::string &camera_label, const std::string &camera_library_name) {
90
91 if (cameras.find(camera_label) == cameras.end()) {
92 helios_runtime_error("ERROR (setCameraSpectralResponseFromLibrary): Camera '" + camera_label + "' does not exist.");
93 }
94
95 const auto &band_labels = cameras.at(camera_label).band_labels;
96
97 if (!context->doesGlobalDataExist("spectral_library_loaded")) {
98 context->loadXML(helios::resolvePluginAsset("radiation", "spectral_data/camera_spectral_library.xml").string().c_str());
99 }
100
101 for (const auto &band: band_labels) {
102 std::string response_spectrum = camera_library_name + "_" + band;
103 if (!context->doesGlobalDataExist(response_spectrum.c_str()) || context->getGlobalDataType(response_spectrum.c_str()) != HELIOS_TYPE_VEC2) {
104 helios_runtime_error("ERROR (setCameraSpectralResponseFromLibrary): Band '" + band + "' referenced in spectral library camera " + camera_library_name + " does not exist for camera '" + camera_label + "'.");
105 }
106
107 cameras.at(camera_label).band_spectral_response[band] = response_spectrum;
108 }
109
110 radiativepropertiesneedupdate = true;
111}
112
113void RadiationModel::addRadiationCameraFromLibrary(const std::string &camera_label, const std::string &library_camera_label, const helios::vec3 &position, const helios::vec3 &lookat, uint antialiasing_samples) {
114 // Call the overloaded version with empty band_labels to use XML labels
115 addRadiationCameraFromLibrary(camera_label, library_camera_label, position, lookat, antialiasing_samples, std::vector<std::string>());
116}
117
118void RadiationModel::addRadiationCameraFromLibrary(const std::string &camera_label, const std::string &library_camera_label, const helios::vec3 &position, const helios::vec3 &lookat, uint antialiasing_samples,
119 const std::vector<std::string> &custom_band_labels) {
120
121 // Resolve library file path
122 std::filesystem::path library_path = helios::resolvePluginAsset("radiation", "camera_library/camera_library.xml");
123
124 // Load and parse XML file using pugixml
125 pugi::xml_document xmldoc;
126 pugi::xml_parse_result result = xmldoc.load_file(library_path.string().c_str());
127
128 if (!result) {
129 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Failed to load camera library file '" + library_path.string() + "'. " + result.description());
130 }
131
132 pugi::xml_node helios_node = xmldoc.child("helios");
133 if (helios_node.empty()) {
134 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Camera library XML must have '<helios>' root tag.");
135 }
136
137 // Find the camera node with matching label
138 pugi::xml_node camera_node;
139 for (pugi::xml_node cam = helios_node.child("camera"); cam; cam = cam.next_sibling("camera")) {
140 std::string label = cam.attribute("label").value();
141 if (label == library_camera_label) {
142 camera_node = cam;
143 break;
144 }
145 }
146
147 if (camera_node.empty()) {
148 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Camera '" + library_camera_label + "' not found in camera library.");
149 }
150
151 // Parse camera parameters
152 std::string manufacturer = camera_node.child("manufacturer").child_value();
153 std::string model = camera_node.child("model").child_value();
154
155 // Parse camera type (required field)
156 std::string camera_type = camera_node.child("type").child_value();
157 if (camera_type.empty()) {
158 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Missing required 'type' field for camera '" + library_camera_label + "'.");
159 }
160 if (camera_type != "rgb" && camera_type != "spectral" && camera_type != "thermal") {
161 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Invalid camera type '" + camera_type + "' for camera '" + library_camera_label + "'. Must be one of: 'rgb', 'spectral', or 'thermal'.");
162 }
163
164 float sensor_width_mm;
165 if (!helios::parse_float(camera_node.child("sensor_width_mm").child_value(), sensor_width_mm)) {
166 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Invalid or missing sensor_width_mm for camera '" + library_camera_label + "'.");
167 }
168
169 int resolution_width;
170 if (!helios::parse_int(camera_node.child("resolution_width").child_value(), resolution_width)) {
171 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Invalid or missing resolution_width for camera '" + library_camera_label + "'.");
172 }
173
174 int resolution_height;
175 if (!helios::parse_int(camera_node.child("resolution_height").child_value(), resolution_height)) {
176 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Invalid or missing resolution_height for camera '" + library_camera_label + "'.");
177 }
178
179 // Parse lens optical focal length (physical focal length, not 35mm equivalent)
180 float focal_length_mm;
181 if (!helios::parse_float(camera_node.child("focal_length_mm").child_value(), focal_length_mm)) {
182 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Invalid or missing focal_length_mm for camera '" + library_camera_label + "'.");
183 }
184
185 float lens_diameter_mm;
186 if (!helios::parse_float(camera_node.child("lens_diameter_mm").child_value(), lens_diameter_mm)) {
187 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Invalid or missing lens_diameter_mm for camera '" + library_camera_label + "'.");
188 }
189
190 // Parse optional focal plane distance (working distance), default to 2.0m if not specified
191 float focal_plane_distance_m = 2.0f;
192 if (camera_node.child("focal_plane_distance_m")) {
193 if (!helios::parse_float(camera_node.child("focal_plane_distance_m").child_value(), focal_plane_distance_m)) {
194 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Invalid focal_plane_distance_m for camera '" + library_camera_label + "'.");
195 }
196 }
197
198 // Parse optional lens metadata
199 std::string lens_make = camera_node.child("lens_make").child_value();
200 std::string lens_model = camera_node.child("lens_model").child_value();
201 std::string lens_specification = camera_node.child("lens_specification").child_value();
202
203 // Parse optional exposure mode (default: "auto")
204 std::string exposure_mode = camera_node.child("exposure").child_value();
205 if (exposure_mode.empty()) {
206 exposure_mode = "auto"; // Default to auto exposure
207 }
208
209 // Parse optional shutter speed (default: 1/125 second)
210 float shutter_speed = 1.0f / 125.0f;
211 if (camera_node.child("shutter_speed")) {
212 if (!helios::parse_float(camera_node.child("shutter_speed").child_value(), shutter_speed)) {
213 std::cerr << "WARNING (RadiationModel::addRadiationCameraFromLibrary): Invalid shutter_speed for camera '" << library_camera_label << "'. Using default 1/125 second." << std::endl;
214 shutter_speed = 1.0f / 125.0f;
215 }
216 }
217
218 // Parse optional white balance mode (default: "auto")
219 std::string white_balance_mode = camera_node.child("white_balance").child_value();
220 if (white_balance_mode.empty()) {
221 white_balance_mode = "auto"; // Default to auto white balance
222 } else if (white_balance_mode != "auto" && white_balance_mode != "off") {
223 std::cerr << "WARNING (RadiationModel::addRadiationCameraFromLibrary): Invalid white_balance mode '" << white_balance_mode << "' for camera '" << library_camera_label << "'. Must be 'auto' or 'off'. Using default 'auto'." << std::endl;
224 white_balance_mode = "auto";
225 }
226
227 // Build CameraProperties struct
228 CameraProperties camera_properties;
229 camera_properties.camera_resolution = helios::make_int2(resolution_width, resolution_height);
230 camera_properties.sensor_width_mm = sensor_width_mm;
231
232 // Calculate HFOV from lens optical focal length and sensor width
233 // HFOV = 2 * atan(sensor_width / (2 * optical_focal_length))
234 float HFOV_rad = 2.0f * atan(sensor_width_mm / (2.0f * focal_length_mm));
235 camera_properties.HFOV = HFOV_rad * 180.0f / M_PI;
236
237 // Set focal plane distance (working distance for ray generation)
238 camera_properties.focal_plane_distance = focal_plane_distance_m;
239
240 // Convert lens optical focal length from mm to meters (for f-number calculations)
241 camera_properties.lens_focal_length = focal_length_mm / 1000.0f;
242
243 // Convert lens diameter from mm to meters
244 camera_properties.lens_diameter = lens_diameter_mm / 1000.0f;
245
246 // FOV aspect ratio will be auto-calculated
247 camera_properties.FOV_aspect_ratio = 0.0f;
248
249 // Set model name
250 camera_properties.model = manufacturer + " " + model;
251
252 // Set lens metadata
253 camera_properties.lens_make = lens_make;
254 camera_properties.lens_model = lens_model;
255 camera_properties.lens_specification = lens_specification;
256
257 // Set exposure settings
258 camera_properties.exposure = exposure_mode;
259 camera_properties.shutter_speed = shutter_speed;
260
261 // Set white balance mode
262 camera_properties.white_balance = white_balance_mode;
263
264 // Parse spectral response data and store in global data
265 // xml_band_labels stores the band labels from the XML file (used for global data naming)
266 std::vector<std::string> xml_band_labels;
267 // spectral_wavelength_ranges stores the wavelength range for each band (for auto-creating bands)
268 std::vector<std::pair<float, float>> spectral_wavelength_ranges;
269
270 for (pugi::xml_node spectral_node = camera_node.child("spectral_response"); spectral_node; spectral_node = spectral_node.next_sibling("spectral_response")) {
271
272 std::string xml_band_label = spectral_node.attribute("label").value();
273 if (xml_band_label.empty()) {
274 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): spectral_response node missing 'label' attribute for camera '" + library_camera_label + "'.");
275 }
276
277 xml_band_labels.push_back(xml_band_label);
278
279 // Parse wavelength-response pairs
280 std::vector<helios::vec2> spectral_data;
281 std::string data_str = spectral_node.child_value();
282
283 if (!data_str.empty()) {
284 std::istringstream data_stream(data_str);
285 float wavelength, response;
286 while (data_stream >> wavelength >> response) {
287 spectral_data.push_back(helios::make_vec2(wavelength, response));
288 }
289 }
290
291 if (spectral_data.empty()) {
292 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): Empty spectral response data for band '" + xml_band_label + "' in camera '" + library_camera_label + "'.");
293 }
294
295 // Store wavelength range for potential band creation
296 spectral_wavelength_ranges.emplace_back(spectral_data.front().x, spectral_data.back().x);
297
298 // Store spectral response in global data with naming convention using XML labels
299 std::string global_data_label = library_camera_label + "_" + xml_band_label;
300 context->setGlobalData(global_data_label.c_str(), spectral_data);
301 }
302
303 if (xml_band_labels.empty()) {
304 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): No spectral response data found for camera '" + library_camera_label + "'.");
305 }
306
307 // Determine effective band labels: use custom labels if provided, otherwise use XML labels
308 std::vector<std::string> effective_band_labels;
309 if (!custom_band_labels.empty()) {
310 if (custom_band_labels.size() != xml_band_labels.size()) {
311 helios_runtime_error("ERROR (RadiationModel::addRadiationCameraFromLibrary): custom_band_labels size (" + std::to_string(custom_band_labels.size()) + ") does not match number of spectral responses in library (" +
312 std::to_string(xml_band_labels.size()) + ") for camera '" + library_camera_label + "'.");
313 }
314 effective_band_labels = custom_band_labels;
315 } else {
316 effective_band_labels = xml_band_labels;
317 }
318
319 // Add radiation bands if they don't exist (using effective band labels)
320 for (size_t i = 0; i < effective_band_labels.size(); i++) {
321 const std::string &band_label = effective_band_labels[i];
322 if (!doesBandExist(band_label)) {
323 float min_wavelength = spectral_wavelength_ranges[i].first;
324 float max_wavelength = spectral_wavelength_ranges[i].second;
325 addRadiationBand(band_label, min_wavelength, max_wavelength);
326
327 // Disable emission for camera bands
328 disableEmission(band_label);
329
330 // Set scattering depth
331 setScatteringDepth(band_label, 3);
332
333 std::cout << "WARNING (RadiationModel::addRadiationCameraFromLibrary): Band '" << band_label << "' did not exist and was automatically created with wavelength range [" << min_wavelength << ", " << max_wavelength << "] nm." << std::endl;
334 }
335 }
336
337 // Create the camera using existing addRadiationCamera method (with effective band labels)
338 addRadiationCamera(camera_label, effective_band_labels, position, lookat, camera_properties, antialiasing_samples);
339
340 // Set the camera type
341 cameras.at(camera_label).camera_type = camera_type;
342
343 // Set spectral responses from the global data we created (mapping effective labels to XML labels)
344 for (size_t i = 0; i < effective_band_labels.size(); i++) {
345 std::string global_data_label = library_camera_label + "_" + xml_band_labels[i];
346 setCameraSpectralResponse(camera_label, effective_band_labels[i], global_data_label);
347 }
348}
349
350void RadiationModel::setCameraPosition(const std::string &camera_label, const helios::vec3 &position) {
351 if (cameras.find(camera_label) == cameras.end()) {
352 helios_runtime_error("ERROR (RadiationModel::setCameraPosition): Camera '" + camera_label + "' does not exist.");
353 } else if (position == cameras.at(camera_label).lookat) {
354 helios_runtime_error("ERROR (RadiationModel::setCameraPosition): Camera position cannot be equal to the 'lookat' position.");
355 }
356
357 cameras.at(camera_label).position = position;
358
359 if (iscameravisualizationenabled) {
360 updateCameraModelPosition(camera_label);
361 }
362}
363
364helios::vec3 RadiationModel::getCameraPosition(const std::string &camera_label) const {
365
366 if (cameras.find(camera_label) == cameras.end()) {
367 helios_runtime_error("ERROR (RadiationModel::getCameraPosition): Camera '" + camera_label + "' does not exist.");
368 }
369
370 return cameras.at(camera_label).position;
371}
372
373void RadiationModel::setCameraLookat(const std::string &camera_label, const helios::vec3 &lookat) {
374 if (cameras.find(camera_label) == cameras.end()) {
375 helios_runtime_error("ERROR (RadiationModel::setCameraLookat): Camera '" + camera_label + "' does not exist.");
376 }
377
378 cameras.at(camera_label).lookat = lookat;
379
380 if (iscameravisualizationenabled) {
381 updateCameraModelPosition(camera_label);
382 }
383}
384
385helios::vec3 RadiationModel::getCameraLookat(const std::string &camera_label) const {
386
387 if (cameras.find(camera_label) == cameras.end()) {
388 helios_runtime_error("ERROR (RadiationModel::getCameraLookat): Camera '" + camera_label + "' does not exist.");
389 }
390
391 return cameras.at(camera_label).lookat;
392}
393
394void RadiationModel::setCameraOrientation(const std::string &camera_label, const helios::vec3 &direction) {
395 if (cameras.find(camera_label) == cameras.end()) {
396 helios_runtime_error("ERROR (RadiationModel::setCameraOrientation): Camera '" + camera_label + "' does not exist.");
397 }
398
399 cameras.at(camera_label).lookat = cameras.at(camera_label).position + direction;
400
401 if (iscameravisualizationenabled) {
402 updateCameraModelPosition(camera_label);
403 }
404}
405
406helios::SphericalCoord RadiationModel::getCameraOrientation(const std::string &camera_label) const {
407
408 if (cameras.find(camera_label) == cameras.end()) {
409 helios_runtime_error("ERROR (RadiationModel::getCameraOrientation): Camera '" + camera_label + "' does not exist.");
410 }
411
412 return cart2sphere(cameras.at(camera_label).lookat - cameras.at(camera_label).position);
413}
414
415void RadiationModel::setCameraOrientation(const std::string &camera_label, const helios::SphericalCoord &direction) {
416 if (cameras.find(camera_label) == cameras.end()) {
417 helios_runtime_error("ERROR (RadiationModel::setCameraOrientation): Camera '" + camera_label + "' does not exist.");
418 }
419
420 cameras.at(camera_label).lookat = cameras.at(camera_label).position + sphere2cart(direction);
421
422 if (iscameravisualizationenabled) {
423 updateCameraModelPosition(camera_label);
424 }
425}
426
427CameraProperties RadiationModel::getCameraParameters(const std::string &camera_label) const {
428
429 // Validate camera exists
430 if (cameras.find(camera_label) == cameras.end()) {
431 helios_runtime_error("ERROR (RadiationModel::getCameraParameters): Camera '" + camera_label + "' does not exist.");
432 }
433
434 // Get reference to camera
435 const auto &camera = cameras.at(camera_label);
436
437 // Create and populate CameraProperties struct
438 CameraProperties camera_properties;
439 camera_properties.camera_resolution = camera.resolution;
440 camera_properties.HFOV = camera.HFOV_degrees;
441 camera_properties.lens_diameter = camera.lens_diameter;
442 camera_properties.focal_plane_distance = camera.focal_length;
443 camera_properties.lens_focal_length = camera.lens_focal_length;
444 camera_properties.sensor_width_mm = camera.sensor_width_mm;
445 camera_properties.model = camera.model;
446 camera_properties.lens_make = camera.lens_make;
447 camera_properties.lens_model = camera.lens_model;
448 camera_properties.lens_specification = camera.lens_specification;
449 camera_properties.exposure = camera.exposure;
450 camera_properties.shutter_speed = camera.shutter_speed;
451 camera_properties.white_balance = camera.white_balance;
452 camera_properties.camera_zoom = camera.camera_zoom;
453 camera_properties.FOV_aspect_ratio = camera.FOV_aspect_ratio;
454
455 return camera_properties;
456}
457
458void RadiationModel::updateCameraParameters(const std::string &camera_label, const CameraProperties &camera_properties) {
459
460 // Validate camera exists
461 if (cameras.find(camera_label) == cameras.end()) {
462 helios_runtime_error("ERROR (RadiationModel::updateCameraParameters): Camera '" + camera_label + "' does not exist.");
463 }
464
465 // Validate camera properties
466 if (camera_properties.camera_resolution.x <= 0 || camera_properties.camera_resolution.y <= 0) {
467 helios_runtime_error("ERROR (RadiationModel::updateCameraParameters): Camera resolution must be at least 1x1.");
468 } else if (camera_properties.HFOV <= 0 || camera_properties.HFOV >= 180.f) {
469 helios_runtime_error("ERROR (RadiationModel::updateCameraParameters): Camera horizontal field of view must be between 0 and 180 degrees.");
470 } else if (camera_properties.camera_zoom <= 0.0f) {
471 helios_runtime_error("ERROR (RadiationModel::updateCameraParameters): camera_zoom must be greater than 0.");
472 }
473
474 // Get reference to camera
475 auto &camera = cameras.at(camera_label);
476
477 // Update camera parameters
478 camera.resolution = camera_properties.camera_resolution;
479 camera.HFOV_degrees = camera_properties.HFOV;
480 camera.lens_diameter = camera_properties.lens_diameter;
481 camera.focal_length = camera_properties.focal_plane_distance;
482 camera.lens_focal_length = camera_properties.lens_focal_length;
483 camera.sensor_width_mm = camera_properties.sensor_width_mm;
484 camera.model = camera_properties.model;
485 camera.exposure = camera_properties.exposure;
486 camera.shutter_speed = camera_properties.shutter_speed;
487 camera.white_balance = camera_properties.white_balance;
488 camera.camera_zoom = camera_properties.camera_zoom;
489
490 // Recalculate FOV_aspect_ratio to ensure square pixels
491 camera.FOV_aspect_ratio = float(camera.resolution.x) / float(camera.resolution.y);
492
493 // Flag that radiative properties need to be updated
494 radiativepropertiesneedupdate = true;
495
496 // Update camera visualization if enabled
497 if (iscameravisualizationenabled) {
498 updateCameraModelPosition(camera_label);
499 }
500}
501
502std::vector<std::string> RadiationModel::getAllCameraLabels() {
503 std::vector<std::string> labels(cameras.size());
504 uint cam = 0;
505 for (const auto &camera: cameras) {
506 labels.at(cam) = camera.second.label;
507 cam++;
508 }
509 return labels;
510}
511
512std::string RadiationModel::writeCameraImage(const std::string &camera, const std::vector<std::string> &bands, const std::string &imagefile_base, const std::string &image_path, int frame, float flux_to_pixel_conversion) {
513
514 // check if camera exists
515 if (cameras.find(camera) == cameras.end()) {
516 std::cout << "ERROR (RadiationModel::writeCameraImage): camera with label " << camera << " does not exist. Skipping image write for this camera." << std::endl;
517 return "";
518 }
519
520 if (bands.size() != 1 && bands.size() != 3) {
521 helios_runtime_error("ERROR (RadiationModel::writeCameraImage): input vector of band labels should either have length of 1 (grayscale image) or length of 3 (RGB image). Skipping image write for this camera.");
522 }
523
524 std::vector<std::vector<float>> camera_data(bands.size());
525
526 uint b = 0;
527 for (const auto &band: bands) {
528
529 // check if band exists
530 if (std::find(cameras.at(camera).band_labels.begin(), cameras.at(camera).band_labels.end(), band) == cameras.at(camera).band_labels.end()) {
531 std::cout << "ERROR (RadiationModel::writeCameraImage): camera " << camera << " band with label " << band << " does not exist. Skipping image write for this camera." << std::endl;
532 return "";
533 }
534
535 camera_data.at(b) = cameras.at(camera).pixel_data.at(band);
536
537 b++;
538 }
539
540 // Apply sRGB gamma compression for 3-channel (RGB) images
541 // This is done on the copy, preserving the original linear data
542 bool is_rgb = (camera_data.size() == 3);
543 if (is_rgb) {
544 for (auto &band_data: camera_data) {
545 for (float &v: band_data) {
546 v = RadiationCamera::lin_to_srgb(std::fmaxf(0.0f, v));
547 }
548 }
549 }
550
551 std::string frame_str;
552 if (frame >= 0) {
553 frame_str = std::to_string(frame);
554 }
555
556 std::string output_path = image_path;
557 if (!image_path.empty() && !validateOutputPath(output_path)) {
558 helios_runtime_error("ERROR (RadiationModel::writeCameraImage): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
559 } else if (!isDirectoryPath(output_path)) {
560 helios_runtime_error("ERROR(RadiationModel::writeCameraImage): Expected a directory path but got a file path for argument 'image_path'.");
561 }
562
563 std::ostringstream outfile;
564 outfile << output_path;
565
566 if (frame >= 0) {
567 outfile << camera << "_" << imagefile_base << "_" << std::setw(5) << std::setfill('0') << frame_str << ".jpeg";
568 } else {
569 outfile << camera << "_" << imagefile_base << ".jpeg";
570 }
571 std::ofstream testfile(outfile.str());
572
573 if (!testfile.is_open()) {
574 std::cout << "ERROR (RadiationModel::writeCameraImage): image file " << outfile.str() << " could not be opened. Check that the path exists and that you have write permission. Skipping image write for this camera." << std::endl;
575 return "";
576 }
577 testfile.close();
578
579 int2 camera_resolution = cameras.at(camera).resolution;
580
581 std::vector<RGBcolor> pixel_data_RGB(camera_resolution.x * camera_resolution.y);
582
583 RGBcolor pixel_color;
584 for (uint j = 0; j < camera_resolution.y; j++) {
585 for (uint i = 0; i < camera_resolution.x; i++) {
586 if (camera_data.size() == 1) {
587 float c = camera_data.front().at(j * camera_resolution.x + i);
588 pixel_color = make_RGBcolor(c, c, c);
589 } else {
590 pixel_color = make_RGBcolor(camera_data.at(0).at(j * camera_resolution.x + i), camera_data.at(1).at(j * camera_resolution.x + i), camera_data.at(2).at(j * camera_resolution.x + i));
591 }
592 pixel_color.scale(flux_to_pixel_conversion);
593 uint ii = camera_resolution.x - i - 1;
594 uint jj = camera_resolution.y - j - 1;
595 pixel_data_RGB.at(jj * camera_resolution.x + ii) = pixel_color;
596 }
597 }
598
599 writeJPEG(outfile.str(), camera_resolution.x, camera_resolution.y, pixel_data_RGB);
600
601 std::string image_filepath = outfile.str();
602
603 // Write JSON metadata if enabled for this camera
604 if (metadata_enabled_cameras.find(camera) != metadata_enabled_cameras.end()) {
605 // Preserve any existing image_processing parameters (e.g., from applyCameraImageCorrections)
606 CameraMetadata::ImageProcessingProperties saved_image_processing;
607 if (camera_metadata.find(camera) != camera_metadata.end()) {
608 saved_image_processing = camera_metadata.at(camera).image_processing;
609 }
610
611 // Re-populate metadata to capture any new data (e.g., agronomic properties)
612 CameraMetadata metadata;
613 populateCameraMetadata(camera, metadata);
614
615 // Restore image_processing parameters
616 metadata.image_processing = saved_image_processing;
617
618 // Set color space based on channel count (sRGB for RGB, linear for grayscale)
619 metadata.image_processing.color_space = is_rgb ? "sRGB" : "linear";
620
621 // Extract just the filename (without directory path) for portability
622 size_t last_slash = image_filepath.find_last_of("/\\");
623 std::string filename_only = (last_slash != std::string::npos) ? image_filepath.substr(last_slash + 1) : image_filepath;
624 metadata.path = filename_only;
625
626 // Store updated metadata and write JSON file
627 camera_metadata[camera] = metadata;
628 writeCameraMetadataFile(camera, output_path);
629 }
630
631 return image_filepath;
632}
633
634std::string RadiationModel::writeNormCameraImage(const std::string &camera, const std::vector<std::string> &bands, const std::string &imagefile_base, const std::string &image_path, int frame) {
635 float maxval = 0;
636 // Find maximum mean value over all bands
637 for (const std::string &band: bands) {
638 std::string global_data_label = "camera_" + camera + "_" + band;
639 if (std::find(cameras.at(camera).band_labels.begin(), cameras.at(camera).band_labels.end(), band) == cameras.at(camera).band_labels.end()) {
640 std::cout << "ERROR (RadiationModel::writeNormCameraImage): camera " << camera << " band with label " << band << " does not exist. Skipping image write for this camera." << std::endl;
641 return "";
642 } else if (!context->doesGlobalDataExist(global_data_label.c_str())) {
643 std::cout << "ERROR (RadiationModel::writeNormCameraImage): image data for camera " << camera << ", band " << band << " has not been created. Did you run the radiation model? Skipping image write for this camera." << std::endl;
644 return "";
645 }
646 std::vector<float> cameradata;
647 context->getGlobalData(global_data_label.c_str(), cameradata);
648 for (float val: cameradata) {
649 if (val > maxval) {
650 maxval = val;
651 }
652 }
653 }
654 // Normalize all bands
655 for (const std::string &band: bands) {
656 std::string global_data_label = "camera_" + camera + "_" + band;
657 std::vector<float> cameradata;
658 context->getGlobalData(global_data_label.c_str(), cameradata);
659 for (float &val: cameradata) {
660 val = val / maxval;
661 }
662 context->setGlobalData(global_data_label.c_str(), cameradata);
663 }
664
665 return RadiationModel::writeCameraImage(camera, bands, imagefile_base, image_path, frame);
666}
667
668void RadiationModel::writeCameraImageData(const std::string &camera, const std::string &band, const std::string &imagefile_base, const std::string &image_path, int frame) {
669
670 // check if camera exists
671 if (cameras.find(camera) == cameras.end()) {
672 std::cout << "ERROR (RadiationModel::writeCameraImageData): camera with label " << camera << " does not exist. Skipping image write for this camera." << std::endl;
673 return;
674 }
675
676 std::vector<float> camera_data;
677
678 // check if band exists
679 if (std::find(cameras.at(camera).band_labels.begin(), cameras.at(camera).band_labels.end(), band) == cameras.at(camera).band_labels.end()) {
680 std::cout << "ERROR (RadiationModel::writeCameraImageData): camera " << camera << " band with label " << band << " does not exist. Skipping image write for this camera." << std::endl;
681 return;
682 }
683
684 std::string global_data_label = "camera_" + camera + "_" + band;
685
686 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
687 std::cout << "ERROR (RadiationModel::writeCameraImageData): image data for camera " << camera << ", band " << band << " has not been created. Did you run the radiation model? Skipping image write for this camera." << std::endl;
688 return;
689 }
690
691 context->getGlobalData(global_data_label.c_str(), camera_data);
692
693 std::string frame_str;
694 if (frame >= 0) {
695 frame_str = std::to_string(frame);
696 }
697
698 std::string output_path = image_path;
699 if (!image_path.empty() && !validateOutputPath(output_path)) {
700 helios_runtime_error("ERROR (RadiationModel::writeCameraImage): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
701 } else if (!isDirectoryPath(output_path)) {
702 helios_runtime_error("ERROR(RadiationModel::writeCameraImage): Expected a directory path but got a file path for argument 'image_path'.");
703 }
704
705 std::ostringstream outfile;
706 outfile << output_path;
707
708 if (frame >= 0) {
709 outfile << camera << "_" << imagefile_base << "_" << std::setw(5) << std::setfill('0') << frame_str << ".txt";
710 } else {
711 outfile << camera << "_" << imagefile_base << ".txt";
712 }
713
714 std::ofstream outfilestream(outfile.str());
715
716 if (!outfilestream.is_open()) {
717 std::cout << "ERROR (RadiationModel::writeCameraImageData): image file " << outfile.str() << " could not be opened. Check that the path exists and that you have write permission. Skipping image write for this camera." << std::endl;
718 return;
719 }
720
721 int2 camera_resolution = cameras.at(camera).resolution;
722
723 for (int j = 0; j < camera_resolution.y; j++) {
724 for (int i = camera_resolution.x - 1; i >= 0; i--) {
725 outfilestream << camera_data.at(j * camera_resolution.x + i) << " ";
726 }
727 outfilestream << "\n";
728 }
729
730 outfilestream.close();
731}
732
733void RadiationModel::setCameraCalibration(CameraCalibration *CameraCalibration) {
734 cameracalibration = CameraCalibration;
735 calibration_flag = true;
736}
737
738void RadiationModel::updateCameraResponse(const std::string &orginalcameralabel, const std::vector<std::string> &sourcelabels_raw, const std::vector<std::string> &cameraresponselabels, vec2 &wavelengthrange,
739 const std::vector<std::vector<float>> &truevalues, const std::string &calibratedmark) {
740
741 std::vector<std::string> objectlabels;
742 vec2 wavelengthrange_c = wavelengthrange;
743 cameracalibration->preprocessSpectra(sourcelabels_raw, cameraresponselabels, objectlabels, wavelengthrange_c);
744
745 RadiationCamera calibratecamera = cameras.at(orginalcameralabel);
746 CameraProperties cameraproperties;
747 cameraproperties.HFOV = calibratecamera.HFOV_degrees;
748 cameraproperties.camera_resolution = calibratecamera.resolution;
749 cameraproperties.focal_plane_distance = calibratecamera.focal_length; // Working distance for ray generation
750 cameraproperties.lens_focal_length = calibratecamera.lens_focal_length; // Optical focal length for aperture
751 cameraproperties.lens_diameter = calibratecamera.lens_diameter;
752 cameraproperties.FOV_aspect_ratio = calibratecamera.FOV_aspect_ratio;
753 cameraproperties.exposure = calibratecamera.exposure;
754 cameraproperties.shutter_speed = calibratecamera.shutter_speed;
755
756 std::vector<uint> UUIDs_target = cameracalibration->getAllColorBoardUUIDs();
757 std::string cameralabel = "calibration";
758 std::map<uint, std::vector<vec2>> simulatedcolorboardspectra;
759 for (uint UUID: UUIDs_target) {
760 simulatedcolorboardspectra.emplace(UUID, NULL);
761 }
762
763 for (uint ID = 0; ID < radiation_sources.size(); ID++) {
765 }
766
767 std::vector<float> wavelengths;
768 context->getGlobalData("wavelengths", wavelengths);
769 int numberwavelengths = wavelengths.size();
770
771 for (int iw = 0; iw < numberwavelengths; iw++) {
772 std::string wavelengthlabel = std::to_string(wavelengths.at(iw));
773
774 std::vector<std::string> sourcelabels;
775 for (std::string sourcelabel_raw: sourcelabels_raw) {
776 std::vector<vec2> icalsource;
777 icalsource.push_back(cameracalibration->processedspectra.at("source").at(sourcelabel_raw).at(iw));
778 icalsource.push_back(cameracalibration->processedspectra.at("source").at(sourcelabel_raw).at(iw));
779 icalsource.at(1).x += 1;
780 std::string sourcelable = "Cal_source_" + sourcelabel_raw;
781 sourcelabels.push_back(sourcelable);
782 context->setGlobalData(sourcelable.c_str(), icalsource);
783 }
784
785 std::vector<vec2> icalcamera(2);
786 icalcamera.at(0).y = 1;
787 icalcamera.at(1).y = 1;
788 icalcamera.at(0).x = wavelengths.at(iw);
789 icalcamera.at(1).x = wavelengths.at(iw) + 1;
790 std::string camlable = "Cal_cameraresponse";
791 context->setGlobalData(camlable.c_str(), icalcamera);
792
793 for (auto objectpair: cameracalibration->processedspectra.at("object")) {
794 std::vector<vec2> spectrum_obj;
795 spectrum_obj.push_back(objectpair.second.at(iw));
796 spectrum_obj.push_back(objectpair.second.at(iw));
797 spectrum_obj.at(1).x += 1;
798 context->setGlobalData(objectpair.first.c_str(), spectrum_obj);
799 }
800
801 RadiationModel::addRadiationBand(wavelengthlabel, std::stof(wavelengthlabel), std::stof(wavelengthlabel) + 1);
802 RadiationModel::disableEmission(wavelengthlabel);
803
804 uint ID = 0;
805 for (std::string sourcelabel_raw: sourcelabels_raw) {
806 RadiationModel::setSourceSpectrum(ID, sourcelabels.at(ID).c_str());
807 RadiationModel::setSourceFlux(ID, wavelengthlabel, 1);
808 ID++;
809 }
810 RadiationModel::setScatteringDepth(wavelengthlabel, 1);
811 RadiationModel::setDiffuseRadiationFlux(wavelengthlabel, 0);
812 RadiationModel::setDiffuseRadiationExtinctionCoeff(wavelengthlabel, 0.f, make_vec3(-0.5, 0.5, 1));
813
814 RadiationModel::addRadiationCamera(cameralabel, {wavelengthlabel}, calibratecamera.position, calibratecamera.lookat, cameraproperties, 10);
815 RadiationModel::setCameraSpectralResponse(cameralabel, wavelengthlabel, camlable);
817 RadiationModel::runBand({wavelengthlabel});
818
819 std::vector<float> camera_data;
820 std::string global_data_label = "camera_" + cameralabel + "_" + wavelengthlabel;
821 context->getGlobalData(global_data_label.c_str(), camera_data);
822
823 std::vector<uint> pixel_labels;
824 std::string global_data_label_UUID = "camera_" + cameralabel + "_pixel_UUID";
825 context->getGlobalData(global_data_label_UUID.c_str(), pixel_labels);
826
827 for (uint j = 0; j < calibratecamera.resolution.y; j++) {
828 for (uint i = 0; i < calibratecamera.resolution.x; i++) {
829 float icdata = camera_data.at(j * calibratecamera.resolution.x + i);
830
831 uint UUID = pixel_labels.at(j * calibratecamera.resolution.x + i) - 1;
832 if (find(UUIDs_target.begin(), UUIDs_target.end(), UUID) != UUIDs_target.end()) {
833 if (simulatedcolorboardspectra.at(UUID).empty()) {
834 simulatedcolorboardspectra.at(UUID).push_back(make_vec2(wavelengths.at(iw), icdata / float(numberwavelengths)));
835 } else if (simulatedcolorboardspectra.at(UUID).back().x == wavelengths.at(iw)) {
836 simulatedcolorboardspectra.at(UUID).back().y += icdata / float(numberwavelengths);
837 } else if (simulatedcolorboardspectra.at(UUID).back().x != wavelengths.at(iw)) {
838 simulatedcolorboardspectra.at(UUID).push_back(make_vec2(wavelengths.at(iw), icdata / float(numberwavelengths)));
839 }
840 }
841 }
842 }
843 }
844 // Update camera response spectra
845 cameracalibration->updateCameraResponseSpectra(cameraresponselabels, calibratedmark, simulatedcolorboardspectra, truevalues);
846 // Reset color board spectra
847 std::vector<uint> UUIDs_colorbd = cameracalibration->getAllColorBoardUUIDs();
848 for (uint UUID: UUIDs_colorbd) {
849 std::string colorboardspectra;
850 context->getPrimitiveData(UUID, "reflectivity_spectrum", colorboardspectra);
851 context->setPrimitiveData(UUID, "reflectivity_spectrum", colorboardspectra + "_raw");
852 }
853}
854
855void RadiationModel::runRadiationImaging(const std::string &cameralabel, const std::vector<std::string> &sourcelabels, const std::vector<std::string> &bandlabels, const std::vector<std::string> &cameraresponselabels, helios::vec2 wavelengthrange,
856 float fluxscale, float diffusefactor, uint scatteringdepth) {
857
858 float sources_fluxsum = 0;
859 std::vector<float> sources_fluxes;
860 for (uint ID = 0; ID < sourcelabels.size(); ID++) {
861 std::vector<vec2> Source_spectrum = loadSpectralData(sourcelabels.at(ID).c_str());
862 sources_fluxes.push_back(RadiationModel::integrateSpectrum(Source_spectrum, wavelengthrange.x, wavelengthrange.y));
863 RadiationModel::setSourceSpectrum(ID, sourcelabels.at(ID).c_str());
864 RadiationModel::setSourceSpectrumIntegral(ID, sources_fluxes.at(ID));
865 sources_fluxsum += sources_fluxes.at(ID);
866 }
867
868 RadiationModel::addRadiationBand(bandlabels.at(0), wavelengthrange.x, wavelengthrange.y);
869 RadiationModel::disableEmission(bandlabels.at(0));
870 for (uint ID = 0; ID < radiation_sources.size(); ID++) {
871 RadiationModel::setSourceFlux(ID, bandlabels.at(0), (1 - diffusefactor) * sources_fluxes.at(ID) * fluxscale);
872 }
873 RadiationModel::setScatteringDepth(bandlabels.at(0), scatteringdepth);
874 RadiationModel::setDiffuseRadiationFlux(bandlabels.at(0), diffusefactor * sources_fluxsum);
875 RadiationModel::setDiffuseRadiationExtinctionCoeff(bandlabels.at(0), 1.f, make_vec3(-0.5, 0.5, 1));
876
877 if (bandlabels.size() > 1) {
878 for (int iband = 1; iband < bandlabels.size(); iband++) {
879 RadiationModel::copyRadiationBand(bandlabels.at(iband - 1), bandlabels.at(iband), wavelengthrange.x, wavelengthrange.y);
880 for (uint ID = 0; ID < radiation_sources.size(); ID++) {
881 RadiationModel::setSourceFlux(ID, bandlabels.at(iband), (1 - diffusefactor) * sources_fluxes.at(ID) * fluxscale);
882 }
883 RadiationModel::setDiffuseRadiationFlux(bandlabels.at(iband), diffusefactor * sources_fluxsum);
884 }
885 }
886
887 for (int iband = 0; iband < bandlabels.size(); iband++) {
888 RadiationModel::setCameraSpectralResponse(cameralabel, bandlabels.at(iband), cameraresponselabels.at(iband));
889 }
890
892 RadiationModel::runBand(bandlabels);
893}
894
895void RadiationModel::runRadiationImaging(const std::vector<std::string> &cameralabels, const std::vector<std::string> &sourcelabels, const std::vector<std::string> &bandlabels, const std::vector<std::string> &cameraresponselabels,
896 helios::vec2 wavelengthrange, float fluxscale, float diffusefactor, uint scatteringdepth) {
897
898 float sources_fluxsum = 0;
899 std::vector<float> sources_fluxes;
900 for (uint ID = 0; ID < sourcelabels.size(); ID++) {
901 std::vector<vec2> Source_spectrum = loadSpectralData(sourcelabels.at(ID).c_str());
902 sources_fluxes.push_back(RadiationModel::integrateSpectrum(Source_spectrum, wavelengthrange.x, wavelengthrange.y));
903 RadiationModel::setSourceSpectrum(ID, sourcelabels.at(ID).c_str());
904 RadiationModel::setSourceSpectrumIntegral(ID, sources_fluxes.at(ID));
905 sources_fluxsum += sources_fluxes.at(ID);
906 }
907
908 RadiationModel::addRadiationBand(bandlabels.at(0), wavelengthrange.x, wavelengthrange.y);
909 RadiationModel::disableEmission(bandlabels.at(0));
910 for (uint ID = 0; ID < radiation_sources.size(); ID++) {
911 RadiationModel::setSourceFlux(ID, bandlabels.at(0), (1 - diffusefactor) * sources_fluxes.at(ID) * fluxscale);
912 }
913 RadiationModel::setScatteringDepth(bandlabels.at(0), scatteringdepth);
914 RadiationModel::setDiffuseRadiationFlux(bandlabels.at(0), diffusefactor * sources_fluxsum);
915 RadiationModel::setDiffuseRadiationExtinctionCoeff(bandlabels.at(0), 1.f, make_vec3(-0.5, 0.5, 1));
916
917 if (bandlabels.size() > 1) {
918 for (int iband = 1; iband < bandlabels.size(); iband++) {
919 RadiationModel::copyRadiationBand(bandlabels.at(iband - 1), bandlabels.at(iband), wavelengthrange.x, wavelengthrange.y);
920 for (uint ID = 0; ID < radiation_sources.size(); ID++) {
921 RadiationModel::setSourceFlux(ID, bandlabels.at(iband), (1 - diffusefactor) * sources_fluxes.at(ID) * fluxscale);
922 }
923 RadiationModel::setDiffuseRadiationFlux(bandlabels.at(iband), diffusefactor * sources_fluxsum);
924 }
925 }
926
927 for (int ic = 0; ic < cameralabels.size(); ic++) {
928 for (int iband = 0; iband < bandlabels.size(); iband++) {
929 RadiationModel::setCameraSpectralResponse(cameralabels.at(ic), bandlabels.at(iband), cameraresponselabels.at(iband));
930 }
931 }
932
933
935 RadiationModel::runBand(bandlabels);
936}
937
938float RadiationModel::getCameraResponseScale(const std::string &orginalcameralabel, const std::vector<std::string> &cameraresponselabels, const std::vector<std::string> &bandlabels, const std::vector<std::string> &sourcelabels, vec2 &wavelengthrange,
939 const std::vector<std::vector<float>> &truevalues) {
940
941
942 RadiationCamera calibratecamera = cameras.at(orginalcameralabel);
943 CameraProperties cameraproperties;
944 cameraproperties.HFOV = calibratecamera.HFOV_degrees;
945 cameraproperties.camera_resolution = calibratecamera.resolution;
946 cameraproperties.focal_plane_distance = calibratecamera.focal_length; // Working distance for ray generation
947 cameraproperties.lens_focal_length = calibratecamera.lens_focal_length; // Optical focal length for aperture
948 cameraproperties.lens_diameter = calibratecamera.lens_diameter;
949 cameraproperties.FOV_aspect_ratio = calibratecamera.FOV_aspect_ratio;
950 cameraproperties.exposure = calibratecamera.exposure;
951 cameraproperties.shutter_speed = calibratecamera.shutter_speed;
952
953 std::string cameralabel = orginalcameralabel + "Scale";
954 RadiationModel::addRadiationCamera(cameralabel, bandlabels, calibratecamera.position, calibratecamera.lookat, cameraproperties, 20);
955 RadiationModel::runRadiationImaging(cameralabel, sourcelabels, bandlabels, cameraresponselabels, wavelengthrange, 1, 0);
956
957 // Get camera spectral response scale based on comparing true values and calibrated image
958 float camerascale = cameracalibration->getCameraResponseScale(cameralabel, cameraproperties.camera_resolution, bandlabels, truevalues);
959 return camerascale;
960}
961
962
963void RadiationModel::writePrimitiveDataLabelMap(const std::string &cameralabel, const std::string &primitive_data_label, const std::string &imagefile_base, const std::string &image_path, int frame, float padvalue) {
964
965 if (cameras.find(cameralabel) == cameras.end()) {
966 helios_runtime_error("ERROR (RadiationModel::writePrimitiveDataLabelMap): Camera '" + cameralabel + "' does not exist.");
967 }
968
969 // Get image UUID labels
970 std::vector<uint> camera_UUIDs;
971 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
972 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
973 helios_runtime_error("ERROR (RadiationModel::writePrimitiveDataLabelMap): Pixel labels for camera '" + cameralabel + "' do not exist. Was the radiation model run to generate labels?");
974 }
975 context->getGlobalData(global_data_label.c_str(), camera_UUIDs);
976 std::vector<uint> pixel_UUIDs = camera_UUIDs;
977 int2 camera_resolution = cameras.at(cameralabel).resolution;
978
979 std::string frame_str;
980 if (frame >= 0) {
981 frame_str = std::to_string(frame);
982 }
983
984 std::string output_path = image_path;
985 if (!image_path.empty() && !validateOutputPath(output_path)) {
986 helios_runtime_error("ERROR (RadiationModel::writePrimitiveDataLabelMap): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
987 } else if (!isDirectoryPath(output_path)) {
988 helios_runtime_error("ERROR(RadiationModel::writePrimitiveDataLabelMap): Expected a directory path but got a file path for argument 'image_path'.");
989 }
990
991 std::ostringstream outfile;
992 outfile << output_path;
993
994 if (frame >= 0) {
995 outfile << cameralabel << "_" << imagefile_base << "_" << std::setw(5) << std::setfill('0') << frame_str << ".txt";
996 } else {
997 outfile << cameralabel << "_" << imagefile_base << ".txt";
998 }
999
1000 // Output label image in ".txt" format
1001 std::ofstream pixel_data(outfile.str());
1002
1003 if (!pixel_data.is_open()) {
1004 helios_runtime_error("ERROR (RadiationModel::writePrimitiveDataLabelMap): Could not open file '" + outfile.str() + "' for writing.");
1005 }
1006
1007 bool empty_flag = true;
1008 // Apply horizontal flip to match mask coordinate system
1009 for (uint j = 0; j < camera_resolution.y; j++) {
1010 for (uint i = 0; i < camera_resolution.x; i++) {
1011 uint ii = camera_resolution.x - i - 1; // horizontal flip
1012 uint UUID = pixel_UUIDs.at(j * camera_resolution.x + ii) - 1;
1013 if (context->doesPrimitiveExist(UUID) && context->doesPrimitiveDataExist(UUID, primitive_data_label.c_str())) {
1014 HeliosDataType datatype = context->getPrimitiveDataType(primitive_data_label.c_str());
1015 if (datatype == HELIOS_TYPE_FLOAT) {
1016 float labeldata;
1017 context->getPrimitiveData(UUID, primitive_data_label.c_str(), labeldata);
1018 pixel_data << labeldata << " ";
1019 empty_flag = false;
1020 } else if (datatype == HELIOS_TYPE_UINT) {
1021 uint labeldata;
1022 context->getPrimitiveData(UUID, primitive_data_label.c_str(), labeldata);
1023 pixel_data << labeldata << " ";
1024 empty_flag = false;
1025 } else if (datatype == HELIOS_TYPE_INT) {
1026 int labeldata;
1027 context->getPrimitiveData(UUID, primitive_data_label.c_str(), labeldata);
1028 pixel_data << labeldata << " ";
1029 empty_flag = false;
1030 } else if (datatype == HELIOS_TYPE_DOUBLE) {
1031 double labeldata;
1032 context->getPrimitiveData(UUID, primitive_data_label.c_str(), labeldata);
1033 pixel_data << labeldata << " ";
1034 empty_flag = false;
1035 } else {
1036 pixel_data << padvalue << " ";
1037 }
1038 } else {
1039 pixel_data << padvalue << " ";
1040 }
1041 }
1042 pixel_data << "\n";
1043 }
1044 pixel_data.close();
1045
1046 if (empty_flag) {
1047 std::cerr << "WARNING (RadiationModel::writePrimitiveDataLabelMap): No primitive data of " << primitive_data_label << " found in camera image. Primitive data map contains only padded values." << std::endl;
1048 }
1049}
1050
1051void RadiationModel::writeObjectDataLabelMap(const std::string &cameralabel, const std::string &object_data_label, const std::string &imagefile_base, const std::string &image_path, int frame, float padvalue) {
1052
1053 if (cameras.find(cameralabel) == cameras.end()) {
1054 helios_runtime_error("ERROR (RadiationModel::writeObjectDataLabelMap): Camera '" + cameralabel + "' does not exist.");
1055 }
1056
1057 // Get image UUID labels
1058 std::vector<uint> camera_UUIDs;
1059 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
1060 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
1061 helios_runtime_error("ERROR (RadiationModel::writeObjectDataLabelMap): Pixel labels for camera '" + cameralabel + "' do not exist. Was the radiation model run to generate labels?");
1062 }
1063 context->getGlobalData(global_data_label.c_str(), camera_UUIDs);
1064 std::vector<uint> pixel_UUIDs = camera_UUIDs;
1065 int2 camera_resolution = cameras.at(cameralabel).resolution;
1066
1067 std::string frame_str;
1068 if (frame >= 0) {
1069 frame_str = std::to_string(frame);
1070 }
1071
1072 std::string output_path = image_path;
1073 if (!image_path.empty() && !validateOutputPath(output_path)) {
1074 helios_runtime_error("ERROR (RadiationModel::writeObjectDataLabelMap): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
1075 } else if (!isDirectoryPath(output_path)) {
1076 helios_runtime_error("ERROR(RadiationModel::writeObjectDataLabelMap): Expected a directory path but got a file path for argument 'image_path'.");
1077 }
1078
1079 std::ostringstream outfile;
1080 outfile << output_path;
1081
1082 if (frame >= 0) {
1083 outfile << cameralabel << "_" << imagefile_base << "_" << std::setw(5) << std::setfill('0') << frame_str << ".txt";
1084 } else {
1085 outfile << cameralabel << "_" << imagefile_base << ".txt";
1086 }
1087
1088 // Output label image in ".txt" format
1089 std::ofstream pixel_data(outfile.str());
1090
1091 if (!pixel_data.is_open()) {
1092 helios_runtime_error("ERROR (RadiationModel::writeObjectDataLabelMap): Could not open file '" + outfile.str() + "' for writing.");
1093 }
1094
1095 bool empty_flag = true;
1096 // Apply horizontal flip to match mask coordinate system
1097 for (uint j = 0; j < camera_resolution.y; j++) {
1098 for (uint i = 0; i < camera_resolution.x; i++) {
1099 uint ii = camera_resolution.x - i - 1; // horizontal flip
1100 uint UUID = pixel_UUIDs.at(j * camera_resolution.x + ii) - 1;
1101 if (!context->doesPrimitiveExist(UUID)) {
1102 pixel_data << padvalue << " ";
1103 continue;
1104 }
1105 uint objID = context->getPrimitiveParentObjectID(UUID);
1106 if (context->doesObjectExist(objID) && context->doesObjectDataExist(objID, object_data_label.c_str())) {
1107 HeliosDataType datatype = context->getObjectDataType(object_data_label.c_str());
1108 if (datatype == HELIOS_TYPE_FLOAT) {
1109 float labeldata;
1110 context->getObjectData(objID, object_data_label.c_str(), labeldata);
1111 pixel_data << labeldata << " ";
1112 empty_flag = false;
1113 } else if (datatype == HELIOS_TYPE_UINT) {
1114 uint labeldata;
1115 context->getObjectData(objID, object_data_label.c_str(), labeldata);
1116 pixel_data << labeldata << " ";
1117 empty_flag = false;
1118 } else if (datatype == HELIOS_TYPE_INT) {
1119 int labeldata;
1120 context->getObjectData(objID, object_data_label.c_str(), labeldata);
1121 pixel_data << labeldata << " ";
1122 empty_flag = false;
1123 } else if (datatype == HELIOS_TYPE_DOUBLE) {
1124 double labeldata;
1125 context->getObjectData(objID, object_data_label.c_str(), labeldata);
1126 pixel_data << labeldata << " ";
1127 empty_flag = false;
1128 } else {
1129 pixel_data << padvalue << " ";
1130 }
1131 } else {
1132 pixel_data << padvalue << " ";
1133 }
1134 }
1135 pixel_data << "\n";
1136 }
1137 pixel_data.close();
1138
1139 if (empty_flag) {
1140 std::cerr << "WARNING (RadiationModel::writeObjectDataLabelMap): No object data of " << object_data_label << " found in camera image. Object data map contains only padded values." << std::endl;
1141 }
1142}
1143
1144void RadiationModel::writeDepthImageData(const std::string &cameralabel, const std::string &imagefile_base, const std::string &image_path, int frame) {
1145
1146 if (cameras.find(cameralabel) == cameras.end()) {
1147 helios_runtime_error("ERROR (RadiationModel::writeDepthImageData): Camera '" + cameralabel + "' does not exist.");
1148 }
1149
1150 std::string global_data_label = "camera_" + cameralabel + "_pixel_depth";
1151 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
1152 helios_runtime_error("ERROR (RadiationModel::writeDepthImageData): Depth data for camera '" + cameralabel + "' does not exist. Was the radiation model run for the camera?");
1153 }
1154 std::vector<float> camera_depth;
1155 context->getGlobalData(global_data_label.c_str(), camera_depth);
1156 helios::vec3 camera_position = cameras.at(cameralabel).position;
1157 helios::vec3 camera_lookat = cameras.at(cameralabel).lookat;
1158
1159 int2 camera_resolution = cameras.at(cameralabel).resolution;
1160
1161 std::string frame_str;
1162 if (frame >= 0) {
1163 frame_str = std::to_string(frame);
1164 }
1165
1166 std::string output_path = image_path;
1167 if (!image_path.empty() && !validateOutputPath(output_path)) {
1168 helios_runtime_error("ERROR (RadiationModel::writeDepthImageData): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
1169 } else if (!isDirectoryPath(output_path)) {
1170 helios_runtime_error("ERROR(RadiationModel::writeDepthImageData): Expected a directory path but got a file path for argument 'image_path'.");
1171 }
1172
1173 std::ostringstream outfile;
1174 outfile << output_path;
1175
1176 if (frame >= 0) {
1177 outfile << cameralabel << "_" << imagefile_base << "_" << std::setw(5) << std::setfill('0') << frame_str << ".txt";
1178 } else {
1179 outfile << cameralabel << "_" << imagefile_base << ".txt";
1180 }
1181
1182 // Output label image in ".txt" format
1183 std::ofstream pixel_data(outfile.str());
1184
1185 if (!pixel_data.is_open()) {
1186 helios_runtime_error("ERROR (RadiationModel::writeDepthImageData): Could not open file '" + outfile.str() + "' for writing.");
1187 }
1188
1189 for (int j = 0; j < camera_resolution.y; j++) {
1190 for (int i = camera_resolution.x - 1; i >= 0; i--) {
1191 pixel_data << camera_depth.at(j * camera_resolution.x + i) << " ";
1192 }
1193 pixel_data << "\n";
1194 }
1195
1196 pixel_data.close();
1197}
1198
1199void RadiationModel::writeNormDepthImage(const std::string &cameralabel, const std::string &imagefile_base, float max_depth, const std::string &image_path, int frame) {
1200
1201 if (cameras.find(cameralabel) == cameras.end()) {
1202 helios_runtime_error("ERROR (RadiationModel::writeNormDepthImage): Camera '" + cameralabel + "' does not exist.");
1203 }
1204
1205 std::string global_data_label = "camera_" + cameralabel + "_pixel_depth";
1206 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
1207 helios_runtime_error("ERROR (RadiationModel::writeNormDepthImage): Depth data for camera '" + cameralabel + "' does not exist. Was the radiation model run for the camera?");
1208 }
1209 std::vector<float> camera_depth;
1210 context->getGlobalData(global_data_label.c_str(), camera_depth);
1211 helios::vec3 camera_position = cameras.at(cameralabel).position;
1212 helios::vec3 camera_lookat = cameras.at(cameralabel).lookat;
1213
1214 int2 camera_resolution = cameras.at(cameralabel).resolution;
1215
1216 std::string frame_str;
1217 if (frame >= 0) {
1218 frame_str = std::to_string(frame);
1219 }
1220
1221 std::string output_path = image_path;
1222 if (!image_path.empty() && !validateOutputPath(output_path)) {
1223 helios_runtime_error("ERROR (RadiationModel::writeNormDepthImage): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
1224 } else if (!isDirectoryPath(output_path)) {
1225 helios_runtime_error("ERROR(RadiationModel::writeNormDepthImage): Expected a directory path but got a file path for argument 'image_path'.");
1226 }
1227
1228 std::ostringstream outfile;
1229 outfile << output_path;
1230
1231 if (frame >= 0) {
1232 outfile << cameralabel << "_" << imagefile_base << "_" << std::setw(5) << std::setfill('0') << frame_str << ".jpeg";
1233 } else {
1234 outfile << cameralabel << "_" << imagefile_base << ".jpeg";
1235 }
1236
1237 float min_depth = 99999;
1238 for (int i = 0; i < camera_depth.size(); i++) {
1239 if (camera_depth.at(i) < 0 || camera_depth.at(i) > max_depth) {
1240 camera_depth.at(i) = max_depth;
1241 }
1242 if (camera_depth.at(i) < min_depth) {
1243 min_depth = camera_depth.at(i);
1244 }
1245 }
1246 for (int i = 0; i < camera_depth.size(); i++) {
1247 camera_depth.at(i) = 1.f - (camera_depth.at(i) - min_depth) / (max_depth - min_depth);
1248 }
1249
1250 std::vector<RGBcolor> pixel_data(camera_resolution.x * camera_resolution.y);
1251
1252 RGBcolor pixel_color;
1253 for (uint j = 0; j < camera_resolution.y; j++) {
1254 for (uint i = 0; i < camera_resolution.x; i++) {
1255
1256 float c = camera_depth.at(j * camera_resolution.x + i);
1257 pixel_color = make_RGBcolor(c, c, c);
1258
1259 uint ii = camera_resolution.x - i - 1;
1260 uint jj = camera_resolution.y - j - 1;
1261 pixel_data.at(jj * camera_resolution.x + ii) = pixel_color;
1262 }
1263 }
1264
1265 writeJPEG(outfile.str(), camera_resolution.x, camera_resolution.y, pixel_data);
1266}
1267
1268// DEPRECATED
1269void RadiationModel::writeImageBoundingBoxes(const std::string &cameralabel, const std::string &primitive_data_label, uint object_class_ID, const std::string &imagefile_base, const std::string &image_path, bool append_label_file, int frame) {
1270
1271 if (cameras.find(cameralabel) == cameras.end()) {
1272 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Camera '" + cameralabel + "' does not exist.");
1273 }
1274
1275 // Get image UUID labels
1276 std::vector<uint> camera_UUIDs;
1277 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
1278 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
1279 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Pixel labels for camera '" + cameralabel + "' do not exist. Was the radiation model run to generate labels?");
1280 }
1281 context->getGlobalData(global_data_label.c_str(), camera_UUIDs);
1282 std::vector<uint> pixel_UUIDs = camera_UUIDs;
1283 int2 camera_resolution = cameras.at(cameralabel).resolution;
1284
1285 std::string frame_str;
1286 if (frame >= 0) {
1287 frame_str = std::to_string(frame);
1288 }
1289
1290 std::string output_path = image_path;
1291 if (!image_path.empty() && !validateOutputPath(output_path)) {
1292 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
1293 } else if (!isDirectoryPath(output_path)) {
1294 helios_runtime_error("ERROR(RadiationModel::writeImageBoundingBoxes): Expected a directory path but got a file path for argument 'image_path'.");
1295 }
1296
1297 std::ostringstream outfile;
1298 outfile << output_path;
1299
1300 if (frame >= 0) {
1301 outfile << cameralabel << "_" << imagefile_base << "_" << std::setw(5) << std::setfill('0') << frame_str << ".txt";
1302 } else {
1303 outfile << cameralabel << "_" << imagefile_base << ".txt";
1304 }
1305
1306 // Output label image in ".txt" format
1307 std::ofstream label_file;
1308 if (append_label_file) {
1309 label_file.open(outfile.str(), std::ios::out | std::ios::app);
1310 } else {
1311 label_file.open(outfile.str());
1312 }
1313
1314 if (!label_file.is_open()) {
1315 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Could not open file '" + outfile.str() + "'.");
1316 }
1317
1318 std::map<int, vec4> pdata_bounds;
1319
1320 for (int j = 0; j < camera_resolution.y; j++) {
1321 for (int i = 0; i < camera_resolution.x; i++) {
1322 uint UUID = pixel_UUIDs.at(j * camera_resolution.x + i) - 1;
1323 if (context->doesPrimitiveExist(UUID) && context->doesPrimitiveDataExist(UUID, primitive_data_label.c_str())) {
1324
1325 uint labeldata;
1326
1327 HeliosDataType datatype = context->getPrimitiveDataType(primitive_data_label.c_str());
1328 if (datatype == HELIOS_TYPE_UINT) {
1329 uint labeldata_ui;
1330 context->getPrimitiveData(UUID, primitive_data_label.c_str(), labeldata_ui);
1331 labeldata = labeldata_ui;
1332 } else if (datatype == HELIOS_TYPE_INT) {
1333 int labeldata_i;
1334 context->getPrimitiveData(UUID, primitive_data_label.c_str(), labeldata_i);
1335 labeldata = (uint) labeldata_i;
1336 } else {
1337 continue;
1338 }
1339
1340 if (pdata_bounds.find(labeldata) == pdata_bounds.end()) {
1341 pdata_bounds[labeldata] = make_vec4(1e6, -1, 1e6, -1);
1342 }
1343
1344 if (i < pdata_bounds[labeldata].x) {
1345 pdata_bounds[labeldata].x = i;
1346 }
1347 if (i > pdata_bounds[labeldata].y) {
1348 pdata_bounds[labeldata].y = i;
1349 }
1350 if (j < pdata_bounds[labeldata].z) {
1351 pdata_bounds[labeldata].z = j;
1352 }
1353 if (j > pdata_bounds[labeldata].w) {
1354 pdata_bounds[labeldata].w = j;
1355 }
1356 }
1357 }
1358 }
1359
1360 for (auto box: pdata_bounds) {
1361 vec4 bbox = box.second;
1362 if (bbox.x == bbox.y || bbox.z == bbox.w) { // filter boxes of zeros size
1363 continue;
1364 }
1365 label_file << object_class_ID << " " << (bbox.x + 0.5 * (bbox.y - bbox.x)) / float(camera_resolution.x) << " " << (bbox.z + 0.5 * (bbox.w - bbox.z)) / float(camera_resolution.y) << " " << std::setprecision(6) << std::fixed
1366 << (bbox.y - bbox.x) / float(camera_resolution.x) << " " << (bbox.w - bbox.z) / float(camera_resolution.y) << std::endl;
1367 }
1368
1369 label_file.close();
1370}
1371
1372// DEPRECATED
1373void RadiationModel::writeImageBoundingBoxes_ObjectData(const std::string &cameralabel, const std::string &object_data_label, uint object_class_ID, const std::string &imagefile_base, const std::string &image_path, bool append_label_file, int frame) {
1374
1375 if (cameras.find(cameralabel) == cameras.end()) {
1376 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Camera '" + cameralabel + "' does not exist.");
1377 }
1378
1379 // Get image UUID labels
1380 std::vector<uint> camera_UUIDs;
1381 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
1382 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
1383 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Pixel labels for camera '" + cameralabel + "' do not exist. Was the radiation model run to generate labels?");
1384 }
1385 context->getGlobalData(global_data_label.c_str(), camera_UUIDs);
1386 std::vector<uint> pixel_UUIDs = camera_UUIDs;
1387 int2 camera_resolution = cameras.at(cameralabel).resolution;
1388
1389 std::string frame_str;
1390 if (frame >= 0) {
1391 frame_str = std::to_string(frame);
1392 }
1393
1394 std::string output_path = image_path;
1395 if (!image_path.empty() && !validateOutputPath(output_path)) {
1396 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
1397 } else if (!isDirectoryPath(output_path)) {
1398 helios_runtime_error("ERROR(RadiationModel::writeImageBoundingBoxes_ObjectData): Expected a directory path but got a file path for argument 'image_path'.");
1399 }
1400
1401 std::ostringstream outfile;
1402 outfile << output_path;
1403
1404 if (frame >= 0) {
1405 outfile << cameralabel << "_" << imagefile_base << "_" << std::setw(5) << std::setfill('0') << frame_str << ".txt";
1406 } else {
1407 outfile << cameralabel << "_" << imagefile_base << ".txt";
1408 }
1409
1410 // Output label image in ".txt" format
1411 std::ofstream label_file;
1412 if (append_label_file) {
1413 label_file.open(outfile.str(), std::ios::out | std::ios::app);
1414 } else {
1415 label_file.open(outfile.str());
1416 }
1417
1418 if (!label_file.is_open()) {
1419 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Could not open file '" + outfile.str() + "'.");
1420 }
1421
1422 std::map<int, vec4> pdata_bounds;
1423
1424 for (int j = 0; j < camera_resolution.y; j++) {
1425 for (int i = 0; i < camera_resolution.x; i++) {
1426 uint ii = camera_resolution.x - i - 1;
1427 uint UUID = pixel_UUIDs.at(j * camera_resolution.x + ii) - 1;
1428
1429 if (!context->doesPrimitiveExist(UUID)) {
1430 continue;
1431 }
1432
1433 uint objID = context->getPrimitiveParentObjectID(UUID);
1434
1435 if (!context->doesObjectExist(objID) || !context->doesObjectDataExist(objID, object_data_label.c_str())) {
1436 continue;
1437 }
1438
1439 uint labeldata;
1440
1441 HeliosDataType datatype = context->getObjectDataType(object_data_label.c_str());
1442 if (datatype == HELIOS_TYPE_UINT) {
1443 uint labeldata_ui;
1444 context->getObjectData(objID, object_data_label.c_str(), labeldata_ui);
1445 labeldata = labeldata_ui;
1446 } else if (datatype == HELIOS_TYPE_INT) {
1447 int labeldata_i;
1448 context->getObjectData(objID, object_data_label.c_str(), labeldata_i);
1449 labeldata = (uint) labeldata_i;
1450 } else {
1451 continue;
1452 }
1453
1454 if (pdata_bounds.find(labeldata) == pdata_bounds.end()) {
1455 pdata_bounds[labeldata] = make_vec4(1e6, -1, 1e6, -1);
1456 }
1457
1458 if (i < pdata_bounds[labeldata].x) {
1459 pdata_bounds[labeldata].x = i;
1460 }
1461 if (i > pdata_bounds[labeldata].y) {
1462 pdata_bounds[labeldata].y = i;
1463 }
1464 if (j < pdata_bounds[labeldata].z) {
1465 pdata_bounds[labeldata].z = j;
1466 }
1467 if (j > pdata_bounds[labeldata].w) {
1468 pdata_bounds[labeldata].w = j;
1469 }
1470 }
1471 }
1472
1473 for (auto box: pdata_bounds) {
1474 vec4 bbox = box.second;
1475 if (bbox.x == bbox.y || bbox.z == bbox.w) { // filter boxes of zeros size
1476 continue;
1477 }
1478 label_file << object_class_ID << " " << (bbox.x + 0.5 * (bbox.y - bbox.x)) / float(camera_resolution.x) << " " << (bbox.z + 0.5 * (bbox.w - bbox.z)) / float(camera_resolution.y) << " " << std::setprecision(6) << std::fixed
1479 << (bbox.y - bbox.x) / float(camera_resolution.x) << " " << (bbox.w - bbox.z) / float(camera_resolution.y) << std::endl;
1480 }
1481
1482 label_file.close();
1483}
1484
1485void RadiationModel::writeImageBoundingBoxes(const std::string &cameralabel, const std::string &primitive_data_label, const uint &object_class_ID, const std::string &image_file, const std::string &classes_txt_file, const std::string &image_path) {
1486 writeImageBoundingBoxes(cameralabel, std::vector<std::string>{primitive_data_label}, std::vector<uint>{object_class_ID}, image_file, classes_txt_file, image_path);
1487}
1488
1489void RadiationModel::writeImageBoundingBoxes(const std::string &cameralabel, const std::vector<std::string> &primitive_data_label, const std::vector<uint> &object_class_ID, const std::string &image_file, const std::string &classes_txt_file,
1490 const std::string &image_path) {
1491
1492 if (cameras.find(cameralabel) == cameras.end()) {
1493 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Camera '" + cameralabel + "' does not exist.");
1494 }
1495
1496 if (primitive_data_label.size() != object_class_ID.size()) {
1497 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): The lengths of primitive_data_label and object_class_ID vectors must be the same.");
1498 }
1499
1500 // Get image UUID labels
1501 std::vector<uint> camera_UUIDs;
1502 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
1503 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
1504 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Pixel labels for camera '" + cameralabel + "' do not exist. Was the radiation model run to generate labels?");
1505 }
1506 context->getGlobalData(global_data_label.c_str(), camera_UUIDs);
1507 std::vector<uint> pixel_UUIDs = camera_UUIDs;
1508 int2 camera_resolution = cameras.at(cameralabel).resolution;
1509
1510 std::string output_path = image_path;
1511 if (!image_path.empty() && !validateOutputPath(output_path)) {
1512 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
1513 } else if (!isDirectoryPath(output_path)) {
1514 helios_runtime_error("ERROR(RadiationModel::writeImageBoundingBoxes): Expected a directory path but got a file path for argument 'image_path'.");
1515 }
1516
1517 std::string outfile_txt = output_path + std::filesystem::path(image_file).stem().string() + ".txt";
1518
1519 std::ofstream label_file(outfile_txt);
1520
1521 if (!label_file.is_open()) {
1522 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Could not open output bounding box file '" + outfile_txt + "'.");
1523 }
1524
1525 // Map to store bounding boxes for each data label class combination
1526 std::map<std::pair<uint, uint>, vec4> pdata_bounds; // (class_id, label_value) -> bbox
1527
1528 // Iterate through all pixels
1529 for (int j = 0; j < camera_resolution.y; j++) {
1530 for (int i = 0; i < camera_resolution.x; i++) {
1531 uint ii = camera_resolution.x - i - 1;
1532 uint UUID = pixel_UUIDs.at(j * camera_resolution.x + ii) - 1;
1533
1534 if (context->doesPrimitiveExist(UUID)) {
1535 // Check each primitive data label
1536 for (size_t label_idx = 0; label_idx < primitive_data_label.size(); label_idx++) {
1537 const std::string &data_label = primitive_data_label[label_idx];
1538 uint class_id = object_class_ID[label_idx];
1539
1540 if (context->doesPrimitiveDataExist(UUID, data_label.c_str())) {
1541 uint labeldata;
1542 bool has_data = false;
1543
1544 HeliosDataType datatype = context->getPrimitiveDataType(data_label.c_str());
1545 if (datatype == HELIOS_TYPE_UINT) {
1546 uint labeldata_ui;
1547 context->getPrimitiveData(UUID, data_label.c_str(), labeldata_ui);
1548 labeldata = labeldata_ui;
1549 has_data = true;
1550 } else if (datatype == HELIOS_TYPE_INT) {
1551 int labeldata_i;
1552 context->getPrimitiveData(UUID, data_label.c_str(), labeldata_i);
1553 labeldata = (uint) labeldata_i;
1554 has_data = true;
1555 }
1556
1557 if (has_data) {
1558 std::pair<uint, uint> key = std::make_pair(class_id, labeldata);
1559
1560 if (pdata_bounds.find(key) == pdata_bounds.end()) {
1561 pdata_bounds[key] = make_vec4(1e6, -1, 1e6, -1);
1562 }
1563
1564 if (i < pdata_bounds[key].x) {
1565 pdata_bounds[key].x = i;
1566 }
1567 if (i > pdata_bounds[key].y) {
1568 pdata_bounds[key].y = i;
1569 }
1570 if (j < pdata_bounds[key].z) {
1571 pdata_bounds[key].z = j;
1572 }
1573 if (j > pdata_bounds[key].w) {
1574 pdata_bounds[key].w = j;
1575 }
1576 }
1577 }
1578 }
1579 }
1580 }
1581 }
1582
1583 for (auto box: pdata_bounds) {
1584 uint class_id = box.first.first;
1585 vec4 bbox = box.second;
1586 if (bbox.x == bbox.y || bbox.z == bbox.w) { // filter boxes of zero size
1587 continue;
1588 }
1589 label_file << class_id << " " << (bbox.x + 0.5 * (bbox.y - bbox.x)) / float(camera_resolution.x) << " " << (bbox.z + 0.5 * (bbox.w - bbox.z)) / float(camera_resolution.y) << " " << std::setprecision(6) << std::fixed
1590 << (bbox.y - bbox.x) / float(camera_resolution.x) << " " << (bbox.w - bbox.z) / float(camera_resolution.y) << std::endl;
1591 }
1592
1593 label_file.close();
1594
1595 std::ofstream classes_txt_stream(output_path + classes_txt_file);
1596 if (!classes_txt_stream.is_open()) {
1597 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes): Could not open output classes file '" + output_path + classes_txt_file + ".");
1598 }
1599 for (int i = 0; i < object_class_ID.size(); i++) {
1600 classes_txt_stream << object_class_ID.at(i) << " " << primitive_data_label.at(i) << std::endl;
1601 }
1602 classes_txt_stream.close();
1603}
1604
1605void RadiationModel::writeImageBoundingBoxes_ObjectData(const std::string &cameralabel, const std::string &object_data_label, const uint &object_class_ID, const std::string &image_file, const std::string &classes_txt_file,
1606 const std::string &image_path) {
1607 writeImageBoundingBoxes_ObjectData(cameralabel, std::vector<std::string>{object_data_label}, std::vector<uint>{object_class_ID}, image_file, classes_txt_file, image_path);
1608}
1609
1610void RadiationModel::writeImageBoundingBoxes_ObjectData(const std::string &cameralabel, const std::vector<std::string> &object_data_label, const std::vector<uint> &object_class_ID, const std::string &image_file, const std::string &classes_txt_file,
1611 const std::string &image_path) {
1612
1613 if (cameras.find(cameralabel) == cameras.end()) {
1614 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Camera '" + cameralabel + "' does not exist.");
1615 }
1616
1617 if (object_data_label.size() != object_class_ID.size()) {
1618 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): The lengths of object_data_label and object_class_ID vectors must be the same.");
1619 }
1620
1621 // Get image UUID labels
1622 std::vector<uint> camera_UUIDs;
1623 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
1624 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
1625 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Pixel labels for camera '" + cameralabel + "' do not exist. Was the radiation model run to generate labels?");
1626 }
1627 context->getGlobalData(global_data_label.c_str(), camera_UUIDs);
1628 std::vector<uint> pixel_UUIDs = camera_UUIDs;
1629 int2 camera_resolution = cameras.at(cameralabel).resolution;
1630
1631 std::string output_path = image_path;
1632 if (!image_path.empty() && !validateOutputPath(output_path)) {
1633 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Invalid image output directory '" + image_path + "'. Check that the path exists and that you have write permission.");
1634 } else if (!isDirectoryPath(output_path)) {
1635 helios_runtime_error("ERROR(RadiationModel::writeImageBoundingBoxes_ObjectData): Expected a directory path but got a file path for argument 'image_path'.");
1636 }
1637
1638 std::string outfile_txt = output_path + std::filesystem::path(image_file).stem().string() + ".txt";
1639
1640 std::ofstream label_file(outfile_txt);
1641
1642 if (!label_file.is_open()) {
1643 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Could not open output bounding box file '" + outfile_txt + "'.");
1644 }
1645
1646 // Map to store bounding boxes for each data label class combination
1647 std::map<std::pair<uint, uint>, vec4> pdata_bounds; // (class_id, label_value) -> bbox
1648
1649 // Iterate through all pixels
1650 // Apply horizontal flip to match mask coordinate system
1651 for (int j = 0; j < camera_resolution.y; j++) {
1652 for (int i = 0; i < camera_resolution.x; i++) {
1653 uint ii = camera_resolution.x - i - 1; // horizontal flip
1654 uint UUID = pixel_UUIDs.at(j * camera_resolution.x + ii) - 1;
1655
1656 if (!context->doesPrimitiveExist(UUID)) {
1657 continue;
1658 }
1659
1660 uint objID = context->getPrimitiveParentObjectID(UUID);
1661
1662 if (!context->doesObjectExist(objID)) {
1663 continue;
1664 }
1665
1666 // Check each object data label
1667 for (size_t label_idx = 0; label_idx < object_data_label.size(); label_idx++) {
1668 const std::string &data_label = object_data_label[label_idx];
1669 uint class_id = object_class_ID[label_idx];
1670
1671 if (context->doesObjectDataExist(objID, data_label.c_str())) {
1672 uint labeldata;
1673 bool has_data = false;
1674
1675 HeliosDataType datatype = context->getObjectDataType(data_label.c_str());
1676 if (datatype == HELIOS_TYPE_UINT) {
1677 uint labeldata_ui;
1678 context->getObjectData(objID, data_label.c_str(), labeldata_ui);
1679 labeldata = labeldata_ui;
1680 has_data = true;
1681 } else if (datatype == HELIOS_TYPE_INT) {
1682 int labeldata_i;
1683 context->getObjectData(objID, data_label.c_str(), labeldata_i);
1684 labeldata = (uint) labeldata_i;
1685 has_data = true;
1686 }
1687
1688 if (has_data) {
1689 std::pair<uint, uint> key = std::make_pair(class_id, labeldata);
1690
1691 if (pdata_bounds.find(key) == pdata_bounds.end()) {
1692 pdata_bounds[key] = make_vec4(1e6, -1, 1e6, -1);
1693 }
1694
1695 if (i < pdata_bounds[key].x) {
1696 pdata_bounds[key].x = i;
1697 }
1698 if (i > pdata_bounds[key].y) {
1699 pdata_bounds[key].y = i;
1700 }
1701 if (j < pdata_bounds[key].z) {
1702 pdata_bounds[key].z = j;
1703 }
1704 if (j > pdata_bounds[key].w) {
1705 pdata_bounds[key].w = j;
1706 }
1707 }
1708 }
1709 }
1710 }
1711 }
1712
1713 for (auto box: pdata_bounds) {
1714 uint class_id = box.first.first;
1715 vec4 bbox = box.second;
1716 if (bbox.x == bbox.y || bbox.z == bbox.w) { // filter boxes of zero size
1717 continue;
1718 }
1719 label_file << class_id << " " << (bbox.x + 0.5 * (bbox.y - bbox.x)) / float(camera_resolution.x) << " " << (bbox.z + 0.5 * (bbox.w - bbox.z)) / float(camera_resolution.y) << " " << std::setprecision(6) << std::fixed
1720 << (bbox.y - bbox.x) / float(camera_resolution.x) << " " << (bbox.w - bbox.z) / float(camera_resolution.y) << std::endl;
1721 }
1722
1723 label_file.close();
1724
1725 std::ofstream classes_txt_stream(output_path + classes_txt_file);
1726 if (!classes_txt_stream.is_open()) {
1727 helios_runtime_error("ERROR (RadiationModel::writeImageBoundingBoxes_ObjectData): Could not open output classes file '" + output_path + classes_txt_file + ".");
1728 }
1729 for (int i = 0; i < object_class_ID.size(); i++) {
1730 classes_txt_stream << object_class_ID.at(i) << " " << object_data_label.at(i) << std::endl;
1731 }
1732 classes_txt_stream.close();
1733}
1734
1735// Helper function to initialize or load existing COCO JSON structure and get image ID
1736std::pair<nlohmann::json, int> RadiationModel::initializeCOCOJsonWithImageId(const std::string &filename, bool append_file, const std::string &cameralabel, const helios::int2 &camera_resolution, const std::string &image_file) {
1737 nlohmann::json coco_json;
1738 int image_id = 0;
1739
1740 if (append_file) {
1741 std::ifstream existing_file(filename);
1742 if (existing_file.is_open()) {
1743 try {
1744 existing_file >> coco_json;
1745 } catch (const std::exception &e) {
1746 coco_json.clear();
1747 }
1748 existing_file.close();
1749 }
1750 }
1751
1752 // Initialize JSON structure if empty
1753 if (coco_json.empty()) {
1754 coco_json["categories"] = nlohmann::json::array();
1755 coco_json["images"] = nlohmann::json::array();
1756 coco_json["annotations"] = nlohmann::json::array();
1757 }
1758
1759 // Extract just the filename (no path) from the image file
1760 std::filesystem::path image_path_obj(image_file);
1761 std::string filename_only = image_path_obj.filename().string();
1762
1763 // Check if this image already exists in the JSON
1764 bool image_exists = false;
1765 for (const auto &img: coco_json["images"]) {
1766 if (img["file_name"] == filename_only) {
1767 image_id = img["id"];
1768 image_exists = true;
1769 break;
1770 }
1771 }
1772
1773 // If image doesn't exist, add it with a new unique ID
1774 if (!image_exists) {
1775 // Find the next available image ID
1776 int max_image_id = -1;
1777 for (const auto &img: coco_json["images"]) {
1778 if (img["id"] > max_image_id) {
1779 max_image_id = img["id"];
1780 }
1781 }
1782 image_id = max_image_id + 1;
1783
1784 // Add the new image entry
1785 nlohmann::json image_entry;
1786 image_entry["id"] = image_id;
1787 image_entry["file_name"] = filename_only;
1788 image_entry["height"] = camera_resolution.y;
1789 image_entry["width"] = camera_resolution.x;
1790 coco_json["images"].push_back(image_entry);
1791 }
1792
1793 return std::make_pair(coco_json, image_id);
1794}
1795
1796// Helper function to initialize or load existing COCO JSON structure (backward compatibility)
1797nlohmann::json RadiationModel::initializeCOCOJson(const std::string &filename, bool append_file, const std::string &cameralabel, const helios::int2 &camera_resolution, const std::string &image_file) {
1798 return initializeCOCOJsonWithImageId(filename, append_file, cameralabel, camera_resolution, image_file).first;
1799}
1800
1801// Helper function to add category to COCO JSON if it doesn't exist
1802void RadiationModel::addCategoryToCOCO(nlohmann::json &coco_json, const std::vector<uint> &object_class_ID, const std::vector<std::string> &category_name) {
1803 if (object_class_ID.size() != category_name.size()) {
1804 helios_runtime_error("ERROR (RadiationModel::addCategoryToCOCO): The lengths of object_class_ID and category_name vectors must be the same.");
1805 }
1806
1807 for (size_t i = 0; i < object_class_ID.size(); ++i) {
1808 bool category_exists = false;
1809 for (auto &cat: coco_json["categories"]) {
1810 if (cat["id"] == object_class_ID[i]) {
1811 category_exists = true;
1812 break;
1813 }
1814 }
1815 if (!category_exists) {
1816 nlohmann::json category;
1817 category["id"] = object_class_ID[i];
1818 category["name"] = category_name[i];
1819 category["supercategory"] = "none";
1820 coco_json["categories"].push_back(category);
1821 }
1822 }
1823}
1824
1825// Helper function to write COCO JSON with proper formatting
1826void RadiationModel::writeCOCOJson(const nlohmann::json &coco_json, const std::string &filename) {
1827 std::ofstream json_file(filename);
1828 if (!json_file.is_open()) {
1829 helios_runtime_error("ERROR (RadiationModel): Could not open file '" + filename + "'.");
1830 }
1831
1832 // Use standard JSON formatting for now (can optimize array formatting later)
1833 json_file << coco_json.dump(2) << std::endl;
1834 json_file.close();
1835}
1836
1837// Helper function to generate label masks from either primitive or object data
1838std::map<int, std::vector<std::vector<bool>>> RadiationModel::generateLabelMasks(const std::string &cameralabel, const std::string &data_label, bool use_object_data) {
1839 std::vector<uint> camera_UUIDs;
1840 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
1841 context->getGlobalData(global_data_label.c_str(), camera_UUIDs);
1842 std::vector<uint> pixel_UUIDs = camera_UUIDs;
1843 int2 camera_resolution = cameras.at(cameralabel).resolution;
1844
1845 std::map<int, std::vector<std::vector<bool>>> label_masks;
1846
1847 // First pass: identify all unique labels and create binary masks
1848 // Apply horizontal flip to match JPEG coordinate system
1849 for (int j = 0; j < camera_resolution.y; j++) {
1850 for (int i = 0; i < camera_resolution.x; i++) {
1851 uint ii = camera_resolution.x - i - 1; // horizontal flip to match JPEG
1852 uint UUID = pixel_UUIDs.at(j * camera_resolution.x + ii) - 1;
1853
1854 if (context->doesPrimitiveExist(UUID)) {
1855 uint labeldata;
1856 bool has_data = false;
1857
1858 if (use_object_data) {
1859 // Object data version
1860 uint objID = context->getPrimitiveParentObjectID(UUID);
1861 if (objID != 0 && context->doesObjectDataExist(objID, data_label.c_str())) {
1862 HeliosDataType datatype = context->getObjectDataType(data_label.c_str());
1863 if (datatype == HELIOS_TYPE_UINT) {
1864 uint labeldata_ui;
1865 context->getObjectData(objID, data_label.c_str(), labeldata_ui);
1866 labeldata = labeldata_ui;
1867 has_data = true;
1868 } else if (datatype == HELIOS_TYPE_INT) {
1869 int labeldata_i;
1870 context->getObjectData(objID, data_label.c_str(), labeldata_i);
1871 labeldata = (uint) labeldata_i;
1872 has_data = true;
1873 }
1874 }
1875 } else {
1876 // Primitive data version
1877 if (context->doesPrimitiveDataExist(UUID, data_label.c_str())) {
1878 HeliosDataType datatype = context->getPrimitiveDataType(data_label.c_str());
1879 if (datatype == HELIOS_TYPE_UINT) {
1880 uint labeldata_ui;
1881 context->getPrimitiveData(UUID, data_label.c_str(), labeldata_ui);
1882 labeldata = labeldata_ui;
1883 has_data = true;
1884 } else if (datatype == HELIOS_TYPE_INT) {
1885 int labeldata_i;
1886 context->getPrimitiveData(UUID, data_label.c_str(), labeldata_i);
1887 labeldata = (uint) labeldata_i;
1888 has_data = true;
1889 }
1890 }
1891 }
1892
1893 if (has_data) {
1894 // Initialize mask for this label if not exists
1895 if (label_masks.find(labeldata) == label_masks.end()) {
1896 label_masks[labeldata] = std::vector<std::vector<bool>>(camera_resolution.y, std::vector<bool>(camera_resolution.x, false));
1897 }
1898 label_masks[labeldata][j][i] = true;
1899 }
1900 }
1901 }
1902 }
1903
1904 return label_masks;
1905}
1906
1907// Helper function to find starting boundary pixel (topmost-leftmost)
1908std::pair<int, int> RadiationModel::findStartingBoundaryPixel(const std::vector<std::vector<bool>> &mask, const helios::int2 &camera_resolution) {
1909 for (int j = 0; j < camera_resolution.y; j++) {
1910 for (int i = 0; i < camera_resolution.x; i++) {
1911 if (mask[j][i]) {
1912 // Check if this pixel is on the boundary
1913 for (int di = -1; di <= 1; di++) {
1914 for (int dj = -1; dj <= 1; dj++) {
1915 if (di == 0 && dj == 0)
1916 continue;
1917 int ni = i + di;
1918 int nj = j + dj;
1919 if (ni < 0 || ni >= camera_resolution.x || nj < 0 || nj >= camera_resolution.y || !mask[nj][ni]) {
1920 return {i, j}; // Found boundary pixel
1921 }
1922 }
1923 }
1924 }
1925 }
1926 }
1927 return {-1, -1}; // No boundary found
1928}
1929
1930// Helper function to trace boundary using Moore neighborhood algorithm
1931std::vector<std::pair<int, int>> RadiationModel::traceBoundaryMoore(const std::vector<std::vector<bool>> &mask, int start_x, int start_y, const helios::int2 &camera_resolution) {
1932 std::vector<std::pair<int, int>> contour;
1933
1934 // 8-connected neighbors in clockwise order starting from East
1935 int dx[] = {1, 1, 0, -1, -1, -1, 0, 1};
1936 int dy[] = {0, 1, 1, 1, 0, -1, -1, -1};
1937
1938 int x = start_x, y = start_y;
1939 int dir = 6; // Start looking West (opposite of East)
1940
1941 do {
1942 contour.push_back({x, y});
1943
1944 // Look for next boundary pixel
1945 int start_dir = (dir + 6) % 8; // Start looking 3 positions counter-clockwise from where we came
1946 bool found = false;
1947
1948 for (int i = 0; i < 8; i++) {
1949 int check_dir = (start_dir + i) % 8;
1950 int nx = x + dx[check_dir];
1951 int ny = y + dy[check_dir];
1952
1953 // Check if this neighbor is inside bounds and inside the mask
1954 if (nx >= 0 && nx < camera_resolution.x && ny >= 0 && ny < camera_resolution.y && mask[ny][nx]) {
1955 x = nx;
1956 y = ny;
1957 dir = check_dir;
1958 found = true;
1959 break;
1960 }
1961 }
1962
1963 if (!found)
1964 break; // No next boundary pixel found
1965
1966 } while (!(x == start_x && y == start_y) && contour.size() < camera_resolution.x * camera_resolution.y);
1967
1968 return contour;
1969}
1970
1971// Helper function to trace boundary using simple connected components
1972std::vector<std::pair<int, int>> RadiationModel::traceBoundarySimple(const std::vector<std::vector<bool>> &mask, int start_x, int start_y, const helios::int2 &camera_resolution) {
1973 std::vector<std::pair<int, int>> contour;
1974 std::set<std::pair<int, int>> visited_boundary;
1975
1976 // Use a simple approach: walk along the boundary
1977 std::queue<std::pair<int, int>> boundary_queue;
1978 boundary_queue.push({start_x, start_y});
1979 visited_boundary.insert({start_x, start_y});
1980
1981 while (!boundary_queue.empty()) {
1982 auto [x, y] = boundary_queue.front();
1983 boundary_queue.pop();
1984 contour.push_back({x, y});
1985
1986 // 8-connected neighbors
1987 for (int di = -1; di <= 1; di++) {
1988 for (int dj = -1; dj <= 1; dj++) {
1989 if (di == 0 && dj == 0)
1990 continue;
1991 int nx = x + di;
1992 int ny = y + dj;
1993
1994 if (nx >= 0 && nx < camera_resolution.x && ny >= 0 && ny < camera_resolution.y && mask[ny][nx] && visited_boundary.find({nx, ny}) == visited_boundary.end()) {
1995
1996 // Check if this pixel is on the boundary
1997 bool is_boundary = false;
1998 for (int ddi = -1; ddi <= 1; ddi++) {
1999 for (int ddj = -1; ddj <= 1; ddj++) {
2000 if (ddi == 0 && ddj == 0)
2001 continue;
2002 int nnx = nx + ddi;
2003 int nny = ny + ddj;
2004 if (nnx < 0 || nnx >= camera_resolution.x || nny < 0 || nny >= camera_resolution.y || !mask[nny][nnx]) {
2005 is_boundary = true;
2006 break;
2007 }
2008 }
2009 if (is_boundary)
2010 break;
2011 }
2012
2013 if (is_boundary) {
2014 boundary_queue.push({nx, ny});
2015 visited_boundary.insert({nx, ny});
2016 }
2017 }
2018 }
2019 }
2020 }
2021
2022 return contour;
2023}
2024
2025// Helper function to generate annotations from label masks
2026std::vector<std::map<std::string, std::vector<float>>> RadiationModel::generateAnnotationsFromMasks(const std::map<int, std::vector<std::vector<bool>>> &label_masks, uint object_class_ID, const helios::int2 &camera_resolution, int image_id) {
2027 std::vector<std::map<std::string, std::vector<float>>> annotations;
2028 int annotation_id = 0;
2029
2030 for (const auto &label_pair: label_masks) {
2031 int label_value = label_pair.first;
2032 const auto &mask = label_pair.second;
2033
2034 // Create a visited mask for connected components
2035 std::vector<std::vector<bool>> visited(camera_resolution.y, std::vector<bool>(camera_resolution.x, false));
2036
2037 // Find all connected components for this label
2038 for (int j = 0; j < camera_resolution.y; j++) {
2039 for (int i = 0; i < camera_resolution.x; i++) {
2040 if (mask[j][i] && !visited[j][i]) {
2041 // Find boundary pixel for this component
2042 int boundary_i = i, boundary_j = j;
2043 bool is_boundary = false;
2044
2045 // Check if this pixel is on the boundary
2046 for (int di = -1; di <= 1; di++) {
2047 for (int dj = -1; dj <= 1; dj++) {
2048 int ni = i + di;
2049 int nj = j + dj;
2050 if (ni < 0 || ni >= camera_resolution.x || nj < 0 || nj >= camera_resolution.y || !mask[nj][ni]) {
2051 is_boundary = true;
2052 boundary_i = i;
2053 boundary_j = j;
2054 break;
2055 }
2056 }
2057 if (is_boundary)
2058 break;
2059 }
2060
2061 if (is_boundary) {
2062 // First, mark all pixels in this connected component using flood fill
2063 std::stack<std::pair<int, int>> stack;
2064 std::vector<std::pair<int, int>> component_pixels;
2065 stack.push({i, j});
2066 visited[j][i] = true;
2067
2068 int min_x = i, max_x = i, min_y = j, max_y = j;
2069 int area = 0;
2070
2071 while (!stack.empty()) {
2072 auto [ci, cj] = stack.top();
2073 stack.pop();
2074 area++;
2075 component_pixels.push_back({ci, cj});
2076
2077 min_x = std::min(min_x, ci);
2078 max_x = std::max(max_x, ci);
2079 min_y = std::min(min_y, cj);
2080 max_y = std::max(max_y, cj);
2081
2082 // Check 4-connected neighbors
2083 for (int di = -1; di <= 1; di++) {
2084 for (int dj = -1; dj <= 1; dj++) {
2085 if (abs(di) + abs(dj) != 1)
2086 continue; // Only 4-connected
2087 int ni = ci + di;
2088 int nj = cj + dj;
2089 if (ni >= 0 && ni < camera_resolution.x && nj >= 0 && nj < camera_resolution.y && mask[nj][ni] && !visited[nj][ni]) {
2090 stack.push({ni, nj});
2091 visited[nj][ni] = true;
2092 }
2093 }
2094 }
2095 }
2096
2097 // Now trace the boundary of this component
2098 auto start_pixel = findStartingBoundaryPixel(mask, camera_resolution);
2099 bool is_boundary_start = false;
2100
2101 if (start_pixel.first >= min_x && start_pixel.first <= max_x && start_pixel.second >= min_y && start_pixel.second <= max_y) {
2102 is_boundary_start = true;
2103 }
2104
2105 if (is_boundary_start) {
2106 // Try Moore neighborhood boundary tracing first
2107 auto contour = traceBoundaryMoore(mask, start_pixel.first, start_pixel.second, camera_resolution);
2108
2109 // If Moore tracing didn't work well, fall back to simple boundary collection
2110 if (contour.size() < 10) {
2111 contour = traceBoundarySimple(mask, start_pixel.first, start_pixel.second, camera_resolution);
2112 }
2113
2114 if (contour.size() >= 3) {
2115 // Create annotation
2116 std::map<std::string, std::vector<float>> annotation;
2117 annotation["id"] = {(float) annotation_id++};
2118 annotation["image_id"] = {(float) image_id};
2119 annotation["category_id"] = {(float) object_class_ID};
2120 annotation["bbox"] = {(float) min_x, (float) min_y, (float) (max_x - min_x), (float) (max_y - min_y)};
2121 annotation["area"] = {(float) area};
2122 annotation["iscrowd"] = {0.0f};
2123
2124 // Convert contour to segmentation format (flatten coordinates)
2125 std::vector<float> segmentation;
2126 for (const auto &point: contour) {
2127 segmentation.push_back((float) point.first); // x coordinate
2128 segmentation.push_back((float) point.second); // y coordinate
2129 }
2130 annotation["segmentation"] = segmentation;
2131
2132 annotations.push_back(annotation);
2133 }
2134 }
2135 }
2136 }
2137 }
2138 }
2139 }
2140
2141 return annotations;
2142}
2143
2144void RadiationModel::writeImageSegmentationMasks(const std::string &cameralabel, const std::string &primitive_data_label, const uint &object_class_ID, const std::string &json_filename, const std::string &image_file,
2145 const std::vector<std::string> &data_attribute_labels, bool append_file) {
2146 writeImageSegmentationMasks(cameralabel, std::vector<std::string>{primitive_data_label}, std::vector<uint>{object_class_ID}, json_filename, image_file, data_attribute_labels, append_file);
2147}
2148
2149void RadiationModel::writeImageSegmentationMasks(const std::string &cameralabel, const std::vector<std::string> &primitive_data_label, const std::vector<uint> &object_class_ID, const std::string &json_filename, const std::string &image_file,
2150 const std::vector<std::string> &data_attribute_labels, bool append_file) {
2151
2152 if (cameras.find(cameralabel) == cameras.end()) {
2153 helios_runtime_error("ERROR (RadiationModel::writeImageSegmentationMasks): Camera '" + cameralabel + "' does not exist.");
2154 }
2155
2156 if (primitive_data_label.size() != object_class_ID.size()) {
2157 helios_runtime_error("ERROR (RadiationModel::writeImageSegmentationMasks): The lengths of primitive_data_label and object_class_ID vectors must be the same.");
2158 }
2159
2160 // Check that camera pixel data exists
2161 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
2162 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
2163 helios_runtime_error("ERROR (RadiationModel::writeImageSegmentationMasks): Pixel labels for camera '" + cameralabel + "' do not exist. Was the radiation model run to generate labels?");
2164 }
2165
2166 // Check that all primitive data labels exist
2167 std::vector<std::string> all_primitive_data = context->listAllPrimitiveDataLabels();
2168 for (const auto &data_label: primitive_data_label) {
2169 if (std::find(all_primitive_data.begin(), all_primitive_data.end(), data_label) == all_primitive_data.end()) {
2170 std::cerr << "WARNING (RadiationModel::writeImageSegmentationMasks): Primitive data label '" << data_label << "' does not exist in the context." << std::endl;
2171 }
2172 }
2173
2174 // Check that image file exists
2175 if (!std::filesystem::exists(image_file)) {
2176 helios_runtime_error("ERROR (RadiationModel::writeImageSegmentationMasks): Image file '" + image_file + "' does not exist.");
2177 }
2178
2179 // Validate and ensure JSON filename has .json extension
2180 std::string validated_json_filename = json_filename;
2181 if (validated_json_filename.length() < 5 || validated_json_filename.substr(validated_json_filename.length() - 5) != ".json") {
2182 validated_json_filename += ".json";
2183 }
2184
2185 // Use the validated filename directly
2186 std::string outfile = validated_json_filename;
2187
2188 // Write annotations to JSON file
2189 int2 camera_resolution = cameras.at(cameralabel).resolution;
2190 auto coco_json_pair = initializeCOCOJsonWithImageId(outfile, append_file, cameralabel, camera_resolution, image_file);
2191 nlohmann::json coco_json = coco_json_pair.first;
2192 int image_id = coco_json_pair.second;
2193 addCategoryToCOCO(coco_json, object_class_ID, primitive_data_label);
2194
2195 // Check which data_attribute_labels exist in primitive or object data
2196 struct AttributeInfo {
2197 std::string label;
2198 bool is_primitive_data;
2199 bool exists;
2200 };
2201 std::vector<AttributeInfo> attribute_info;
2202
2203 if (!data_attribute_labels.empty()) {
2204 std::vector<std::string> all_primitive_data = context->listAllPrimitiveDataLabels();
2205 std::vector<std::string> all_object_data = context->listAllObjectDataLabels();
2206
2207 for (const auto &attr_label: data_attribute_labels) {
2208 AttributeInfo info;
2209 info.label = attr_label;
2210 info.exists = false;
2211
2212 if (std::find(all_primitive_data.begin(), all_primitive_data.end(), attr_label) != all_primitive_data.end()) {
2213 info.is_primitive_data = true;
2214 info.exists = true;
2215 } else if (std::find(all_object_data.begin(), all_object_data.end(), attr_label) != all_object_data.end()) {
2216 info.is_primitive_data = false;
2217 info.exists = true;
2218 }
2219
2220 if (info.exists) {
2221 attribute_info.push_back(info);
2222 }
2223 }
2224 }
2225
2226 bool use_attributes = !attribute_info.empty();
2227
2228 // Get pixel UUID data
2229 std::vector<uint> pixel_UUIDs;
2230 std::string pixel_UUID_label = "camera_" + cameralabel + "_pixel_UUID";
2231 context->getGlobalData(pixel_UUID_label.c_str(), pixel_UUIDs);
2232
2233 // Process each data label and class ID pair
2234 for (size_t i = 0; i < primitive_data_label.size(); ++i) {
2235 // Generate label masks using helper function (primitive data version)
2236 std::map<int, std::vector<std::vector<bool>>> label_masks = generateLabelMasks(cameralabel, primitive_data_label[i], false);
2237
2238 // Generate annotations from masks using helper function
2239 std::vector<std::map<std::string, std::vector<float>>> annotations = generateAnnotationsFromMasks(label_masks, object_class_ID[i], camera_resolution, image_id);
2240
2241 // Calculate mean attribute values for each mask if requested
2242 std::vector<std::map<std::string, double>> mean_attribute_values_per_component;
2243 if (use_attributes) {
2244 // For each label mask, find connected components and calculate mean attribute values
2245 for (const auto &label_pair: label_masks) {
2246 const auto &mask = label_pair.second;
2247 std::vector<std::vector<bool>> visited(camera_resolution.y, std::vector<bool>(camera_resolution.x, false));
2248
2249 for (int j = 0; j < camera_resolution.y; j++) {
2250 for (int i_px = 0; i_px < camera_resolution.x; i_px++) {
2251 if (mask[j][i_px] && !visited[j][i_px]) {
2252 // Found a new connected component - gather all pixels
2253 std::stack<std::pair<int, int>> stack;
2254 std::vector<std::pair<int, int>> component_pixels;
2255 stack.push({i_px, j});
2256 visited[j][i_px] = true;
2257
2258 while (!stack.empty()) {
2259 auto [ci, cj] = stack.top();
2260 stack.pop();
2261 component_pixels.push_back({ci, cj});
2262
2263 // Check 4-connected neighbors
2264 for (int di = -1; di <= 1; di++) {
2265 for (int dj = -1; dj <= 1; dj++) {
2266 if (abs(di) + abs(dj) != 1)
2267 continue;
2268 int ni = ci + di;
2269 int nj = cj + dj;
2270 if (ni >= 0 && ni < camera_resolution.x && nj >= 0 && nj < camera_resolution.y && mask[nj][ni] && !visited[nj][ni]) {
2271 stack.push({ni, nj});
2272 visited[nj][ni] = true;
2273 }
2274 }
2275 }
2276 }
2277
2278 // Calculate mean attribute values for this component (for all attributes)
2279 std::map<std::string, double> component_attributes;
2280 for (const auto &attr: attribute_info) {
2281 double sum = 0.0;
2282 int count = 0;
2283
2284 for (const auto &[px_i, px_j]: component_pixels) {
2285 uint ii = camera_resolution.x - px_i - 1; // horizontal flip because component_pixels are in mask space
2286 uint UUID = pixel_UUIDs.at(px_j * camera_resolution.x + ii) - 1;
2287
2288 if (context->doesPrimitiveExist(UUID)) {
2289 double value = 0.0;
2290 bool has_value = false;
2291
2292 if (attr.is_primitive_data) {
2293 if (context->doesPrimitiveDataExist(UUID, attr.label.c_str())) {
2294 HeliosDataType datatype = context->getPrimitiveDataType(attr.label.c_str());
2295 if (datatype == HELIOS_TYPE_INT) {
2296 int val;
2297 context->getPrimitiveData(UUID, attr.label.c_str(), val);
2298 value = static_cast<double>(val);
2299 has_value = true;
2300 } else if (datatype == HELIOS_TYPE_UINT) {
2301 uint val;
2302 context->getPrimitiveData(UUID, attr.label.c_str(), val);
2303 value = static_cast<double>(val);
2304 has_value = true;
2305 } else if (datatype == HELIOS_TYPE_FLOAT) {
2306 float val;
2307 context->getPrimitiveData(UUID, attr.label.c_str(), val);
2308 value = static_cast<double>(val);
2309 has_value = true;
2310 } else if (datatype == HELIOS_TYPE_DOUBLE) {
2311 context->getPrimitiveData(UUID, attr.label.c_str(), value);
2312 has_value = true;
2313 }
2314 }
2315 } else {
2316 uint objID = context->getPrimitiveParentObjectID(UUID);
2317 if (objID != 0 && context->doesObjectDataExist(objID, attr.label.c_str())) {
2318 HeliosDataType datatype = context->getObjectDataType(attr.label.c_str());
2319 if (datatype == HELIOS_TYPE_INT) {
2320 int val;
2321 context->getObjectData(objID, attr.label.c_str(), val);
2322 value = static_cast<double>(val);
2323 has_value = true;
2324 } else if (datatype == HELIOS_TYPE_UINT) {
2325 uint val;
2326 context->getObjectData(objID, attr.label.c_str(), val);
2327 value = static_cast<double>(val);
2328 has_value = true;
2329 } else if (datatype == HELIOS_TYPE_FLOAT) {
2330 float val;
2331 context->getObjectData(objID, attr.label.c_str(), val);
2332 value = static_cast<double>(val);
2333 has_value = true;
2334 } else if (datatype == HELIOS_TYPE_DOUBLE) {
2335 context->getObjectData(objID, attr.label.c_str(), value);
2336 has_value = true;
2337 }
2338 }
2339 }
2340
2341 if (has_value) {
2342 sum += value;
2343 count++;
2344 }
2345 }
2346 }
2347
2348 if (count > 0) {
2349 component_attributes[attr.label] = sum / count;
2350 } else {
2351 component_attributes[attr.label] = 0.0; // Default if no valid data
2352 }
2353 }
2354
2355 mean_attribute_values_per_component.push_back(component_attributes);
2356 }
2357 }
2358 }
2359 }
2360 }
2361
2362 // Find the highest existing annotation ID to avoid conflicts
2363 int max_annotation_id = -1;
2364 for (const auto &existing_ann: coco_json["annotations"]) {
2365 if (existing_ann["id"] > max_annotation_id) {
2366 max_annotation_id = existing_ann["id"];
2367 }
2368 }
2369
2370 // Add new annotations for this data label
2371 size_t ann_idx = 0;
2372 for (const auto &ann: annotations) {
2373 nlohmann::json json_annotation;
2374 json_annotation["id"] = max_annotation_id + 1;
2375 json_annotation["image_id"] = (int) ann.at("image_id")[0];
2376 json_annotation["category_id"] = (int) ann.at("category_id")[0];
2377
2378 const auto &bbox = ann.at("bbox");
2379 json_annotation["bbox"] = {(int) bbox[0], (int) bbox[1], (int) bbox[2], (int) bbox[3]};
2380 json_annotation["area"] = (int) ann.at("area")[0];
2381
2382 const auto &seg = ann.at("segmentation");
2383 std::vector<int> segmentation_coords;
2384 for (float coord: seg) {
2385 segmentation_coords.push_back((int) coord);
2386 }
2387 json_annotation["segmentation"] = {segmentation_coords};
2388 json_annotation["iscrowd"] = (int) ann.at("iscrowd")[0];
2389
2390 // Add attributes if requested
2391 if (use_attributes && ann_idx < mean_attribute_values_per_component.size()) {
2392 json_annotation["attributes"] = mean_attribute_values_per_component[ann_idx];
2393 }
2394
2395 coco_json["annotations"].push_back(json_annotation);
2396 max_annotation_id++;
2397 ann_idx++;
2398 }
2399 }
2400
2401 // Write JSON to file
2402 writeCOCOJson(coco_json, outfile);
2403}
2404
2405void RadiationModel::writeImageSegmentationMasks_ObjectData(const std::string &cameralabel, const std::string &object_data_label, const uint &object_class_ID, const std::string &json_filename, const std::string &image_file,
2406 const std::vector<std::string> &data_attribute_labels, bool append_file) {
2407 writeImageSegmentationMasks_ObjectData(cameralabel, std::vector<std::string>{object_data_label}, std::vector<uint>{object_class_ID}, json_filename, image_file, data_attribute_labels, append_file);
2408}
2409
2410void RadiationModel::writeImageSegmentationMasks_ObjectData(const std::string &cameralabel, const std::vector<std::string> &object_data_label, const std::vector<uint> &object_class_ID, const std::string &json_filename, const std::string &image_file,
2411 const std::vector<std::string> &data_attribute_labels, bool append_file) {
2412
2413 if (cameras.find(cameralabel) == cameras.end()) {
2414 helios_runtime_error("ERROR (RadiationModel::writeImageSegmentationMasks_ObjectData): Camera '" + cameralabel + "' does not exist.");
2415 }
2416
2417 if (object_data_label.size() != object_class_ID.size()) {
2418 helios_runtime_error("ERROR (RadiationModel::writeImageSegmentationMasks_ObjectData): The lengths of object_data_label and object_class_ID vectors must be the same.");
2419 }
2420
2421 // Check that camera pixel data exists
2422 std::string global_data_label = "camera_" + cameralabel + "_pixel_UUID";
2423 if (!context->doesGlobalDataExist(global_data_label.c_str())) {
2424 helios_runtime_error("ERROR (RadiationModel::writeImageSegmentationMasks_ObjectData): Pixel labels for camera '" + cameralabel + "' do not exist. Was the radiation model run to generate labels?");
2425 }
2426
2427 // Check that all object data labels exist
2428 std::vector<std::string> all_object_data = context->listAllObjectDataLabels();
2429 for (const auto &data_label: object_data_label) {
2430 if (std::find(all_object_data.begin(), all_object_data.end(), data_label) == all_object_data.end()) {
2431 std::cerr << "WARNING (RadiationModel::writeImageSegmentationMasks_ObjectData): Object data label '" << data_label << "' does not exist in the context." << std::endl;
2432 }
2433 }
2434
2435 // Check that image file exists
2436 if (!std::filesystem::exists(image_file)) {
2437 helios_runtime_error("ERROR (RadiationModel::writeImageSegmentationMasks_ObjectData): Image file '" + image_file + "' does not exist.");
2438 }
2439
2440 // Validate and ensure JSON filename has .json extension
2441 std::string validated_json_filename = json_filename;
2442 if (validated_json_filename.length() < 5 || validated_json_filename.substr(validated_json_filename.length() - 5) != ".json") {
2443 validated_json_filename += ".json";
2444 }
2445
2446 // Use the validated filename directly
2447 std::string outfile = validated_json_filename;
2448
2449 // Write annotations to JSON file
2450 int2 camera_resolution = cameras.at(cameralabel).resolution;
2451 auto coco_json_pair = initializeCOCOJsonWithImageId(outfile, append_file, cameralabel, camera_resolution, image_file);
2452 nlohmann::json coco_json = coco_json_pair.first;
2453 int image_id = coco_json_pair.second;
2454 addCategoryToCOCO(coco_json, object_class_ID, object_data_label);
2455
2456 // Check which data_attribute_labels exist in primitive or object data
2457 struct AttributeInfo {
2458 std::string label;
2459 bool is_primitive_data;
2460 bool exists;
2461 };
2462 std::vector<AttributeInfo> attribute_info;
2463
2464 if (!data_attribute_labels.empty()) {
2465 std::vector<std::string> all_primitive_data = context->listAllPrimitiveDataLabels();
2466 std::vector<std::string> all_object_data = context->listAllObjectDataLabels();
2467
2468 for (const auto &attr_label: data_attribute_labels) {
2469 AttributeInfo info;
2470 info.label = attr_label;
2471 info.exists = false;
2472
2473 if (std::find(all_primitive_data.begin(), all_primitive_data.end(), attr_label) != all_primitive_data.end()) {
2474 info.is_primitive_data = true;
2475 info.exists = true;
2476 } else if (std::find(all_object_data.begin(), all_object_data.end(), attr_label) != all_object_data.end()) {
2477 info.is_primitive_data = false;
2478 info.exists = true;
2479 }
2480
2481 if (info.exists) {
2482 attribute_info.push_back(info);
2483 }
2484 }
2485 }
2486
2487 bool use_attributes = !attribute_info.empty();
2488
2489 // Get pixel UUID data
2490 std::vector<uint> pixel_UUIDs;
2491 std::string pixel_UUID_label = "camera_" + cameralabel + "_pixel_UUID";
2492 context->getGlobalData(pixel_UUID_label.c_str(), pixel_UUIDs);
2493
2494 // Process each data label and class ID pair
2495 for (size_t i = 0; i < object_data_label.size(); ++i) {
2496 // Generate label masks using helper function (object data version)
2497 std::map<int, std::vector<std::vector<bool>>> label_masks = generateLabelMasks(cameralabel, object_data_label[i], true);
2498
2499 // Find the highest existing annotation ID to avoid conflicts
2500 int max_annotation_id = -1;
2501 for (const auto &existing_ann: coco_json["annotations"]) {
2502 if (existing_ann["id"] > max_annotation_id) {
2503 max_annotation_id = existing_ann["id"];
2504 }
2505 }
2506
2507 // Generate annotations from masks and calculate attributes together
2508 // This ensures 1:1 correspondence between annotations and their attributes
2509 for (const auto &label_pair: label_masks) {
2510 const auto &mask = label_pair.second;
2511
2512 // Create a visited mask for connected components
2513 std::vector<std::vector<bool>> visited(camera_resolution.y, std::vector<bool>(camera_resolution.x, false));
2514
2515 // Find all connected components for this label
2516 for (int j = 0; j < camera_resolution.y; j++) {
2517 for (int i_px = 0; i_px < camera_resolution.x; i_px++) {
2518 if (mask[j][i_px] && !visited[j][i_px]) {
2519 // Find boundary pixel for this component
2520 int boundary_i = i_px, boundary_j = j;
2521 bool is_boundary = false;
2522
2523 // Check if this pixel is on the boundary
2524 for (int di = -1; di <= 1; di++) {
2525 for (int dj = -1; dj <= 1; dj++) {
2526 int ni = i_px + di;
2527 int nj = j + dj;
2528 if (ni < 0 || ni >= camera_resolution.x || nj < 0 || nj >= camera_resolution.y || !mask[nj][ni]) {
2529 is_boundary = true;
2530 boundary_i = i_px;
2531 boundary_j = j;
2532 break;
2533 }
2534 }
2535 if (is_boundary)
2536 break;
2537 }
2538
2539 if (is_boundary) {
2540 // First, mark all pixels in this connected component using flood fill
2541 std::stack<std::pair<int, int>> stack;
2542 std::vector<std::pair<int, int>> component_pixels;
2543 stack.push({i_px, j});
2544 visited[j][i_px] = true;
2545
2546 int min_x = i_px, max_x = i_px, min_y = j, max_y = j;
2547 int area = 0;
2548
2549 while (!stack.empty()) {
2550 auto [ci, cj] = stack.top();
2551 stack.pop();
2552 area++;
2553 component_pixels.push_back({ci, cj});
2554
2555 min_x = std::min(min_x, ci);
2556 max_x = std::max(max_x, ci);
2557 min_y = std::min(min_y, cj);
2558 max_y = std::max(max_y, cj);
2559
2560 // Check 4-connected neighbors
2561 for (int di = -1; di <= 1; di++) {
2562 for (int dj = -1; dj <= 1; dj++) {
2563 if (abs(di) + abs(dj) != 1)
2564 continue; // Only 4-connected
2565 int ni = ci + di;
2566 int nj = cj + dj;
2567 if (ni >= 0 && ni < camera_resolution.x && nj >= 0 && nj < camera_resolution.y && mask[nj][ni] && !visited[nj][ni]) {
2568 stack.push({ni, nj});
2569 visited[nj][ni] = true;
2570 }
2571 }
2572 }
2573 }
2574
2575 // Now trace the boundary of this component
2576 auto start_pixel = findStartingBoundaryPixel(mask, camera_resolution);
2577 bool is_boundary_start = false;
2578
2579 if (start_pixel.first >= min_x && start_pixel.first <= max_x && start_pixel.second >= min_y && start_pixel.second <= max_y) {
2580 is_boundary_start = true;
2581 }
2582
2583 if (is_boundary_start) {
2584 // Try Moore neighborhood boundary tracing first
2585 auto contour = traceBoundaryMoore(mask, start_pixel.first, start_pixel.second, camera_resolution);
2586
2587 // If Moore tracing didn't work well, fall back to simple boundary collection
2588 if (contour.size() < 10) {
2589 contour = traceBoundarySimple(mask, start_pixel.first, start_pixel.second, camera_resolution);
2590 }
2591
2592 if (contour.size() >= 3) {
2593 // Calculate mean attribute values for this component (for all attributes)
2594 std::map<std::string, double> component_attributes;
2595 if (use_attributes) {
2596 for (const auto &attr: attribute_info) {
2597 double sum = 0.0;
2598 int count = 0;
2599
2600 for (const auto &[px_i, px_j]: component_pixels) {
2601 uint ii = camera_resolution.x - px_i - 1;
2602 uint UUID = pixel_UUIDs.at(px_j * camera_resolution.x + ii) - 1;
2603
2604 if (context->doesPrimitiveExist(UUID)) {
2605 double value = 0.0;
2606 bool has_value = false;
2607
2608 if (attr.is_primitive_data) {
2609 if (context->doesPrimitiveDataExist(UUID, attr.label.c_str())) {
2610 HeliosDataType datatype = context->getPrimitiveDataType(attr.label.c_str());
2611 if (datatype == HELIOS_TYPE_INT) {
2612 int val;
2613 context->getPrimitiveData(UUID, attr.label.c_str(), val);
2614 value = static_cast<double>(val);
2615 has_value = true;
2616 } else if (datatype == HELIOS_TYPE_UINT) {
2617 uint val;
2618 context->getPrimitiveData(UUID, attr.label.c_str(), val);
2619 value = static_cast<double>(val);
2620 has_value = true;
2621 } else if (datatype == HELIOS_TYPE_FLOAT) {
2622 float val;
2623 context->getPrimitiveData(UUID, attr.label.c_str(), val);
2624 value = static_cast<double>(val);
2625 has_value = true;
2626 } else if (datatype == HELIOS_TYPE_DOUBLE) {
2627 context->getPrimitiveData(UUID, attr.label.c_str(), value);
2628 has_value = true;
2629 }
2630 }
2631 } else {
2632 uint objID = context->getPrimitiveParentObjectID(UUID);
2633 if (objID != 0 && context->doesObjectDataExist(objID, attr.label.c_str())) {
2634 HeliosDataType datatype = context->getObjectDataType(attr.label.c_str());
2635 if (datatype == HELIOS_TYPE_INT) {
2636 int val;
2637 context->getObjectData(objID, attr.label.c_str(), val);
2638 value = static_cast<double>(val);
2639 has_value = true;
2640 } else if (datatype == HELIOS_TYPE_UINT) {
2641 uint val;
2642 context->getObjectData(objID, attr.label.c_str(), val);
2643 value = static_cast<double>(val);
2644 has_value = true;
2645 } else if (datatype == HELIOS_TYPE_FLOAT) {
2646 float val;
2647 context->getObjectData(objID, attr.label.c_str(), val);
2648 value = static_cast<double>(val);
2649 has_value = true;
2650 } else if (datatype == HELIOS_TYPE_DOUBLE) {
2651 context->getObjectData(objID, attr.label.c_str(), value);
2652 has_value = true;
2653 }
2654 }
2655 }
2656
2657 if (has_value) {
2658 sum += value;
2659 count++;
2660 }
2661 }
2662 }
2663
2664 if (count > 0) {
2665 component_attributes[attr.label] = sum / count;
2666 } else {
2667 component_attributes[attr.label] = 0.0; // Default if no valid data
2668 }
2669 }
2670 }
2671
2672 // Create annotation with attributes
2673 nlohmann::json json_annotation;
2674 json_annotation["id"] = max_annotation_id + 1;
2675 json_annotation["image_id"] = image_id;
2676 json_annotation["category_id"] = (int) object_class_ID[i];
2677 json_annotation["bbox"] = {min_x, min_y, max_x - min_x, max_y - min_y};
2678 json_annotation["area"] = area;
2679 json_annotation["iscrowd"] = 0;
2680
2681 // Convert contour to segmentation format (flatten coordinates)
2682 std::vector<int> segmentation_coords;
2683 for (const auto &point: contour) {
2684 segmentation_coords.push_back(point.first); // x coordinate
2685 segmentation_coords.push_back(point.second); // y coordinate
2686 }
2687 json_annotation["segmentation"] = {segmentation_coords};
2688
2689 // Add attributes if requested
2690 if (use_attributes) {
2691 json_annotation["attributes"] = component_attributes;
2692 }
2693
2694 coco_json["annotations"].push_back(json_annotation);
2695 max_annotation_id++;
2696 }
2697 }
2698 } else {
2699 // Mark all pixels in this non-boundary component as visited
2700 std::stack<std::pair<int, int>> stack;
2701 stack.push({i_px, j});
2702 visited[j][i_px] = true;
2703
2704 while (!stack.empty()) {
2705 auto [ci, cj] = stack.top();
2706 stack.pop();
2707
2708 // Check 4-connected neighbors
2709 for (int di = -1; di <= 1; di++) {
2710 for (int dj = -1; dj <= 1; dj++) {
2711 if (abs(di) + abs(dj) != 1)
2712 continue;
2713 int ni = ci + di;
2714 int nj = cj + dj;
2715 if (ni >= 0 && ni < camera_resolution.x && nj >= 0 && nj < camera_resolution.y && mask[nj][ni] && !visited[nj][ni]) {
2716 stack.push({ni, nj});
2717 visited[nj][ni] = true;
2718 }
2719 }
2720 }
2721 }
2722 }
2723 }
2724 }
2725 }
2726 }
2727 }
2728
2729 // Write JSON to file
2730 writeCOCOJson(coco_json, outfile);
2731}
2732
2733void RadiationModel::setPadValue(const std::string &cameralabel, const std::vector<std::string> &bandlabels, const std::vector<float> &padvalues) {
2734 for (uint b = 0; b < bandlabels.size(); b++) {
2735 std::string bandlabel = bandlabels.at(b);
2736
2737 std::string image_value_label = "camera_" + cameralabel + "_" + bandlabel;
2738 std::vector<float> cameradata;
2739 context->getGlobalData(image_value_label.c_str(), cameradata);
2740
2741 std::vector<uint> camera_UUIDs;
2742 std::string image_UUID_label = "camera_" + cameralabel + "_pixel_UUID";
2743 context->getGlobalData(image_UUID_label.c_str(), camera_UUIDs);
2744
2745 for (uint i = 0; i < cameradata.size(); i++) {
2746 uint UUID = camera_UUIDs.at(i) - 1;
2747 if (!context->doesPrimitiveExist(UUID)) {
2748 cameradata.at(i) = padvalues.at(b);
2749 }
2750 }
2751 context->setGlobalData(image_value_label.c_str(), cameradata);
2752 }
2753}
2754
2755void RadiationModel::calibrateCamera(const std::string &originalcameralabel, const std::vector<std::string> &sourcelabels, const std::vector<std::string> &cameraresplabels_raw, const std::vector<std::string> &bandlabels, const float scalefactor,
2756 const std::vector<std::vector<float>> &truevalues, const std::string &calibratedmark) {
2757
2758 if (cameras.find(originalcameralabel) == cameras.end()) {
2759 helios_runtime_error("ERROR (RadiationModel::calibrateCamera): Camera " + originalcameralabel + " does not exist.");
2760 } else if (radiation_sources.empty()) {
2761 helios_runtime_error("ERROR (RadiationModel::calibrateCamera): No radiation sources were added to the radiation model. Cannot perform calibration.");
2762 }
2763
2764 CameraCalibration cameracalibration_(context);
2765 if (!calibration_flag) {
2766 std::cout << "No color board added, use default color calibration." << std::endl;
2767 cameracalibration = &cameracalibration_;
2768 vec3 centrelocation = make_vec3(0, 0, 0.2); // Location of color board
2769 vec3 rotationrad = make_vec3(0, 0, 1.5705); // Rotation angle of color board
2770 cameracalibration->addDefaultColorboard(centrelocation, 0.1, rotationrad);
2771 }
2772 vec2 wavelengthrange = make_vec2(-10000, 10000);
2773
2774 // Calibrated camera response labels
2775 std::vector<std::string> cameraresplabels_cal(cameraresplabels_raw.size());
2776
2777 for (int iband = 0; iband < bandlabels.size(); iband++) {
2778 cameraresplabels_cal.at(iband) = calibratedmark + "_" + cameraresplabels_raw.at(iband);
2779 }
2780
2781 RadiationModel::runRadiationImaging(originalcameralabel, sourcelabels, bandlabels, cameraresplabels_raw, wavelengthrange, 1, 0);
2782 // Update camera responses
2783 RadiationModel::updateCameraResponse(originalcameralabel, sourcelabels, cameraresplabels_raw, wavelengthrange, truevalues, calibratedmark);
2784
2785 float camerascale = RadiationModel::getCameraResponseScale(originalcameralabel, cameraresplabels_cal, bandlabels, sourcelabels, wavelengthrange, truevalues);
2786
2787 std::cout << "Camera response scale: " << camerascale << std::endl;
2788 // Scale and write calibrated camera responses
2789 cameracalibration->writeCalibratedCameraResponses(cameraresplabels_raw, calibratedmark, camerascale * scalefactor);
2790}
2791
2792void RadiationModel::calibrateCamera(const std::string &originalcameralabel, const float scalefactor, const std::vector<std::vector<float>> &truevalues, const std::string &calibratedmark) {
2793
2794 if (cameras.find(originalcameralabel) == cameras.end()) {
2795 helios_runtime_error("ERROR (RadiationModel::calibrateCamera): Camera " + originalcameralabel + " does not exist.");
2796 } else if (radiation_sources.empty()) {
2797 helios_runtime_error("ERROR (RadiationModel::calibrateCamera): No radiation sources were added to the radiation model. Cannot perform calibration.");
2798 }
2799
2800 CameraCalibration cameracalibration_(context);
2801 if (!calibration_flag) {
2802 std::cout << "No color board added, use default color calibration." << std::endl;
2803 vec3 centrelocation = make_vec3(0, 0, 0.2); // Location of color board
2804 vec3 rotationrad = make_vec3(0, 0, 1.5705); // Rotation angle of color board
2805 cameracalibration_.addDefaultColorboard(centrelocation, 0.1, rotationrad);
2806 RadiationModel::setCameraCalibration(&cameracalibration_);
2807 }
2808
2809 vec2 wavelengthrange = make_vec2(-10000, 10000);
2810
2811 std::vector<std::string> bandlabels = cameras.at(originalcameralabel).band_labels;
2812
2813 // Get camera response spectra labels from camera
2814 std::vector<std::string> cameraresplabels_cal(cameras.at(originalcameralabel).band_spectral_response.size());
2815 std::vector<std::string> cameraresplabels_raw = cameraresplabels_cal;
2816
2817 int iband = 0;
2818 for (auto &band: cameras.at(originalcameralabel).band_spectral_response) {
2819 cameraresplabels_raw.at(iband) = band.second;
2820 cameraresplabels_cal.at(iband) = calibratedmark + "_" + band.second;
2821 iband++;
2822 }
2823
2824 // Get labels of radiation sources from camera
2825 std::vector<std::string> sourcelabels(radiation_sources.size());
2826 int isource = 0;
2827 for (auto &source: radiation_sources) {
2828 if (source.source_spectrum.empty()) {
2829 helios_runtime_error("ERROR (RadiationModel::calibrateCamera): A spectral distribution was not specified for source " + source.source_spectrum_label + ". Cannot perform camera calibration.");
2830 }
2831 sourcelabels.at(isource) = source.source_spectrum_label;
2832 isource++;
2833 }
2834
2836 RadiationModel::runBand(bandlabels);
2837 // Update camera responses
2838 RadiationModel::updateCameraResponse(originalcameralabel, sourcelabels, cameraresplabels_raw, wavelengthrange, truevalues, calibratedmark);
2839
2840 float camerascale = RadiationModel::getCameraResponseScale(originalcameralabel, cameraresplabels_cal, bandlabels, sourcelabels, wavelengthrange, truevalues);
2841
2842 std::cout << "Camera response scale: " << camerascale << std::endl;
2843 // Scale and write calibrated camera responses
2844 cameracalibration->writeCalibratedCameraResponses(cameraresplabels_raw, calibratedmark, camerascale * scalefactor);
2845}
2846
2847std::vector<helios::vec2> RadiationModel::generateGaussianCameraResponse(float FWHM, float mu, float centrawavelength, const helios::int2 &wavebandrange) {
2848
2849 // Convert FWHM to sigma
2850 float sigma = FWHM / (2 * std::sqrt(2 * std::log(2)));
2851
2852 size_t lenspectra = wavebandrange.y - wavebandrange.x;
2853 std::vector<helios::vec2> cameraresponse(lenspectra);
2854
2855
2856 for (int i = 0; i < lenspectra; ++i) {
2857 cameraresponse.at(i).x = float(wavebandrange.x + i);
2858 }
2859
2860 // Gaussian function
2861 for (size_t i = 0; i < lenspectra; ++i) {
2862 cameraresponse.at(i).y = centrawavelength * std::exp(-std::pow((cameraresponse.at(i).x - mu), 2) / (2 * std::pow(sigma, 2)));
2863 }
2864
2865
2866 return cameraresponse;
2867}
2868
2869void RadiationModel::applyCameraImageCorrections(const std::string &cameralabel, const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float saturation_adjustment, float brightness_adjustment,
2870 float contrast_adjustment) {
2871
2872 if (cameras.find(cameralabel) == cameras.end()) {
2873 helios_runtime_error("ERROR (RadiationModel::applyCameraImageCorrections): Camera '" + cameralabel + "' does not exist.");
2874 }
2875 RadiationCamera &camera = cameras.at(cameralabel);
2876 if (camera.pixel_data.find(red_band_label) == camera.pixel_data.end() || camera.pixel_data.find(green_band_label) == camera.pixel_data.end() || camera.pixel_data.find(blue_band_label) == camera.pixel_data.end()) {
2877 helios_runtime_error("ERROR (RadiationModel::applyCameraImageCorrections): One or more specified band labels do not exist for the camera pixel data.");
2878 }
2879
2880 // Store parameters for metadata output
2881 if (camera_metadata.find(cameralabel) == camera_metadata.end()) {
2882 camera_metadata[cameralabel] = CameraMetadata();
2883 }
2884 camera_metadata[cameralabel].image_processing.saturation_adjustment = saturation_adjustment;
2885 camera_metadata[cameralabel].image_processing.brightness_adjustment = brightness_adjustment;
2886 camera_metadata[cameralabel].image_processing.contrast_adjustment = contrast_adjustment;
2887
2888 // NOTE: Auto-exposure is now automatically applied during rendering based on camera exposure setting
2889 // NOTE: White balance is now automatically applied during rendering based on camera white_balance setting
2890 // NOTE: sRGB gamma compression is now applied during image export in writeCameraImage()
2891
2892 // Step 0: Apply lens flare effect if enabled (before other adjustments)
2893 if (camera.lens_flare_enabled) {
2894 LensFlare lens_flare(camera.lens_flare_properties, camera.resolution);
2895 lens_flare.apply(camera.pixel_data, camera.resolution);
2896 }
2897
2898 // Step 1: Brightness and contrast adjustments in linear space
2899 if (brightness_adjustment != 1.f || contrast_adjustment != 1.f) {
2900 camera.adjustBrightnessContrast(red_band_label, green_band_label, blue_band_label, brightness_adjustment, contrast_adjustment);
2901 }
2902
2903 // Step 2: Saturation adjustment
2904 if (saturation_adjustment != 1.f) {
2905 camera.adjustSaturation(red_band_label, green_band_label, blue_band_label, saturation_adjustment);
2906 }
2907}
2908
2909void RadiationModel::applyImageProcessingPipeline(const std::string &cameralabel, const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float saturation_adjustment, float brightness_adjustment,
2910 float contrast_adjustment, float gain_adjustment) {
2911 applyCameraImageCorrections(cameralabel, red_band_label, green_band_label, blue_band_label, saturation_adjustment, brightness_adjustment, contrast_adjustment);
2912}
2913
2915
2916 float min_P = (std::numeric_limits<float>::max)();
2917 float max_P = 0.0f;
2918 for (const auto &[channel_label, data]: pixel_data) {
2919 for (float v: data) {
2920 if (v < min_P) {
2921 min_P = v;
2922 }
2923 if (v > max_P) {
2924 max_P = v;
2925 }
2926 }
2927 }
2928
2929 for (auto &[channel_label, data]: pixel_data) {
2930 for (float &v: data) {
2931 v = (v - min_P) / (max_P - min_P); // Normalize to [0, 1]
2932 }
2933 }
2934}
2935
2936void RadiationCamera::whiteBalance(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float p) {
2937
2938#ifdef HELIOS_DEBUG
2939 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
2940 helios_runtime_error("ERROR (RadiationModel::whiteBalance): One or more specified band labels do not exist for the camera pixel data.");
2941 }
2942#endif
2943
2944 auto &data_red = pixel_data.at(red_band_label);
2945 auto &data_green = pixel_data.at(green_band_label);
2946 auto &data_blue = pixel_data.at(blue_band_label);
2947
2948 const std::size_t N = data_red.size();
2949 if (data_green.size() != N || data_blue.size() != N) {
2950 throw std::invalid_argument("All channels must have the same length");
2951 }
2952 if (p < 1.0f) {
2953 throw std::invalid_argument("Minkowski exponent p must satisfy p >= 1");
2954 }
2955
2956 // Compute Minkowski means:
2957 // \[ M_R = \Bigl(\frac{1}{N}\sum_{i=1}^{N}R_i^p\Bigr)^{1/p},\quad
2958 // M_G = \Bigl(\frac{1}{N}\sum_{i=1}^{N}G_i^p\Bigr)^{1/p},\quad
2959 // M_B = \Bigl(\frac{1}{N}\sum_{i=1}^{N}B_i^p\Bigr)^{1/p} \]
2960 float acc_r = 0.0f, acc_g = 0.0f, acc_b = 0.0f;
2961 for (std::size_t i = 0; i < N; ++i) {
2962 acc_r += std::pow(data_red[i], p);
2963 acc_g += std::pow(data_green[i], p);
2964 acc_b += std::pow(data_blue[i], p);
2965 }
2966 float mean_r_p = acc_r / static_cast<float>(N);
2967 float mean_g_p = acc_g / static_cast<float>(N);
2968 float mean_b_p = acc_b / static_cast<float>(N);
2969
2970 float M_R = std::pow(mean_r_p, 1.0f / p);
2971 float M_G = std::pow(mean_g_p, 1.0f / p);
2972 float M_B = std::pow(mean_b_p, 1.0f / p);
2973
2974 // Avoid division by zero
2975 const float eps = 1e-6f;
2976 if (M_R < eps || M_G < eps || M_B < eps) {
2977 throw std::runtime_error("Channel Minkowski mean too small");
2978 }
2979
2980 // Compute gray reference:
2981 // \[ M = \frac{M_R + M_G + M_B}{3} \]
2982 float M = (M_R + M_G + M_B) / 3.0f;
2983
2984 // Derive per-channel gains:
2985 // \[ s_R = M / M_R,\quad s_G = M / M_G,\quad s_B = M / M_B \]
2986 helios::vec3 scale;
2987 scale.x = M / M_R;
2988 scale.y = M / M_G;
2989 scale.z = M / M_B;
2990
2991 // Apply gains to each pixel:
2992 // \[ R'_i = s_R\,R_i,\quad G'_i = s_G\,G_i,\quad B'_i = s_B\,B_i \]
2993 for (std::size_t i = 0; i < N; ++i) {
2994 data_red[i] *= scale.x;
2995 data_green[i] *= scale.y;
2996 data_blue[i] *= scale.z;
2997 }
2998}
2999
3000void RadiationCamera::whiteBalanceGrayEdge(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, int derivative_order, float p) {
3001
3002#ifdef HELIOS_DEBUG
3003 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3004 helios_runtime_error("ERROR (RadiationModel::whiteBalanceGrayEdge): One or more specified band labels do not exist for the camera pixel data.");
3005 }
3006#endif
3007
3008 auto &data_red = pixel_data.at(red_band_label);
3009 auto &data_green = pixel_data.at(green_band_label);
3010 auto &data_blue = pixel_data.at(blue_band_label);
3011
3012 const int width = resolution.x;
3013 const int height = resolution.y;
3014 const std::size_t N = width * height;
3015
3016 if (p < 1.0f) {
3017 throw std::invalid_argument("Minkowski exponent p must satisfy p >= 1");
3018 }
3019 if (derivative_order < 1 || derivative_order > 2) {
3020 throw std::invalid_argument("Derivative order must be 1 or 2");
3021 }
3022
3023 // Compute derivatives using simple finite differences
3024 std::vector<float> deriv_red(N, 0.0f);
3025 std::vector<float> deriv_green(N, 0.0f);
3026 std::vector<float> deriv_blue(N, 0.0f);
3027
3028 if (derivative_order == 1) {
3029 // First-order derivatives (gradient magnitude)
3030 for (int y = 1; y < height - 1; ++y) {
3031 for (int x = 1; x < width - 1; ++x) {
3032 int idx = y * width + x;
3033
3034 // Sobel operator for gradient estimation
3035 float dx_r = (data_red[(y - 1) * width + (x + 1)] + 2 * data_red[y * width + (x + 1)] + data_red[(y + 1) * width + (x + 1)]) -
3036 (data_red[(y - 1) * width + (x - 1)] + 2 * data_red[y * width + (x - 1)] + data_red[(y + 1) * width + (x - 1)]) / 8.0f;
3037 float dy_r = (data_red[(y + 1) * width + (x - 1)] + 2 * data_red[(y + 1) * width + x] + data_red[(y + 1) * width + (x + 1)]) -
3038 (data_red[(y - 1) * width + (x - 1)] + 2 * data_red[(y - 1) * width + x] + data_red[(y - 1) * width + (x + 1)]) / 8.0f;
3039 deriv_red[idx] = std::sqrt(dx_r * dx_r + dy_r * dy_r);
3040
3041 float dx_g = (data_green[(y - 1) * width + (x + 1)] + 2 * data_green[y * width + (x + 1)] + data_green[(y + 1) * width + (x + 1)]) -
3042 (data_green[(y - 1) * width + (x - 1)] + 2 * data_green[y * width + (x - 1)] + data_green[(y + 1) * width + (x - 1)]) / 8.0f;
3043 float dy_g = (data_green[(y + 1) * width + (x - 1)] + 2 * data_green[(y + 1) * width + x] + data_green[(y + 1) * width + (x + 1)]) -
3044 (data_green[(y - 1) * width + (x - 1)] + 2 * data_green[(y - 1) * width + x] + data_green[(y - 1) * width + (x + 1)]) / 8.0f;
3045 deriv_green[idx] = std::sqrt(dx_g * dx_g + dy_g * dy_g);
3046
3047 float dx_b = (data_blue[(y - 1) * width + (x + 1)] + 2 * data_blue[y * width + (x + 1)] + data_blue[(y + 1) * width + (x + 1)]) -
3048 (data_blue[(y - 1) * width + (x - 1)] + 2 * data_blue[y * width + (x - 1)] + data_blue[(y + 1) * width + (x - 1)]) / 8.0f;
3049 float dy_b = (data_blue[(y + 1) * width + (x - 1)] + 2 * data_blue[(y + 1) * width + x] + data_blue[(y + 1) * width + (x + 1)]) -
3050 (data_blue[(y - 1) * width + (x - 1)] + 2 * data_blue[(y - 1) * width + x] + data_blue[(y - 1) * width + (x + 1)]) / 8.0f;
3051 deriv_blue[idx] = std::sqrt(dx_b * dx_b + dy_b * dy_b);
3052 }
3053 }
3054 } else {
3055 // Second-order derivatives (Laplacian)
3056 for (int y = 1; y < height - 1; ++y) {
3057 for (int x = 1; x < width - 1; ++x) {
3058 int idx = y * width + x;
3059
3060 deriv_red[idx] = std::abs(data_red[(y - 1) * width + x] + data_red[(y + 1) * width + x] + data_red[y * width + (x - 1)] + data_red[y * width + (x + 1)] - 4 * data_red[idx]);
3061
3062 deriv_green[idx] = std::abs(data_green[(y - 1) * width + x] + data_green[(y + 1) * width + x] + data_green[y * width + (x - 1)] + data_green[y * width + (x + 1)] - 4 * data_green[idx]);
3063
3064 deriv_blue[idx] = std::abs(data_blue[(y - 1) * width + x] + data_blue[(y + 1) * width + x] + data_blue[y * width + (x - 1)] + data_blue[y * width + (x + 1)] - 4 * data_blue[idx]);
3065 }
3066 }
3067 }
3068
3069 // Compute Minkowski means of derivatives
3070 float acc_r = 0.0f, acc_g = 0.0f, acc_b = 0.0f;
3071 int valid_pixels = 0;
3072
3073 for (std::size_t i = 0; i < N; ++i) {
3074 if (deriv_red[i] > 0 || deriv_green[i] > 0 || deriv_blue[i] > 0) {
3075 acc_r += std::pow(deriv_red[i], p);
3076 acc_g += std::pow(deriv_green[i], p);
3077 acc_b += std::pow(deriv_blue[i], p);
3078 valid_pixels++;
3079 }
3080 }
3081
3082 if (valid_pixels == 0) {
3083 // No edges detected, fall back to standard white balance
3084 whiteBalance(red_band_label, green_band_label, blue_band_label, p);
3085 return;
3086 }
3087
3088 float mean_r_p = acc_r / static_cast<float>(valid_pixels);
3089 float mean_g_p = acc_g / static_cast<float>(valid_pixels);
3090 float mean_b_p = acc_b / static_cast<float>(valid_pixels);
3091
3092 float M_R = std::pow(mean_r_p, 1.0f / p);
3093 float M_G = std::pow(mean_g_p, 1.0f / p);
3094 float M_B = std::pow(mean_b_p, 1.0f / p);
3095
3096 // Avoid division by zero
3097 const float eps = 1e-6f;
3098 if (M_R < eps || M_G < eps || M_B < eps) {
3099 // Fall back to standard white balance
3100 whiteBalance(red_band_label, green_band_label, blue_band_label, p);
3101 return;
3102 }
3103
3104 // Compute gray reference
3105 float M = (M_R + M_G + M_B) / 3.0f;
3106
3107 // Derive per-channel gains
3108 helios::vec3 scale;
3109 scale.x = M / M_R;
3110 scale.y = M / M_G;
3111 scale.z = M / M_B;
3112
3113 // Apply gains to each pixel
3114 for (std::size_t i = 0; i < N; ++i) {
3115 data_red[i] *= scale.x;
3116 data_green[i] *= scale.y;
3117 data_blue[i] *= scale.z;
3118 }
3119}
3120
3121void RadiationCamera::whiteBalanceWhitePatch(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float percentile) {
3122
3123#ifdef HELIOS_DEBUG
3124 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3125 helios_runtime_error("ERROR (RadiationModel::whiteBalanceWhitePatch): One or more specified band labels do not exist for the camera pixel data.");
3126 }
3127#endif
3128
3129 if (percentile <= 0.0f || percentile > 1.0f) {
3130 throw std::invalid_argument("Percentile must be in range (0, 1]");
3131 }
3132
3133 auto &data_red = pixel_data.at(red_band_label);
3134 auto &data_green = pixel_data.at(green_band_label);
3135 auto &data_blue = pixel_data.at(blue_band_label);
3136
3137 const std::size_t N = data_red.size();
3138
3139 // Find the percentile values for each channel
3140 std::vector<float> sorted_red = data_red;
3141 std::vector<float> sorted_green = data_green;
3142 std::vector<float> sorted_blue = data_blue;
3143
3144 std::size_t k = static_cast<std::size_t>(percentile * (N - 1));
3145
3146 std::nth_element(sorted_red.begin(), sorted_red.begin() + k, sorted_red.end());
3147 std::nth_element(sorted_green.begin(), sorted_green.begin() + k, sorted_green.end());
3148 std::nth_element(sorted_blue.begin(), sorted_blue.begin() + k, sorted_blue.end());
3149
3150 float white_r = sorted_red[k];
3151 float white_g = sorted_green[k];
3152 float white_b = sorted_blue[k];
3153
3154 // Avoid division by zero
3155 const float eps = 1e-6f;
3156 if (white_r < eps || white_g < eps || white_b < eps) {
3157 throw std::runtime_error("White patch values too small");
3158 }
3159
3160 // Apply gains to normalize to white
3161 for (std::size_t i = 0; i < N; ++i) {
3162 data_red[i] /= white_r;
3163 data_green[i] /= white_g;
3164 data_blue[i] /= white_b;
3165 }
3166}
3167
3168
3169void RadiationCamera::whiteBalanceSpectral(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, helios::Context *context) {
3170
3171#ifdef HELIOS_DEBUG
3172 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3173 helios_runtime_error("ERROR (RadiationCamera::whiteBalanceSpectral): One or more specified band labels do not exist for the camera pixel data.");
3174 }
3175#endif
3176
3177 // Check if spectral response data exists for all bands
3178 if (band_spectral_response.find(red_band_label) == band_spectral_response.end() || band_spectral_response.find(green_band_label) == band_spectral_response.end() || band_spectral_response.find(blue_band_label) == band_spectral_response.end()) {
3179 helios_runtime_error("ERROR (RadiationCamera::whiteBalanceSpectral): Spectral response data not found for one or more bands. Ensure camera spectral responses are properly initialized.");
3180 }
3181
3182 // Get spectral response identifiers
3183 std::string red_response_id = band_spectral_response.at(red_band_label);
3184 std::string green_response_id = band_spectral_response.at(green_band_label);
3185 std::string blue_response_id = band_spectral_response.at(blue_band_label);
3186
3187 // Skip if using uniform response (cannot apply spectral white balance)
3188 if (red_response_id == "uniform" && green_response_id == "uniform" && blue_response_id == "uniform") {
3189 return;
3190 }
3191
3192 // Access spectral response data from global data (assuming vec2 format: wavelength, response)
3193 std::vector<helios::vec2> red_spectrum, green_spectrum, blue_spectrum;
3194
3195 if (red_response_id != "uniform" && context->doesGlobalDataExist(red_response_id.c_str())) {
3196 context->getGlobalData(red_response_id.c_str(), red_spectrum);
3197 }
3198 if (green_response_id != "uniform" && context->doesGlobalDataExist(green_response_id.c_str())) {
3199 context->getGlobalData(green_response_id.c_str(), green_spectrum);
3200 }
3201 if (blue_response_id != "uniform" && context->doesGlobalDataExist(blue_response_id.c_str())) {
3202 context->getGlobalData(blue_response_id.c_str(), blue_spectrum);
3203 }
3204
3205 // Verify we have spectral data for all channels
3206 if (red_spectrum.empty() || green_spectrum.empty() || blue_spectrum.empty()) {
3207 helios_runtime_error("ERROR (RadiationCamera::whiteBalanceSpectral): Could not retrieve spectral response curves for all bands from global data.");
3208 }
3209
3210 // Compute integrated response (area under curve) for each channel using trapezoidal integration
3211 // This represents the total sensitivity of each channel assuming a flat light source spectrum
3212 float red_integrated = 0.0f, green_integrated = 0.0f, blue_integrated = 0.0f;
3213
3214 for (size_t i = 1; i < red_spectrum.size(); ++i) {
3215 float dw = red_spectrum[i].x - red_spectrum[i - 1].x;
3216 red_integrated += 0.5f * (red_spectrum[i].y + red_spectrum[i - 1].y) * dw;
3217 }
3218 for (size_t i = 1; i < green_spectrum.size(); ++i) {
3219 float dw = green_spectrum[i].x - green_spectrum[i - 1].x;
3220 green_integrated += 0.5f * (green_spectrum[i].y + green_spectrum[i - 1].y) * dw;
3221 }
3222 for (size_t i = 1; i < blue_spectrum.size(); ++i) {
3223 float dw = blue_spectrum[i].x - blue_spectrum[i - 1].x;
3224 blue_integrated += 0.5f * (blue_spectrum[i].y + blue_spectrum[i - 1].y) * dw;
3225 }
3226
3227 // Check for valid integrated values
3228 if (red_integrated <= 0 || green_integrated <= 0 || blue_integrated <= 0) {
3229 helios_runtime_error("ERROR (RadiationCamera::whiteBalanceSpectral): Invalid integrated spectral response (non-positive value). Check spectral response data.");
3230 }
3231
3232 // Compute white balance factors relative to each channel's integrated spectral response
3233 // Normalize relative to the maximum integrated response to preserve brightness
3234 // This ensures that an object with flat spectral reflectance appears correctly white balanced
3235 // while keeping the brightest channel at unity gain (factor = 1.0)
3236 float max_integrated = std::max({red_integrated, green_integrated, blue_integrated});
3237
3238 helios::vec3 white_balance_factors;
3239 white_balance_factors.x = max_integrated / red_integrated;
3240 white_balance_factors.y = max_integrated / green_integrated;
3241 white_balance_factors.z = max_integrated / blue_integrated;
3242
3243 // Apply white balance factors to pixel data
3244 auto &data_red = pixel_data.at(red_band_label);
3245 auto &data_green = pixel_data.at(green_band_label);
3246 auto &data_blue = pixel_data.at(blue_band_label);
3247
3248 const std::size_t N = data_red.size();
3249 for (std::size_t i = 0; i < N; ++i) {
3250 data_red[i] *= white_balance_factors.x;
3251 data_green[i] *= white_balance_factors.y;
3252 data_blue[i] *= white_balance_factors.z;
3253 }
3254}
3255
3256void RadiationCamera::reinhardToneMapping(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label) {
3257
3258#ifdef HELIOS_DEBUG
3259 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3260 helios_runtime_error("ERROR (RadiationModel::reinhardToneMapping): One or more specified band labels do not exist for the camera pixel data.");
3261 }
3262#endif
3263
3264 const std::size_t N = resolution.x * resolution.y;
3265 constexpr float eps = 1e-6f;
3266
3267 auto &data_red = pixel_data.at(red_band_label);
3268 auto &data_green = pixel_data.at(green_band_label);
3269 auto &data_blue = pixel_data.at(blue_band_label);
3270 for (std::size_t i = 0; i < N; ++i) {
3271 float R = data_red[i], G = data_green[i], B = data_blue[i];
3272 float L = luminance(R, G, B);
3273 float s = (L > eps) ? (L / (1.0f + L)) / L : 0.0f;
3274
3275 data_red[i] = R * s;
3276 data_green[i] = G * s;
3277 data_blue[i] = B * s;
3278 }
3279}
3280
3281void RadiationCamera::applyGain(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float percentile) {
3282
3283#ifdef HELIOS_DEBUG
3284 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3285 helios_runtime_error("ERROR (RadiationModel::applyGain): One or more specified band labels do not exist for the camera pixel data.");
3286 }
3287#endif
3288
3289 const std::size_t N = resolution.x * resolution.y;
3290
3291 auto &data_red = pixel_data.at(red_band_label);
3292 auto &data_green = pixel_data.at(green_band_label);
3293 auto &data_blue = pixel_data.at(blue_band_label);
3294
3295 std::vector<float> luminance_pixel;
3296 luminance_pixel.reserve(N);
3297 for (std::size_t i = 0; i < N; ++i) {
3298 luminance_pixel.push_back(luminance(data_red[i], data_green[i], data_blue[i]));
3299 }
3300
3301 std::size_t k = std::size_t(percentile * (luminance_pixel.size() - 1));
3302 std::nth_element(luminance_pixel.begin(), luminance_pixel.begin() + k, luminance_pixel.end());
3303 float peak = luminance_pixel[k];
3304 float gain = (peak > 0.0f) ? 1.0f / peak : 1.0f;
3305
3306 for (auto &[channel, data]: pixel_data) {
3307 for (float &v: data) {
3308 v *= gain;
3309 }
3310 }
3311}
3312
3313void RadiationCamera::globalHistogramEqualization(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label) {
3314
3315#ifdef HELIOS_DEBUG
3316 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3317 helios_runtime_error("ERROR (RadiationModel::globalHistogramEquilization): One or more specified band labels do not exist for the camera pixel data.");
3318 }
3319#endif
3320
3321 const size_t N = resolution.x * resolution.y;
3322 const float eps = 1e-6f;
3323
3324 auto &data_red = pixel_data.at(red_band_label);
3325 auto &data_green = pixel_data.at(green_band_label);
3326 auto &data_blue = pixel_data.at(blue_band_label);
3327
3328 /* luminance array and store original chromaticity */
3329 std::vector<float> lum(N);
3330 std::vector<float> chroma_r(N), chroma_g(N), chroma_b(N);
3331
3332 for (size_t i = 0; i < N; ++i) {
3333 vec3 p(data_red[i], data_green[i], data_blue[i]);
3334 lum[i] = 0.2126f * p.x + 0.7152f * p.y + 0.0722f * p.z;
3335
3336 // Store chromaticity ratios (color information)
3337 if (lum[i] > eps) {
3338 chroma_r[i] = p.x / lum[i];
3339 chroma_g[i] = p.y / lum[i];
3340 chroma_b[i] = p.z / lum[i];
3341 } else {
3342 chroma_r[i] = 1.0f;
3343 chroma_g[i] = 1.0f;
3344 chroma_b[i] = 1.0f;
3345 }
3346 }
3347
3348 /* build CDF on 2048-bin histogram */
3349 const int B = 2048;
3350 std::vector<int> hist(B, 0);
3351 for (float v: lum) {
3352 int b = int(std::clamp(v, 0.0f, 1.0f - eps) * B);
3353 if (b >= 0 && b < 2048) {
3354 hist[b]++;
3355 }
3356 }
3357 std::vector<float> cdf(B);
3358 int acc = 0;
3359 for (int b = 0; b < B; ++b) {
3360 acc += hist[b];
3361 cdf[b] = float(acc) / float(N);
3362 }
3363
3364 /* remap - only adjust luminance, preserve chromaticity */
3365 for (size_t i = 0; i < N; ++i) {
3366 // Handle bright pixels (> 1.0) specially
3367 if (lum[i] >= 1.0f) {
3368 data_red[i] = std::min(1.0f, data_red[i]);
3369 data_green[i] = std::min(1.0f, data_green[i]);
3370 data_blue[i] = std::min(1.0f, data_blue[i]);
3371 continue;
3372 }
3373
3374 int b = int(std::clamp(lum[i], 0.0f, 1.0f - eps) * B);
3375
3376 if (b < 0 || b >= 2048) {
3377 continue;
3378 }
3379
3380 constexpr float k = 0.2f; // how far to pull towards equalised value (0.2–0.3 OK)
3381 constexpr float cs = 0.2f; // S-curve strength (0.4–0.7 recommended)
3382
3383 float Yeq = cdf[b]; // equalised luminance ∈[0,1]
3384 float Ynew = (1.0f - k) * lum[i] + k * Yeq; // partial equalisation
3385
3386 /* symmetric S-curve centred at 0.5 : y = ½ + (x–½)*(1+cs–2·cs·|x–½|) */
3387 float t = Ynew - 0.5f;
3388 Ynew = 0.5f + t * (1.0f + cs - 2.0f * cs * std::fabs(t));
3389
3390 // Reconstruct RGB using new luminance but original chromaticity
3391 data_red[i] = Ynew * chroma_r[i];
3392 data_green[i] = Ynew * chroma_g[i];
3393 data_blue[i] = Ynew * chroma_b[i];
3394 }
3395}
3396
3397void RadiationCamera::adjustSBC(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float saturation, float brightness, float contrast) {
3398#ifdef HELIOS_DEBUG
3399 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3400 helios_runtime_error("ERROR (RadiationModel::adjustSBC): One or more specified band labels do not exist for the camera pixel data.");
3401 }
3402#endif
3403
3404 constexpr float kRedW = 0.2126f;
3405 constexpr float kGreenW = 0.7152f;
3406 constexpr float kBlueW = 0.0722f;
3407
3408 const size_t N = resolution.x * resolution.y;
3409
3410 auto &data_red = pixel_data.at(red_band_label);
3411 auto &data_green = pixel_data.at(green_band_label);
3412 auto &data_blue = pixel_data.at(blue_band_label);
3413
3414 for (int i = 0; i < N; ++i) {
3415
3416 helios::vec3 p(data_red[i], data_green[i], data_blue[i]);
3417
3418 /* ----- 1. luminance ----- */
3419 float Y = kRedW * p.x + kGreenW * p.y + kBlueW * p.z;
3420
3421 /* ----- 2. saturation ----- */
3422 p = helios::vec3(Y, Y, Y) + (p - helios::vec3(Y, Y, Y)) * saturation;
3423
3424 /* ----- 3. brightness (gain) ----- */
3425 p *= brightness;
3426
3427 /* ----- 4. contrast ----- */
3428 p = (p - helios::vec3(0.5f, 0.5f, 0.5f)) * contrast + helios::vec3(0.5f, 0.5f, 0.5f);
3429
3430 /* ----- 5. clamp to valid range ----- */
3431 data_red[i] = clamp(p.x, 0.0f, 1.0f);
3432 data_green[i] = clamp(p.y, 0.0f, 1.0f);
3433 data_blue[i] = clamp(p.z, 0.0f, 1.0f);
3434 }
3435}
3436
3437// void RadiationCamera::applyCCM(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label) {
3438//
3439// const std::size_t N = resolution.x * resolution.y;
3440// auto &data_red = pixel_data.at(red_band_label);
3441// auto &data_green = pixel_data.at(green_band_label);
3442// auto &data_blue = pixel_data.at(blue_band_label);
3443// for (std::size_t i = 0; i < N; ++i) {
3444// float R = data_red[i], G = data_green[i], B = data_blue[i];
3445// data_red[i] = color_correction_matrix[0] * R + color_correction_matrix[1] * G + color_correction_matrix[2] * B + color_correction_matrix[9];
3446// data_green[i] = color_correction_matrix[3] * R + color_correction_matrix[4] * G + color_correction_matrix[5] * B + color_correction_matrix[10];
3447// data_blue[i] = color_correction_matrix[6] * R + color_correction_matrix[7] * G + color_correction_matrix[8] * B + color_correction_matrix[11];
3448// }
3449// }
3450
3451void RadiationCamera::gammaCompress(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label) {
3452
3453#ifdef HELIOS_DEBUG
3454 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3455 helios_runtime_error("ERROR (RadiationModel::gammaCompress): One or more specified band labels do not exist for the camera pixel data.");
3456 }
3457#endif
3458
3459 for (float &v: pixel_data.at(red_band_label)) {
3460 v = lin_to_srgb(std::fmaxf(0.0f, v));
3461 }
3462 for (float &v: pixel_data.at(green_band_label)) {
3463 v = lin_to_srgb(std::fmaxf(0.0f, v));
3464 }
3465 for (float &v: pixel_data.at(blue_band_label)) {
3466 v = lin_to_srgb(std::fmaxf(0.0f, v));
3467 }
3468}
3469
3470// New methods for improved image processing pipeline
3471
3472void RadiationCamera::autoExposure(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float gain_multiplier) {
3473#ifdef HELIOS_DEBUG
3474 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3475 helios_runtime_error("ERROR (RadiationModel::autoExposure): One or more specified band labels do not exist for the camera pixel data.");
3476 }
3477#endif
3478
3479 auto &data_red = pixel_data.at(red_band_label);
3480 auto &data_green = pixel_data.at(green_band_label);
3481 auto &data_blue = pixel_data.at(blue_band_label);
3482
3483 const std::size_t N = data_red.size();
3484
3485 // Calculate luminance for each pixel
3486 std::vector<float> luminance_values(N);
3487 for (std::size_t i = 0; i < N; ++i) {
3488 luminance_values[i] = luminance(data_red[i], data_green[i], data_blue[i]);
3489 }
3490
3491 // Sort luminance values to find percentiles
3492 std::vector<float> sorted_luminance = luminance_values;
3493 std::sort(sorted_luminance.begin(), sorted_luminance.end());
3494
3495 // Calculate 95th percentile for exposure (prevents bright outliers from under-exposing scene)
3496 std::size_t p95_idx = static_cast<std::size_t>(0.95f * (N - 1));
3497 float p95_luminance = sorted_luminance[p95_idx];
3498
3499 // Calculate median luminance for scene analysis
3500 std::size_t median_idx = N / 2;
3501 float median_luminance = sorted_luminance[median_idx];
3502
3503 // Target median luminance scaled appropriately for the data range
3504 // Since RGB data is not normalized to [0,1], we need to scale the target accordingly
3505 float target_median = 0.18f; // Calibrated based on empirical testing
3506 float auto_gain = target_median / std::max(median_luminance, 1e-6f);
3507
3508 // Clamp auto-gain to reasonable range to prevent over/under exposure
3509 // auto_gain = std::clamp(auto_gain, 0.0005f, 0.5f);
3510
3511 // Apply final gain (auto-exposure * manual adjustment)
3512 float final_gain = auto_gain * gain_multiplier;
3513
3514 // Apply gain to all channels
3515 for (std::size_t i = 0; i < N; ++i) {
3516 data_red[i] *= final_gain;
3517 data_green[i] *= final_gain;
3518 data_blue[i] *= final_gain;
3519 }
3520}
3521
3523 // Skip if pixel_data is empty (camera hasn't been rendered yet)
3524 if (pixel_data.empty()) {
3525 return;
3526 }
3527
3528 // Verify that all expected bands exist in pixel_data
3529 for (const auto &band: band_labels) {
3530 if (pixel_data.find(band) == pixel_data.end()) {
3531 return; // Skip exposure if not all bands are populated yet
3532 }
3533 }
3534
3535 // Parse exposure mode
3536 std::string exposure_mode = exposure;
3537
3538 // Manual mode: no automatic exposure scaling
3539 if (exposure_mode == "manual") {
3540 return;
3541 }
3542
3543 // Auto mode: apply automatic exposure based on camera type
3544 if (exposure_mode == "auto") {
3545 // Determine camera type: if not set explicitly, infer from band count
3546 std::string cam_type;
3547 if (!camera_type.empty()) {
3548 cam_type = camera_type;
3549 } else {
3550 // Infer type for manually created cameras
3551 cam_type = (band_labels.size() >= 3) ? "rgb" : "spectral";
3552 }
3553
3554 if (cam_type == "thermal") {
3555 // Thermal cameras: skip exposure adjustment
3556 return;
3557 } else if (cam_type == "rgb" && band_labels.size() >= 3) {
3558 // RGB cameras: luminance-based auto-exposure (18% gray target)
3559
3560 // Use the first 3 bands as RGB (or find bands named "red", "green", "blue")
3561 std::string red_band, green_band, blue_band;
3562 for (const auto &band: band_labels) {
3563 if (band.find("red") != std::string::npos || band.find("Red") != std::string::npos || band.find("RED") != std::string::npos) {
3564 red_band = band;
3565 } else if (band.find("green") != std::string::npos || band.find("Green") != std::string::npos || band.find("GREEN") != std::string::npos) {
3566 green_band = band;
3567 } else if (band.find("blue") != std::string::npos || band.find("Blue") != std::string::npos || band.find("BLUE") != std::string::npos) {
3568 blue_band = band;
3569 }
3570 }
3571
3572 // Fallback to first 3 bands if named bands not found
3573 if (red_band.empty())
3574 red_band = band_labels[0];
3575 if (green_band.empty())
3576 green_band = band_labels[1];
3577 if (blue_band.empty())
3578 blue_band = band_labels[2];
3579
3580 auto &data_red = pixel_data.at(red_band);
3581 auto &data_green = pixel_data.at(green_band);
3582 auto &data_blue = pixel_data.at(blue_band);
3583
3584 const std::size_t N = data_red.size();
3585
3586 // Calculate luminance for each pixel
3587 std::vector<float> luminance_values(N);
3588 for (std::size_t i = 0; i < N; ++i) {
3589 luminance_values[i] = luminance(data_red[i], data_green[i], data_blue[i]);
3590 }
3591
3592 // Sort to find median
3593 std::vector<float> sorted_luminance = luminance_values;
3594 std::sort(sorted_luminance.begin(), sorted_luminance.end());
3595
3596 std::size_t median_idx = N / 2;
3597 float median_luminance = sorted_luminance[median_idx];
3598
3599 // Target 18% gray
3600 float target_median = 0.18f;
3601 float auto_gain = target_median / std::max(median_luminance, 1e-6f);
3602
3603 // Apply gain to all bands
3604 for (auto &band_pair: pixel_data) {
3605 auto &data = band_pair.second;
3606 for (std::size_t i = 0; i < N; ++i) {
3607 data[i] *= auto_gain;
3608 }
3609 }
3610
3611 } else if (cam_type == "spectral") {
3612 // Spectral cameras: per-band normalization
3613 for (auto &band_pair: pixel_data) {
3614 auto &data = band_pair.second;
3615 const std::size_t N = data.size();
3616
3617 // Calculate median for this band
3618 std::vector<float> sorted_data = data;
3619 std::sort(sorted_data.begin(), sorted_data.end());
3620
3621 std::size_t median_idx = N / 2;
3622 float median_value = sorted_data[median_idx];
3623
3624 // Target 18% gray for each band independently
3625 float target_median = 0.18f;
3626 float band_gain = target_median / std::max(median_value, 1e-6f);
3627
3628 // Apply gain to this band
3629 for (std::size_t i = 0; i < N; ++i) {
3630 data[i] *= band_gain;
3631 }
3632 }
3633 } else {
3634 helios_runtime_error("ERROR (RadiationCamera::applyCameraExposure): Unknown camera_type '" + cam_type + "'. Must be 'rgb', 'spectral', or 'thermal'.");
3635 }
3636 return;
3637 }
3638
3639 // ISO mode: "ISOXXX" (e.g., "ISO100", "ISO200", etc.)
3640 if (exposure_mode.substr(0, 3) == "ISO" || exposure_mode.substr(0, 3) == "iso") {
3641 // Parse ISO value
3642 int iso_value;
3643 try {
3644 iso_value = std::stoi(exposure_mode.substr(3));
3645 } catch (...) {
3646 helios_runtime_error("ERROR (RadiationCamera::applyCameraExposure): Invalid ISO format '" + exposure_mode + "'. Expected format: 'ISOXXX' (e.g., 'ISO100').");
3647 }
3648
3649 if (iso_value <= 0) {
3650 helios_runtime_error("ERROR (RadiationCamera::applyCameraExposure): ISO value must be positive. Got: " + std::to_string(iso_value));
3651 }
3652
3653 // Validate that lens_focal_length is set (required for ISO mode)
3654 if (lens_focal_length <= 0) {
3655 helios_runtime_error("ERROR (RadiationCamera::applyCameraExposure): ISO mode requires lens_focal_length to be set. Camera '" + label + "' has lens_focal_length = " + std::to_string(lens_focal_length) +
3656 ". Either set it explicitly or use 'auto' or 'manual' exposure mode.");
3657 }
3658
3659 // Calculate f-number from lens diameter and optical focal length
3660 // f-number = lens_focal_length / lens_diameter
3661 float f_number = lens_focal_length / std::max(lens_diameter, 1e-6f);
3662
3663 // Reference camera settings (chosen to match typical photography)
3664 const float ref_iso = 100.0f;
3665 const float ref_shutter = 1.0f / 125.0f;
3666 const float ref_f_number = 2.8f;
3667
3668 // Calibration to match auto-exposure behavior
3669 // Typical Helios scenes have raw median ~10, auto-exposure targets 0.0675
3670 // At reference settings (ISO 100, 1/125s, f/2.8), we want the same result as auto
3671 const float typical_scene_median = 10.0f;
3672 const float target_median = 0.0675f;
3673
3674 // Calculate exposure from camera settings (proportional to ISO × t / N²)
3675 // Higher ISO → brighter, longer shutter → brighter, wider aperture (smaller N) → brighter
3676 float exposure = (float(iso_value) * shutter_speed) / (f_number * f_number);
3677 float ref_exposure = (ref_iso * ref_shutter) / (ref_f_number * ref_f_number);
3678
3679 // Calibration: at reference settings with typical scene, achieve target_median
3680 // Required gain at reference: target_median / typical_scene_median
3681 // This gain must equal: ref_exposure × calibration_factor
3682 // Therefore: calibration_factor = (target_median / typical_scene_median) / ref_exposure
3683 float ref_gain = target_median / typical_scene_median;
3684 float calibration_factor = ref_gain / ref_exposure;
3685
3686 // Final exposure multiplier
3687 float exposure_multiplier = exposure * calibration_factor;
3688
3689 // Apply exposure to all bands
3690 const std::size_t N = pixel_data.begin()->second.size();
3691 for (auto &band_pair: pixel_data) {
3692 auto &data = band_pair.second;
3693 for (std::size_t i = 0; i < N; ++i) {
3694 data[i] *= exposure_multiplier;
3695 }
3696 }
3697 return;
3698 }
3699
3700 // Unknown exposure mode
3701 helios_runtime_error("ERROR (RadiationCamera::applyCameraExposure): Unknown exposure mode '" + exposure_mode + "'. Must be 'auto', 'ISOXXX' (e.g., 'ISO100'), or 'manual'.");
3702}
3703
3705 // Skip if pixel_data is empty (camera hasn't been rendered yet)
3706 if (pixel_data.empty()) {
3707 return;
3708 }
3709
3710 // Verify that all expected bands exist in pixel_data
3711 for (const auto &band: band_labels) {
3712 if (pixel_data.find(band) == pixel_data.end()) {
3713 return; // Skip white balance if not all bands are populated yet
3714 }
3715 }
3716
3717 // Parse white balance mode
3718 std::string wb_mode = white_balance;
3719
3720 // "off" mode: no white balance correction
3721 if (wb_mode == "off") {
3722 return;
3723 }
3724
3725 // Skip white balance for single-channel images (grayscale/thermal)
3726 if (band_labels.size() < 3) {
3727 return;
3728 }
3729
3730 // "auto" mode: apply spectral white balance
3731 if (wb_mode == "auto") {
3732 // For 3+ channel images, apply white balance to first 3 channels
3733 // Assume standard RGB ordering for the first 3 bands
3734 std::string red_band = band_labels[0];
3735 std::string green_band = band_labels[1];
3736 std::string blue_band = band_labels[2];
3737
3738 try {
3739 whiteBalanceSpectral(red_band, green_band, blue_band, context);
3740 } catch (const std::exception &e) {
3741 // If spectral white balance fails (e.g., no spectral data), silently skip
3742 // This matches the behavior of whiteBalanceSpectral which returns early
3743 // when all bands use "uniform" response
3744 }
3745 return;
3746 }
3747
3748 // Unknown white balance mode
3749 helios_runtime_error("ERROR (RadiationCamera::applyCameraWhiteBalance): Unknown white_balance mode '" + wb_mode + "'. Must be 'auto' or 'off'.");
3750}
3751
3752void RadiationCamera::adjustBrightnessContrast(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float brightness, float contrast) {
3753#ifdef HELIOS_DEBUG
3754 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3755 helios_runtime_error("ERROR (RadiationModel::adjustBrightnessContrast): One or more specified band labels do not exist for the camera pixel data.");
3756 }
3757#endif
3758
3759 auto &data_red = pixel_data.at(red_band_label);
3760 auto &data_green = pixel_data.at(green_band_label);
3761 auto &data_blue = pixel_data.at(blue_band_label);
3762
3763 const std::size_t N = data_red.size();
3764
3765 for (std::size_t i = 0; i < N; ++i) {
3766 // Apply brightness adjustment
3767 float r = data_red[i] * brightness;
3768 float g = data_green[i] * brightness;
3769 float b = data_blue[i] * brightness;
3770
3771 // Apply contrast adjustment (around 0.5 midpoint in linear space)
3772 r = 0.5f + (r - 0.5f) * contrast;
3773 g = 0.5f + (g - 0.5f) * contrast;
3774 b = 0.5f + (b - 0.5f) * contrast;
3775
3776 // Store results (allow values outside [0,1] range for HDR processing)
3777 data_red[i] = r;
3778 data_green[i] = g;
3779 data_blue[i] = b;
3780 }
3781}
3782
3783void RadiationCamera::adjustSaturation(const std::string &red_band_label, const std::string &green_band_label, const std::string &blue_band_label, float saturation) {
3784#ifdef HELIOS_DEBUG
3785 if (pixel_data.find(red_band_label) == pixel_data.end() || pixel_data.find(green_band_label) == pixel_data.end() || pixel_data.find(blue_band_label) == pixel_data.end()) {
3786 helios_runtime_error("ERROR (RadiationModel::adjustSaturation): One or more specified band labels do not exist for the camera pixel data.");
3787 }
3788#endif
3789
3790 auto &data_red = pixel_data.at(red_band_label);
3791 auto &data_green = pixel_data.at(green_band_label);
3792 auto &data_blue = pixel_data.at(blue_band_label);
3793
3794 const std::size_t N = data_red.size();
3795
3796 for (std::size_t i = 0; i < N; ++i) {
3797 float r = data_red[i];
3798 float g = data_green[i];
3799 float b = data_blue[i];
3800
3801 // Calculate luminance for this pixel
3802 float lum = luminance(r, g, b);
3803
3804 // Apply saturation adjustment by interpolating between luminance (grayscale) and original color
3805 data_red[i] = lum + saturation * (r - lum);
3806 data_green[i] = lum + saturation * (g - lum);
3807 data_blue[i] = lum + saturation * (b - lum);
3808 }
3809}
3810
3811// -------------------- Camera Metadata Export Methods -------------------- //
3812
3813std::string RadiationModel::detectLightingType() const {
3814 if (radiation_sources.empty()) {
3815 return "none";
3816 }
3817
3818 bool has_sun = false;
3819 bool has_artificial = false;
3820
3821 for (const auto &source: radiation_sources) {
3822 if (source.source_type == RADIATION_SOURCE_TYPE_COLLIMATED || source.source_type == RADIATION_SOURCE_TYPE_SUN_SPHERE) {
3823 has_sun = true;
3824 } else if (source.source_type == RADIATION_SOURCE_TYPE_SPHERE || source.source_type == RADIATION_SOURCE_TYPE_RECTANGLE || source.source_type == RADIATION_SOURCE_TYPE_DISK) {
3825 has_artificial = true;
3826 }
3827 }
3828
3829 if (has_sun && has_artificial) {
3830 return "mixed";
3831 } else if (has_sun) {
3832 return "sunlight";
3833 } else if (has_artificial) {
3834 return "artificial";
3835 } else {
3836 return "none";
3837 }
3838}
3839
3840float RadiationModel::calculateCameraTiltAngle(const helios::vec3 &position, const helios::vec3 &lookat) const {
3841 // Calculate viewing direction vector
3842 helios::vec3 direction = lookat - position;
3843 direction.normalize();
3844
3845 // Calculate tilt from horizontal (0 = horizontal, 90 = straight down, -90 = straight up)
3846 // The z component gives us the vertical component of the direction
3847 // Tilt angle = -asin(direction.z) in degrees
3848 float tilt_angle_deg = -asin(direction.z) * 180.0f / M_PI;
3849
3850 return tilt_angle_deg;
3851}
3852
3853void RadiationModel::computeAgronomicProperties(const std::string &camera_label, CameraMetadata::AgronomicProperties &props) const {
3854 // Validate camera exists
3855 if (cameras.find(camera_label) == cameras.end()) {
3856 helios_runtime_error("ERROR (RadiationModel::computeAgronomicProperties): Camera '" + camera_label + "' does not exist.");
3857 }
3858
3859 const auto &cam = cameras.at(camera_label);
3860
3861 // Clear any existing data
3862 props.plant_species.clear();
3863 props.plant_count.clear();
3864 props.plant_height_m.clear();
3865 props.plant_age_days.clear();
3866 props.plant_stage.clear();
3867 props.leaf_area_m2.clear();
3868 props.weed_pressure = "";
3869
3870 // Load pixel UUID map from global data
3871 std::vector<uint> pixel_UUIDs;
3872 std::string pixel_UUID_label = "camera_" + camera_label + "_pixel_UUID";
3873 if (!context->doesGlobalDataExist(pixel_UUID_label.c_str())) {
3874 // No pixel UUID data available - skip agronomic properties
3875 return;
3876 }
3877 context->getGlobalData(pixel_UUID_label.c_str(), pixel_UUIDs);
3878
3879 // Map: species_name -> set of unique plantIDs for that species
3880 std::map<std::string, std::set<int>> species_to_plantIDs;
3881
3882 // Set of all plantIDs that are weeds
3883 std::set<int> weed_plantIDs;
3884
3885 // Set of all unique plantIDs (for weed pressure calculation)
3886 std::set<int> all_plantIDs;
3887
3888 // Maps for new agronomic properties (per species, per plantID)
3889 std::map<std::string, std::map<int, float>> species_plant_heights; // species -> (plantID -> height)
3890 std::map<std::string, std::map<int, float>> species_plant_ages; // species -> (plantID -> age)
3891 std::map<std::string, std::map<int, std::string>> species_plant_stages; // species -> (plantID -> stage)
3892 std::map<std::string, std::map<int, float>> species_plant_leaf_areas; // species -> (plantID -> leaf area)
3893 std::map<std::string, std::map<int, int>> species_plant_pixel_counts; // species -> (plantID -> pixel count) for weighted averaging
3894
3895 // Iterate through all pixels to find unique objects and query their data
3896 for (uint j = 0; j < cam.resolution.y; j++) {
3897 for (uint i = 0; i < cam.resolution.x; i++) {
3898 uint pixel_index = j * cam.resolution.x + i;
3899
3900 if (pixel_index >= pixel_UUIDs.size()) {
3901 continue;
3902 }
3903
3904 uint UUID_plus_one = pixel_UUIDs.at(pixel_index);
3905 if (UUID_plus_one == 0) {
3906 // Sky pixel, skip
3907 continue;
3908 }
3909
3910 uint UUID = UUID_plus_one - 1;
3911
3912 // Check if primitive exists
3913 if (!context->doesPrimitiveExist(UUID)) {
3914 continue;
3915 }
3916
3917 // Get parent object ID
3918 uint objID = context->getPrimitiveParentObjectID(UUID);
3919 if (objID == 0) {
3920 // Primitive has no parent object, skip
3921 continue;
3922 }
3923
3924 // Query plant_name (species)
3925 std::string plant_name;
3926 bool has_plant_name = false;
3927 if (context->doesObjectDataExist(objID, "plant_name")) {
3928 HeliosDataType datatype = context->getObjectDataType("plant_name");
3929 if (datatype == HELIOS_TYPE_STRING) {
3930 context->getObjectData(objID, "plant_name", plant_name);
3931 has_plant_name = true;
3932 }
3933 }
3934
3935 // Query plantID
3936 int plantID = -1;
3937 bool has_plantID = false;
3938 if (context->doesObjectDataExist(objID, "plantID")) {
3939 HeliosDataType datatype = context->getObjectDataType("plantID");
3940 if (datatype == HELIOS_TYPE_INT) {
3941 context->getObjectData(objID, "plantID", plantID);
3942 has_plantID = true;
3943 } else if (datatype == HELIOS_TYPE_UINT) {
3944 uint plantID_uint;
3945 context->getObjectData(objID, "plantID", plantID_uint);
3946 plantID = static_cast<int>(plantID_uint);
3947 has_plantID = true;
3948 }
3949 }
3950
3951 // Query plant_type (to identify weeds)
3952 std::string plant_type;
3953 bool has_plant_type = false;
3954 if (context->doesObjectDataExist(objID, "plant_type")) {
3955 HeliosDataType datatype = context->getObjectDataType("plant_type");
3956 if (datatype == HELIOS_TYPE_STRING) {
3957 context->getObjectData(objID, "plant_type", plant_type);
3958 has_plant_type = true;
3959 }
3960 }
3961
3962 // Query plant_height (for new agronomic metadata)
3963 float plant_height = 0.0f;
3964 bool has_plant_height = false;
3965 if (context->doesObjectDataExist(objID, "plant_height")) {
3966 HeliosDataType datatype = context->getObjectDataType("plant_height");
3967 if (datatype == HELIOS_TYPE_FLOAT) {
3968 context->getObjectData(objID, "plant_height", plant_height);
3969 has_plant_height = true;
3970 }
3971 }
3972
3973 // Query age (for new agronomic metadata)
3974 float age = 0.0f;
3975 bool has_age = false;
3976 if (context->doesObjectDataExist(objID, "age")) {
3977 HeliosDataType datatype = context->getObjectDataType("age");
3978 if (datatype == HELIOS_TYPE_FLOAT) {
3979 context->getObjectData(objID, "age", age);
3980 has_age = true;
3981 }
3982 }
3983
3984 // Query phenology_stage (for new agronomic metadata)
3985 std::string phenology_stage;
3986 bool has_phenology_stage = false;
3987 if (context->doesObjectDataExist(objID, "phenology_stage")) {
3988 HeliosDataType datatype = context->getObjectDataType("phenology_stage");
3989 if (datatype == HELIOS_TYPE_STRING) {
3990 context->getObjectData(objID, "phenology_stage", phenology_stage);
3991 has_phenology_stage = true;
3992 }
3993 }
3994
3995 // Get primitive surface area (for leaf area calculation)
3996 float primitive_area = context->getPrimitiveArea(UUID);
3997
3998 // Only process if we have the required data
3999 if (has_plant_name && has_plantID) {
4000 // Add plantID to the species set
4001 species_to_plantIDs[plant_name].insert(plantID);
4002
4003 // Add to all plantIDs set
4004 all_plantIDs.insert(plantID);
4005
4006 // Check if this plant is a weed
4007 if (has_plant_type && plant_type == "weed") {
4008 weed_plantIDs.insert(plantID);
4009 }
4010
4011 // Accumulate new agronomic data per species and plantID
4012 if (has_plant_height) {
4013 species_plant_heights[plant_name][plantID] = plant_height;
4014 }
4015 if (has_age) {
4016 species_plant_ages[plant_name][plantID] = age;
4017 }
4018 if (has_phenology_stage) {
4019 species_plant_stages[plant_name][plantID] = phenology_stage;
4020 }
4021
4022 // Accumulate leaf area for this plant
4023 species_plant_leaf_areas[plant_name][plantID] += primitive_area;
4024
4025 // Track pixel count for weighted averaging
4026 species_plant_pixel_counts[plant_name][plantID]++;
4027 }
4028 }
4029 }
4030
4031 // If no valid data was found, leave properties empty
4032 if (species_to_plantIDs.empty()) {
4033 return;
4034 }
4035
4036 // Build plant_species and plant_count vectors
4037 for (const auto &species_pair: species_to_plantIDs) {
4038 props.plant_species.push_back(species_pair.first);
4039 props.plant_count.push_back(static_cast<int>(species_pair.second.size()));
4040 }
4041
4042 // Compute new agronomic properties per species (parallel to plant_species vector)
4043 for (const auto &species_pair: species_to_plantIDs) {
4044 const std::string &species = species_pair.first;
4045 const std::set<int> &plantIDs = species_pair.second;
4046
4047 // --- Plant Height (weighted average by pixel count) ---
4048 if (species_plant_heights.find(species) != species_plant_heights.end()) {
4049 float total_weighted_height = 0.0f;
4050 int total_pixels = 0;
4051 for (int plantID: plantIDs) {
4052 if (species_plant_heights.at(species).find(plantID) != species_plant_heights.at(species).end()) {
4053 float height = species_plant_heights.at(species).at(plantID);
4054 int pixel_count = species_plant_pixel_counts.at(species).at(plantID);
4055 total_weighted_height += height * static_cast<float>(pixel_count);
4056 total_pixels += pixel_count;
4057 }
4058 }
4059 if (total_pixels > 0) {
4060 props.plant_height_m.push_back(total_weighted_height / static_cast<float>(total_pixels));
4061 } else {
4062 props.plant_height_m.push_back(0.0f);
4063 }
4064 } else {
4065 props.plant_height_m.push_back(0.0f);
4066 }
4067
4068 // --- Plant Age (weighted average by pixel count) ---
4069 if (species_plant_ages.find(species) != species_plant_ages.end()) {
4070 float total_weighted_age = 0.0f;
4071 int total_pixels = 0;
4072 for (int plantID: plantIDs) {
4073 if (species_plant_ages.at(species).find(plantID) != species_plant_ages.at(species).end()) {
4074 float age = species_plant_ages.at(species).at(plantID);
4075 int pixel_count = species_plant_pixel_counts.at(species).at(plantID);
4076 total_weighted_age += age * static_cast<float>(pixel_count);
4077 total_pixels += pixel_count;
4078 }
4079 }
4080 if (total_pixels > 0) {
4081 props.plant_age_days.push_back(total_weighted_age / static_cast<float>(total_pixels));
4082 } else {
4083 props.plant_age_days.push_back(0.0f);
4084 }
4085 } else {
4086 props.plant_age_days.push_back(0.0f);
4087 }
4088
4089 // --- Plant Stage (mode - most common stage) ---
4090 if (species_plant_stages.find(species) != species_plant_stages.end()) {
4091 std::map<std::string, int> stage_counts;
4092 for (int plantID: plantIDs) {
4093 if (species_plant_stages.at(species).find(plantID) != species_plant_stages.at(species).end()) {
4094 std::string stage = species_plant_stages.at(species).at(plantID);
4095 stage_counts[stage]++;
4096 }
4097 }
4098 // Find most common stage
4099 std::string mode_stage;
4100 int max_count = 0;
4101 for (const auto &stage_pair: stage_counts) {
4102 if (stage_pair.second > max_count) {
4103 max_count = stage_pair.second;
4104 mode_stage = stage_pair.first;
4105 }
4106 }
4107 props.plant_stage.push_back(mode_stage);
4108 } else {
4109 props.plant_stage.push_back("");
4110 }
4111
4112 // --- Leaf Area (sum of all leaf areas for this species) ---
4113 if (species_plant_leaf_areas.find(species) != species_plant_leaf_areas.end()) {
4114 float total_leaf_area = 0.0f;
4115 for (int plantID: plantIDs) {
4116 if (species_plant_leaf_areas.at(species).find(plantID) != species_plant_leaf_areas.at(species).end()) {
4117 total_leaf_area += species_plant_leaf_areas.at(species).at(plantID);
4118 }
4119 }
4120 props.leaf_area_m2.push_back(total_leaf_area);
4121 } else {
4122 props.leaf_area_m2.push_back(0.0f);
4123 }
4124 }
4125
4126 // Calculate weed pressure
4127 if (!all_plantIDs.empty()) {
4128 float weed_fraction = static_cast<float>(weed_plantIDs.size()) / static_cast<float>(all_plantIDs.size());
4129 float weed_percentage = weed_fraction * 100.0f;
4130
4131 if (weed_percentage <= 20.0f) {
4132 props.weed_pressure = "low";
4133 } else if (weed_percentage <= 40.0f) {
4134 props.weed_pressure = "moderate";
4135 } else {
4136 props.weed_pressure = "high";
4137 }
4138 }
4139}
4140
4141void RadiationModel::populateCameraMetadata(const std::string &camera_label, CameraMetadata &metadata) const {
4142 // Validate camera exists
4143 if (cameras.find(camera_label) == cameras.end()) {
4144 helios_runtime_error("ERROR (RadiationModel::populateCameraMetadata): Camera '" + camera_label + "' does not exist.");
4145 }
4146
4147 const auto &cam = cameras.at(camera_label);
4148
4149 // --- Camera Properties --- //
4150 metadata.camera_properties.width = cam.resolution.x;
4151 metadata.camera_properties.height = cam.resolution.y;
4152 metadata.camera_properties.channels = static_cast<int>(cam.band_labels.size());
4153 metadata.camera_properties.type = cam.camera_type;
4154
4155 // Calculate sensor dimensions from HFOV and sensor_width_mm
4156 // sensor_width is already in mm, stored in the camera
4157 metadata.camera_properties.sensor_width = cam.sensor_width_mm;
4158
4159 // Calculate VFOV from HFOV and aspect ratio
4160 float VFOV_degrees = cam.HFOV_degrees / cam.FOV_aspect_ratio;
4161
4162 // Calculate sensor height from sensor width and aspect ratio
4163 metadata.camera_properties.sensor_height = cam.sensor_width_mm / cam.FOV_aspect_ratio;
4164
4165 // Back-calculate optical focal length from HFOV and sensor width for metadata export
4166 // IMPORTANT: All metadata values reflect the REFERENCE state at zoom=1.0, not the zoomed state.
4167 // - focal_length is calculated from the base HFOV (cam.HFOV_degrees), not effective HFOV
4168 // - sensor dimensions are physical properties unaffected by zoom
4169 // - The zoom value itself is written separately so users can reconstruct effective parameters
4170 // This ensures metadata accurately reflects the configured camera geometry (HFOV)
4171 // Note: For ISO exposure calculations, we use lens_focal_length which may differ from this value
4172 float HFOV_rad = cam.HFOV_degrees * M_PI / 180.0f;
4173 float optical_focal_length_mm = cam.sensor_width_mm / (2.0f * tan(HFOV_rad / 2.0f));
4174 metadata.camera_properties.focal_length = optical_focal_length_mm;
4175
4176 // Calculate aperture (f-number = optical_focal_length / lens_diameter)
4177 if (cam.lens_diameter > 0) {
4178 float lens_diameter_mm = cam.lens_diameter * 1000.0f; // Convert meters to mm
4179 float f_number = optical_focal_length_mm / lens_diameter_mm;
4180 std::ostringstream aperture_str;
4181 aperture_str << "f/" << std::fixed << std::setprecision(1) << f_number;
4182 metadata.camera_properties.aperture = aperture_str.str();
4183 } else {
4184 metadata.camera_properties.aperture = "pinhole";
4185 }
4186
4187 // Camera model
4188 metadata.camera_properties.model = cam.model;
4189
4190 // Lens metadata
4191 metadata.camera_properties.lens_make = cam.lens_make;
4192 metadata.camera_properties.lens_model = cam.lens_model;
4193 metadata.camera_properties.lens_specification = cam.lens_specification;
4194
4195 // Exposure settings
4196 metadata.camera_properties.exposure = cam.exposure;
4197 metadata.camera_properties.shutter_speed = cam.shutter_speed;
4198
4199 // White balance mode
4200 metadata.camera_properties.white_balance = cam.white_balance;
4201
4202 // Zoom setting
4203 metadata.camera_properties.camera_zoom = cam.camera_zoom;
4204
4205 // --- Location Properties --- //
4206 helios::Location loc = context->getLocation();
4207 metadata.location_properties.latitude = loc.latitude_deg;
4208 metadata.location_properties.longitude = loc.longitude_deg;
4209
4210 // --- Acquisition Properties --- //
4211 helios::Date date = context->getDate();
4212 helios::Time time = context->getTime();
4213
4214 // Format date as YYYY-MM-DD
4215 std::ostringstream date_str;
4216 date_str << date.year << "-" << std::setw(2) << std::setfill('0') << date.month << "-" << std::setw(2) << std::setfill('0') << date.day;
4217 metadata.acquisition_properties.date = date_str.str();
4218
4219 // Format time as HH:MM:SS
4220 std::ostringstream time_str;
4221 time_str << std::setw(2) << std::setfill('0') << time.hour << ":" << std::setw(2) << std::setfill('0') << time.minute << ":" << std::setw(2) << std::setfill('0') << time.second;
4222 metadata.acquisition_properties.time = time_str.str();
4223
4224 metadata.acquisition_properties.UTC_offset = loc.UTC_offset;
4225 metadata.acquisition_properties.camera_height_m = cam.position.z;
4226 metadata.acquisition_properties.camera_angle_deg = calculateCameraTiltAngle(cam.position, cam.lookat);
4227 metadata.acquisition_properties.light_source = detectLightingType();
4228
4229 // --- Agronomic Properties --- //
4230 computeAgronomicProperties(camera_label, metadata.agronomic_properties);
4231
4232 // Note: path field is left empty and will be set when image is written
4233 metadata.path = "";
4234}
4235
4236void RadiationModel::enableCameraMetadata(const std::string &camera_label) {
4237 // Validate camera exists
4238 if (cameras.find(camera_label) == cameras.end()) {
4239 helios_runtime_error("ERROR (RadiationModel::enableCameraMetadata): Camera '" + camera_label + "' does not exist.");
4240 }
4241
4242 // Preserve any existing image_processing parameters (e.g., from applyCameraImageCorrections)
4243 CameraMetadata::ImageProcessingProperties saved_image_processing;
4244 if (camera_metadata.find(camera_label) != camera_metadata.end()) {
4245 saved_image_processing = camera_metadata.at(camera_label).image_processing;
4246 }
4247
4248 // Populate metadata from camera properties and context
4249 CameraMetadata metadata;
4250 populateCameraMetadata(camera_label, metadata);
4251
4252 // Restore image_processing parameters
4253 metadata.image_processing = saved_image_processing;
4254
4255 // Store metadata and mark camera as enabled for metadata writing
4256 camera_metadata[camera_label] = metadata;
4257 metadata_enabled_cameras.insert(camera_label);
4258}
4259
4260void RadiationModel::enableCameraMetadata(const std::vector<std::string> &camera_labels) {
4261 // Enable metadata for each camera in the vector
4262 for (const auto &camera_label: camera_labels) {
4263 enableCameraMetadata(camera_label);
4264 }
4265}
4266
4267CameraMetadata RadiationModel::getCameraMetadata(const std::string &camera_label) const {
4268 // Validate camera exists
4269 if (cameras.find(camera_label) == cameras.end()) {
4270 helios_runtime_error("ERROR (RadiationModel::getCameraMetadata): Camera '" + camera_label + "' does not exist.");
4271 }
4272
4273 // Re-populate metadata to ensure it reflects current state (e.g., light sources, updated context data)
4274 CameraMetadata metadata;
4275 populateCameraMetadata(camera_label, metadata);
4276
4277 return metadata;
4278}
4279
4280void RadiationModel::setCameraMetadata(const std::string &camera_label, const CameraMetadata &metadata) {
4281 // Validate camera exists
4282 if (cameras.find(camera_label) == cameras.end()) {
4283 helios_runtime_error("ERROR (RadiationModel::setCameraMetadata): Camera '" + camera_label + "' does not exist.");
4284 }
4285
4286 camera_metadata[camera_label] = metadata;
4287}
4288
4289std::string RadiationModel::writeCameraMetadataFile(const std::string &camera_label, const std::string &output_path) const {
4290 // Validate camera has metadata
4291 if (camera_metadata.find(camera_label) == camera_metadata.end()) {
4292 helios_runtime_error("ERROR (RadiationModel::writeCameraMetadataFile): No metadata set for camera '" + camera_label + "'.");
4293 }
4294
4295 const auto &metadata = camera_metadata.at(camera_label);
4296
4297 // Helper lambda to format floats with specific decimal precision for clean JSON output
4298 // Converts to string with fixed precision, then parses as double to avoid float representation artifacts
4299 auto format_float = [](float value, int decimals) -> double {
4300 std::ostringstream oss;
4301 oss << std::fixed << std::setprecision(decimals) << value;
4302 return std::stod(oss.str());
4303 };
4304
4305 // Build JSON structure matching schema
4306 nlohmann::json j;
4307 j["path"] = metadata.path;
4308
4309 j["camera_properties"]["height"] = metadata.camera_properties.height;
4310 j["camera_properties"]["width"] = metadata.camera_properties.width;
4311 j["camera_properties"]["channels"] = metadata.camera_properties.channels;
4312 j["camera_properties"]["type"] = metadata.camera_properties.type;
4313 j["camera_properties"]["focal_length"] = format_float(metadata.camera_properties.focal_length, 2);
4314 j["camera_properties"]["aperture"] = metadata.camera_properties.aperture;
4315 j["camera_properties"]["sensor_width"] = format_float(metadata.camera_properties.sensor_width, 2);
4316 j["camera_properties"]["sensor_height"] = format_float(metadata.camera_properties.sensor_height, 2);
4317 j["camera_properties"]["model"] = metadata.camera_properties.model;
4318
4319 // Only include lens fields if they're not empty
4320 if (!metadata.camera_properties.lens_make.empty()) {
4321 j["camera_properties"]["lens_make"] = metadata.camera_properties.lens_make;
4322 }
4323 if (!metadata.camera_properties.lens_model.empty()) {
4324 j["camera_properties"]["lens_model"] = metadata.camera_properties.lens_model;
4325 }
4326 if (!metadata.camera_properties.lens_specification.empty()) {
4327 j["camera_properties"]["lens_specification"] = metadata.camera_properties.lens_specification;
4328 }
4329
4330 // Exposure settings
4331 j["camera_properties"]["exposure"] = metadata.camera_properties.exposure;
4332 j["camera_properties"]["shutter_speed"] = format_float(metadata.camera_properties.shutter_speed, 6);
4333
4334 // White balance mode
4335 j["camera_properties"]["white_balance"] = metadata.camera_properties.white_balance;
4336
4337 // Camera zoom setting
4338 j["camera_properties"]["zoom"] = format_float(metadata.camera_properties.camera_zoom, 2);
4339
4340 j["location_properties"]["latitude"] = format_float(metadata.location_properties.latitude, 6);
4341 j["location_properties"]["longitude"] = format_float(metadata.location_properties.longitude, 6);
4342
4343 j["acquisition_properties"]["date"] = metadata.acquisition_properties.date;
4344 j["acquisition_properties"]["time"] = metadata.acquisition_properties.time;
4345 j["acquisition_properties"]["UTC_offset"] = format_float(metadata.acquisition_properties.UTC_offset, 1);
4346 j["acquisition_properties"]["camera_height_m"] = format_float(metadata.acquisition_properties.camera_height_m, 2);
4347 j["acquisition_properties"]["camera_angle_deg"] = format_float(metadata.acquisition_properties.camera_angle_deg, 2);
4348 j["acquisition_properties"]["light_source"] = metadata.acquisition_properties.light_source;
4349
4350 // Always include image_processing section with color_space
4351 const auto &img_proc = metadata.image_processing;
4352 j["image_processing"]["saturation_adjustment"] = format_float(img_proc.saturation_adjustment, 2);
4353 j["image_processing"]["brightness_adjustment"] = format_float(img_proc.brightness_adjustment, 2);
4354 j["image_processing"]["contrast_adjustment"] = format_float(img_proc.contrast_adjustment, 2);
4355 j["image_processing"]["color_space"] = img_proc.color_space;
4356
4357 // Only include agronomic_properties if data is available
4358 if (!metadata.agronomic_properties.plant_species.empty()) {
4359 j["agronomic_properties"]["plant_species"] = metadata.agronomic_properties.plant_species;
4360 j["agronomic_properties"]["plant_count"] = metadata.agronomic_properties.plant_count;
4361
4362 // Format new agronomic fields with appropriate precision
4363 if (!metadata.agronomic_properties.plant_height_m.empty()) {
4364 std::vector<double> formatted_heights;
4365 for (float height: metadata.agronomic_properties.plant_height_m) {
4366 formatted_heights.push_back(format_float(height, 2));
4367 }
4368 j["agronomic_properties"]["plant_height_m"] = formatted_heights;
4369 }
4370
4371 if (!metadata.agronomic_properties.plant_age_days.empty()) {
4372 std::vector<double> formatted_ages;
4373 for (float age: metadata.agronomic_properties.plant_age_days) {
4374 formatted_ages.push_back(format_float(age, 1));
4375 }
4376 j["agronomic_properties"]["plant_age_days"] = formatted_ages;
4377 }
4378
4379 if (!metadata.agronomic_properties.plant_stage.empty()) {
4380 j["agronomic_properties"]["plant_stage"] = metadata.agronomic_properties.plant_stage;
4381 }
4382
4383 if (!metadata.agronomic_properties.leaf_area_m2.empty()) {
4384 std::vector<double> formatted_leaf_areas;
4385 for (float area: metadata.agronomic_properties.leaf_area_m2) {
4386 formatted_leaf_areas.push_back(format_float(area, 4));
4387 }
4388 j["agronomic_properties"]["leaf_area_m2"] = formatted_leaf_areas;
4389 }
4390
4391 j["agronomic_properties"]["weed_pressure"] = metadata.agronomic_properties.weed_pressure;
4392 }
4393
4394 // Generate JSON filename (replace image extension with .json)
4395 std::string json_filename = metadata.path;
4396 size_t ext_pos = json_filename.find_last_of(".");
4397 if (ext_pos != std::string::npos) {
4398 json_filename = json_filename.substr(0, ext_pos) + ".json";
4399 } else {
4400 json_filename += ".json";
4401 }
4402
4403 // Construct full path with output directory
4404 std::string json_path = output_path + json_filename;
4405
4406 // Write to file
4407 std::ofstream json_file(json_path);
4408 if (!json_file.is_open()) {
4409 helios_runtime_error("ERROR (RadiationModel::writeCameraMetadataFile): Failed to open file '" + json_path + "' for writing.");
4410 }
4411 json_file << j.dump(2) << std::endl; // Pretty print with 2-space indentation
4412 json_file.close();
4413
4414 return json_path;
4415}
4416
4417void RadiationModel::enableCameraLensFlare(const std::string &camera_label) {
4418 if (cameras.find(camera_label) == cameras.end()) {
4419 helios_runtime_error("ERROR (RadiationModel::enableCameraLensFlare): Camera '" + camera_label + "' does not exist.");
4420 }
4421 cameras.at(camera_label).lens_flare_enabled = true;
4422}
4423
4424void RadiationModel::disableCameraLensFlare(const std::string &camera_label) {
4425 if (cameras.find(camera_label) == cameras.end()) {
4426 helios_runtime_error("ERROR (RadiationModel::disableCameraLensFlare): Camera '" + camera_label + "' does not exist.");
4427 }
4428 cameras.at(camera_label).lens_flare_enabled = false;
4429}
4430
4431bool RadiationModel::isCameraLensFlareEnabled(const std::string &camera_label) const {
4432 if (cameras.find(camera_label) == cameras.end()) {
4433 helios_runtime_error("ERROR (RadiationModel::isCameraLensFlareEnabled): Camera '" + camera_label + "' does not exist.");
4434 }
4435 return cameras.at(camera_label).lens_flare_enabled;
4436}
4437
4438void RadiationModel::setCameraLensFlareProperties(const std::string &camera_label, const LensFlareProperties &properties) {
4439 if (cameras.find(camera_label) == cameras.end()) {
4440 helios_runtime_error("ERROR (RadiationModel::setCameraLensFlareProperties): Camera '" + camera_label + "' does not exist.");
4441 }
4442
4443 // Validate properties
4444 if (properties.aperture_blade_count < 3) {
4445 helios_runtime_error("ERROR (RadiationModel::setCameraLensFlareProperties): aperture_blade_count must be at least 3.");
4446 }
4447 if (properties.coating_efficiency < 0.0f || properties.coating_efficiency > 1.0f) {
4448 helios_runtime_error("ERROR (RadiationModel::setCameraLensFlareProperties): coating_efficiency must be in range [0.0, 1.0].");
4449 }
4450 if (properties.ghost_intensity < 0.0f) {
4451 helios_runtime_error("ERROR (RadiationModel::setCameraLensFlareProperties): ghost_intensity must be non-negative.");
4452 }
4453 if (properties.starburst_intensity < 0.0f) {
4454 helios_runtime_error("ERROR (RadiationModel::setCameraLensFlareProperties): starburst_intensity must be non-negative.");
4455 }
4456 if (properties.intensity_threshold < 0.0f || properties.intensity_threshold > 1.0f) {
4457 helios_runtime_error("ERROR (RadiationModel::setCameraLensFlareProperties): intensity_threshold must be in range [0.0, 1.0].");
4458 }
4459 if (properties.ghost_count < 1) {
4460 helios_runtime_error("ERROR (RadiationModel::setCameraLensFlareProperties): ghost_count must be at least 1.");
4461 }
4462
4463 cameras.at(camera_label).lens_flare_properties = properties;
4464}
4465
4467 if (cameras.find(camera_label) == cameras.end()) {
4468 helios_runtime_error("ERROR (RadiationModel::getCameraLensFlareProperties): Camera '" + camera_label + "' does not exist.");
4469 }
4470 return cameras.at(camera_label).lens_flare_properties;
4471}