1.3.77
 
Loading...
Searching...
No Matches
RadiationCameraEXIF.cpp
Go to the documentation of this file.
1
18#include "RadiationModel.h"
19
20#include <cmath>
21#include <cstdio>
22#include <iomanip>
23#include <sstream>
24#include <string>
25
26#include "global.h"
27
28using namespace helios;
29
30namespace {
31
33 std::string formatExifDateTime(const helios::Date &date, const helios::Time &time) {
34 std::ostringstream oss;
35 oss << std::setw(4) << std::setfill('0') << date.year << ":"
36 << std::setw(2) << std::setfill('0') << date.month << ":"
37 << std::setw(2) << std::setfill('0') << date.day << " "
38 << std::setw(2) << std::setfill('0') << time.hour << ":"
39 << std::setw(2) << std::setfill('0') << time.minute << ":"
40 << std::setw(2) << std::setfill('0') << time.second;
41 return oss.str();
42 }
43
45
50 void toUTC(const helios::Date &local_date, const helios::Time &local_time, float utc_offset_helios_west_positive,
51 helios::Date &utc_date, helios::Time &utc_time) {
52
53 const int hour_offset_int = static_cast<int>(std::floor(utc_offset_helios_west_positive));
54 const float fractional_hour = utc_offset_helios_west_positive - static_cast<float>(hour_offset_int);
55 const int minute_offset = static_cast<int>(std::round(fractional_hour * 60.0f));
56
57 int total_minutes = static_cast<int>(local_time.hour) * 60 + static_cast<int>(local_time.minute) + hour_offset_int * 60 + minute_offset;
58 const int total_seconds = static_cast<int>(local_time.second);
59
60 int day_shift = 0;
61 while (total_minutes < 0) {
62 total_minutes += 24 * 60;
63 day_shift -= 1;
64 }
65 while (total_minutes >= 24 * 60) {
66 total_minutes -= 24 * 60;
67 day_shift += 1;
68 }
69
70 // Resolve day_shift via Julian-day arithmetic with year wraparound.
71 int year = local_date.year;
72 int jd = local_date.JulianDay() + day_shift;
73 while (jd < 1) {
74 year -= 1;
75 const int prev_year_days = (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) ? 366 : 365;
76 jd += prev_year_days;
77 }
78 while (true) {
79 const int year_days = (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) ? 366 : 365;
80 if (jd <= year_days) break;
81 jd -= year_days;
82 year += 1;
83 }
84
85 utc_date = helios::Julian2Calendar(jd, year);
86 utc_time.hour = total_minutes / 60;
87 utc_time.minute = total_minutes % 60;
88 utc_time.second = total_seconds;
89 }
90
92 std::string formatStandardOffset(float utc_offset_helios_west_positive) {
93 // Standard convention is positive for East of UTC, so we negate.
94 float std_offset = -utc_offset_helios_west_positive;
95 char sign = (std_offset >= 0.f) ? '+' : '-';
96 float abs_off = std::fabs(std_offset);
97 int hh = static_cast<int>(std::floor(abs_off));
98 int mm = static_cast<int>(std::round((abs_off - static_cast<float>(hh)) * 60.0f));
99 // Handle minute rollover from rounding.
100 if (mm >= 60) {
101 mm -= 60;
102 hh += 1;
103 }
104 char buf[8];
105 std::snprintf(buf, sizeof(buf), "%c%02d:%02d", sign, hh, mm);
106 return std::string(buf);
107 }
108
109} // namespace
110
111void RadiationModel::populateImageEXIF(const std::string &camera_label, helios::ImageEXIFData &exif) const {
112
113 if (cameras.find(camera_label) == cameras.end()) {
114 helios_runtime_error("ERROR (RadiationModel::populateImageEXIF): Camera '" + camera_label + "' does not exist.");
115 }
116
117 const auto &cam = cameras.at(camera_label);
118
119 // ---- IFD0: identification + datetime ----
120
121 // Make: use the explicit manufacturer field if set (typical for library cameras); otherwise
122 // fall back to "Helios". For library-loaded cameras the `model` field carries the legacy
123 // "<manufacturer> <model>" concatenation, so we strip the manufacturer prefix when writing
124 // the Model tag — photogrammetry sensor databases key on Make and Model separately.
125 if (!cam.manufacturer.empty()) {
126 exif.make = cam.manufacturer;
127 const std::string prefix = cam.manufacturer + " ";
128 if (cam.model.size() > prefix.size() && cam.model.compare(0, prefix.size(), prefix) == 0) {
129 exif.model = cam.model.substr(prefix.size());
130 } else {
131 exif.model = cam.model;
132 }
133 } else {
134 exif.make = "Helios";
135 exif.model = cam.model.empty() ? std::string("HeliosCamera") : cam.model;
136 }
137 exif.software = "Helios";
138 exif.orientation = 1;
139
140 const helios::Date local_date = context->getDate();
141 const helios::Time local_time = context->getTime();
142 const helios::Location loc = context->getLocation();
143
144 const std::string local_dt = formatExifDateTime(local_date, local_time);
145 exif.datetime = local_dt;
146 exif.datetime_original = local_dt;
147 exif.datetime_digitized = local_dt;
148 exif.offset_time = formatStandardOffset(loc.UTC_offset);
149
150 // ---- Exif SubIFD: intrinsics + exposure ----
151
152 // Back-compute optical focal length in mm from HFOV and physical sensor width so the EXIF
153 // value matches the actual rendering geometry (rather than `cam.lens_focal_length`, which
154 // may have been set independently). This mirrors what populateCameraMetadata() exports
155 // to the JSON sidecar.
156 if (cam.sensor_width_mm > 0.f && cam.HFOV_degrees > 0.f) {
157 const float HFOV_rad = cam.HFOV_degrees * static_cast<float>(M_PI) / 180.f;
158 const float optical_focal_length_mm = cam.sensor_width_mm / (2.0f * std::tan(HFOV_rad / 2.0f));
159 exif.focal_length_mm = optical_focal_length_mm;
160
161 // FocalPlaneXResolution / YResolution: pixels per cm (unit = 3).
162 const float sensor_width_cm = cam.sensor_width_mm / 10.0f;
163 const float sensor_height_mm = (cam.FOV_aspect_ratio > 0.f) ? (cam.sensor_width_mm / cam.FOV_aspect_ratio) : cam.sensor_width_mm;
164 const float sensor_height_cm = sensor_height_mm / 10.0f;
165 if (sensor_width_cm > 0.f && cam.resolution.x > 0) {
166 exif.focal_plane_x_resolution = static_cast<float>(cam.resolution.x) / sensor_width_cm;
167 }
168 if (sensor_height_cm > 0.f && cam.resolution.y > 0) {
169 exif.focal_plane_y_resolution = static_cast<float>(cam.resolution.y) / sensor_height_cm;
170 }
171 exif.focal_plane_resolution_unit = 3; // cm
172 }
173
174 exif.pixel_x_dimension = cam.resolution.x;
175 exif.pixel_y_dimension = cam.resolution.y;
176 exif.exposure_time_s = cam.shutter_speed;
177
178 // FNumber + MaxApertureValue (APEX). The radiation camera has a single fixed aperture
179 // determined by lens_diameter; if the aperture is closed (pinhole, lens_diameter == 0)
180 // both tags are omitted.
181 if (cam.lens_diameter > 0.f && exif.focal_length_mm > 0.f) {
182 const float lens_diameter_mm = cam.lens_diameter * 1000.0f; // m -> mm
183 const float f_number = exif.focal_length_mm / lens_diameter_mm;
184 exif.f_number = f_number;
185 // APEX: Av = 2 * log2(N).
186 exif.max_aperture_value_apex = 2.0f * std::log2(f_number);
187 }
188
189 // SubjectDistance: working distance from camera to focal plane (`cam.focal_length` in meters).
190 if (cam.focal_length > 0.f) {
191 exif.subject_distance_m = cam.focal_length;
192 }
193
194 // FocalLengthIn35mmFilm: scale the optical focal length by 36 mm / sensor_width_mm.
195 if (exif.focal_length_mm > 0.f && cam.sensor_width_mm > 0.f) {
196 const float fl35 = exif.focal_length_mm * (36.0f / cam.sensor_width_mm);
197 if (fl35 > 0.f) {
198 exif.focal_length_in_35mm = static_cast<unsigned int>(std::round(fl35));
199 }
200 }
201
202 // ExposureMode: 0 = auto, 1 = manual. "ISOxxx" specifies ISO directly so the camera is
203 // in manual exposure control.
204 if (cam.exposure == "auto") {
205 exif.exposure_mode = 0;
206 } else if (cam.exposure == "manual" ||
207 (cam.exposure.size() > 3 && cam.exposure.compare(0, 3, "ISO") == 0)) {
208 exif.exposure_mode = 1;
209 }
210
211 // WhiteBalance: 0 = auto, 1 = manual (here used for "off" / fixed gains).
212 if (cam.white_balance == "auto") {
213 exif.white_balance = 0;
214 } else if (!cam.white_balance.empty()) {
215 exif.white_balance = 1;
216 }
217
218 // DigitalZoomRatio: written as-is from cam.camera_zoom (1.0 means no zoom).
219 if (cam.camera_zoom > 0.f) {
220 exif.digital_zoom_ratio = cam.camera_zoom;
221 }
222
223 // ExposureBiasValue: the applied auto-exposure gain expressed in EV stops.
224 // `applied_exposure_gain` is a linear multiplier; EV = log2(gain). gain==1 => 0 EV.
225 if (cam.applied_exposure_gain > 0.f) {
226 exif.exposure_bias_ev = std::log2(cam.applied_exposure_gain);
227 exif.has_exposure_bias = true;
228 }
229
230 // Parse "ISOxxx" prefix from the exposure mode string (e.g. "ISO100").
231 if (cam.exposure.size() > 3 && cam.exposure.compare(0, 3, "ISO") == 0) {
232 try {
233 int iso_int = std::stoi(cam.exposure.substr(3));
234 if (iso_int > 0) {
235 exif.iso = static_cast<unsigned int>(iso_int);
236 }
237 } catch (...) {
238 // Not an integer ISO; leave exif.iso at 0 so the tag is omitted.
239 }
240 }
241
242 exif.lens_make = cam.lens_make;
243 exif.lens_model = cam.lens_model;
244 exif.lens_specification = cam.lens_specification;
245
246 // ---- GPS IFD: lat/lon/alt + bearing + UTC date/time ----
247
248 // Flat-earth conversion centered at the Location origin. Convention: +Y = North, +X = East,
249 // +Z = up. Helios's longitude convention is +W; standard EXIF is +E, so we negate.
250 const double lat0_deg = static_cast<double>(loc.latitude_deg);
251 const double lon0_deg_std = -static_cast<double>(loc.longitude_deg); // +W -> +E
252 const double lat0_rad = lat0_deg * M_PI / 180.0;
253 const double meters_per_deg_lat = 111320.0;
254 const double meters_per_deg_lon = 111320.0 * std::cos(lat0_rad);
255
256 exif.latitude_deg = lat0_deg + (static_cast<double>(cam.position.y) / meters_per_deg_lat);
257 exif.longitude_deg = lon0_deg_std;
258 if (meters_per_deg_lon > 0.0) {
259 exif.longitude_deg += (static_cast<double>(cam.position.x) / meters_per_deg_lon);
260 }
261 exif.altitude_m = static_cast<double>(loc.altitude_m) + static_cast<double>(cam.position.z);
262
263 // Bearing of the optical axis projected onto the horizontal plane (0 = North, 90 = East).
264 helios::vec3 dir = cam.lookat - cam.position;
265 dir.normalize();
266 double yaw_deg = std::atan2(static_cast<double>(dir.x), static_cast<double>(dir.y)) * 180.0 / M_PI;
267 if (yaw_deg < 0.0) yaw_deg += 360.0;
268 exif.img_direction_deg = yaw_deg;
269
270 // GPS UTC date/time.
271 helios::Date utc_date;
272 helios::Time utc_time;
273 toUTC(local_date, local_time, loc.UTC_offset, utc_date, utc_time);
274
275 {
276 std::ostringstream ds;
277 ds << std::setw(4) << std::setfill('0') << utc_date.year << ":"
278 << std::setw(2) << std::setfill('0') << utc_date.month << ":"
279 << std::setw(2) << std::setfill('0') << utc_date.day;
280 exif.gps_datestamp = ds.str();
281 }
282 {
283 std::ostringstream ts;
284 ts << std::setw(2) << std::setfill('0') << utc_time.hour << ":"
285 << std::setw(2) << std::setfill('0') << utc_time.minute << ":"
286 << std::setw(2) << std::setfill('0') << utc_time.second;
287 exif.gps_timestamp_hms = ts.str();
288 }
289
290 exif.gps_valid = true;
291
292 // ---- XMP: Pix4D Camera namespace yaw/pitch/roll ----
293 //
294 // pitch is "+down" (Pix4D convention). roll is 0 in v1 because RadiationCamera has no
295 // up-vector field; this is documented as a known limitation.
296 exif.yaw_deg = yaw_deg;
297 exif.pitch_deg = -static_cast<double>(std::asin(static_cast<double>(dir.z))) * 180.0 / M_PI;
298 exif.roll_deg = 0.0;
299 exif.xmp_valid = true;
300}