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