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 | |