Spaces:
Runtime error
Runtime error
#!/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) | |