Module themisasi.fov
Source code
import logging
import xarray
import numpy as np
from typing import Tuple
from pymap3d.haversine import anglesep
import pymap3d as pm
from pymap3d.vincenty import vdist
import histutils.findnearest as fnd
try:
import scipy.ndimage as ndi
except ImportError:
ndi = None
def getimgind(imgs: xarray.Dataset, lla: np.ndarray,
az: np.ndarray, el: np.ndarray) -> np.ndarray:
""" find pixels in images according to lat,lon,alt or az,el spec"""
if lla is not None:
lla = np.atleast_2d(lla)
assert lla.ndim == 2
assert lla.shape[1] == 3
N = lla.shape[0]
elif az is not None and el is not None:
az = np.atleast_1d(az)
el = np.atleast_1d(el)
assert az.ndim == 1 and el.ndim == 1
N = az.size
else:
raise ValueError('must specify LLA or az/el')
# %% work
ind = np.empty((N, 2), dtype=int)
if lla is not None:
az, el, _ = pm.geodetic2aer(lla[:, 0], lla[:, 1], lla[:, 2]*1000,
imgs.lat, imgs.lon, imgs.alt_m)
for i, (a, e) in enumerate(zip(az, el)):
ind[i, :] = fnd.findClosestAzel(imgs.az, imgs.el, a, e)
ind = np.unique(ind, axis=0)
assert ind.ndim == 2
assert ind.shape[1] == 2 # row, column
return ind
def projected_coord(imgs: xarray.Dataset, ind: np.ndarray, lla: Tuple[float, float, float]):
az = imgs.az[ind[:, 0], ind[:, 1]].values.squeeze()
el = imgs.el[ind[:, 0], ind[:, 1]].values.squeeze()
alt_m = lla[2]*1000 if lla is not None else 100e3
plat, plon, palt_m = pm.aer2geodetic(az, el, alt_m / np.sin(np.radians(el)),
imgs.lat, imgs.lon, imgs.alt_m)
return az, el, plat, plon, palt_m
def line2plane(cam: xarray.Dataset) -> xarray.Dataset:
"""
previously, you find the row, col pixels along the line from the other camera to magnetic zenith.
In this function, you find the indices of the plane passing through that line and both cameras.
Inputs:
-------
row: row indices of line
col: column indices of line
nPlane: number of samples (pixels) to take for plane.
If the plane is horizontal in the images, it would be approximately the number of x-pixels.
If diagonal, it would be about sqrt(2)*Nx.
If less, the image is sampled more sparsely, which is in effect recducing the resolution of the image.
This parameter is not critical.
"""
# %% linear regression
polycoeff = np.polyfit(cam['cols'], cam['rows'], deg=3, full=False)
# %% columns (x) to cut from picture (have to pick either rows or cols, arbitrarily I use cols)
# NOT range, NEED dtype= for arange api
# %% rows (y) to cut from picture
cam['cutrow'] = np.rint(np.polyval(polycoeff, cam['cutcol'])).astype(int)
assert (cam['cutrow'] >= 0).all() and (cam['cutrow'] < cam.y.size).all(
), 'impossible least squares fit for 1-D cut\n is your video orientation correct? are you outside the FOV?'
# DONT DO THIS: cutrow.clip(0,self.supery,cutrow)
# %% angle from magnetic zenith corresponding to those pixels
anglesep_deg = anglesep(cam.Bel, cam.Baz,
# NEED .values or you'll get 2-D instead of 1-D!
cam.el.values[cam.cutrow, cam.cutcol],
cam.az.values[cam.cutrow, cam.cutcol])
if cam.verbose:
from maatplotlib.pyplot import figure
ax = figure().gca()
ax.plot(cam.cutcol, anglesep_deg, label='angle_sep from MZ')
ax.plot(cam.cutcol, cam.el.values[cam.cutrow,
cam.cutcol], label='elevation angle [deg]')
ax.legend()
assert anglesep_deg.ndim == 1
if not np.isfinite(anglesep_deg).all():
logging.error(f'did you pick areas outside the {cam.filename} FOV?')
# %% assemble angular distances from magnetic zenith--these are used in the tomography algorithm
cam['angle_deg'], cam['angleMagzenind'] = sky2beam(
anglesep_deg, cam.cutcol.size)
return cam
def sky2beam(anglesep_deg, Ncol: int):
angle_deg = np.empty(Ncol, float)
# whether minimum angle distance from MZ is slightly positive or slightly negative, this should be OK
MagZenInd = anglesep_deg.argmin()
angle_deg[MagZenInd:] = 90. + anglesep_deg[MagZenInd:]
angle_deg[:MagZenInd] = 90. - anglesep_deg[:MagZenInd]
# %% LSQ
col = np.arange(Ncol, dtype=int)
polycoeff = np.polyfit(col, angle_deg, deg=1, full=False)
return np.polyval(polycoeff, col), MagZenInd
def mergefov(w0: xarray.Dataset, w1: xarray.Dataset, projalt: float = 110e3, method: str = None):
"""
inputs:
-------
w0: wide FOV data, particularly az/el
w1: other camera FOV data contained in w0
projalt: projection altitude METERS
ofn: plot filename
fovrow: to vastly speedup intiial overlap exploration, pick row(s) of pixels to examing--it could take half an hour + otherwise.
find the ECEF x,y,z, at 110km altitude for narrow camera outer pixel
boundary, then find the closest pixels in the wide FOV to those points.
Remember, it can be (much) faster to brute force this calculation than to use
k-d tree.
"""
if projalt < 1e3:
logging.warning(
f'this function expects meters, you picked projection altitude {projalt/1e3} km')
# %% print distance from wide camera to narrow camera (just for information)
print(
f"intercamera distance with {w0.site}: {vdist(w0.lat,w0.lon, w1.lat,w1.lon)[0]/1e3:.1f} kilometers")
# %% ENU projection from cam0 to cam1
e1, n1, u1 = pm.geodetic2enu(w1.lat, w1.lon, w1.alt_m,
w0.lat, w0.lon, w0.alt_m)
# %% find the ENU of narrow FOV pixels at 110km from narrow FOV
w1 = pixelmask(w1, method)
if method is not None and method.lower() == 'mzslice':
w0 = pixelmask(w0, method)
azSlice0, elSlice0, rSlice0 = pm.ecef2aer(w1.x2mz, w1.y2mz, w1.z2mz,
w0.lat, w0.lon, w0.alt_m)
azSlice1, elSlice1, rSlice1 = pm.ecef2aer(w0.x2mz, w0.y2mz, w0.z2mz,
w1.lat, w1.lon, w1.alt_m)
# find image indices (mask) corresponding to slice az,el
w0['rows'], w0['cols'] = fnd.findClosestAzel(w0['az'].where(w0['fovmask']),
w0['el'].where(w0['fovmask']),
azSlice0, elSlice0)
w0.attrs['Brow'], w0.attrs['Bcol'] = fnd.findClosestAzel(
w0['az'], w0['el'], w0.Baz, w0.Bel)
w1['rows'], w1['cols'] = fnd.findClosestAzel(w1['az'].where(w1['fovmask']),
w1['el'].where(w1['fovmask']),
azSlice1, elSlice1)
w1.attrs['Brow'], w1.attrs['Bcol'] = fnd.findClosestAzel(
w1['az'], w1['el'], w1.Baz, w1.Bel)
else:
# csc(x) = 1/sin(x)
slantrange = projalt / \
np.sin(np.radians(np.ma.masked_invalid(
w1['el'].where(w1['fovmask']))))
assert (slantrange >= projalt).all(
), 'slantrange must be >= projection altitude'
e0, n0, u0 = pm.aer2enu(w1['az'], w1['el'], slantrange)
# %% find az,el to narrow FOV from ASI FOV
az0, el0, _ = pm.enu2aer(e0 - e1, n0 - n1, u0 - u1)
assert (el0 >= 0).all(
), 'FOVs may not overlap, negative elevation from cam0 to cam1'
# %% nearest neighbor brute force
w0['rows'], w0['cols'] = fnd.findClosestAzel(w0['az'], w0['el'], az0, el0)
return w0, w1
def pixelmask(data: xarray.Dataset, method: str = None) -> xarray.Dataset:
"""
Use list because image may not be square
returns mask of chosen pixels
"""
if method is None or method == 'rect': # outermost edge of image (trivial)
mask = np.zeros(data['az'].shape, dtype=bool)
mask[:, 0] = True
mask[0, :] = True
mask[:, -1] = True
mask[-1, :] = True
elif method == 'perimeter': # perimeter for arbitrary shapes e.g all-sky cameras
if ndi is None:
raise ImportError('pip install scipy')
mask = ndi.distance_transform_cdt(~np.isnan(data['az']), 'taxicab') == 1
elif method.lower() == 'mzslice':
"""
Assuming the imagers are all-sky, we arbitrarily discard pixels of low elevation as distortion is high low to horizon.
"""
MIN_EL = 5 # degrees, arbitrary
if data.srpts is None:
return ValueError('must include slant range points')
mask = np.zeros(data['az'].shape, dtype=bool)
mask[data['el'] >= MIN_EL] = True
data['fovmask'] = (('y', 'x'), mask)
data.attrs['x2mz'], data.attrs['y2mz'], data.attrs['z2mz'] = pm.aer2ecef(data.attrs['Baz'], data.attrs['Bel'],
data.srpts,
data.attrs['lat'], data.attrs['lon'],
data.attrs['alt_m'])
return data
else:
raise ValueError(f'unknown mask {method}')
# %% sanity check
if ~mask.any():
raise ValueError('no FOV overlap found')
data['fovmask'] = (('y', 'x'), mask)
return data
Functions
def getimgind(imgs, lla, az, el)
-
find pixels in images according to lat,lon,alt or az,el spec
Source code
def getimgind(imgs: xarray.Dataset, lla: np.ndarray, az: np.ndarray, el: np.ndarray) -> np.ndarray: """ find pixels in images according to lat,lon,alt or az,el spec""" if lla is not None: lla = np.atleast_2d(lla) assert lla.ndim == 2 assert lla.shape[1] == 3 N = lla.shape[0] elif az is not None and el is not None: az = np.atleast_1d(az) el = np.atleast_1d(el) assert az.ndim == 1 and el.ndim == 1 N = az.size else: raise ValueError('must specify LLA or az/el') # %% work ind = np.empty((N, 2), dtype=int) if lla is not None: az, el, _ = pm.geodetic2aer(lla[:, 0], lla[:, 1], lla[:, 2]*1000, imgs.lat, imgs.lon, imgs.alt_m) for i, (a, e) in enumerate(zip(az, el)): ind[i, :] = fnd.findClosestAzel(imgs.az, imgs.el, a, e) ind = np.unique(ind, axis=0) assert ind.ndim == 2 assert ind.shape[1] == 2 # row, column return ind
def line2plane(cam)
-
previously, you find the row, col pixels along the line from the other camera to magnetic zenith. In this function, you find the indices of the plane passing through that line and both cameras.
Inputs:
row: row indices of line col: column indices of line nPlane: number of samples (pixels) to take for plane. If the plane is horizontal in the images, it would be approximately the number of x-pixels. If diagonal, it would be about sqrt(2)*Nx. If less, the image is sampled more sparsely, which is in effect recducing the resolution of the image. This parameter is not critical.
Source code
def line2plane(cam: xarray.Dataset) -> xarray.Dataset: """ previously, you find the row, col pixels along the line from the other camera to magnetic zenith. In this function, you find the indices of the plane passing through that line and both cameras. Inputs: ------- row: row indices of line col: column indices of line nPlane: number of samples (pixels) to take for plane. If the plane is horizontal in the images, it would be approximately the number of x-pixels. If diagonal, it would be about sqrt(2)*Nx. If less, the image is sampled more sparsely, which is in effect recducing the resolution of the image. This parameter is not critical. """ # %% linear regression polycoeff = np.polyfit(cam['cols'], cam['rows'], deg=3, full=False) # %% columns (x) to cut from picture (have to pick either rows or cols, arbitrarily I use cols) # NOT range, NEED dtype= for arange api # %% rows (y) to cut from picture cam['cutrow'] = np.rint(np.polyval(polycoeff, cam['cutcol'])).astype(int) assert (cam['cutrow'] >= 0).all() and (cam['cutrow'] < cam.y.size).all( ), 'impossible least squares fit for 1-D cut\n is your video orientation correct? are you outside the FOV?' # DONT DO THIS: cutrow.clip(0,self.supery,cutrow) # %% angle from magnetic zenith corresponding to those pixels anglesep_deg = anglesep(cam.Bel, cam.Baz, # NEED .values or you'll get 2-D instead of 1-D! cam.el.values[cam.cutrow, cam.cutcol], cam.az.values[cam.cutrow, cam.cutcol]) if cam.verbose: from maatplotlib.pyplot import figure ax = figure().gca() ax.plot(cam.cutcol, anglesep_deg, label='angle_sep from MZ') ax.plot(cam.cutcol, cam.el.values[cam.cutrow, cam.cutcol], label='elevation angle [deg]') ax.legend() assert anglesep_deg.ndim == 1 if not np.isfinite(anglesep_deg).all(): logging.error(f'did you pick areas outside the {cam.filename} FOV?') # %% assemble angular distances from magnetic zenith--these are used in the tomography algorithm cam['angle_deg'], cam['angleMagzenind'] = sky2beam( anglesep_deg, cam.cutcol.size) return cam
def mergefov(w0, w1, projalt=110000.0, method=None)
-
inputs:
w0: wide FOV data, particularly az/el w1: other camera FOV data contained in w0 projalt: projection altitude METERS ofn: plot filename fovrow: to vastly speedup intiial overlap exploration, pick row(s) of pixels to examing–it could take half an hour + otherwise.
find the ECEF x,y,z, at 110km altitude for narrow camera outer pixel boundary, then find the closest pixels in the wide FOV to those points. Remember, it can be (much) faster to brute force this calculation than to use k-d tree.
Source code
def mergefov(w0: xarray.Dataset, w1: xarray.Dataset, projalt: float = 110e3, method: str = None): """ inputs: ------- w0: wide FOV data, particularly az/el w1: other camera FOV data contained in w0 projalt: projection altitude METERS ofn: plot filename fovrow: to vastly speedup intiial overlap exploration, pick row(s) of pixels to examing--it could take half an hour + otherwise. find the ECEF x,y,z, at 110km altitude for narrow camera outer pixel boundary, then find the closest pixels in the wide FOV to those points. Remember, it can be (much) faster to brute force this calculation than to use k-d tree. """ if projalt < 1e3: logging.warning( f'this function expects meters, you picked projection altitude {projalt/1e3} km') # %% print distance from wide camera to narrow camera (just for information) print( f"intercamera distance with {w0.site}: {vdist(w0.lat,w0.lon, w1.lat,w1.lon)[0]/1e3:.1f} kilometers") # %% ENU projection from cam0 to cam1 e1, n1, u1 = pm.geodetic2enu(w1.lat, w1.lon, w1.alt_m, w0.lat, w0.lon, w0.alt_m) # %% find the ENU of narrow FOV pixels at 110km from narrow FOV w1 = pixelmask(w1, method) if method is not None and method.lower() == 'mzslice': w0 = pixelmask(w0, method) azSlice0, elSlice0, rSlice0 = pm.ecef2aer(w1.x2mz, w1.y2mz, w1.z2mz, w0.lat, w0.lon, w0.alt_m) azSlice1, elSlice1, rSlice1 = pm.ecef2aer(w0.x2mz, w0.y2mz, w0.z2mz, w1.lat, w1.lon, w1.alt_m) # find image indices (mask) corresponding to slice az,el w0['rows'], w0['cols'] = fnd.findClosestAzel(w0['az'].where(w0['fovmask']), w0['el'].where(w0['fovmask']), azSlice0, elSlice0) w0.attrs['Brow'], w0.attrs['Bcol'] = fnd.findClosestAzel( w0['az'], w0['el'], w0.Baz, w0.Bel) w1['rows'], w1['cols'] = fnd.findClosestAzel(w1['az'].where(w1['fovmask']), w1['el'].where(w1['fovmask']), azSlice1, elSlice1) w1.attrs['Brow'], w1.attrs['Bcol'] = fnd.findClosestAzel( w1['az'], w1['el'], w1.Baz, w1.Bel) else: # csc(x) = 1/sin(x) slantrange = projalt / \ np.sin(np.radians(np.ma.masked_invalid( w1['el'].where(w1['fovmask'])))) assert (slantrange >= projalt).all( ), 'slantrange must be >= projection altitude' e0, n0, u0 = pm.aer2enu(w1['az'], w1['el'], slantrange) # %% find az,el to narrow FOV from ASI FOV az0, el0, _ = pm.enu2aer(e0 - e1, n0 - n1, u0 - u1) assert (el0 >= 0).all( ), 'FOVs may not overlap, negative elevation from cam0 to cam1' # %% nearest neighbor brute force w0['rows'], w0['cols'] = fnd.findClosestAzel(w0['az'], w0['el'], az0, el0) return w0, w1
def pixelmask(data, method=None)
-
Use list because image may not be square
returns mask of chosen pixels
Source code
def pixelmask(data: xarray.Dataset, method: str = None) -> xarray.Dataset: """ Use list because image may not be square returns mask of chosen pixels """ if method is None or method == 'rect': # outermost edge of image (trivial) mask = np.zeros(data['az'].shape, dtype=bool) mask[:, 0] = True mask[0, :] = True mask[:, -1] = True mask[-1, :] = True elif method == 'perimeter': # perimeter for arbitrary shapes e.g all-sky cameras if ndi is None: raise ImportError('pip install scipy') mask = ndi.distance_transform_cdt(~np.isnan(data['az']), 'taxicab') == 1 elif method.lower() == 'mzslice': """ Assuming the imagers are all-sky, we arbitrarily discard pixels of low elevation as distortion is high low to horizon. """ MIN_EL = 5 # degrees, arbitrary if data.srpts is None: return ValueError('must include slant range points') mask = np.zeros(data['az'].shape, dtype=bool) mask[data['el'] >= MIN_EL] = True data['fovmask'] = (('y', 'x'), mask) data.attrs['x2mz'], data.attrs['y2mz'], data.attrs['z2mz'] = pm.aer2ecef(data.attrs['Baz'], data.attrs['Bel'], data.srpts, data.attrs['lat'], data.attrs['lon'], data.attrs['alt_m']) return data else: raise ValueError(f'unknown mask {method}') # %% sanity check if ~mask.any(): raise ValueError('no FOV overlap found') data['fovmask'] = (('y', 'x'), mask) return data
def projected_coord(imgs, ind, lla)
-
Source code
def projected_coord(imgs: xarray.Dataset, ind: np.ndarray, lla: Tuple[float, float, float]): az = imgs.az[ind[:, 0], ind[:, 1]].values.squeeze() el = imgs.el[ind[:, 0], ind[:, 1]].values.squeeze() alt_m = lla[2]*1000 if lla is not None else 100e3 plat, plon, palt_m = pm.aer2geodetic(az, el, alt_m / np.sin(np.radians(el)), imgs.lat, imgs.lon, imgs.alt_m) return az, el, plat, plon, palt_m
def sky2beam(anglesep_deg, Ncol)
-
Source code
def sky2beam(anglesep_deg, Ncol: int): angle_deg = np.empty(Ncol, float) # whether minimum angle distance from MZ is slightly positive or slightly negative, this should be OK MagZenInd = anglesep_deg.argmin() angle_deg[MagZenInd:] = 90. + anglesep_deg[MagZenInd:] angle_deg[:MagZenInd] = 90. - anglesep_deg[:MagZenInd] # %% LSQ col = np.arange(Ncol, dtype=int) polycoeff = np.polyfit(col, angle_deg, deg=1, full=False) return np.polyval(polycoeff, col), MagZenInd