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, Optional, Type | |
import PIL.ImageDraw | |
import numpy as np | |
from interfaces import LabelDict | |
from utils.geometry import get_bounding_box_relative_to_original_report | |
import utils.audiology as Audiology | |
class Label(object): | |
def __init__(self, label_dict: dict, audiogram_coordinates: dict, correction_angle: float): | |
bbox = label_dict["boundingBox"] | |
self.p1 = { | |
"x": bbox["x"], | |
"y": bbox["y"] | |
} | |
self.p2 = { | |
"x": bbox["x"] + bbox["width"], | |
"y": bbox["y"] + bbox["height"] | |
} | |
self.dimensions = { | |
"width": bbox["width"], | |
"height": bbox["height"] | |
} | |
self.text = label_dict["text"] | |
self.absolute_bounding_box = get_bounding_box_relative_to_original_report(bbox, audiogram_coordinates, correction_angle) | |
def draw(self, canvas: PIL.ImageDraw): | |
"""Draws the label on the canvas (image) passed. | |
Parameters | |
---------- | |
canvas : PIL.ImageDraw | |
The PIL.ImageDraw on which the labels are to be displayed. | |
""" | |
color = "rgb(255,0,0)" if self.is_frequency() else "rgb(0,0,255)" | |
canvas.rectangle( | |
(self.p1["x"], self.p1["y"], self.p2["x"], self.p2["y"]), | |
outline=color, | |
width=3 | |
) | |
canvas.text((self.p1["x"], self.p1["y"] - 10), str(self.get_value()), fill=color) | |
def get_type(self) -> str: | |
"""Returns the type of label. | |
Returns | |
------- | |
str | |
The type of label (`threshold` or `frequency`). | |
""" | |
if self.is_frequency(): | |
return "frequency" | |
elif self.is_threshold(): | |
return "threshold" | |
else: | |
return None | |
def get_value(self) -> int: | |
"""Returns the numerical value of the label. | |
Returns | |
------- | |
int | |
The numerical value of the label (in dB if threshold, or in Hz if frequency). | |
""" | |
if not self.is_frequency() and not self.is_threshold(): | |
raise "Attempted to get the value of a label which is not a frequency or threshold." | |
raw_value = float(self.text.lower()\ | |
.rstrip("hz")\ | |
.rstrip("h")\ | |
.rstrip("khz")\ | |
.rstrip("k")) | |
# If the value extracted is < 100 and corresponds to one of the | |
# standard frequency values, the value is in kHz, which we can | |
# convert to Hz. | |
if self.is_frequency() and raw_value < 100: | |
return raw_value * 1000 | |
return raw_value | |
def is_frequency(self) -> bool: | |
"""Checks if the label corresponds to a frequency. | |
Returns | |
------- | |
bool | |
True if the label corresponds to a frequency, False otherwise. | |
""" | |
if not isinstance(self.text, str): | |
return False | |
try: | |
stripped_label = self.text.lower()\ | |
.rstrip("hz")\ | |
.rstrip("h")\ | |
.rstrip("khz")\ | |
.rstrip("k") | |
frequency_label = float(stripped_label) | |
return frequency_label in Audiology.OCTAVE_FREQS_HZ \ | |
or frequency_label in Audiology.OCTAVE_FREQS_KHZ | |
except ValueError: | |
return False # label cannot be converted to a float | |
def is_threshold(self) -> bool: | |
"""Checks if the label corresponds to a threshold. | |
Returns | |
------- | |
bool | |
True if the label corresponds to a threshold, False otherwise. | |
""" | |
try: | |
value = int(self.text) | |
return value in list(range(-10, 130, 10)) | |
except ValueError: | |
return False | |
def get_area(self) -> int: | |
"""Computes the area of the label's bounding box. | |
Returns | |
------- | |
int | |
The area of the label's bounding box in pixels squared. | |
""" | |
return self.dimensions["height"] * self.dimensions["width"] | |
def overlaps_vertically_with(self, label: "Label") -> bool: | |
"""Checks of the label overlaps vertically with the label passed. | |
Returns | |
------- | |
bool | |
True if the labels overlap and False otherwise. | |
""" | |
return (self.p1["y"] >= label.p1["y"] and self.p1["y"] <= label.p2["y"]) \ | |
or (self.p2["y"] >= label.p1["y"] and self.p2["y"] <= label.p2["y"]) \ | |
or (label.p1["y"] >= self.p1["y"] and label.p1["y"] <= self.p2["y"]) \ | |
or (label.p2["y"] >= self.p1["y"] and label.p2["y"] <= self.p2["y"]) | |
def overlaps_horizontally_with(self, label: "Label") -> bool: | |
"""Checks of the label overlaps horizontally with the label passed. | |
Returns | |
------- | |
bool | |
True if the labels overlap and False otherwise. | |
""" | |
return (self.p1["x"] >= label.p1["x"] and self.p1["x"] <= label.p2["x"]) \ | |
or (self.p2["x"] >= label.p1["x"] and self.p2["x"] <= label.p2["x"]) \ | |
or (label.p1["x"] >= self.p1["x"] and label.p1["x"] <= self.p2["x"]) \ | |
or (label.p2["x"] >= self.p1["x"] and label.p2["x"] <= self.p2["x"]) | |
def overlaps_with(self, label: "Label") -> bool: | |
"""Checks of the label overlaps vertically OR horizontally with the label passed. | |
Returns | |
------- | |
bool | |
True if the labels overlap and False otherwise. | |
""" | |
return self.overlaps_vertically_with(label) and self.overlaps_horizontally_with(label) | |
def encompasses_x_value(self, x: int) -> bool: | |
"""Checks of the the pixel value of x pass is encompassed in the label's x range. | |
Returns | |
------- | |
bool | |
True if x is in the label's x range and False otherwise. | |
""" | |
return x >= self.p1["x"] and x <= self.p2["x"] | |
def encompasses_y_value(self, y: int) -> bool: | |
"""Checks of the the pixel value of y pass is encompassed in the label's y range. | |
Returns | |
------- | |
bool | |
True if y is in the label's y range and False otherwise. | |
""" | |
return y >= self.p1["y"] and y <= self.p2["y"] | |
def get_center(self) -> dict: | |
"""Returns the center of the label's bounding box. | |
Returns | |
------- | |
dict | |
A dictionary describing the center of the label's bounding box | |
of the form { "x": int, "y": int }. | |
""" | |
center = { | |
"x": int((self.p1["x"] + self.p2["x"]) / 2), | |
"y": int((self.p1["y"] + self.p2["y"]) / 2) | |
} | |
return center | |
def find_closest_line(self, lines: List["Line"]) -> "Line": | |
"""Find the closest line to the label. | |
If the label corresponds to a frequency, the line is vertical, | |
otherwise it is a horizontal line. | |
Parameters | |
---------- | |
lines : List[Line] | |
The set of lines detected in the audiogram image. | |
Returns | |
------- | |
Line | |
The closest line. | |
""" | |
if self.is_threshold(): | |
lines = [line for line in lines if line.is_horizontal()] | |
closest_line_distance = 100000 | |
closest_line_index = None | |
distances = [] | |
for i, line in enumerate(lines): | |
distance = abs(line.get_y() - self.get_center()["y"]) | |
distances.append(distance) | |
if distance < closest_line_distance: | |
closest_line_index = i | |
closest_line_distance = distance | |
return lines[closest_line_index], distances[closest_line_index] | |
elif self.is_frequency(): | |
lines = [line for line in lines if line.is_vertical()] | |
closest_line_distance = 100000 | |
closest_line_index = None | |
distances = [] | |
for i, line in enumerate(lines): | |
distance = abs(line.get_x() - self.get_center()["x"]) | |
distances.append(distance) | |
if distance < closest_line_distance: | |
closest_line_index = i | |
closest_line_distance = distance | |
return lines[closest_line_index], distances[closest_line_index] | |
else: | |
raise "Error: Tried to find the closest line to a label that corresponds neither to a frequency nor a threshold." | |
def to_dict(self) -> dict: | |
"""Returns the label as a dictionary. | |
More thorough description of the function here. | |
Returns | |
------- | |
dict | |
The label as a dictionary with the keys `boundingBox` and `value`. | |
""" | |
return { | |
"boundingBox": self.absolute_bounding_box, | |
"value": self.text | |
} | |
def __str__(self): | |
return f"Textbox(x={self.p1['x']}, y={self.p1['y']}, text={self.text})" | |
def __repr__(self): | |
return self.__str__() | |