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