kopia lustrzana https://github.com/OpenDroneMap/ODM
153 wiersze
5.3 KiB
Python
153 wiersze
5.3 KiB
Python
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 |