Spaces:
Running
Running
import os, math, cv2, random | |
import numpy as np | |
from itertools import combinations | |
from PIL import Image | |
from dataclasses import dataclass, field | |
from typing import List, Dict | |
class LeafSkeleton: | |
cfg: str | |
Dirs: str | |
leaf_type: str | |
all_points: list | |
dir_temp: str | |
file_name: str | |
width: int | |
height: int | |
logger: object | |
do_show_QC_images: bool = False | |
do_save_QC_images: bool = False | |
classes: float = None | |
points_list: float = None | |
image: float = None | |
ordered_midvein: float = None | |
midvein_fit: float = None | |
midvein_fit_points: float = None | |
ordered_midvein_length: float = None | |
has_midvein = False | |
is_split = False | |
ordered_petiole: float = None | |
ordered_petiole_length: float = None | |
has_ordered_petiole = False | |
has_apex: bool = False | |
apex_left: float = None | |
apex_right: float = None | |
apex_center: float = None | |
apex_angle_type: str = 'NA' | |
apex_angle_degrees: float = None | |
has_base: bool = False | |
base_left: float = None | |
base_right: float = None | |
base_center: float = None | |
base_angle_type: str = 'NA' | |
base_angle_degrees: float = None | |
has_lamina_tip: bool = False | |
lamina_tip: float = None | |
has_lamina_base: bool = False | |
lamina_base: float = None | |
has_lamina_length: bool = False | |
lamina_fit: float = None | |
lamina_length: float = None | |
has_width: bool = False | |
lamina_width: float = None | |
width_left: float = None | |
width_right: float = None | |
has_lobes: bool = False | |
lobe_count: float = None | |
lobes: float = None | |
def __init__(self, cfg, logger, Dirs, leaf_type, all_points, height, width, dir_temp, file_name) -> None: | |
# Store the necessary arguments as instance attributes | |
self.cfg = cfg | |
self.Dirs = Dirs | |
self.leaf_type = leaf_type | |
self.all_points = all_points | |
self.height = height | |
self.width = width | |
self.dir_temp = dir_temp | |
self.file_name = file_name | |
logger.name = f'[{leaf_type} - {file_name}]' | |
self.logger = logger | |
self.init_lists_dicts() | |
# Setup | |
self.set_cfg_values() | |
self.define_landmark_classes() | |
self.setup_QC_image() | |
self.setup_final_image() | |
self.parse_all_points() | |
self.convert_YOLO_bbox_to_point() | |
# Start with ordering the midvein and petiole | |
self.order_midvein() | |
self.order_petiole() | |
# print(self.ordered_midvein) | |
# Split the image using the midvein IF has_midvein == True | |
self.split_image_by_midvein() | |
# Process angles IF is_split == True. Need orientation to pick the appropriate pts for angle calcs | |
self.determine_apex() | |
self.determine_base() | |
self.determine_lamina_tip() | |
self.determine_lamina_base() | |
self.determine_lamina_length('QC') | |
self.determine_width() | |
self.determine_lobes() | |
self.determine_petiole() # straight length of petiole vs. ordered_petiole length which is tracing the petiole | |
self.restrictions() | |
# creates self.is_complete_leaf = False and self.is_leaf_no_width = False | |
# can add less restrictive options later, but for now only very complete leaves will pass | |
self.redo_measurements() | |
self.create_final_image() | |
self.translate_measurements_to_full_image() | |
self.show_QC_image() | |
self.show_final_image() | |
# self.save_QC_image() | |
# print('hi') | |
def get(self, attribute, default=None): | |
return getattr(self, attribute, default) | |
def split_image_by_midvein(self): | |
if self.has_midvein: | |
n_fit = 1 | |
# Convert the points to a numpy array | |
points_arr = np.array(self.ordered_midvein) | |
# Fit a line to the points | |
self.midvein_fit = np.polyfit(points_arr[:, 0], points_arr[:, 1], n_fit) | |
if len(self.midvein_fit) < 1: | |
self.midvein_fit = None | |
else: | |
# Plot a sample of points from along the line | |
max_dim = max(self.height, self.width) | |
if max_dim < 400: | |
num_points = 40 | |
elif max_dim < 1000: | |
num_points = 80 | |
else: | |
num_points = 120 | |
# Get the endpoints of the line segment that lies within the bounds of the image | |
x1 = 0 | |
y1 = int(self.midvein_fit[0] * x1 + self.midvein_fit[1]) | |
x2 = self.width - 1 | |
y2 = int(self.midvein_fit[0] * x2 + self.midvein_fit[1]) | |
denom = self.midvein_fit[0] | |
if denom == 0: | |
denom = 0.0000000001 | |
if y1 < 0: | |
y1 = 0 | |
x1 = int((y1 - self.midvein_fit[1]) / denom) | |
if y2 >= self.height: | |
y2 = self.height - 1 | |
x2 = int((y2 - self.midvein_fit[1]) / denom) | |
# Sample num_points points along the line segment within the bounds of the image | |
x_vals = np.linspace(x1, x2, num_points) | |
y_vals = self.midvein_fit[0] * x_vals + self.midvein_fit[1] | |
# Remove any points that are outside the bounds of the image | |
indices = np.where((y_vals >= 0) & (y_vals < self.height))[0] | |
x_vals = x_vals[indices] | |
y_vals = y_vals[indices] | |
# Recompute y-values using the line equation and updated x-values | |
y_vals = self.midvein_fit[0] * x_vals + self.midvein_fit[1] | |
self.midvein_fit_points = np.column_stack((x_vals, y_vals)) | |
self.is_split = True | |
# Draw line of fit | |
for point in self.midvein_fit_points: | |
cv2.circle(self.image, tuple(point.astype(int)), radius=1, color=(255, 255, 255), thickness=-1) | |
'''def split_image_by_midvein(self): # cubic | |
if self.file_name == 'B_774373024_Ebenaceae_Diospyros_glutinifera__L__469-164-888-632': | |
print('hi') | |
if self.has_midvein: | |
n_fit = 3 | |
# Convert the points to a numpy array | |
points_arr = np.array(self.ordered_midvein) | |
# Fit a curve to the points | |
self.midvein_fit = np.polyfit(points_arr[:, 0], points_arr[:, 1], n_fit) | |
# Plot a sample of points from along the curve | |
max_dim = max(self.height, self.width) | |
if max_dim < 400: | |
num_points = 40 | |
elif max_dim < 1000: | |
num_points = 80 | |
else: | |
num_points = 120 | |
# Get the endpoints of the curve segment that lies within the bounds of the image | |
x1 = 0 | |
y1 = int(self.midvein_fit[0] * x1**3 + self.midvein_fit[1] * x1**2 + self.midvein_fit[2] * x1 + self.midvein_fit[3]) | |
x2 = self.width - 1 | |
y2 = int(self.midvein_fit[0] * x2**3 + self.midvein_fit[1] * x2**2 + self.midvein_fit[2] * x2 + self.midvein_fit[3]) | |
# Sample num_points y-values that are evenly spaced within the bounds of the image | |
y_vals = np.linspace(0, self.height - 1, num_points) | |
# Compute the corresponding x-values using the polynomial | |
p = np.poly1d(self.midvein_fit) | |
x_vals = np.zeros(num_points) | |
for i, y in enumerate(y_vals): | |
roots = p - y | |
real_roots = roots.r[np.isreal(roots.r)].real | |
x_val = real_roots[(real_roots >= 0) & (real_roots < self.width)] | |
if len(x_val) > 0: | |
x_vals[i] = x_val[0] | |
# Remove any points that are outside the bounds of the image | |
indices = np.where((y_vals > 0) & (y_vals < self.height-1))[0] | |
x_vals = x_vals[indices] | |
y_vals = y_vals[indices] | |
# Recompute y-values using the polynomial and updated x-values | |
y_vals = self.midvein_fit[0] * x_vals**3 + self.midvein_fit[1] * x_vals**2 + self.midvein_fit[2] * x_vals + self.midvein_fit[3] | |
self.midvein_fit_points = np.column_stack((x_vals, y_vals)) | |
self.is_split = True''' | |
def determine_apex(self): | |
if self.is_split: | |
can_get_angle = False | |
if 'apex_angle' in self.points_list: | |
if 'lamina_tip' in self.points_list: | |
self.apex_center, self.points_list['apex_angle'] = self.get_closest_point_to_sampled_points(self.points_list['apex_angle'], self.points_list['lamina_tip']) | |
can_get_angle = True | |
elif self.midvein_fit_points.shape[0] > 0: | |
self.apex_center, self.points_list['apex_angle'] = self.get_closest_point_to_sampled_points(self.points_list['apex_angle'], self.midvein_fit_points) | |
can_get_angle = True | |
if can_get_angle: | |
left = [] | |
right = [] | |
for point in self.points_list['apex_angle']: | |
loc = self.point_position_relative_to_line(point, self.midvein_fit) | |
if loc == 'right': | |
right.append(point) | |
elif loc == 'left': | |
left.append(point) | |
if (left == []) or (right == []): | |
self.has_apex = False | |
if (left == []) and (right != []): | |
self.apex_right, right = self.get_far_point(right, self.apex_center) | |
self.apex_left = None | |
elif (right == []) and (left != []): | |
self.apex_left, left = self.get_far_point(left, self.apex_center) | |
self.apex_right = None | |
else: | |
self.apex_left = None | |
self.apex_right = None | |
else: | |
self.has_apex = True | |
self.apex_left, left = self.get_far_point(left, self.apex_center) | |
self.apex_right, right = self.get_far_point(right, self.apex_center) | |
# print(self.points_list['apex_angle']) | |
# print(f'apex_center: {self.apex_center} apex_left: {self.apex_left} apex_right: {self.apex_right}') | |
self.logger.debug(f"[apex_angle_list] {self.points_list['apex_angle']}") | |
self.logger.debug(f"[apex_center] {self.apex_center} [apex_left] {self.apex_left} [apex_right] {self.apex_right}") | |
if self.has_apex: | |
self.apex_angle_type, self.apex_angle_degrees = self.determine_reflex(self.apex_left, self.apex_right, self.apex_center) | |
# print(f'angle_type {self.apex_angle_type} angle {self.apex_angle_degrees}') | |
self.logger.debug(f"[angle_type] {self.apex_angle_type} [angle] {self.apex_angle_degrees}") | |
else: | |
self.apex_angle_type = 'NA' | |
self.apex_angle_degrees = None | |
self.logger.debug(f"[angle_type] {self.apex_angle_type} [angle] {self.apex_angle_degrees}") | |
if self.has_apex: | |
if self.apex_center is not None: | |
cv2.circle(self.image, self.apex_center, radius=3, color=(0, 255, 0), thickness=-1) | |
if self.apex_left is not None: | |
cv2.circle(self.image, self.apex_left, radius=3, color=(255, 0, 0), thickness=-1) | |
if self.apex_right is not None: | |
cv2.circle(self.image, self.apex_right, radius=3, color=(0, 0, 255), thickness=-1) | |
def determine_apex_redo(self): | |
self.logger.debug(f"[apex_angle_list REDO] ") | |
self.logger.debug(f"[apex_center REDO] {self.apex_center} [apex_left] {self.apex_left} [apex_right] {self.apex_right}") | |
if self.has_apex: | |
self.apex_angle_type, self.apex_angle_degrees = self.determine_reflex(self.apex_left, self.apex_right, self.apex_center) | |
self.logger.debug(f"[angle_type REDO] {self.apex_angle_type} [angle] {self.apex_angle_degrees}") | |
else: | |
self.apex_angle_type = 'NA' | |
self.apex_angle_degrees = None | |
self.logger.debug(f"[angle_type REDO] {self.apex_angle_type} [angle] {self.apex_angle_degrees}") | |
if self.has_apex: | |
if self.apex_center is not None: | |
cv2.circle(self.image, self.apex_center, radius=11, color=(0, 255, 0), thickness=2) | |
if self.apex_left is not None: | |
cv2.circle(self.image, self.apex_left, radius=3, color=(255, 0, 0), thickness=-1) | |
if self.apex_right is not None: | |
cv2.circle(self.image, self.apex_right, radius=3, color=(0, 0, 255), thickness=-1) | |
def determine_base_redo(self): | |
self.logger.debug(f"[base_angle_list REDO] ") | |
self.logger.debug(f"[base_center REDO] {self.base_center} [base_left] {self.base_left} [base_right] {self.base_right}") | |
if self.has_base: | |
self.base_angle_type, self.base_angle_degrees = self.determine_reflex(self.base_left, self.base_right, self.base_center) | |
self.logger.debug(f"[angle_type REDO] {self.base_angle_type} [angle] {self.base_angle_degrees}") | |
else: | |
self.base_angle_type = 'NA' | |
self.base_angle_degrees = None | |
self.logger.debug(f"[angle_type REDO] {self.base_angle_type} [angle] {self.base_angle_degrees}") | |
if self.has_base: | |
if self.base_center is not None: | |
cv2.circle(self.image, self.base_center, radius=11, color=(0, 255, 0), thickness=2) | |
if self.base_left is not None: | |
cv2.circle(self.image, self.base_left, radius=3, color=(255, 0, 0), thickness=-1) | |
if self.base_right is not None: | |
cv2.circle(self.image, self.base_right, radius=3, color=(0, 0, 255), thickness=-1) | |
def determine_base(self): | |
if self.is_split: | |
can_get_angle = False | |
if 'base_angle' in self.points_list: | |
if 'lamina_base' in self.points_list: | |
self.base_center, self.points_list['base_angle'] = self.get_closest_point_to_sampled_points(self.points_list['base_angle'], self.points_list['lamina_base']) | |
can_get_angle = True | |
elif self.midvein_fit_points.shape[0] > 0: | |
self.base_center, self.points_list['base_angle'] = self.get_closest_point_to_sampled_points(self.points_list['base_angle'], self.midvein_fit_points) | |
can_get_angle = True | |
if can_get_angle: | |
left = [] | |
right = [] | |
for point in self.points_list['base_angle']: | |
loc = self.point_position_relative_to_line(point, self.midvein_fit) | |
if loc == 'right': | |
right.append(point) | |
elif loc == 'left': | |
left.append(point) | |
if (left == []) or (right == []): | |
self.has_base = False | |
if (left == []) and (right != []): | |
self.base_right, right = self.get_far_point(right, self.base_center) | |
self.base_left = None | |
elif (right == []) and (left != []): | |
self.base_left, left = self.get_far_point(left, self.base_center) | |
self.base_right = None | |
else: | |
self.base_left = None | |
self.base_right = None | |
else: | |
self.has_base = True | |
self.base_left, left = self.get_far_point(left, self.base_center) | |
self.base_right, right = self.get_far_point(right, self.base_center) | |
# print(self.points_list['base_angle']) | |
# print(f'base_center: {self.base_center} base_left: {self.base_left} base_right: {self.base_right}') | |
self.logger.debug(f"[base_angle_list] {self.points_list['base_angle']}") | |
self.logger.debug(f"[base_center] {self.base_center} [base_left] {self.base_left} [base_right] {self.base_right}") | |
if self.has_base: | |
self.base_angle_type, self.base_angle_degrees = self.determine_reflex(self.base_left, self.base_right, self.base_center) | |
# print(f'angle_type {self.base_angle_type} angle {self.base_angle_degrees}') | |
self.logger.debug(f"[angle_type] {self.base_angle_type} [angle] {self.base_angle_degrees}") | |
else: | |
self.base_angle_type = 'NA' | |
self.base_angle_degrees = None | |
self.logger.debug(f"[angle_type] {self.base_angle_type} [angle] {self.base_angle_degrees}") | |
if self.has_base: | |
if self.base_center: | |
cv2.circle(self.image, self.base_center, radius=3, color=(0, 255, 0), thickness=-1) | |
if self.base_left: | |
cv2.circle(self.image, self.base_left, radius=3, color=(255, 0, 0), thickness=-1) | |
if self.base_right: | |
cv2.circle(self.image, self.base_right, radius=3, color=(0, 0, 255), thickness=-1) | |
def determine_lamina_tip(self): | |
if 'lamina_tip' in self.points_list: | |
self.has_lamina_tip = True | |
if self.apex_center: | |
self.lamina_tip, self.lamina_tip_alternate = self.get_closest_point_to_sampled_points(self.points_list['lamina_tip'], self.apex_center) | |
elif len(self.midvein_fit_points) > 0: | |
self.lamina_tip, self.lamina_tip_alternate = self.get_closest_point_to_sampled_points(self.points_list['lamina_tip'], self.midvein_fit_points) | |
else: | |
if len(self.points_list['lamina_tip']) == 1: | |
self.lamina_tip = self.points_list['lamina_tip'][0] | |
self.lamina_tip_alternate = None | |
else: # blindly choose the most "central points" | |
centroid = tuple(np.mean(self.points_list['lamina_tip'], axis=0)) | |
self.lamina_tip = min(self.points_list['lamina_tip'], key=lambda p: np.linalg.norm(np.array(p) - np.array(centroid))) | |
self.lamina_tip_alternate = None # TODO finish this | |
# if lamina_tip is closer to midvein_fit_points, then apex_center = lamina_tip | |
if self.apex_center and (len(self.midvein_fit_points) > 0): | |
d_apex = self.calc_min_distance(self.apex_center, self.midvein_fit_points) | |
d_lamina = self.calc_min_distance(self.lamina_tip, self.midvein_fit_points) | |
if d_lamina < d_apex: | |
cv2.circle(self.image, self.apex_center, radius=5, color=(255, 255, 255), thickness=3) # white hollow, indicates switch | |
cv2.circle(self.image, self.lamina_tip, radius=3, color=(0, 255, 0), thickness=-1) # repaint the point, indicates switch | |
self.apex_center = self.lamina_tip | |
if self.has_apex: | |
self.apex_angle_type, self.apex_angle_degrees = self.determine_reflex(self.apex_left, self.apex_right, self.apex_center) | |
else: | |
if self.apex_center: | |
self.has_lamina_tip = True | |
self.lamina_tip = self.apex_center | |
self.lamina_tip_alternate = None | |
if self.lamina_tip: | |
cv2.circle(self.image, self.lamina_tip, radius=5, color=(255, 0, 230), thickness=2) # pink solid | |
if self.lamina_tip_alternate: | |
for pt in self.lamina_tip_alternate: | |
cv2.circle(self.image, pt, radius=3, color=(255, 0, 230), thickness=-1) # pink hollow | |
def determine_lamina_base(self): | |
if 'lamina_base' in self.points_list: | |
self.has_lamina_base = True | |
if self.base_center: | |
self.lamina_base, self.lamina_base_alternate = self.get_closest_point_to_sampled_points(self.points_list['lamina_base'], self.base_center) | |
elif len(self.midvein_fit_points) > 0: | |
self.lamina_base, self.lamina_base_alternate = self.get_closest_point_to_sampled_points(self.points_list['lamina_base'], self.midvein_fit_points) | |
else: | |
if len(self.points_list['lamina_base']) == 1: | |
self.lamina_base = self.points_list['lamina_base'][0] | |
self.lamina_base_alternate = None | |
else: # blindly choose the most "central points" | |
centroid = tuple(np.mean(self.points_list['lamina_base'], axis=0)) | |
self.lamina_base = min(self.points_list['lamina_base'], key=lambda p: np.linalg.norm(np.array(p) - np.array(centroid))) | |
self.lamina_base_alternate = None | |
# if has_lamina_tip is closer to midvein_fit_points, then base_center = has_lamina_tip | |
if self.base_center and (len(self.midvein_fit_points) > 0): | |
d_base = self.calc_min_distance(self.base_center, self.midvein_fit_points) | |
d_lamina = self.calc_min_distance(self.lamina_base, self.midvein_fit_points) | |
if d_lamina < d_base: | |
cv2.circle(self.image, self.base_center, radius=5, color=(255, 255, 255), thickness=3) # white hollow, indicates switch | |
cv2.circle(self.image, self.lamina_base, radius=3, color=(0, 255, 0), thickness=-1) # repaint the point, indicates switch | |
self.base_center = self.lamina_base | |
if self.has_base: | |
self.base_angle_type, self.base_angle_degrees = self.determine_reflex(self.base_left, self.base_right, self.base_center) | |
else: | |
if self.base_center: | |
self.has_lamina_base = True | |
self.lamina_base = self.base_center | |
self.lamina_base_alternate = None | |
if self.lamina_base: | |
cv2.circle(self.image, self.lamina_base, radius=5, color=(0, 100, 255), thickness=2) # orange | |
if self.lamina_base_alternate: | |
for pt in self.lamina_base_alternate: | |
cv2.circle(self.image, pt, radius=3, color=(0, 100, 255), thickness=-1) # orange hollow | |
def determine_lamina_length(self, QC_or_final): | |
if self.has_lamina_base and self.has_lamina_tip: | |
self.lamina_length = self.distance(self.lamina_base, self.lamina_tip) | |
ends = np.array([self.lamina_base, self.lamina_tip]) | |
self.lamina_fit = np.polyfit(ends[:, 0], ends[:, 1], 1) | |
self.has_lamina_length = True | |
# r_base = 0 | |
r_base = 16 | |
# col = (0, 100, 0) | |
col = (0, 0, 0) | |
if QC_or_final == 'QC': | |
cv2.line(self.image, self.lamina_base, self.lamina_tip, col, 2 + r_base) | |
else: | |
cv2.line(self.image_final, self.lamina_base, self.lamina_tip, col, 2 + r_base) | |
else: | |
col = (0, 0, 0) | |
r_base = 16 | |
if self.has_lamina_base and (not self.has_lamina_tip) and self.has_apex: # lamina base and apex center | |
self.lamina_length = self.distance(self.lamina_base, self.apex_center) | |
ends = np.array([self.lamina_base, self.apex_center]) | |
self.lamina_fit = np.polyfit(ends[:, 0], ends[:, 1], 1) | |
self.has_lamina_length = True | |
if QC_or_final == 'QC': | |
cv2.line(self.image, self.lamina_base, self.apex_center, col, 2 + r_base) | |
else: | |
cv2.line(self.image, self.lamina_base, self.apex_center, col, 2 + r_base) | |
elif self.has_lamina_tip and (not self.has_lamina_base) and self.has_base: # lamina tip and base center | |
self.lamina_length = self.distance(self.lamina_tip, self.base_center) | |
ends = np.array([self.lamina_tip, self.apex_center]) | |
self.lamina_fit = np.polyfit(ends[:, 0], ends[:, 1], 1) | |
self.has_lamina_length = True | |
if QC_or_final == 'QC': | |
cv2.line(self.image, self.lamina_tip, self.apex_center, col, 2 + r_base) | |
else: | |
cv2.line(self.image, self.lamina_tip, self.apex_center, col, 2 + r_base) | |
elif (not self.has_lamina_tip) and (not self.has_lamina_base) and self.has_apex and self.has_base: # apex center and base center | |
self.lamina_length = self.distance(self.apex_center, self.base_center) | |
ends = np.array([self.base_center, self.apex_center]) | |
self.lamina_fit = np.polyfit(ends[:, 0], ends[:, 1], 1) | |
self.has_lamina_length = True | |
if QC_or_final == 'QC': | |
cv2.line(self.image, self.base_center, self.apex_center, col, 2 + r_base) | |
else: | |
cv2.line(self.image, self.base_center, self.apex_center, col, 2 + r_base) # 0, 175, 200 | |
else: | |
self.lamina_length = None | |
self.lamina_fit = None | |
self.has_lamina_length = False | |
def determine_width(self): | |
if (('lamina_width' in self.points_list) and ((self.midvein_fit is not None and len(self.midvein_fit) > 0) or (self.lamina_fit is not None))): | |
left = [] | |
right = [] | |
if len(self.midvein_fit) > 0: # try using the midvein as a reference first | |
for point in self.points_list['lamina_width']: | |
loc = self.point_position_relative_to_line(point, self.midvein_fit) | |
if loc == 'right': | |
right.append(point) | |
elif loc == 'left': | |
left.append(point) | |
elif len(self.lamina_fit) > 0: # then try just the lamina tip/base | |
for point in self.points_list['lamina_width']: | |
loc = self.point_position_relative_to_line(point, self.lamina_fit) | |
if loc == 'right': | |
right.append(point) | |
elif loc == 'left': | |
left.append(point) | |
else: | |
self.has_width = False | |
self.width_left = None | |
self.width_right = None | |
self.lamina_width = None | |
if (left == []) or (right == []) or not self.has_width: | |
self.has_width = False | |
self.width_left = None | |
self.width_right = None | |
self.lamina_width = None | |
else: | |
self.has_width = True | |
if len(self.midvein_fit) > 0: | |
self.width_left, self.width_right = self.find_most_orthogonal_vectors(left, right, self.midvein_fit) | |
self.lamina_width = self.distance(self.width_left, self.width_right) | |
self.order_points_plot([self.width_left, self.width_right], 'lamina_width', 'QC') | |
else: # get shortest width if the nidvein is absent for comparison | |
self.width_left, self.width_right = self.find_min_width(left, right) | |
self.lamina_width = self.distance(self.width_left, self.width_right) | |
self.order_points_plot([self.width_left, self.width_right], 'lamina_width_alt', 'QC') | |
else: | |
self.has_width = False | |
self.width_left = None | |
self.width_right = None | |
self.lamina_width = None | |
def determine_lobes(self): | |
if 'lobe_tip' in self.points_list: | |
self.has_lobes = True | |
self.lobe_count = len(self.points_list['lobe_tip']) | |
self.lobes = self.points_list['lobe_tip'] | |
for lobe in self.lobes: | |
cv2.circle(self.image, tuple(lobe), radius=6, color=(0, 255, 255), thickness=3) | |
def determine_petiole(self): | |
if 'petiole_tip' in self.points_list: | |
self.has_petiole_tip = True | |
if len(self.points_list['petiole_tip']) == 1: | |
self.petiole_tip = self.points_list['petiole_tip'][0] | |
self.petiole_tip_alternate = None | |
else: # blindly choose the most "central points" | |
centroid = tuple(np.mean(self.points_list['petiole_tip'], axis=0)) | |
self.petiole_tip = min(self.points_list['petiole_tip'], key=lambda p: np.linalg.norm(np.array(p) - np.array(centroid))) | |
self.petiole_tip_alternate = None | |
# Straight length of petiole points | |
if self.has_ordered_petiole: | |
self.petiole_tip_opposite, self.petiole_tip_alternate = self.get_far_point(self.ordered_petiole, self.petiole_tip) | |
self.petiole_length = self.distance(self.petiole_tip_opposite, self.petiole_tip) | |
self.order_points_plot([self.petiole_tip_opposite, self.petiole_tip], 'petiole_tip', 'QC') | |
else: | |
self.petiole_tip_opposite = None | |
self.petiole_length = None | |
# Straight length of petiole tip to lamina base | |
if self.lamina_base is not None: | |
self.petiole_length_to_lamina_base = self.distance(self.lamina_base, self.petiole_tip) | |
self.petiole_tip_opposite_alternate = self.lamina_base | |
self.order_points_plot([self.petiole_tip_opposite_alternate, self.petiole_tip], 'petiole_tip_alt', 'QC') | |
elif self.base_center: | |
self.petiole_length_to_lamina_base = self.distance(self.base_center, self.petiole_tip) | |
self.petiole_tip_opposite_alternate = self.base_center | |
self.order_points_plot([self.petiole_tip_opposite_alternate, self.petiole_tip], 'petiole_tip_alt', 'QC') | |
else: | |
self.petiole_length_to_lamina_base = None | |
self.petiole_tip_opposite_alternate = None | |
def redo_measurements(self): | |
if self.has_width: | |
self.lamina_width = self.distance(self.width_left, self.width_right) | |
if self.has_ordered_petiole: | |
self.ordered_petiole_length, self.ordered_petiole = self.get_length_of_ordered_points(self.ordered_petiole, 'petiole_trace') | |
if self.has_midvein: | |
self.ordered_midvein_length, self.ordered_midvein = self.get_length_of_ordered_points(self.ordered_midvein, 'midvein_trace') | |
if self.has_apex: | |
self.apex_angle_type, self.apex_angle_degrees = self.determine_reflex(self.apex_left, self.apex_right, self.apex_center) | |
if self.has_base: | |
self.base_angle_type, self.base_angle_degrees = self.determine_reflex(self.base_left, self.base_right, self.base_center) | |
self.determine_lamina_length('final') # Calling just in case, should already be updated | |
def translate_measurements_to_full_image(self): | |
loc = self.file_name.split('__')[-1] | |
self.add_x = int(loc.split('-')[0]) | |
self.add_y = int(loc.split('-')[1]) | |
if self.has_base: | |
self.t_base_center = [self.base_center[0] + self.add_x, self.base_center[1] + self.add_y] | |
self.t_base_left = [self.base_left[0] + self.add_x, self.base_left[1] + self.add_y] | |
self.t_base_right = [self.base_right[0] + self.add_x, self.base_right[1] + self.add_y] | |
if self.has_apex: | |
self.t_apex_center = [self.apex_center[0] + self.add_x, self.apex_center[1] + self.add_y] | |
self.t_apex_left = [self.apex_left[0] + self.add_x, self.apex_left[1] + self.add_y] | |
self.t_apex_right = [self.apex_right[0] + self.add_x, self.apex_right[1] + self.add_y] | |
if self.has_lamina_base: | |
self.t_lamina_base = [self.lamina_base[0] + self.add_x, self.lamina_base[1] + self.add_y] | |
if self.has_lamina_tip: | |
self.t_lamina_tip = [self.lamina_tip[0] + self.add_x, self.lamina_tip[1] + self.add_y] | |
if self.has_lobes: | |
self.t_lobes = [] | |
for point in self.lobes: | |
new_x = int(point[0]) + self.add_x | |
new_y = int(point[1]) + self.add_y | |
new_point = [new_x, new_y] | |
self.t_lobes.append(new_point) | |
if self.has_midvein: | |
self.t_midvein_fit_points = [] | |
for point in self.midvein_fit_points: | |
new_x = int(point[0]) + self.add_x | |
new_y = int(point[1]) + self.add_y | |
new_point = [new_x, new_y] | |
self.t_midvein_fit_points.append(new_point) | |
self.t_midvein = [] | |
for point in self.ordered_midvein: | |
new_x = int(point[0]) + self.add_x | |
new_y = int(point[1]) + self.add_y | |
new_point = [new_x, new_y] | |
self.t_midvein.append(new_point) | |
if self.has_ordered_petiole: | |
self.t_petiole = [] | |
for point in self.ordered_petiole: | |
new_x = int(point[0]) + self.add_x | |
new_y = int(point[1]) + self.add_y | |
new_point = [new_x, new_y] | |
self.t_petiole.append(new_point) | |
if self.has_width: | |
self.t_width_left = [self.width_left[0] + self.add_x, self.width_left[1] + self.add_y] | |
self.t_width_right = [self.width_right[0] + self.add_x, self.width_right[1] + self.add_y] | |
if self.width_infer is not None: | |
self.t_width_infer = [] | |
for point in self.width_infer: | |
new_x = int(point[0]) + self.add_x | |
new_y = int(point[1]) + self.add_y | |
new_point = [new_x, new_y] | |
self.t_width_infer.append(new_point) | |
def create_final_image(self): | |
self.is_complete_leaf = False ########################################################################################################################################################### | |
self.is_leaf_no_width = False | |
# r_base = 0 | |
r_base = 16 | |
if (self.has_apex and self.has_base and self.has_ordered_petiole and self.has_midvein and self.has_width): | |
self.is_complete_leaf = True | |
self.order_points_plot([self.width_left, self.width_right], 'lamina_width', 'final') | |
self.order_points_plot(self.ordered_midvein, 'midvein_trace', 'final') | |
self.order_points_plot(self.ordered_petiole, 'petiole_trace', 'final') | |
self.order_points_plot([self.apex_left, self.apex_center, self.apex_right], self.apex_angle_type, 'final') | |
self.order_points_plot([self.base_left, self.base_center, self.base_right], self.base_angle_type, 'final') | |
self.determine_lamina_length('final') # try | |
# Lamina tip and base | |
if self.has_lamina_tip: | |
cv2.circle(self.image_final, self.lamina_tip, radius=4 + r_base, color=(0, 255, 0), thickness=2) | |
cv2.circle(self.image_final, self.lamina_tip, radius=2 + r_base, color=(255, 255, 255), thickness=-1) | |
if self.has_lamina_base: | |
cv2.circle(self.image_final, self.lamina_base, radius=4 + r_base, color=(255, 0, 0), thickness=2) | |
cv2.circle(self.image_final, self.lamina_base, radius=2 + r_base, color=(255, 255, 255), thickness=-1) | |
# Apex angle | |
# if self.apex_center != []: | |
# cv2.circle(self.image_final, self.apex_center, radius=3, color=(0, 255, 0), thickness=-1) | |
if self.apex_left is not None: | |
cv2.circle(self.image_final, self.apex_left, radius=3 + r_base, color=(255, 0, 0), thickness=-1) | |
if self.apex_right is not None: | |
cv2.circle(self.image_final, self.apex_right, radius=3 + r_base, color=(0, 0, 255), thickness=-1) | |
# Base angle | |
# if self.base_center: | |
# cv2.circle(self.image_final, self.base_center, radius=3, color=(0, 255, 0), thickness=-1) | |
if self.base_left: | |
cv2.circle(self.image_final, self.base_left, radius=3 + r_base, color=(255, 0, 0), thickness=-1) | |
if self.base_right: | |
cv2.circle(self.image_final, self.base_right, radius=3 + r_base, color=(0, 0, 255), thickness=-1) | |
# Lobes | |
if self.has_lobes: | |
for lobe in self.lobes: | |
cv2.circle(self.image, tuple(lobe), radius=6 + r_base, color=(0, 255, 255), thickness=3) | |
elif self.has_apex and self.has_base and self.has_ordered_petiole and self.has_midvein and (not self.has_width): | |
self.is_leaf_no_width = True | |
self.order_points_plot(self.ordered_midvein, 'midvein_trace', 'final') | |
self.order_points_plot(self.ordered_petiole, 'petiole_trace', 'final') | |
self.order_points_plot([self.apex_left, self.apex_center, self.apex_right], self.apex_angle_type, 'final') | |
self.order_points_plot([self.base_left, self.base_center, self.base_right], self.base_angle_type, 'final') | |
self.determine_lamina_length('final') | |
# Lamina tip and base | |
if self.has_lamina_tip: | |
cv2.circle(self.image_final, self.lamina_tip, radius=4 + r_base, color=(0, 255, 0), thickness=2) | |
cv2.circle(self.image_final, self.lamina_tip, radius=2 + r_base, color=(255, 255, 255), thickness=-1) | |
if self.has_lamina_base: | |
cv2.circle(self.image_final, self.lamina_base, radius=4 + r_base, color=(255, 0, 0), thickness=2) | |
cv2.circle(self.image_final, self.lamina_base, radius=2 + r_base, color=(255, 255, 255), thickness=-1) | |
# Apex angle | |
# if self.apex_center != []: | |
# cv2.circle(self.image_final, self.apex_center, radius=3, color=(0, 255, 0), thickness=-1) | |
if self.apex_left is not None: | |
cv2.circle(self.image_final, self.apex_left, radius=3 + r_base, color=(255, 0, 0), thickness=-1) | |
if self.apex_right is not None: | |
cv2.circle(self.image_final, self.apex_right, radius=3 + r_base, color=(0, 0, 255), thickness=-1) | |
# Base angle | |
# if self.base_center: | |
# cv2.circle(self.image_final, self.base_center, radius=3, color=(0, 255, 0), thickness=-1) | |
if self.base_left: | |
cv2.circle(self.image_final, self.base_left, radius=3 + r_base, color=(255, 0, 0), thickness=-1) | |
if self.base_right: | |
cv2.circle(self.image_final, self.base_right, radius=3 + r_base, color=(0, 0, 255), thickness=-1) | |
# Draw line of fit | |
for point in self.width_infer: | |
point[0] = np.clip(point[0], 0, self.width - 1) | |
point[1] = np.clip(point[1], 0, self.height - 1) | |
cv2.circle(self.image_final, tuple(point.astype(int)), radius=4 + r_base, color=(0, 0, 255), thickness=-1) | |
# Lobes | |
if self.has_lobes: | |
for lobe in self.lobes: | |
cv2.circle(self.image, tuple(lobe), radius=6 + r_base, color=(0, 255, 255), thickness=3) | |
def restrictions(self): | |
# self.check_tips() | |
self.connect_midvein_to_tips() | |
self.connect_petiole_to_midvein() | |
self.check_crossing_width() | |
def check_tips(self): # TODO need to check the sides to prevent base from ending up on the tip side. just need to check which side of the oredered list to pull from | |
if max([self.height, self.width]) < 200: | |
scale_factor = 0.25 | |
elif max([self.height, self.width]) < 500: | |
scale_factor = 0.5 | |
else: | |
scale_factor = 1 | |
if self.has_lamina_base: | |
second_last_dir = np.array(self.ordered_midvein[-1]) - np.array(self.lamina_base) | |
end_vector_mag = np.linalg.norm(second_last_dir) | |
avg_dist = np.mean([np.linalg.norm(np.array(self.ordered_midvein[i])-np.array(self.ordered_midvein[i-1])) for i in range(1, len(self.ordered_midvein))]) | |
if (end_vector_mag > (scale_factor * 0.01 * avg_dist * len(self.ordered_midvein))): | |
self.lamina_base = self.ordered_midvein[-1] | |
cv2.circle(self.image, self.lamina_base, radius=4, color=(0, 0, 0), thickness=-1) | |
cv2.circle(self.image, self.lamina_base, radius=8, color=(0, 0, 255), thickness=2) | |
self.logger.debug(f'Check Tips - lamina base - made lamina base the last midvein point') | |
else: | |
self.logger.debug(f'Check Tips - lamina base - kept lamina base') | |
if self.has_lamina_tip: | |
second_last_dir = np.array(self.ordered_midvein[0]) - np.array(self.lamina_tip) | |
end_vector_mag = np.linalg.norm(second_last_dir) | |
avg_dist = np.mean([np.linalg.norm(np.array(self.ordered_midvein[i])-np.array(self.ordered_midvein[i-1])) for i in range(1, len(self.ordered_midvein))]) | |
if (end_vector_mag > (scale_factor * 0.01 * avg_dist * len(self.ordered_midvein))): | |
self.lamina_tip = self.ordered_midvein[-1] | |
cv2.circle(self.image, self.lamina_tip, radius=4, color=(0, 0, 0), thickness=-1) | |
cv2.circle(self.image, self.lamina_tip, radius=8, color=(0, 0, 255), thickness=2) | |
self.logger.debug(f'Check Tips - lamina tip - made lamina tip the first midvein point') | |
else: | |
self.logger.debug(f'Check Tips - lamina tip - kept lamina tip') | |
def connect_midvein_to_tips(self): | |
self.logger.debug(f'Restrictions [Midvein Connect] - connect_midvein_to_tips()') | |
if self.has_midvein: | |
if self.has_lamina_tip: | |
original_lamina_tip = self.lamina_tip | |
start_or_end = self.add_tip(self.lamina_tip) | |
self.logger.debug(f'Restrictions [Midvein Connect] - Lamina tip [{self.lamina_tip}]') | |
self.ordered_midvein, move_midvein = self.check_momentum_complex(self.ordered_midvein, True, start_or_end) | |
if move_midvein: # the tip changed the momentum too much | |
self.logger.debug(f'Restrictions [Midvein Connect] - REDO APEX ANGLE - SWAP LAMINA TIP FOR FIRST MIDVEIN POINT') | |
# get midvein point cloases to tip | |
# new_endpoint_side, _ = self.get_closest_point_to_sampled_points(self.ordered_midvein, original_lamina_tip) | |
# new_endpoint, _ = self.get_closest_point_to_sampled_points([self.ordered_midvein[0], self.ordered_midvein[-1]], new_endpoint_side) | |
# change the apex to new endpoint | |
self.lamina_tip = self.ordered_midvein[0] | |
self.apex_center = self.ordered_midvein[0] | |
self.determine_lamina_length('QC') | |
self.determine_apex_redo() | |
# cv2.imshow('img', self.image) | |
# cv2.waitKey(0) | |
# self.order_points_plot(self.ordered_midvein, 'midvein_trace') | |
self.logger.debug(f'Restrictions [Midvein Connect] - connected lamina tip to midvein') | |
else: | |
self.logger.debug(f'Restrictions [Midvein Connect] - lacks lamina tip') | |
if self.has_lamina_base: | |
original_lamina_base = self.lamina_base | |
start_or_end = self.add_tip(self.lamina_base) | |
self.logger.debug(f'Restrictions [Midvein Connect] - Lamina base [{self.lamina_base}]') | |
self.ordered_midvein, move_midvein = self.check_momentum_complex(self.ordered_midvein, True, start_or_end) | |
if move_midvein: # the tip changed the momentum too much | |
self.logger.debug(f'Restrictions [Midvein Connect] - REDO BASE ANGLE - SWAP LAMINA BASE FOR LAST MIDVEIN POINT') | |
# get midvein point cloases to tip | |
# new_endpoint_side, _ = self.get_closest_point_to_sampled_points(self.ordered_midvein, original_lamina_base) | |
# new_endpoint, _ = self.get_closest_point_to_sampled_points([self.ordered_midvein[0], self.ordered_midvein[-1]], new_endpoint_side) | |
# change the apex to new endpoint | |
self.lamina_base = self.ordered_midvein[-1] | |
self.base_center = self.ordered_midvein[-1] | |
self.determine_lamina_length('QC') | |
self.determine_base_redo() | |
# self.order_points_plot(self.ordered_midvein, 'midvein_trace') | |
self.logger.debug(f'Restrictions [Midvein Connect] - connected lamina base to midvein') | |
else: | |
self.logger.debug(f'Restrictions [Midvein Connect] - lacks lamina base') | |
def connect_petiole_to_midvein(self): | |
if self.has_ordered_petiole and self.has_midvein: | |
if len(self.ordered_petiole) > 0 and len(self.ordered_midvein) > 0: | |
# Find the closest pair of points between ordered_petiole and ordered_midvein | |
min_dist = np.inf | |
closest_petiole_idx = None | |
closest_midvein_idx = None | |
for i, petiole_point in enumerate(self.ordered_petiole): | |
for j, midvein_point in enumerate(self.ordered_midvein): | |
# Convert petiole_point and midvein_point to NumPy arrays | |
petiole_point = np.array(petiole_point) | |
midvein_point = np.array(midvein_point) | |
# Calculate the distance between the two points | |
dist = np.linalg.norm(petiole_point - midvein_point) | |
if dist < min_dist: | |
min_dist = dist | |
closest_petiole_idx = i | |
closest_midvein_idx = j | |
# Calculate the midpoint between the closest points | |
petiole_point = self.ordered_petiole[closest_petiole_idx] | |
midvein_point = self.ordered_midvein[closest_midvein_idx] | |
midpoint = (int((petiole_point[0] + midvein_point[0]) / 2), int((petiole_point[1] + midvein_point[1]) / 2)) | |
# Determine whether the midpoint should be added to the beginning or end of each list | |
petiole_dist_to_end = np.linalg.norm(np.array(self.ordered_petiole[closest_petiole_idx]) - np.array(self.ordered_petiole[-1])) | |
midvein_dist_to_end = np.linalg.norm(np.array(self.ordered_midvein[closest_midvein_idx]) - np.array(self.ordered_midvein[-1])) | |
if (petiole_dist_to_end < midvein_dist_to_end): | |
# Add the midpoint to the end of the petiole list and the beginning of the midvein list | |
self.ordered_midvein.insert(0, midpoint) | |
self.ordered_petiole.append(midpoint) | |
self.lamina_base = midpoint | |
cv2.circle(self.image, self.lamina_base, radius=4, color=(0, 255, 0), thickness=-1) | |
cv2.circle(self.image, self.lamina_base, radius=6, color=(0, 0, 0), thickness=2) | |
else: | |
# Add the midpoint to the end of the midvein list and the beginning of the petiole list | |
self.ordered_petiole.insert(0, midpoint) | |
self.ordered_midvein.append(midpoint) | |
self.lamina_base = midpoint | |
cv2.circle(self.image, self.lamina_base, radius=4, color=(0, 255, 0), thickness=-1) | |
cv2.circle(self.image, self.lamina_base, radius=6, color=(0, 0, 0), thickness=2) | |
# If the momentum changed, then move the apex/base centers to the begninning/end of the new midvein. | |
# self.ordered_midvein, move_midvein = self.check_momentum(self.ordered_midvein, True) | |
# self.ordered_petiole, move_petiole = self.check_momentum(self.ordered_petiole, True) | |
# if move_midvein or move_petiole: | |
# self.logger.debug(f'') | |
self.order_points_plot(self.ordered_midvein, 'midvein_trace', 'QC') | |
self.order_points_plot(self.ordered_petiole, 'petiole_trace', 'QC') | |
def check_crossing_width(self): | |
self.logger.debug(f'Restrictions [Crossing Width Line] - check_crossing_width()') | |
self.width_infer = None | |
if self.has_width: | |
self.logger.debug(f'Restrictions [Crossing Width Line] - has width') | |
# Given two points | |
x1, y1 = self.width_left | |
x2, y2 = self.width_right | |
# Calculate the slope and y-intercept | |
denom = (x2 - x1) | |
if denom == 0: | |
denom = 0.00000000001 | |
m = (y2 - y1) / denom | |
b = y1 - m * x1 | |
line_params = [m, b] | |
self.restrict_by_width_relation(line_params) | |
elif not self.has_width: | |
# generate approximate width line | |
self.logger.debug(f'Restrictions [Crossing Width Line] - infer width') | |
if self.has_apex and self.has_base: | |
line_params = self.infer_width_relation() | |
self.restrict_by_width_relation(line_params) | |
else: | |
self.has_ordered_petiole = False | |
self.has_apex = False | |
self.has_base = False | |
self.has_valid_apex_loc = False | |
self.has_valid_base_loc = False | |
self.logger.debug(f'Restrictions [Crossing Width Line] - CANNOT VALIDATE APEX, BASE, PETIOLE LOCATIONS') | |
else: | |
self.logger.debug(f'Restrictions [Crossing Width Line] - width fail *** ERROR ***') | |
def infer_width_relation(self): | |
top = [np.array((self.apex_center[0], self.apex_center[1])), np.array((self.apex_left[0], self.apex_left[1])), np.array((self.apex_right[0], self.apex_right[1]))] | |
bottom = [np.array((self.base_center[0], self.base_center[1])), np.array((self.base_left[0], self.base_left[1])), np.array((self.base_right[0], self.base_right[1]))] | |
if self.has_ordered_petiole: | |
bottom = bottom + [np.array(pt) for pt in self.ordered_petiole] | |
if self.has_midvein: | |
midvein = np.array(self.ordered_midvein) | |
self.logger.debug(f'Restrictions [Crossing Width Line] - infer width - using midvein points') | |
else: | |
self.logger.debug(f'Restrictions [Crossing Width Line] - infer width - estimating midvein points') | |
x_increment = (centroid2[0] - centroid1[0]) / 11 | |
y_increment = (centroid2[1] - centroid1[1]) / 11 | |
midvein = [] | |
for i in range(1, 11): | |
x = centroid1[0] + i * x_increment | |
y = centroid1[1] + i * y_increment | |
midvein.append([x, y]) | |
# find the centroids of each group of points | |
centroid1 = np.mean(top, axis=0) | |
centroid2 = np.mean(bottom, axis=0) | |
# calculate the midpoint between the centroids | |
midpoint = (centroid1 + centroid2) / 2 | |
# calculate the vector between the centroids | |
centroid_vector = centroid2 - centroid1 | |
# calculate the vector perpendicular to the centroid vector | |
perp_vector = np.array([-centroid_vector[1], centroid_vector[0]]) | |
# normalize the perpendicular vector | |
perp_unit_vector = perp_vector / np.linalg.norm(perp_vector) | |
# define the length of the line segment | |
# line_segment_length = np.linalg.norm(centroid_vector) / 2 | |
# calculate the maximum length of the line segment that can be drawn inside the image | |
max_line_segment_length = min(midpoint[0], midpoint[1], self.width - midpoint[0], self.height - midpoint[1]) | |
# calculate the step size | |
step_size = max_line_segment_length / 5 | |
# generate 10 points along the line that is perpendicular to the centroid vector and goes through the midpoint | |
points = [] | |
for i in range(-5, 6): | |
point = midpoint + i * step_size * perp_unit_vector | |
points.append(point) | |
# find the equation of the line passing through the midpoint and with the perpendicular unit vector as the slope | |
b = midpoint[1] - perp_unit_vector[1] * midpoint[0] | |
if perp_unit_vector[0] == 0: | |
denom = 0.0000000001 | |
else: | |
denom = perp_unit_vector[0] | |
m = perp_unit_vector[1] / denom | |
self.width_infer = points | |
# Draw line of fit | |
for point in points: | |
point[0] = np.clip(point[0], 0, self.width - 1) | |
point[1] = np.clip(point[1], 0, self.height - 1) | |
cv2.circle(self.image, tuple(point.astype(int)), radius=2, color=(0, 0, 255), thickness=-1) | |
return [m, b] | |
def restrict_by_width_relation(self, line_params): | |
''' | |
Are the tips on the same side | |
''' | |
if self.has_lamina_base and self.has_lamina_tip: | |
loc_tip = self.point_position_relative_to_line(self.lamina_tip, line_params) | |
loc_base = self.point_position_relative_to_line(self.lamina_base, line_params) | |
if loc_tip == loc_base: | |
self.has_lamina_base = False | |
self.has_lamina_tip = False | |
cv2.circle(self.image, self.lamina_tip, radius=5, color=(0, 0, 0), thickness=2) # pink solid | |
cv2.circle(self.image, self.lamina_base, radius=5, color=(0, 0, 0), thickness=2) # purple | |
self.logger.debug(f'Restrictions [Lamina Tip/Base] - fail - Lamina tip and base are on same side') | |
else: | |
self.logger.debug(f'Restrictions [Lamina Tip/Base] - pass - Lamina tip and base are on opposite side') | |
''' | |
are all apex and base values on their respecitive sides? | |
''' | |
self.has_valid_apex_loc = False | |
self.has_valid_base_loc = False | |
apex_side = 'NA' | |
base_side = 'NA' | |
if self.has_apex: | |
loc_left = self.point_position_relative_to_line(self.apex_left, line_params) | |
loc_right = self.point_position_relative_to_line(self.apex_right, line_params) | |
loc_center = self.point_position_relative_to_line(self.apex_center, line_params) | |
if loc_left == loc_right == loc_center: # all the same | |
apex_side = loc_center | |
self.has_valid_apex_loc = True | |
else: | |
self.has_valid_apex_loc = False | |
self.logger.debug(f'Restrictions [Angles] - has_valid_apex_loc = False, apex loc crosses width') | |
else: | |
self.logger.debug(f'Restrictions [Angles] - has_valid_apex_loc = False, no apex') | |
if self.has_base: | |
loc_left_b = self.point_position_relative_to_line(self.base_left, line_params) | |
loc_right_b = self.point_position_relative_to_line(self.base_right, line_params) | |
loc_center_b = self.point_position_relative_to_line(self.base_center, line_params) | |
if loc_left_b == loc_right_b == loc_center_b: # all the same | |
base_side = loc_center_b | |
self.has_valid_base_loc = True | |
else: | |
self.logger.debug(f'Restrictions [Angles] - has_valid_base_loc = False, base loc crosses width') | |
else: | |
self.logger.debug(f'Restrictions [Angles] - has_valid_base_loc = False') | |
if self.has_valid_apex_loc and self.has_valid_base_loc and (base_side != apex_side): | |
self.logger.debug(f'Restrictions [Angles] - pass - apex and base') | |
elif (base_side == apex_side) and (self.has_apex) and (self.has_base): | |
self.has_valid_apex_loc = False | |
self.has_valid_base_loc = False | |
### This is most restrictive | |
self.has_apex = False | |
self.has_base = False | |
self.order_points_plot([self.apex_left, self.apex_center, self.apex_right], 'failed_angle', 'QC') | |
self.order_points_plot([self.base_left, self.base_center, self.base_right], 'failed_angle', 'QC') | |
self.logger.debug(f'Restrictions [Angles] - fail - apex and base') | |
elif (not self.has_valid_apex_loc) and (self.has_apex): | |
self.has_apex = False | |
self.order_points_plot([self.apex_left, self.apex_center, self.apex_right], 'failed_angle', 'QC') | |
self.logger.debug(f'Restrictions [Angles] - fail - apex') | |
elif (not self.has_valid_base_loc) and (self.has_base): | |
self.has_base = False | |
self.order_points_plot([self.base_left, self.base_center, self.base_right], 'failed_angle', 'QC') | |
self.logger.debug(f'Restrictions [Angles] - fail - base') | |
else: | |
self.logger.debug(f'Restrictions [Angles] - no change') | |
''' | |
does the petiole cross the width loc? | |
''' | |
if self.has_ordered_petiole: | |
petiole_check = [] | |
for point in self.ordered_petiole: | |
check_val = self.point_position_relative_to_line(point, line_params) | |
petiole_check.append(check_val) | |
petiole_check = list(set(petiole_check)) | |
self.logger.debug(f'Restrictions [Petiole] - petiole set = {petiole_check}') | |
if len(petiole_check) == 1: | |
self.has_ordered_petiole = True # Keep the petiole | |
petiole_check = petiole_check[0] | |
self.logger.debug(f'Restrictions [Petiole] - petiole does not cross width - pass') | |
else: | |
self.has_ordered_petiole = False # Reject the petiole, it crossed the center | |
self.logger.debug(f'Restrictions [Petiole] - petiole does cross width - fail') | |
else: | |
self.logger.debug(f'Restrictions [Petiole] - has_ordered_petiole = False') | |
''' | |
Is the lamina base on the same side as the petiole? | |
happens after the other checks... | |
''' | |
if self.has_lamina_base and self.has_lamina_tip and self.has_ordered_petiole: | |
# base is not on the same side as petiole, swap IF base and tip are already opposite | |
if loc_base != petiole_check: | |
if loc_base != loc_tip: # make sure that the tips are on opposite sides, if yes, swap the base and tip | |
hold_data = self.lamina_tip | |
self.lamina_tip = self.lamina_base | |
self.lamina_base = hold_data | |
cv2.circle(self.image, self.lamina_tip, radius=9, color=(255, 0, 230), thickness=2) # pink solid | |
cv2.circle(self.image, self.lamina_base, radius=9, color=(0, 100, 255), thickness=2) # purple | |
self.logger.debug(f'Restrictions [Petiole/Lamina Tip Same Side] - pass - swapped lamina tip and lamina base') | |
else: | |
self.has_lamina_base = False | |
self.has_lamina_tip = False | |
self.logger.debug(f'Restrictions [Petiole/Lamina Tip Same Side] - fail - lamina base not on same side as petiole, base and tip are on same side') | |
else: # base is on correct side | |
if loc_base == loc_tip: # base and tip are on the same side. error | |
self.has_lamina_base = False | |
self.has_lamina_tip = False | |
self.logger.debug(f'Restrictions [Petiole/Lamina Tip Same Side] - fail - base and tip are on the same side, but base and petiole are ok') | |
else: | |
self.logger.debug(f'Restrictions [Petiole/Lamina Tip Same Side] - pass - no swap') | |
def add_tip(self, tip): | |
# Calculate the distances between the first and last points in midvein and the new point | |
dist_start = math.dist(self.ordered_midvein[0], tip) | |
dist_end = math.dist(self.ordered_midvein[-1], tip) | |
# Append tip to the beginning of the list if it's closer to the first point, otherwise append it to the end of the list | |
if dist_start < dist_end: | |
self.ordered_midvein.insert(0, tip) | |
start_or_end = 'start' | |
self.logger.debug(f'Restrictions [Midvein Connect] - tip added to beginning of ordered_midvein') | |
else: | |
self.ordered_midvein.append(tip) | |
start_or_end = 'end' | |
self.logger.debug(f'Restrictions [Midvein Connect] - tip added to end of ordered_midvein') | |
return start_or_end | |
def find_min_width(self, left, right): | |
left_vectors = np.array(left)[:, np.newaxis, :] | |
right_vectors = np.array(right)[np.newaxis, :, :] | |
distances = np.linalg.norm(left_vectors - right_vectors, axis=2) | |
indices = np.unravel_index(np.argmin(distances), distances.shape) | |
return left[indices[0]], right[indices[1]] | |
def find_most_orthogonal_vectors(self, left, right, midvein_fit): | |
left_vectors = np.array(left)[:, np.newaxis, :] - np.array(right)[np.newaxis, :, :] | |
right_vectors = -left_vectors | |
midvein_vector = np.array(midvein_fit[-1]) - np.array(midvein_fit[0]) | |
midvein_vector /= np.linalg.norm(midvein_vector) | |
dot_products = np.abs(np.sum(left_vectors * midvein_vector, axis=2)) + np.abs(np.sum(right_vectors * midvein_vector, axis=2)) | |
indices = np.unravel_index(np.argmax(dot_products), dot_products.shape) | |
return left[indices[0]], right[indices[1]] | |
def determine_reflex(self, apex_left, apex_right, apex_center): | |
vector_left_to_center = np.array([apex_center[0] - apex_left[0], apex_center[1] - apex_left[1]]) | |
vector_right_to_center = np.array([apex_center[0] - apex_right[0], apex_center[1] - apex_right[1]]) | |
# Calculate the vector pointing to the average midvein trace value | |
midvein_trace_arr = np.array([(x, y) for x, y in self.ordered_midvein]) | |
midvein_trace_avg = midvein_trace_arr.mean(axis=0) | |
vector_to_midvein_trace = midvein_trace_avg - np.array(apex_center) | |
# Determine whether the angle is reflex or not | |
if np.dot(vector_left_to_center, vector_to_midvein_trace) > 0 and np.dot(vector_right_to_center, vector_to_midvein_trace) > 0: | |
angle_type = 'reflex' | |
else: | |
angle_type = 'not_reflex' | |
angle = self.calculate_angle(apex_left, apex_center, apex_right) | |
if angle_type == 'reflex': | |
angle = 360 - angle | |
self.order_points_plot([apex_left, apex_center, apex_right], angle_type, 'QC') | |
return angle_type, angle | |
def calculate_angle(self, p1, p2, p3): | |
# Calculate the vectors between the points | |
v1 = (p1[0] - p2[0], p1[1] - p2[1]) | |
v2 = (p3[0] - p2[0], p3[1] - p2[1]) | |
# Calculate the dot product and magnitudes of the vectors | |
dot_product = v1[0]*v2[0] + v1[1]*v2[1] | |
mag_v1 = math.sqrt(v1[0]**2 + v1[1]**2) | |
mag_v2 = math.sqrt(v2[0]**2 + v2[1]**2) | |
if mag_v1 == 0: | |
mag_v1 = 0.000000001 | |
if mag_v2 == 0: | |
mag_v2 = 0.000000001 | |
# Calculate the cosine of the angle | |
denom = (mag_v1 * mag_v2) | |
if denom == 0: | |
denom = 0.000000001 | |
cos_angle = dot_product / denom | |
# Calculate the angle in radians and degrees | |
angle_rad = math.acos(min(max(cos_angle, -1), 1)) | |
angle_deg = math.degrees(angle_rad) | |
return angle_deg | |
def calc_min_distance(self, point, reference_points): | |
# Convert the points and reference points to numpy arrays | |
points_arr = np.atleast_2d(point) | |
reference_arr = np.array(reference_points) | |
# Calculate the distances between each point in "points" and each point in "reference_points" | |
dists = np.linalg.norm(points_arr[:, np.newaxis, :] - reference_arr, axis=2) | |
distance = np.min(dists, axis=1) | |
return distance | |
def get_closest_point_to_sampled_points(self, points, reference_points): | |
# Convert the points and reference points to numpy arrays | |
points_arr = np.array(points) | |
reference_arr = np.array(reference_points) | |
# Calculate the distances between each point in "points" and each point in "reference_points" | |
dists = np.linalg.norm(points_arr[:, np.newaxis, :] - reference_arr, axis=2) | |
distances = np.min(dists, axis=1) | |
# Get the index of the closest point | |
closest_idx = np.argmin(distances) | |
# Remove the closest point from the list of points | |
return points.pop(closest_idx), points | |
def get_far_point(self, points, reference_point): | |
# Calculate the distances between each point and the reference points | |
distances = [math.dist(point, reference_point) for point in points] | |
# Get the index of the closest point | |
closest_idx = distances.index(max(distances)) | |
far_point = points.pop(closest_idx) | |
# Remove the closest point from the list of points | |
return far_point, points | |
'''def point_position_relative_to_line(self, point, line_params): | |
# Extract the cubic coefficients from the line parameters | |
a, b, c, d = line_params | |
# Determine the x-coordinate of the point where it intersects with the line | |
# We solve the cubic equation ax^3 + bx^2 + cx + d = y for x, given y = point[1] | |
f = lambda x: a*x**3 + b*x**2 + c*x + d - point[1] | |
roots = np.roots([a, b, c, d-point[1]]) | |
real_roots = roots[np.isreal(roots)].real | |
if len(real_roots) == 0: | |
return "left" # point is below the curve | |
x_intersection = real_roots[0] | |
# Determine the midpoint of the line | |
mid_x = self.width / 2 | |
mid_y = self.height / 2 | |
# Determine if the point is to the left or right of the line | |
if self.height > self.width: | |
if point[0] < x_intersection: | |
return "left" | |
else: | |
return "right" | |
else: | |
if point[1] < a*mid_x**3 + b*mid_x**2 + c*mid_x + d: | |
return "left" | |
else: | |
return "right"''' | |
def point_position_relative_to_line(self, point, line_params): | |
# Extract the slope and y-intercept from the line parameters | |
slope, y_intercept = line_params | |
if (slope == 0.0) or (slope == 0): | |
slope = 0.00000000000001 | |
# Determine the x-coordinate of the point where it intersects with the line | |
x_intersection = (point[1] - y_intercept) / slope | |
# Determine the midpoint of the line | |
mid_x = self.width / 2 | |
mid_y = self.height / 2 | |
# Determine if the point is to the left or right of the line | |
if self.height > self.width: | |
if point[0] < x_intersection: | |
return "left" | |
else: | |
return "right" | |
else: | |
if point[1] < slope * (point[0] - mid_x) + mid_y: | |
return "left" #below | |
else: | |
return "right" #above | |
def rotate_points(self, points, angle_cw): | |
# Calculate the center of the image | |
center_x = self.width / 2 | |
center_y = self.height / 2 | |
# Translate the points to the center | |
translated_points = [(point[0]-center_x, point[1]-center_y) for point in points] | |
# Convert the angle to radians | |
angle_cw = math.radians(angle_cw) | |
# Rotate the points | |
rotated_points = [(round(point[0]*math.cos(angle_cw)-point[1]*math.sin(angle_cw)), round(point[0]*math.sin(angle_cw)+point[1]*math.cos(angle_cw))) for point in translated_points] | |
# Translate the points back to the original origin | |
return [(point[0]+center_x, point[1]+center_y) for point in rotated_points] | |
def order_petiole(self): | |
if 'petiole_trace' in self.points_list: | |
if len(self.points_list['petiole_trace']) >= 5: | |
self.logger.debug(f"Ordered Petiole - Raw list contains {len(self.points_list['petiole_trace'])} points - using momentum") | |
self.ordered_petiole = self.order_points(self.points_list['petiole_trace']) | |
self.ordered_petiole = self.remove_duplicate_points(self.ordered_petiole) | |
self.ordered_petiole = self.check_momentum(self.ordered_petiole, False) | |
self.order_points_plot(self.ordered_petiole, 'petiole_trace', 'QC') | |
self.ordered_petiole_length, self.ordered_petiole = self.get_length_of_ordered_points(self.ordered_petiole, 'petiole_trace') | |
self.has_ordered_petiole = True | |
elif len(self.points_list['petiole_trace']) >= 2: | |
self.logger.debug(f"Ordered Petiole - Raw list contains {len(self.points_list['petiole_trace'])} points - SKIPPING momentum") | |
self.ordered_petiole = self.order_points(self.points_list['petiole_trace']) | |
self.ordered_petiole = self.remove_duplicate_points(self.ordered_petiole) | |
self.order_points_plot(self.ordered_petiole, 'petiole_trace', 'QC') | |
self.ordered_petiole_length, self.ordered_petiole = self.get_length_of_ordered_points(self.ordered_petiole, 'petiole_trace') | |
self.has_ordered_petiole = True | |
else: | |
self.logger.debug(f"Ordered Petiole - Raw list contains {len(self.points_list['petiole_trace'])} points - SKIPPING PETIOLE") | |
def order_midvein(self): | |
if 'midvein_trace' in self.points_list: | |
if len(self.points_list['midvein_trace']) >= 5: | |
self.logger.debug(f"Ordered Midvein - Raw list contains {len(self.points_list['midvein_trace'])} points - using momentum") | |
self.ordered_midvein = self.order_points(self.points_list['midvein_trace']) | |
self.ordered_midvein = self.remove_duplicate_points(self.ordered_midvein) | |
self.ordered_midvein = self.check_momentum(self.ordered_midvein, False) | |
self.order_points_plot(self.ordered_midvein, 'midvein_trace', 'QC') | |
self.ordered_midvein_length, self.ordered_midvein = self.get_length_of_ordered_points(self.ordered_midvein, 'midvein_trace') | |
self.has_midvein = True | |
else: | |
self.logger.debug(f"Ordered Midvein - Raw list contains {len(self.points_list['midvein_trace'])} points - SKIPPING MIDVEIN") | |
def check_momentum(self, coords, info): | |
original_coords = coords | |
# find middle index of coordinates | |
mid_idx = len(coords) // 2 | |
# set up variables for running average | |
running_avg = np.array(coords[mid_idx-1]) | |
avg_count = 1 | |
# iterate over coordinates to check momentum change | |
prev_vec = np.array(coords[mid_idx-1]) - np.array(coords[mid_idx-2]) | |
cur_idx = mid_idx - 1 | |
while cur_idx >= 0: | |
cur_vec = np.array(coords[cur_idx]) - np.array(coords[cur_idx-1]) | |
# add current point to running average | |
running_avg = (running_avg * avg_count + np.array(coords[cur_idx])) / (avg_count + 1) | |
avg_count += 1 | |
# check for momentum change | |
if self.check_momentum_change(prev_vec, cur_vec): | |
break | |
prev_vec = cur_vec | |
cur_idx -= 1 | |
# use running average to check for momentum change | |
cur_vec = np.array(coords[cur_idx]) - running_avg | |
if self.check_momentum_change(prev_vec, cur_vec): | |
cur_idx += 1 | |
prev_vec = np.array(coords[mid_idx+1]) - np.array(coords[mid_idx]) | |
cur_idx2 = mid_idx + 1 | |
while cur_idx2 < len(coords): | |
# check if current index is out of range | |
if cur_idx2 >= len(coords): | |
break | |
cur_vec = np.array(coords[cur_idx2]) - np.array(coords[cur_idx2-1]) | |
# add current point to running average | |
running_avg = (running_avg * avg_count + np.array(coords[cur_idx2])) / (avg_count + 1) | |
avg_count += 1 | |
# check for momentum change | |
if self.check_momentum_change(prev_vec, cur_vec): | |
break | |
prev_vec = cur_vec | |
cur_idx2 += 1 | |
# use running average to check for momentum change | |
if cur_idx2 < len(coords): | |
cur_vec = np.array(coords[cur_idx2]) - running_avg | |
if self.check_momentum_change(prev_vec, cur_vec): | |
cur_idx2 -= 1 | |
# remove problematic points and subsequent points from list of coordinates | |
new_coords = coords[:cur_idx2] + coords[mid_idx:cur_idx2:-1] | |
if info: | |
return new_coords, len(original_coords) != len(new_coords) | |
else: | |
return new_coords | |
# define function to check for momentum change | |
def check_momentum_change(self, prev_vec, cur_vec): | |
dot_product = np.dot(prev_vec, cur_vec) | |
prev_norm = np.linalg.norm(prev_vec) | |
cur_norm = np.linalg.norm(cur_vec) | |
denom = (prev_norm * cur_norm) | |
if denom == 0: | |
denom = 0.0000000001 | |
cos_theta = dot_product / denom | |
theta = np.arccos(cos_theta) | |
return abs(theta) > np.pi / 2 | |
'''def check_momentum_complex(self, coords, info, start_or_end): | |
original_coords = coords | |
# find middle index of coordinates | |
mid_idx = len(coords) // 2 | |
# get directional vectors for first-middle, middle-last, and second-first and second-last pairs of points | |
first_middle_dir = np.array(coords[1]) - np.array(coords[0]) | |
middle_last_dir = np.array(coords[-1]) - np.array(coords[-2]) | |
second_first_dir = np.array(coords[1]) - np.array(coords[2]) | |
second_last_dir = np.array(coords[-1]) - np.array(coords[-3]) | |
if start_or_end == 'end': | |
# check directional change for first-middle vector | |
cur_idx = 2 | |
while cur_idx < len(coords): | |
cur_vec = np.array(coords[cur_idx]) - np.array(coords[cur_idx-1]) | |
if self.check_momentum_change_complex(first_middle_dir, cur_vec): | |
break | |
cur_idx += 1 | |
cur_idx2 = len(coords) - 2 | |
elif start_or_end == 'start': | |
# check directional change for last-middle vector | |
cur_idx2 = len(coords)-3 | |
while cur_idx2 >= 0: | |
cur_vec = np.array(coords[cur_idx2]) - np.array(coords[cur_idx2+1]) | |
if self.check_momentum_change_complex(middle_last_dir, cur_vec): | |
break | |
cur_idx2 -= 1 | |
cur_idx = 1 | |
# check directional change for second-first and second-last vectors | |
second_first_change = self.check_momentum_change_complex(second_first_dir, first_middle_dir) | |
second_last_change = self.check_momentum_change_complex(second_last_dir, middle_last_dir) | |
# remove problematic points and subsequent points from list of coordinates | |
if cur_idx <= cur_idx2: | |
new_coords = coords[:cur_idx+1] + coords[cur_idx2:mid_idx:-1] + coords[cur_idx+1:cur_idx2+1] | |
else: | |
new_coords = coords[:mid_idx+1] + coords[cur_idx2:cur_idx:-1] + coords[mid_idx+1:cur_idx2+1] | |
self.logger.debug(f'Original midvein points - {self.ordered_midvein}') | |
self.logger.debug(f'Momentum midvein points - {new_coords}') | |
if info: | |
return new_coords, len(original_coords) != len(new_coords) or second_first_change or second_last_change | |
else: | |
return new_coords''' | |
def check_momentum_complex(self, coords, info, start_or_end): # Works, but removes ALL points after momentum change | |
original_coords = coords | |
if max([self.height, self.width]) < 200: | |
scale_factor = 0.25 | |
elif max([self.height, self.width]) < 500: | |
scale_factor = 0.5 | |
else: | |
scale_factor = 1 | |
self.logger.debug(f'Scale factor - [{scale_factor}]') | |
# find middle index of coordinates | |
mid_idx = len(coords) // 2 | |
# get directional vectors for first-middle, middle-last, and second-first and second-last pairs of points | |
first_middle_dir = np.array(coords[1]) - np.array(coords[mid_idx]) | |
middle_last_dir = np.array(coords[mid_idx]) - np.array(coords[-2]) | |
second_first_dir = np.array(coords[1]) - np.array(coords[0]) | |
second_last_dir = np.array(coords[-1]) - np.array(coords[-2]) | |
if start_or_end == 'end': | |
# check directional change for first-middle vector | |
cur_idx_list = [] | |
cur_idx = 2 | |
while cur_idx < len(coords): | |
cur_vec = np.array(coords[cur_idx]) - np.array(coords[cur_idx-1]) | |
if self.check_momentum_change_complex(first_middle_dir, cur_vec): | |
# break | |
cur_idx_list.append(cur_idx) | |
cur_idx += 1 | |
if len(cur_idx_list) > 0: | |
cur_idx = max(cur_idx_list) | |
else: | |
cur_idx = len(coords) | |
# remove problematic points and subsequent points from list of coordinates | |
end_vector_mag = np.linalg.norm(second_last_dir) | |
avg_dist = np.mean([np.linalg.norm(np.array(coords[i])-np.array(coords[i-1])) for i in range(1, len(coords))]) | |
new_coords = coords | |
if (end_vector_mag > (scale_factor * 0.01 * avg_dist * len(new_coords))) and (len(cur_idx_list) > 0): | |
# new_coords = coords[:cur_idx+1] + coords[-2:cur_idx:-1][::-1] #coords[-2:cur_idx:-1] | |
new_coords = coords[:len(new_coords)-1]# + coords[-2:cur_idx:-1][::-1] #coords[-2:cur_idx:-1] | |
self.logger.debug(f'Momentum - removing last point') | |
else: | |
self.logger.debug(f'Momentum - change not detected, no change') | |
elif start_or_end == 'start': | |
# check directional change for last-middle vector | |
cur_idx2_list = [] | |
cur_idx2 = len(coords)-3 | |
while cur_idx2 >= 0: | |
cur_vec = np.array(coords[cur_idx2]) - np.array(coords[cur_idx2+1]) | |
if self.check_momentum_change_complex(middle_last_dir, cur_vec): | |
# break | |
cur_idx2_list.append(cur_idx2) | |
cur_idx2 -= 1 | |
if len(cur_idx2_list) > 0: | |
cur_idx2 = min(cur_idx2_list) | |
else: | |
cur_idx2 = 0 | |
# remove problematic points and subsequent points from list of coordinates | |
new_coords = coords | |
start_vector_mag = np.linalg.norm(second_first_dir) | |
avg_dist = np.mean([np.linalg.norm(np.array(coords[i])-np.array(coords[i-1])) for i in range(1, len(coords))]) | |
if (start_vector_mag > (scale_factor * 0.01 * avg_dist * len(new_coords))) and (len(cur_idx2_list) > 0): | |
# new_coords = coords[:mid_idx+1] + coords[cur_idx2:mid_idx:-1][::-1] # #coords[cur_idx2:mid_idx:-1] | |
new_coords = coords[1:]#ur_idx2-1] + coords[mid_idx+1:] | |
# new_coords = coords[cur_idx2:mid_idx+1][::-1] + coords[mid_idx+1:] | |
self.logger.debug(f'Momentum - removing first point') | |
else: | |
self.logger.debug(f'Momentum - change not detected, no change') | |
else: | |
print('hi') | |
# check directional change for second-first and second-last vectors | |
# second_first_change = self.check_momentum_change_complex(second_first_dir, first_middle_dir) | |
# second_last_change = self.check_momentum_change_complex(second_last_dir, middle_last_dir) | |
self.logger.debug(f'Original midvein points complex - {start_or_end} - {self.ordered_midvein}') | |
self.logger.debug(f'Momentum midvein points complex - {start_or_end} - {new_coords}') | |
if info: | |
return new_coords, len(original_coords) != len(new_coords) #or second_first_change or second_last_change | |
else: | |
return new_coords | |
'''def check_momentum_complex(self, coords, info, start_or_end): # does not seem to work | |
original_coords = coords | |
# get directional vectors for first-middle, middle-last, and second-first and second-last pairs of points | |
first_middle_dir = np.array(coords[1]) - np.array(coords[0]) | |
middle_last_dir = np.array(coords[-1]) - np.array(coords[-2]) | |
second_first_dir = np.array(coords[1]) - np.array(coords[2]) | |
second_last_dir = np.array(coords[-1]) - np.array(coords[-3]) | |
# calculate running average momentum and check if endpoints are different | |
if start_or_end == 'end': | |
end_vector_mag = np.linalg.norm(first_middle_dir) | |
avg_dist = np.mean([np.linalg.norm(np.array(coords[i])-np.array(coords[i-1])) for i in range(1, len(coords))]) | |
running_avg_momentum = np.mean([np.linalg.norm(np.array(coords[i])-np.array(coords[i-1])) for i in range(len(coords)-10, len(coords))]) | |
endpoint_diff = np.linalg.norm(np.array(coords[-1])-self.ordered_midvein[-1]) > 0.1*running_avg_momentum | |
elif start_or_end == 'start': | |
start_vector_mag = np.linalg.norm(middle_last_dir) | |
avg_dist = np.mean([np.linalg.norm(np.array(coords[i])-np.array(coords[i-1])) for i in range(1, len(coords))]) | |
running_avg_momentum = np.mean([np.linalg.norm(np.array(coords[i])-np.array(coords[i-1])) for i in range(10)]) | |
endpoint_diff = np.linalg.norm(np.array(coords[0])-self.ordered_midvein[0]) > 0.1*running_avg_momentum | |
# remove problematic points and subsequent points from list of coordinates | |
if start_or_end == 'end' and endpoint_diff: | |
cur_idx = 2 | |
while cur_idx < len(coords): | |
cur_vec = np.array(coords[cur_idx]) - np.array(coords[cur_idx-1]) | |
if self.check_momentum_change_complex(first_middle_dir, cur_vec): | |
break | |
cur_idx += 1 | |
new_coords = coords[:cur_idx+1] + coords[-2:cur_idx:-1][::-1] | |
elif start_or_end == 'start' and endpoint_diff: | |
cur_idx2 = len(coords)-3 | |
while cur_idx2 >= 0: | |
cur_vec = np.array(coords[cur_idx2]) - np.array(coords[cur_idx2+1]) | |
if self.check_momentum_change_complex(middle_last_dir, cur_vec): | |
break | |
cur_idx2 -= 1 | |
new_coords = coords[:1] + coords[cur_idx2:0:-1][::-1] | |
else: | |
new_coords = coords | |
# check directional change for second-first and second-last vectors | |
second_first_change = self.check_momentum_change_complex(second_first_dir, first_middle_dir) | |
second_last_change = self.check_momentum_change_complex(second_last_dir, middle_last_dir) | |
self.logger.debug(f'Original midvein points - {self.ordered_midvein}') | |
self.logger.debug(f'Momentum midvein points - {new_coords}') | |
if info: | |
return new_coords, len(original_coords) != len(new_coords) #or second_first_change or second_last_change or endpoint_diff | |
else: | |
return new_coords''' | |
# define function to check for momentum change | |
def check_momentum_change_complex(self, prev_vec, cur_vec): | |
dot_product = np.dot(prev_vec, cur_vec) | |
prev_norm = np.linalg.norm(prev_vec) | |
cur_norm = np.linalg.norm(cur_vec) | |
denom = (prev_norm * cur_norm) | |
if denom == 0: | |
denom = 0.0000000001 | |
cos_theta = dot_product / denom | |
theta = np.arccos(cos_theta) | |
return abs(theta) > np.pi / 2 | |
def remove_duplicate_points(self, points): | |
unique_set = set() | |
new_list = [] | |
for item in points: | |
if item not in unique_set: | |
unique_set.add(item) | |
new_list.append(item) | |
return new_list | |
def order_points_plot(self, points, version, QC_or_final): | |
# thk_base = 0 | |
thk_base = 16 | |
if version == 'midvein_trace': | |
# color = (0, 255, 0) | |
color = (0, 255, 255) # yellow | |
thick = 2 + thk_base | |
elif version == 'petiole_trace': | |
color = (255, 255, 0) | |
thick = 2 + thk_base | |
elif version == 'lamina_width': | |
color = (0, 0, 255) | |
thick = 2 + thk_base | |
elif version == 'lamina_width_alt': | |
color = (100, 100, 255) | |
thick = 2 + thk_base | |
elif version == 'not_reflex': | |
color = (200, 0, 123) | |
thick = 3 + thk_base | |
elif version == 'reflex': | |
color = (0, 120, 200) | |
thick = 3 + thk_base | |
elif version == 'petiole_tip_alt': | |
color = (255, 55, 100) | |
thick = 1 + thk_base | |
elif version == 'petiole_tip': | |
color = (100, 255, 55) | |
thick = 1 + thk_base | |
elif version == 'failed_angle': | |
color = (0, 0, 0) | |
thick = 3 + thk_base | |
# Convert the points to a numpy array and round to integer values | |
points_arr = np.round(np.array(points)).astype(int) | |
# Draw a green line connecting all of the points | |
if QC_or_final == 'QC': | |
for i in range(len(points_arr) - 1): | |
cv2.line(self.image, tuple(points_arr[i]), tuple(points_arr[i+1]), color, thick) | |
else: | |
for i in range(len(points_arr) - 1): | |
cv2.line(self.image_final, tuple(points_arr[i]), tuple(points_arr[i+1]), color, thick) | |
def get_length_of_ordered_points(self, points, name): | |
# if self.file_name == 'B_774373631_Ebenaceae_Diospyros_buxifolia__L__438-687-578-774': | |
# print('hi') | |
total_length = 0 | |
total_length_first_pass = 0 | |
for i in range(len(points) - 1): | |
x1, y1 = points[i] | |
x2, y2 = points[i+1] | |
segment_length = math.sqrt((x2-x1)**2 + (y2-y1)**2) | |
total_length_first_pass += segment_length | |
cutoff = total_length_first_pass / 2 | |
# print(f'Total length of {name}: {total_length_first_pass}') | |
# print(f'points length {len(points)}') | |
self.logger.debug(f"Total length of {name}: {total_length_first_pass}") | |
self.logger.debug(f"Points length {len(points)}") | |
# If there are more than 2 points, this will exclude extreme outliers, or | |
# misordered points that don't belong | |
if len(points) > 2: | |
pop_ind = [] | |
for i in range(len(points) - 1): | |
x1, y1 = points[i] | |
x2, y2 = points[i+1] | |
segment_length = math.sqrt((x2-x1)**2 + (y2-y1)**2) | |
if segment_length < cutoff: | |
total_length += segment_length | |
else: | |
pop_ind.append(i) | |
for exclude in pop_ind: | |
points.pop(exclude) | |
# print(f'Total length of {name}: {total_length}') | |
# print(f'Excluded {len(pop_ind)} points') | |
# print(f'points length {len(points)}') | |
self.logger.debug(f"Total length of {name}: {total_length}") | |
self.logger.debug(f"Excluded {len(pop_ind)} points") | |
self.logger.debug(f"Points length {len(points)}") | |
else: | |
total_length = total_length_first_pass | |
return total_length, points | |
def convert_YOLO_bbox_to_point(self): | |
for point_type, bbox in self.points_list.items(): | |
xy_points = [] | |
for point in bbox: | |
x = point[0] | |
y = point[1] | |
w = point[2] | |
h = point[3] | |
x1 = int((x - w/2) * self.width) | |
y1 = int((y - h/2) * self.height) | |
x2 = int((x + w/2) * self.width) | |
y2 = int((y + h/2) * self.height) | |
xy_points.append((int((x1+x2)/2), int((y1+y2)/2))) | |
self.points_list[point_type] = xy_points | |
def parse_all_points(self): | |
points_list = {} | |
for sublist in self.all_points: | |
key = sublist[0] | |
value = sublist[1:] | |
key = self.swap_number_for_string(key) | |
if key not in points_list: | |
points_list[key] = [] | |
points_list[key].append(value) | |
# print(points_list) | |
self.points_list = points_list | |
def swap_number_for_string(self, key): | |
for k, v in self.classes.items(): | |
if v == key: | |
return k | |
return key | |
def distance(self, point1, point2): | |
x1, y1 = point1 | |
x2, y2 = point2 | |
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) | |
def order_points(self, points): | |
# height = max(points, key=lambda point: point[1])[1] - min(points, key=lambda point: point[1])[1] | |
# width = max(points, key=lambda point: point[0])[0] - min(points, key=lambda point: point[0])[0] | |
if self.height > self.width: | |
start_point = min(points, key=lambda point: point[1]) | |
end_point = max(filter(lambda point: point[0] == max(points, key=lambda point: point[0])[0], points), key=lambda point: point[1]) | |
else: | |
start_point = min(points, key=lambda point: point[0]) | |
end_point = max(filter(lambda point: point[1] == max(points, key=lambda point: point[1])[1], points), key=lambda point: point[0]) | |
tour = [start_point] | |
unvisited = set(points) - {start_point} | |
while unvisited: | |
nearest = min(unvisited, key=lambda point: self.distance(tour[-1], point)) | |
tour.append(nearest) | |
unvisited.remove(nearest) | |
tour.append(end_point) | |
return tour | |
def define_landmark_classes(self): | |
self.classes = { | |
'apex_angle': 0, | |
'base_angle': 1, | |
'lamina_base': 2, | |
'lamina_tip': 3, | |
'lamina_width': 4, | |
'lobe_tip': 5, | |
'midvein_trace': 6, | |
'petiole_tip': 7, | |
'petiole_trace': 8 | |
} | |
def set_cfg_values(self): | |
self.do_show_QC_images = self.cfg['leafmachine']['landmark_detector']['do_show_QC_images'] | |
self.do_save_QC_images = self.cfg['leafmachine']['landmark_detector']['do_save_QC_images'] | |
self.do_show_final_images = self.cfg['leafmachine']['landmark_detector']['do_show_final_images'] | |
self.do_save_final_images = self.cfg['leafmachine']['landmark_detector']['do_save_final_images'] | |
def setup_QC_image(self): | |
self.image = cv2.imread(os.path.join(self.dir_temp, '.'.join([self.file_name, 'jpg']))) | |
if self.leaf_type == 'Landmarks_Whole_Leaves': | |
self.path_QC_image = os.path.join(self.Dirs.landmarks_whole_leaves_overlay_QC, '.'.join([self.file_name, 'jpg'])) | |
elif self.leaf_type == 'Landmarks_Partial_Leaves': | |
self.path_QC_image = os.path.join(self.Dirs.landmarks_partial_leaves_overlay_QC, '.'.join([self.file_name, 'jpg'])) | |
def setup_final_image(self): | |
self.image_final = cv2.imread(os.path.join(self.dir_temp, '.'.join([self.file_name, 'jpg']))) | |
if self.leaf_type == 'Landmarks_Whole_Leaves': | |
self.path_image_final = os.path.join(self.Dirs.landmarks_whole_leaves_overlay_final, '.'.join([self.file_name, 'jpg'])) | |
elif self.leaf_type == 'Landmarks_Partial_Leaves': | |
self.path_image_final = os.path.join(self.Dirs.landmarks_partial_leaves_overlay_final, '.'.join([self.file_name, 'jpg'])) | |
def show_QC_image(self): | |
if self.do_show_QC_images: | |
cv2.imshow('QC image', self.image) | |
cv2.waitKey(0) | |
def show_final_image(self): | |
if self.do_show_final_images: | |
cv2.imshow('Final image', self.image_final) | |
cv2.waitKey(0) | |
def save_QC_image(self): | |
if self.do_save_QC_images: | |
cv2.imwrite(self.path_QC_image, self.image) | |
def get_QC(self): | |
return self.image | |
def get_final(self): | |
return self.image_final | |
def init_lists_dicts(self): | |
# Initialize all lists and dictionaries | |
self.classes = {} | |
self.points_list = [] | |
self.image = [] | |
self.ordered_midvein = [] | |
self.midvein_fit = [] | |
self.midvein_fit_points = [] | |
self.ordered_petiole = [] | |
self.apex_left = self.apex_left or None | |
self.apex_right = self.apex_right or None | |
self.apex_center = self.apex_center or None | |
self.base_left = self.base_left or None | |
self.base_right = self.base_right or None | |
self.base_center = self.base_center or None | |
self.lamina_tip = self.lamina_tip or None | |
self.lamina_base = self.lamina_base or None | |
self.width_left = self.width_left or None | |
self.width_right = self.width_right or None | |