#!/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 from PIL import ImageDraw from digitizer.report_components.line import Line from digitizer.report_components.label import Label from digitizer.report_components.symbol import Symbol import utils.audiology as Audiology from utils.exceptions import InsufficientLinesException class Grid(object): def __init__(self, report, labels, threshold=150): lines = report.detect_lines(threshold=threshold) lines = [line for line in lines if line.is_vertical() or line.is_horizontal()] frequency_labels = [label for label in labels if label.is_frequency()] threshold_labels = [label for label in labels if label.is_threshold()] if len(lines) == 0 or \ all([line.is_vertical() for line in lines]) \ or all([line.is_horizontal() for line in lines]): raise InsufficientLinesException() x_lines = [label.find_closest_line(lines) for label in frequency_labels] x_pixels = [line[0].get_x() for line in x_lines] x_frequency = [label.get_value() for label in frequency_labels] self.x_distances = [line[1] for line in x_lines] y_lines = [label.find_closest_line(lines) for label in threshold_labels] y_pixels = [line[0].get_y() for line in y_lines] y_threshold = [label.get_value() for label in threshold_labels] self.y_distances = [line[1] for line in y_lines] x_points = sorted(list(zip(x_pixels, x_frequency)), key=lambda p: p[0]) y_points = sorted(list(zip(y_pixels, y_threshold)), key=lambda p: p[0]) # Take the first and last points for the octaves (frequencies) o_max = Audiology.frequency_to_octave(x_points[-1][1]) # max octave x_max = x_points[-1][0] # max pixel value o_min = Audiology.frequency_to_octave(x_points[0][1]) x_min = x_points[0][0] # Take the first and last points for the thresholds t_max = y_points[-1][1] # max threshold y_max = y_points[-1][0] # max pixel value t_min = y_points[0][1] y_min = y_points[0][0] if x_min == x_max or y_max == y_min: raise InsufficientLinesException() # Derive the forward and reverse mapping functions via simple linear # interpolation using the **OCTAVE SCALE** (which is linear), because # the frequency scale is logarithmic. self.pixel_freq_map = lambda p: Audiology.octave_to_frequency(o_min + (o_max - o_min)*(p - x_min)/(x_max - x_min)) self.freq_pixel_map = lambda f: x_min + (Audiology.frequency_to_octave(f) - o_min)*(x_max - x_min)/(o_max - o_min) # Linear interpolation can be applied directly to the thresholds, # because the threshold axis is linear. self.pixel_threshold_map = lambda p: t_min + (t_max - t_min)*(p - y_min)/(y_max - y_min) self.threshold_pixel_map = lambda t: y_min + (t - t_min)*(y_max - y_min)/(t_max - t_min) def get_x(self, frequency: float) -> int: """Given a frequency value, returns the x coordinate predicted by the grid. Parameters ---------- frequency : float The frequency value whose x-position on the image is to be determined. Returns ------- int The x position (in pixels) of the frequency, as predicted by the grid. """ return self.freq_pixel_map(frequency) def get_frequency(self, symbol: Symbol) -> float: """Returns the frequency of the symbol. Parameters ---------- symbol : Symbol The symbol whose frequency is to be extracted using the computed grid. Returns ------- float The frequency value (in Hz). """ return self.pixel_freq_map(symbol.get_center()["x"]) def get_snapped_frequency(self, symbol: Symbol, epsilon: float = 0.15) -> float: """Returns the frequency of the symbol, snapped to the nearest commonly recorded frequency (all octaves and select semi-octaves). Parameters ---------- symbol : Symbol The symbol whose frequency is to be extracted using the computed grid. epsilon: float Distance (in octaves) below which the bone threshold is snapped to the nearest frequency as opposed to shifted to the nearest threshold in the direction of the corresponding ear. Returns ------- int The `snapped-to-the-grid` frequency value (in Hz). """ if symbol.conduction == "air": return Audiology.round_frequency(self.pixel_freq_map(symbol.get_center()["x"])) else: return Audiology.round_frequency_bone(self.pixel_freq_map(symbol.get_center()["x"]), symbol.ear, epsilon=epsilon) def get_y(self, threshold: float) -> int: """Given a threshold value, returns the y coordinate predicted by the grid. Parameters ---------- threshold : float The threshold value whose y-position on the image is to be determined. Returns ------- int The y position (in pixels) of the threshold, as predicted by the grid. """ return self.threshold_pixel_map(threshold) def get_threshold(self, symbol: Symbol) -> int: """Returns the threshold of the symbol. Parameters ---------- symbol : Symbol The symbol whose threshold is to be extracted using the computed grid. Returns ------- int The threshold value. """ return self.pixel_threshold_map(symbol.get_center()["y"]) def get_snapped_threshold(self, symbol: Symbol) -> int: """Returns the threshold of the symbol, snapped to the nearest 5dB. Parameters ---------- symbol : Symbol The symbol whose threshold is to be extracted using the computed grid. Returns ------- int The `snapped-to-the-grid` threshold value. """ return Audiology.round_threshold(self.pixel_threshold_map(symbol.get_center()["y"])) def draw( self, image_drawer: ImageDraw, frequency_range: List[int] = [125, 8000], threshold_range: List[int] = [-10, 120], color: str = "rgb(255,0,0)" ): """Draws the calculated grid on the provided image. Parameters ---------- image : PIL.ImageDraw The `ImageDraw` object with which the grid is to be drawn. frequency_range : [int, int] The minimum and maximum value of the frequencies to be included (default: [250, 8000]). threshold_range : [int, int] The minimum and maximum value of the threshold to be included (default: [-10, 120]). color: str Color of the grid as a string of the form =`rgb(R,G,B)`. """ lines = [] for freq in Audiology.OCTAVE_FREQS_HZ: x = self.get_x(freq) y1 = self.get_y(threshold_range[0]) y2 = self.get_y(threshold_range[1]) line = Line(x, y1, x, y2, color=color, label=freq) line.draw(image_drawer) for threshold in Audiology.THRESHOLDS: x1 = self.get_x(frequency_range[0]) x2 = self.get_x(frequency_range[1]) y = self.get_y(threshold) line = Line(x1, y, x2, y, color=color, label=threshold) line.draw(image_drawer)