#!/usr/bin/env python3 """ Copyright (c) 2020, Carleton University Biomedical Informatics Collaboratory This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. """ from typing import List import numpy as np from digitizer.report_components.line import Line def compute_deviation_sum(angle: float, lines: List[Line]) -> float: """Given a candidate angle and a list of lines, computes the sum of the deviation of these lines from the horizontal or vertical axis. Parameters ---------- angle : float The candidate angle in degrees. lines : List[Line] All the lines used in computing the sum of deviation. Returns ------- float The sum of the deviations from all lines with the vertical or horizontal axis. """ residual_angle_sum = 0 for line in lines: if line.get_angle() > -45 and line.get_angle() < 45: residual = abs((line.get_angle() - angle)) residual_angle_sum += residual else: residual = 90 - abs(line.get_angle() - angle) residual_angle_sum += residual return abs(residual_angle_sum) def compute_rotation_angle(perpendicular_lines: List[Line]) -> float: """Given a list of lines, returns the angle that must be applied to the image so that lines that all lines that intersect another line at roughly a right angle (+/- some tolerance) as close to vertical or horizontal as possible. Parameters ---------- perpendicular_lines : List[Line} The lines extracted from the image. These lines are expected to come from an isolated audiogram grid. tolerance : float Two lines intersecting at 90 +/- `tolerance` degrees are considered perpendicular. Returns ------- float The correction angle that must be applied to the image so as to make perpendicular lines as close as possible to horizontal or vertical. """ # Find the angle that minimizes the sum of distances to the nearest axis #angle. angle_range = np.arange(-44, 44, step=0.5) errors = [ compute_deviation_sum(angle, perpendicular_lines) for angle in angle_range ] correction_angle = angle_range[np.argmin(errors)] return correction_angle def apply_rotation(point: dict, rotation_angle: float) -> dict: new_x = (np.cos(rotation_angle) * point["x"] - np.sin(rotation_angle) * -point["y"]) new_y = (np.sin(rotation_angle) * point["x"] + np.cos(rotation_angle) * -point["y"]) return { **point, "x": new_x, "y": -new_y } def get_bounding_box_relative_to_original_report(bounding_box, audiogram_coordinates, correction_angle): absolute_corrected_coordinates = { "x": bounding_box["x"] + audiogram_coordinates["x"], "y": bounding_box["y"] + audiogram_coordinates["y"] } raw_coordinates = apply_rotation(absolute_corrected_coordinates, -correction_angle) correction_angle_rad = np.radians(correction_angle) side_length = bounding_box["width"] * np.sin(correction_angle_rad) + bounding_box["width"] * np.cos(correction_angle_rad) if correction_angle_rad <= 0: return { "x": absolute_corrected_coordinates["x"] - bounding_box["width"] * np.sin(correction_angle_rad), "y": absolute_corrected_coordinates["y"], "width": side_length, "height": side_length } else: return { "x": absolute_corrected_coordinates["x"], "y": absolute_corrected_coordinates["y"] - bounding_box["height"] * np.sin(correction_angle_rad), "width": side_length, "height": side_length }