weiren119's picture
Feat: app.py
34acdd0
raw
history blame
7.74 kB
#!/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)