from opendm import dls import math import numpy as np from opendm import log # Loosely based on https://github.com/micasense/imageprocessing/blob/master/micasense/utils.py def dn_to_radiance(photo, image): """ Convert Digital Number values to Radiance values :param photo ODM_Photo :param image numpy array containing image data :return numpy array with radiance image values """ image = image.astype("float32") if len(image.shape) != 3: raise ValueError("Image should have shape length of 3 (got: %s)" % len(image.shape)) # Handle thermal bands (experimental) if photo.band_name == 'LWIR': image -= (273.15 * 100.0) # Convert Kelvin to Celsius image *= 0.01 return image # All others a1, a2, a3 = photo.get_radiometric_calibration() dark_level = photo.get_dark_level() exposure_time = photo.exposure_time gain = photo.get_gain() photometric_exp = photo.get_photometric_exposure() if a1 is None and photometric_exp is None: log.ODM_WARNING("Cannot perform radiometric calibration, no FNumber/Exposure Time or Radiometric Calibration EXIF tags found in %s. Using Digital Number." % photo.filename) return image if a1 is None and photometric_exp is not None: a1 = photometric_exp V, x, y = vignette_map(photo) if x is None: x, y = np.meshgrid(np.arange(photo.width), np.arange(photo.height)) if dark_level is not None: image -= dark_level # Normalize DN to 0 - 1.0 bit_depth_max = photo.get_bit_depth_max() if bit_depth_max: image /= bit_depth_max if V is not None: # vignette correction V = np.repeat(V[:, :, np.newaxis], image.shape[2], axis=2) image *= V if exposure_time and a2 is not None and a3 is not None: # row gradient correction R = 1.0 / (1.0 + a2 * y / exposure_time - a3 * y) R = np.repeat(R[:, :, np.newaxis], image.shape[2], axis=2) image *= R # Floor any negative radiances to zero (can happend due to noise around blackLevel) if dark_level is not None: image[image < 0] = 0 # apply the radiometric calibration - i.e. scale by the gain-exposure product and # multiply with the radiometric calibration coefficient if gain is not None and exposure_time is not None: image /= (gain * exposure_time) image *= a1 return image def vignette_map(photo): x_vc, y_vc = photo.get_vignetting_center() polynomial = photo.get_vignetting_polynomial() if x_vc and polynomial: # append 1., so that we can call with numpy polyval polynomial.append(1.0) vignette_poly = np.array(polynomial) # perform vignette correction # get coordinate grid across image x, y = np.meshgrid(np.arange(photo.width), np.arange(photo.height)) # meshgrid returns transposed arrays # x = x.T # y = y.T # compute matrix of distances from image center r = np.hypot((x - x_vc), (y - y_vc)) # compute the vignette polynomial for each distance - we divide by the polynomial so that the # corrected image is image_corrected = image_original * vignetteCorrection vignette = 1.0 / np.polyval(vignette_poly, r) return vignette, x, y return None, None, None def dn_to_reflectance(photo, image, use_sun_sensor=True): radiance = dn_to_radiance(photo, image) irradiance = compute_irradiance(photo, use_sun_sensor=use_sun_sensor) return radiance * math.pi / irradiance def compute_irradiance(photo, use_sun_sensor=True): # Thermal? if photo.band_name == "LWIR": return 1.0 # Some cameras (Micasense) store the value (nice! just return) hirradiance = photo.get_horizontal_irradiance() if hirradiance is not None: return hirradiance # TODO: support for calibration panels if use_sun_sensor and photo.get_sun_sensor(): # Estimate it dls_orientation_vector = np.array([0,0,-1]) sun_vector_ned, sensor_vector_ned, sun_sensor_angle, \ solar_elevation, solar_azimuth = dls.compute_sun_angle([photo.latitude, photo.longitude], photo.get_dls_pose(), photo.get_utc_time(), dls_orientation_vector) angular_correction = dls.fresnel(sun_sensor_angle) # TODO: support for direct and scattered irradiance direct_to_diffuse_ratio = 6.0 # Assumption, clear skies spectral_irradiance = photo.get_sun_sensor() percent_diffuse = 1.0 / direct_to_diffuse_ratio sensor_irradiance = spectral_irradiance / angular_correction # Find direct irradiance in the plane normal to the sun untilted_direct_irr = sensor_irradiance / (percent_diffuse + np.cos(sun_sensor_angle)) direct_irradiance = untilted_direct_irr scattered_irradiance = untilted_direct_irr * percent_diffuse # compute irradiance on the ground using the solar altitude angle horizontal_irradiance = direct_irradiance * np.sin(solar_elevation) + scattered_irradiance return horizontal_irradiance elif use_sun_sensor: log.ODM_WARNING("No sun sensor values found for %s" % photo.filename) return 1.0