Skip to content

Commit

Permalink
standardise exception, log & docstring quoting
Browse files Browse the repository at this point in the history
  • Loading branch information
dugalh committed Oct 4, 2023
1 parent 0374f88 commit 226f7fc
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 163 deletions.
38 changes: 20 additions & 18 deletions simple_ortho/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,18 @@ def _get_intrinsic(
# TODO: incorporate orientation from exif

if len(im_size) != 2:
raise ValueError('`im_size` should contain 2 values: (width, height).')
raise ValueError("'im_size' should contain 2 values: (width, height).")
im_size = np.array(im_size)
if sensor_size is not None and len(sensor_size) != 2:
raise ValueError('`sensor_size` should contain 2 values: (width, height).')
raise ValueError("'sensor_size' should contain 2 values: (width, height).")
focal_len = np.array(focal_len)
if focal_len.size > 2:
raise ValueError('`focal_len` should contain at most 2 values.')
raise ValueError("'focal_len' should contain at most 2 values.")

# find the xy focal lengths in pixels
if sensor_size is None:
logger.warning(
'`sensor_size` not specified, assuming square pixels and `focal_len` normalised by sensor width.'
"'sensor_size' not specified, assuming square pixels and 'focal_len' normalised by sensor width."
)
sigma_xy = (focal_len * im_size[0]) * np.ones(2)
else:
Expand All @@ -114,7 +114,7 @@ def _get_extrinsic(
if xyz is None or opk is None:
return None, None
elif len(xyz) != 3 or len(opk) != 3:
raise ValueError('`xyz` and `opk` should contain 3 values.')
raise ValueError("'xyz' and 'opk' should contain 3 values.")

# See https://support.pix4d.com/hc/en-us/articles/202559089-How-are-the-Internal-and-External-Camera-Parameters
# -defined
Expand All @@ -130,15 +130,15 @@ def _get_extrinsic(
def _check_world_coordinates(xyz: np.ndarray):
""" Utility function to check world coordinate dimensions. """
if not (xyz.ndim == 2 and xyz.shape[0] == 3):
raise ValueError(f'`xyz` should be a 3xN 2D array.')
raise ValueError(f"'xyz' should be a 3xN 2D array.")
if xyz.dtype != np.float64:
raise ValueError(f'`xyz` should have float64 data type.')
raise ValueError(f"'xyz' should have float64 data type.")

@staticmethod
def _check_pixel_coordinates(ji: np.ndarray):
""" Utility function to check pixel coordinate dimensions. """
if not (ji.ndim == 2 and ji.shape[0] == 2):
raise ValueError(f'`ji` should be a 2xN 2D array.')
raise ValueError(f"'ji' should be a 2xN 2D array.")

def _check_init(self):
""" Utility function to check if exterior parameters are initialised. """
Expand All @@ -161,10 +161,10 @@ def _horizon_fov(self) -> bool:
def _get_undistort_intrinsic(self, alpha: float):
"""
Return a new camera intrinsic matrix for an undistorted image that is the same size as the source image.
`alpha` (0-1) controls the portion of the source included in the distorted image.
For `alpha`=1, the undistorted image includes all source pixels and some invalid (nodata) areas. For `alpha`=0,
the undistorted image includes the largest portion of the source image that allows all undistorted pixels to be
valid.
``alpha`` (0-1) controls the portion of the source included in the distorted image.
For ``alpha=1``, the undistorted image includes all source pixels and some invalid (nodata) areas. For
``alpha=0``, the undistorted image includes the largest portion of the source image that allows all undistorted
pixels to be valid.
"""
# Adapted from and equivalent to cv2.getOptimalNewCameraMatrix(newImageSize=self._im_size,
# centerPrincipalPoint=False).
Expand Down Expand Up @@ -201,8 +201,8 @@ def _get_rectangles(im_size: Union[Tuple[int, int], np.ndarray]):
return K_undistort

def _get_undistort_maps(self, alpha: float) -> Tuple[Optional[Tuple[np.ndarray, np.ndarray]], np.ndarray]:
# TODO: make a design decision if internal methods can have keyword args with default values or should be
# forced to positional args
# TODO: make a design decision if internal methods can have keyword args for __init__ args with default values
# or should be forced to positional args
"""" Return cv2.remap() maps for undistorting an image, and intrinsic matrix for undistorted image. """
return None, self._K

Expand Down Expand Up @@ -285,7 +285,7 @@ def pixel_to_world_z(self, ji: np.ndarray, z: Union[float, np.ndarray], distort:
isinstance(z, np.ndarray) and
(z.ndim != 1 or (z.shape[0] != 1 and ji.shape[1] != 1 and z.shape[0] != ji.shape[1]))
): # yapf: disable
raise ValueError(f'`z` should be a single value or 1-by-N array where `ji` is 2-by-N or 2-by-1.')
raise ValueError(f"'z' should be a single value or 1-by-N array where 'ji' is 2-by-N or 2-by-1.")

# transform pixel coordinates to camera coordinates
xyz_ = self._pixel_to_camera(ji) if distort else Camera._pixel_to_camera(self, ji)
Expand Down Expand Up @@ -447,10 +447,12 @@ def __init__(
Undistorted image scaling (0-1). 0 results in an undistorted image with all valid pixels. 1 results in an
undistorted image that keeps all source pixels.
"""
Camera.__init__(self, im_size, focal_len, sensor_size=sensor_size, cx=cx, cy=cy, xyz=xyz, opk=opk)

OpenCVCamera.__init__(
self, im_size, focal_len, sensor_size=sensor_size, k1=k1, k2=k2, p1=p1, p2=p2, k3=k3, cx=cx, cy=cy,
xyz=xyz, opk=opk, alpha=alpha
)
# ensure all coefficients are in _dist_param for _camera_to_pixel
self._dist_param = np.array([k1, k2, p1, p2, k3])
self._undistort_maps, self._K_undistort = self._get_undistort_maps(alpha)

def _camera_to_pixel(self, xyz_: np.ndarray) -> np.ndarray:
# Brown model adapted from the OpenSFM implementation:
Expand Down
64 changes: 37 additions & 27 deletions simple_ortho/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ def _read_src_crs(filename: Path) -> Optional[rio.CRS]:
""" Read CRS from source image file. """
with suppress_no_georef(), rio.open(filename, 'r') as im:
if not im.crs:
logger.debug(f'No CRS found for source image: {filename.name}')
logger.debug(f"No CRS found for source image: '{filename.name}'")
else:
logger.debug(f"Found source image '{filename.name}' CRS: '{im.crs.to_proj4()}'")
return im.crs


Expand All @@ -123,7 +125,7 @@ def _crs_cb(ctx: click.Context, param: click.Parameter, crs: str):
# read CRS from string
crs = rio.CRS.from_string(crs)
except Exception as ex:
raise click.BadParameter(f'{crs}. {str(ex)}', param=param)
raise click.BadParameter(f'{str(ex)}', param=param)
if crs.is_geographic:
raise click.BadParameter(f"CRS should be a projected, not geographic system.", param=param)
return crs
Expand All @@ -144,7 +146,7 @@ def _odm_proj_dir_cb(ctx: click.Context, param: click.Parameter, proj_dir: Path)
for req_path in req_paths:
req_path = proj_dir.joinpath(req_path)
if not req_path.exists():
raise click.BadParameter(f'Could not find {req_path}.', param=param)
raise click.BadParameter(f"Could not find '{req_path}'.", param=param)
return proj_dir


Expand All @@ -171,41 +173,43 @@ def _ortho(
ext_param = ext_param_dict.get(src_file.name, ext_param_dict.get(src_file.stem, None))
if not ext_param:
raise click.BadParameter(
f"Could not find parameters for '{src_file.name}'.", param_hint='--ext-param'
f"Could not find parameters for '{src_file.name}'.", param_hint="'-ep' / '--ext-param'"
)

# get interior params for ext_param
if ext_param['camera']:
cam_id = ext_param['camera']
if cam_id not in int_param_dict:
raise click.BadParameter(f"Could not find parameters for camera '{cam_id}'.", param_hint='--int-param')
raise click.BadParameter(
f"Could not find parameters for camera '{cam_id}'.", param_hint="'-ip' / '--int-param'"
)
int_param = int_param_dict[cam_id]
elif len(int_param_dict) == 1:
cam_id = None
int_param = list(int_param_dict.values())[0]
else:
raise click.BadParameter(f"'camera' ID for {src_file.name} should be specified.", param_hint='--ext-param')
raise click.BadParameter(
f"'camera' ID for '{src_file.name}' should be specified.", param_hint="'-ep' / '--ext-param'"
)

# get camera if it exists, otherwise create camera, then update with exterior parameters and store
camera = cameras[cam_id] if cam_id in cameras else create_camera(**int_param, alpha=alpha)
camera.update(xyz=ext_param['xyz'], opk=ext_param['opk'])
cameras[cam_id] = camera

# create camera and ortho objects
# TODO: generalise exterior params / camera so that the cli can just expand the dict and not need to know
# about the internals
try:
ortho = Ortho(src_file, dem_file, camera, crs, dem_band=dem_band)
except DemBandError as ex:
raise click.BadParameter(str(ex), param_hint='--dem_band')
raise click.BadParameter(str(ex), param_hint="'-db' / '--dem_band'")
ortho_file = out_dir.joinpath(f'{src_file.stem}_ORTHO.tif')

# orthorectify
logger.info(f'Orthorectifying {src_file.name} ({src_i +1 } of {len(src_files)}):')
logger.info(f"Orthorectifying '{src_file.name}' ({src_i +1 } of {len(src_files)}):")
ortho.process(ortho_file, overwrite=overwrite, **kwargs)


# TODO: add mosaic, and write param options
# TODO: add mosaic and lla-crs options
# Define click options that are common to more than one command
src_files_arg = click.argument(
'src_files', nargs=-1, metavar='SOURCE...', type=click.Path(exists=True, dir_okay=False, path_type=Path),
Expand Down Expand Up @@ -343,7 +347,7 @@ def ortho(
parameters are supported in orthority (.geojson), custom CSV (.csv), and ODM / OpenSfM reconstruction (.json)
formats. Note that parameter file extensions are used to distinguish their format.
If possible, an ortho CRS will be read from other sources, or auto-determined when :option:`--crs <oty-ortho
If possible, a world / ortho CRS will be read from other sources, or auto-determined when :option:`--crs <oty-ortho
--crs>` is not passed.
See the `online docs <?>`_ for more detail on file formats and CRS.
Expand All @@ -362,9 +366,9 @@ def ortho(
oty ortho --int-param reconstruction.json --ext-param reconstruction.json --write-params
Orthorectify images matching `*rgb.tif` using DEM `dem.tif`, and `int_param.yaml` interior &
`ext_param.csv` exterior parameter files. Specify a 1m ortho resolution and `EPSG:32651` CRS. Write ortho files to
the `data` directory using `deflate` compression and a `uint16` data type::
Orthorectify images matching '*rgb.tif' using DEM 'dem.tif', and 'int_param.yaml' interior &
'ext_param.csv' exterior parameter files. Specify a 1m ortho resolution and 'EPSG:32651' CRS. Write ortho files to
the 'data' directory using 'deflate' compression and a 'uint16' data type::
oty ortho --dem dem.tif --int-param int_param.yaml --ext-param ext_param.csv --res 1 --crs EPSG:32651 --out-dir data --compress deflate --dtype uint16 *rgb.tif
Expand All @@ -386,9 +390,11 @@ def ortho(
elif int_param_file.suffix.lower() == '.json':
int_param_dict = io.read_osfm_int_param(int_param_file)
else:
raise click.BadParameter(f"'{int_param_file.suffix}' file type not supported.", param_hint='--int-param')
raise click.BadParameter(
f"'{int_param_file.suffix}' file type not supported.", param_hint="'-ip' / '--int-param'"
)
except ParamFileError as ex:
raise click.BadParameter(str(ex), param_hint='--int-param')
raise click.BadParameter(str(ex), param_hint="'-ip' / '--int-param'")

# read exterior params
try:
Expand All @@ -399,10 +405,12 @@ def ortho(
elif ext_param_file.suffix.lower() == '.geojson':
reader = io.OtyReader(ext_param_file, crs=crs)
else:
raise click.BadParameter(f"'{ext_param_file.suffix}' file type not supported.", param_hint='--ext-param')
raise click.BadParameter(
f"'{ext_param_file.suffix}' file type not supported.", param_hint="'-ep' / '--ext-param'"
)

except ParamFileError as ex:
raise click.BadParameter(str(ex), param_hint='--ext-param')
raise click.BadParameter(str(ex), param_hint="'-ep' / '--ext-param'")

except CrsMissingError:
raise click.MissingParameter(param_hint="'-c' / '--crs'", param_type='option')
Expand Down Expand Up @@ -450,11 +458,11 @@ def exif(src_files: Tuple[Path, ...], crs: rio.CRS, **kwargs):
Examples
========
Orthorectify images matching `*rgb.tif` with DEM `dem.tif`::
Orthorectify images matching '*rgb.tif' with DEM 'dem.tif'::
oty exif --dem dem.tif *rgb.tif
Write internal and external parameters for images matching `*rgb.tif` to orthority format files, and exit::
Write internal and external parameters for images matching '*rgb.tif' to orthority format files, and exit::
oty exif --write-params *rgb.tif
Expand Down Expand Up @@ -508,24 +516,24 @@ def odm(proj_dir: Path, resolution: Tuple[float, float], **kwargs):
and can be generated by running ODM with the `--dsm <https://docs.opendronemap.org/arguments/dsm/#dsm>`_ option.
By default, the ortho resolution is read from the ODM orthophoto. If that does not exist, it is read from the DSM.
Ortho images & parameter files are placed in the `<odm project>/orthority` directory.
Ortho images & parameter files are placed in the '<odm project>/orthority' directory.
The ``ortho`` sub-command can be used for more control over options.
\b
Examples
========
Orthorectify images in `<odm project>/images` directory using the `<odm project>` camera models and DSM::
Orthorectify images in '<odm project>/images' directory using the '<odm project>' camera models and DSM::
oty odm --proj-dir <odm project>
Write the `<odm project>` interior and exterior parameters to orthority format files, and exit::
Write the '<odm project>' interior and exterior parameters to orthority format files, and exit::
oty odm --proj-dir <odm project> --write-params
Orthorectify images in `<odm project>/images` directory using the `<odm project>` camera models and DSM. Use an
ortho resolution of 0.1m and `lanczos` interpolation to remap source to ortho.
Orthorectify images in '<odm project>/images' directory using the '<odm project>' camera models and DSM. Use an
ortho resolution of 0.1m and 'lanczos' interpolation to remap source to ortho.
oty odm --proj-dir <odm project> --res 0.1 --interp lanczos
"""
Expand All @@ -534,7 +542,9 @@ def odm(proj_dir: Path, resolution: Tuple[float, float], **kwargs):
src_exts = ['.jpg', '.jpeg', '.tif', '.tiff']
src_files = tuple([p for p in proj_dir.joinpath('images').glob('*.*') if p.suffix.lower() in src_exts])
if len(src_files) == 0:
raise click.BadParameter(f'No images found in {proj_dir.joinpath("images")}.', param_hint='--odm-root')
raise click.BadParameter(
f"No images found in '{proj_dir.joinpath('images')}'.", param_hint="'-pd' / '--proj-dir'"
)

# set crs and resolution from ODM orthophoto or DSM
orthophoto_file = proj_dir.joinpath('odm_orthophoto', 'odm_orthophoto.tif')
Expand Down
6 changes: 3 additions & 3 deletions simple_ortho/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def from_odm(cls, cam_type: str):
""" Convert from ODM / OpenSFM projection type. """
cam_type = 'brown' if cam_type == 'perspective' else cam_type
if cam_type not in cls.__members__:
raise ValueError(f'Unsupported ODM / OpenSFM camera type: {cam_type}')
raise ValueError(f"Unsupported ODM / OpenSFM camera type: '{cam_type}'")
return cls(cam_type)


Expand Down Expand Up @@ -87,7 +87,7 @@ def __str__(self):

@classmethod
def cv_list(cls) -> List:
""" A list of OpenCV compatible `Interp` values. """
""" A list of OpenCV compatible :class:`~rasterio.enums.Interp` values. """
_cv_list = []
for interp in list(cls):
try:
Expand All @@ -104,7 +104,7 @@ def to_cv(self) -> int:
nearest=cv2.INTER_NEAREST,
)
if self._name_ not in name_to_cv:
raise ValueError(f'OpenCV does not support `{self._name_}` interpolation.')
raise ValueError(f"OpenCV does not support '{self._name_}' interpolation.")
return name_to_cv[self._name_]

def to_rio(self) -> Resampling:
Expand Down
4 changes: 2 additions & 2 deletions simple_ortho/exif.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def __init__(self, filename: Union[str, Path]):
"""
filename = Path(filename)
if not filename.exists():
raise FileNotFoundError(f'File not found: {filename}')
raise FileNotFoundError(f"File not found: '{filename}'")

with suppress_no_georef(), rio.open(filename, 'r') as ds:
namespaces = ds.tag_namespaces()
Expand All @@ -119,7 +119,7 @@ def __init__(self, filename: Union[str, Path]):
xmp_str = ds.tags(ns='xml:XMP')['xml:XMP'].strip('xml:XMP=')
xmp_dict = xml_to_flat_dict(xmp_str)
else:
logger.warning(f'{filename.name} contains no XMP metadata')
logger.warning(f"'{filename.name}' contains no XMP metadata")
xmp_dict = {}

self._filename = filename
Expand Down
Loading

0 comments on commit 226f7fc

Please sign in to comment.