Source code for upsp.cam_cal_utils.parsers

import xml.etree.ElementTree as ET
import json
import csv
import numpy as np


[docs]def read_tgts(tgts_file_path, output_target_types=None): """Returns the targets in the tgts file Parameters ---------- tgts_file_path : str File path to the tgts file output_target_types : str, list of str, optional If not None, only targets with a type of or in `output_target_types` will be read. If None, all target types will be read Returns ------- targets : list of dict List of targets. Each target is of the form:: { 'target_type' : class_string, 'tvec' : [x, y, z], 'norm', : [x, y, z], 'size': float, 'name': str, 'idx': int, 'zones': [i, j, k] } """ # Package output_target_types if (output_target_types is not None) and (type(output_target_types) is not list): output_target_types = [output_target_types] targets = [] with open(tgts_file_path, "r") as f: csv_reader = csv.reader(f, delimiter=" ") line_type = None for row in csv_reader: # Populate the line from the csv line = [] for item in row: if item != "": line.append(item) # If the length of the line is greater than 1, attempt to populate the # the list of targets if len(line) > 1: # Read in items listed under '*Targets' if line_type == "*Targets": # If the last element has 'st' for 'sharpie target' it is a dot if "st" in line[-1]: target_type = "dot" # If the last element has 'mK' for 'masked Kulite' it is a Kulite # (and visible to the camera) elif "mK" in line[-1]: target_type = "kulite" # If the last element has 'pK' for 'painted Kulite' it is a Kulite # (and not visible to the camera) elif "pK" in line[-1]: target_type = "painted_kulite" # Otherwise this item is unknown else: target_type = line[-1] # Ignore items in other categories of the tgts file else: continue # If output_target_types was not given, or the target_type is one of the # output_target_types given, grab this target if (output_target_types is None) or ( target_type in output_target_types ): targets.append( { "target_type": target_type, "tvec": np.expand_dims([float(x) for x in line[1:4]], 1), "norm": np.expand_dims([float(x) for x in line[4:7]], 1), "size": float(line[7]), "name": line[-1], "idx": int(line[0]), "zones": (int(line[8]), int(line[9]), int(line[10])), } ) # If the length of the line is 1 or 0, set the line_type to the line's item # if it has one, or set it to None if it has no items else: line_type = line[0] if len(line) == 1 else None return targets
[docs]def read_pascal_voc(annot_path): """Return image objects from a PASCAL VOC XML file Reads the input file(s) and returns an list of dicts, where each dict contains the class, bounding box, and flags of an image object. Parameters ---------- annot_path : list or str This can be a string, or a list of strings (or list-like). Each string is the path to a label Returns ------- out : dict or list If `annot_path` is a single label path, the output is a list of dicts. If `annot_path` is a list of label paths (or list-like), the output is a list of lists, where each inner list if a list of dicts. Each dict is represents an image object and has the keys 'class', 'x1', 'x2', 'y1', 'y2', 'difficult', and 'truncated'. """ # Assert that annot_path is of the right form error_msg = "Error in read_pascal_voc. Input 'annot_path' should be a filepath\ string or list (or list-like) of filepath strings." assert type(annot_path) is str or type(annot_path[0]) is str, error_msg # Check if annot_path is a string. If it is not, it should be a list (or list-like) if type(annot_path) is not str: output = [] for path in annot_path: output.append(read_pascal_voc(path)) return output # Converts the .xml into a tree file et = ET.parse(annot_path) element = et.getroot() # Parses PASCAL VOC data into python objects element_objs = element.findall("object") # Populate the annotation_data with the filepath, and image # width & height targets = [] for element_obj in element_objs: # Populate the class_count and class_mapping return variables class_name = element_obj.find("name").text # Populate bounding box information obj_bbox = element_obj.find("bndbox") x1 = int(round(float(obj_bbox.find("xmin").text))) y1 = int(round(float(obj_bbox.find("ymin").text))) x2 = int(round(float(obj_bbox.find("xmax").text))) y2 = int(round(float(obj_bbox.find("ymax").text))) difficulty = int(element_obj.find("difficult").text) truncated = int(element_obj.find("truncated").text) # LabelImg used the same coordinate system as OpenCV # LabelImg clips x1 and y1 to 1, even if the true value is 0 # For future work, that should be fixed in the annotations. if truncated: if x1 == 1: x1 = 0 if y1 == 1: y1 = 0 # Populate annotation_data's bounding box field targets.append( { "class": class_name, "x1": x1, "x2": x2, "y1": y1, "y2": y2, "difficult": difficulty, "truncated": truncated, } ) return targets
[docs]def read_wind_tunnel_data(wtd_file_path, items=("ALPHA", "BETA", "PHI", "STRUTZ")): """Read specified wind tunnel data from `wtd_file_path` Parameters ---------- wtd_file_path : str Filepath to wind tunnel data file items : container, optional Items requested from the file. By default ALPHA, BETA, PHI and STRUTZ are returned. Returns ------- tunnel_vals : dict Dictionary with keys of specified items and values of associated wtd values. """ # Read in the wind tunel data file with open(wtd_file_path, "r") as f: f.readline() csv_reader = csv.DictReader(f, delimiter="\t") tunnel_vals = next(csv_reader) # Convert values to floats tunnel_vals = {k: float(v) for k, v in tunnel_vals.items()} # Remove all but the relevant for k in list(tunnel_vals.keys()): if k not in items: del tunnel_vals[k] return tunnel_vals
[docs]def convert_cv2_cm_to_uPSP_cm(cameraMatrix, dims): """Converts a standard camera matrix to a uPSP camera matrix OpenCV (and the standard) cameraMatrix uses the absolute position of the optical principal point. Since uPSP often crops images, the absoulte position varies from configuration to configuration even if the optics haven't changed. So instead, the position of the principal point relative to the image center is saved. Parameters ---------- cameraMatrix : np.ndarray, shape (3, 3) Camera matrix of the form: ``[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]`` where f is the focal length and (cx, cy) is the absolute position of the optical principal point dims : tuple, length 2 Image dimensions (width, height) Returns ------- uPSP_cameraMatrix : np.ndarray, shape (3, 3) Converted camera matrix of the form: ``[[fx, 0, dcx], [0, fy, dcy], [0, 0, 1]]`` cx = w/2 + dcx and cy = h/2 + dcy where w and h are the image width and height See Also -------- convert_uPSP_cm_to_cv2_cm : inverse conversion """ # Cx (Principal Point X) cx = cameraMatrix[0][2] # Delta Cx (Principal Point Y) cy = cameraMatrix[1][2] # Get offset from image center and principal point dcx = cx - (dims[1] / 2) dcy = cy - (dims[0] / 2) # Return a copy of the uPSP cameraMatrix with the modified values uPSP_cameraMatrix = np.copy(cameraMatrix) uPSP_cameraMatrix[0][2] = dcx uPSP_cameraMatrix[1][2] = dcy return uPSP_cameraMatrix
[docs]def convert_uPSP_cm_to_cv2_cm(uPSP_cameraMatrix, dims): """Converts a uPSP camera matrix to a standard camera matrix OpenCV (and the standard) cameraMatrix uses the absolute position of the optical principal point. Since uPSP often crops images, the absoulte position varies from configuration to configuration even if the optics haven't changed. So instead, the position of the principal point relative to the image center is saved. To use OpenCV functions, it has to be converted back to the standard OpenCV camera matrix. Parameters ---------- uPSP_cameraMatrix : np.ndarray, shape (3, 3) Camera matrix of the form: ``[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]`` where f is the focal length and (cx, cy) is the absolute position of the optical principal point dims : tuple, length 2 Image dimensions (width, height) Returns ------- cameraMatrix : np.ndarray, shape (3, 3) Converted camera matrix of the form: ``[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]`` cx = w/2 + dcx and cy = h/2 + dcy where w and h are the image width and height See Also -------- convert_uPSP_cm_to_cv2_cm : inverse conversion """ # Delta Cx (Principal Point X) dcx = uPSP_cameraMatrix[0][2] # Delta Cx (Principal Point Y) dcy = uPSP_cameraMatrix[1][2] # Offset the image center by (dcx, dcy) image_center = (dims[1] / 2, dims[0] / 2) principal_point = (image_center[0] + dcx, image_center[1] + dcy) # Return a copy of the uPSP cameraMatrix with the modified values cameraMatrix = np.copy(uPSP_cameraMatrix) cameraMatrix[0][2] = principal_point[0] cameraMatrix[1][2] = principal_point[1] return cameraMatrix
[docs]def read_internal_params(internal_cal_path, dims, read_sensor=False): """ Returns the internal (intrinsic) camera parameters Parameters ---------- internal_cal_dir : str Path to internal calibrations dims : tuple Image dimensions (image height, image width) read_sensor: bool, optional If True, additionally read and return the sensor resolution and size Returns ------- cameraMatrix : np.ndarray, shape (3, 3) Camera matrix of the form ``[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]`` where f is the focal length and (cx, cy) is the position of the optical principal point relative to the image center distCoeffs : np.ndarray, shape (5,) Distortion coefficients of the form ``[k1, k2, p1, p2, k3]`` where k1, k2, and k3 are the radial distortion terms and p1 and p2 are the tangential distortion terms sensor_resolution : np.ndarray, shape (3,) Camera sensor resolution, only returned if `read_sensor` is True sensor_size : np.ndarray, shape (2,) Camera sensor physical size, only returned if `read_sensor` is True """ # Read the internal calibration parameters with open(internal_cal_path, "r") as f: incal = json.load(f) uPSP_cameraMatrix = np.array(incal["uPSP_cameraMatrix"]) distCoeffs = np.array(incal["distCoeffs"]) # Convert the written uPSP_cameraMatrix to the OpenCV cameraMatrix cameraMatrix = convert_uPSP_cm_to_cv2_cm(uPSP_cameraMatrix, dims) # If read_sensor is False, just return the cameraMatrix and distCoeffs if not read_sensor: return cameraMatrix, distCoeffs # Otherwise return the sensor parameters as well else: sensor_resolution = np.array(incal["sensor_resolution"]) sensor_size = np.array(incal["sensor_size"]) return cameraMatrix, distCoeffs, sensor_resolution, sensor_size
[docs]def read_camera_tunnel_cal(cal_path, dims, read_sensor=False): """ Returns the internal (intrinsic) and external (extrinsic) camera calibration parameters Parameters ---------- cal_path : str Path to camera calibration dims : tuple Image dimensions (image height, image width) read_sensor: bool, optional If True, additionally read and return the sensor resolution and size Returns ------- cameraMatrix : np.ndarray, shape (3, 3) Camera matrix of the form ``[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]`` where f is the focal length and (cx, cy) is the position of the optical principal point relative to the image center distCoeffs : np.ndarray, shape (5,) Distortion coefficients of the form ``[k1, k2, p1, p2, k3]`` where k1, k2, and k3 are the radial distortion terms and p1 and p2 are the tangential distortion terms rmat : np.ndarray, shape (3, 3) Rotation matrix from camera to tunnel tvec : np.ndarray, shape (3,) Translation vector from camera to tunnel sensor_resolution : np.ndarray, shape (2,) Sensor size of camera in pixels, only returned if `read_sensor` is True sensor_size : np.ndarray, shape (2,) Sensor size of camera in inches, only returned if `read_sensor` is True """ # Read the camera calibration parameters with open(cal_path, "r") as f: cal = json.load(f) uPSP_cameraMatrix = np.array(cal["uPSP_cameraMatrix"]) distCoeffs = np.array(cal["distCoeffs"]) tvec = np.array(cal["tvec"]).reshape(3, 1) rmat = np.array(cal["rmat"]) # Convert the written uPSP_cameraMatrix to the OpenCV cameraMatrix cameraMatrix = convert_uPSP_cm_to_cv2_cm(uPSP_cameraMatrix, dims) # If read_sensor is False, just return the cameraMatrix and distCoeffs if not read_sensor: return rmat, tvec, cameraMatrix, distCoeffs # Otherwise return the sensor parameters as well else: sensor_resolution = np.array(cal["sensor_resolution"]) sensor_size = np.array(cal["sensor_size"]) return rmat, tvec, cameraMatrix, distCoeffs, sensor_resolution, sensor_size
[docs]def read_json(path): """Safely reads a json file and returns the associated dict Parameters ---------- path : path-like File path to the tgts file Returns ------- dict dict of json file items """ # Safely read the json file with open(path, "r") as f: return json.load(f)
[docs]def read_test_config(path): """Safely reads a test config file and returns the associated dict with arrays Parameters ---------- path : path-like File path to the tgts file Returns ------- dict dict of json file items """ # Safely read the json file with open(path, "r") as f: test_config = json.load(f) # Transcribe the test config into numpy arrays if possible test_config_np = {} for key, val in test_config.items(): try: # If the length is 3, it is a (3, 1) or (3, 3) if len(val) == 3: if type(val[0]) == float: test_config_np[key] = np.expand_dims(val, 1) elif len(val[0]) == 3: test_config_np[key] = np.array(val) else: test_config_np[key] = val # If the length if not 3, it is a float or long array else: test_config_np[key] = val # If an exception is raised, just add the actual value for the key except Exception as e: test_config_np[key] = val return test_config_np