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 | |
from sklearn.linear_model import LinearRegression | |
from scipy.optimize import fsolve, minimize | |
class ArmatureSkeleton: | |
cfg: str | |
Dirs: str | |
leaf_type: str | |
all_points: list | |
dir_temp: str | |
file_name: str | |
width: int | |
height: int | |
logger: object | |
is_complete: bool = False | |
keep_going: bool = False | |
do_show_QC_images: bool = False | |
do_save_QC_images: bool = False | |
classes: int = 0 | |
points_list: int = 0 | |
image: int = 0 | |
ordered_middle: int = 0 | |
midvein_fit: int = 0 | |
midvein_fit_points: int = 0 | |
ordered_midvein_length: float = 0.0 | |
has_middle = False | |
has_outer = False | |
has_tip = False | |
is_split = False | |
ordered_petiole: int = 0 | |
ordered_petiole_length: float = 0.0 | |
has_ordered_petiole = False | |
has_apex: bool = False | |
apex_left: int = 0 | |
apex_right: int = 0 | |
apex_center: int = 0 | |
apex_angle_type: str = 'NA' | |
apex_angle_degrees: float = 0.0 | |
has_base: bool = False | |
base_left: int = 0 | |
base_right: int = 0 | |
base_center: int = 0 | |
base_angle_type: str = 'NA' | |
base_angle_degrees: float = 0.0 | |
has_lamina_base: bool = False | |
lamina_base: int = 0 | |
has_lamina_length: bool = False | |
lamina_fit: int = 0 | |
lamina_length: float = 0.0 | |
has_width: bool = False | |
lamina_width: float = 0.0 | |
width_left: float = 0.0 | |
width_right: float = 0.0 | |
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_angle_image() | |
self.setup_final_image() | |
self.parse_all_points() | |
self.convert_YOLO_bbox_to_point() | |
if (len(self.points_list['outer']) > 6) and (len(self.points_list['middle']) > 3): | |
self.keep_going = True | |
""" Landmarks """ | |
if self.keep_going: | |
# Start with ordering the midvein and petiole | |
self.order_middle() | |
# print(self.ordered_midvein) | |
if self.keep_going: | |
# Split the image using the midvein IF has_midvein == True | |
self.split_image_by_middle() | |
if self.keep_going: | |
self.group_outer_points() | |
if self.keep_going: | |
# Measure | |
self.measure_armature() | |
if self.keep_going: | |
# calc tangent angle of outer and inner polys | |
self.calc_angle_tangent() | |
if self.keep_going: | |
self.calc_angle_curl() | |
if self.keep_going: | |
# self.calc_angle_bend() | |
self.calc_curvature_radius() | |
if self.keep_going: | |
self.calc_direct_length() | |
# self.show_QC_image() | |
# self.show_angle_image() | |
self.is_complete = True # TODO add ways to set True | |
def measure_armature(self): | |
# wb = width_base = line between the last outer and inner points | |
# Define the line function | |
def line_func(x): | |
return self.wb_slope * x + self.wb_intercept | |
def middle_func(x): | |
return self.middle_poly[0]*x**2 + self.middle_poly[1]*x + self.middle_poly[2] | |
# Define the difference function | |
def line_middle_diff(x): | |
return line_func(x) - middle_func(x) | |
# Convert the points to numpy arrays | |
last_point_right = np.array(self.last_point_right) | |
last_point_left = np.array(self.last_point_left) | |
# Calculate the Euclidean distance between the points | |
self.width_base = np.linalg.norm(last_point_right - last_point_left) | |
print("The distance between the last points of the right and left segments is:", self.width_base) | |
# Intersection of the width and the middlepoly# Draw a line between the last points of the outer_left and outer_right segments | |
cv2.line(self.image, (int(self.last_point_left[0]), int(self.last_point_left[1])), (int(self.last_point_right[0]), int(self.last_point_right[1])), gc('white'), thickness=2) | |
cv2.line(self.image_angles, (int(self.last_point_left[0]), int(self.last_point_left[1])), (int(self.last_point_right[0]), int(self.last_point_right[1])), color=gc('white'), thickness=2) | |
# Calculate the slope and y-intercept of the line | |
self.wb_slope = (self.last_point_right[1] - self.last_point_left[1]) / (self.last_point_right[0] - self.last_point_left[0]) | |
self.wb_intercept = self.last_point_left[1] - self.wb_slope * self.last_point_left[0] | |
# Find the intersection point | |
intersection_x = fsolve(line_middle_diff, 0)[0] | |
intersection_y = line_func(intersection_x) | |
self.width_base_inter = [(int(intersection_x), int(intersection_y))] | |
# Calculate the midpoint between the last points | |
self.width_base_mid = (last_point_right + last_point_left) / 2 | |
cv2.circle(self.image, (int(intersection_x), int(intersection_y)), radius=2, color=gc('green'), thickness=-1) | |
cv2.circle(self.image, (int(intersection_x), int(intersection_y)), radius=4, color=gc('black'), thickness=2) | |
cv2.circle(self.image, (int(self.width_base_mid[0]), int(self.width_base_mid[1])), radius=2, color=gc('red'), thickness=-1) | |
cv2.circle(self.image, (int(self.width_base_mid[0]), int(self.width_base_mid[1])), radius=4, color=gc('black'), thickness=2) | |
print("The intersection point of the line and the middle polynomial is:", (intersection_x, intersection_y)) | |
def calc_direct_length(self): | |
# Calculate the x-coordinate of the intersection point | |
x_intersection = (self.wb_intercept_perpendicular - self.wb_intercept) / (self.wb_slope - self.wb_slope_perpendicular) | |
# Calculate the y-coordinate of the intersection point | |
y_intersection = self.wb_slope * x_intersection + self.wb_intercept | |
# Store the intersection point as self.wb_origin | |
self.wb_origin = np.array([x_intersection, y_intersection]) | |
# Calculate the distance between the intersection point and self.inter_point | |
self.length_direct = np.linalg.norm(self.wb_origin - self.inter_point) | |
# Plot a 2-pixel thick red line from self.wb_origin to self.inter_point | |
cv2.line(self.image_angles, tuple(map(int, self.wb_origin)), tuple(map(int, self.inter_point)), gc('red'), thickness=2) | |
def calc_curvature_radius(self): | |
def fit_circle_least_squares(points): | |
if len(points) <= 1: | |
return 0.0, (0, 0) | |
def calc_residuals(params, points): | |
x0, y0, r = params | |
residuals = np.sqrt((points[:, 0] - x0) ** 2 + (points[:, 1] - y0) ** 2) - r | |
return residuals | |
def objective(params, points): | |
return np.sum(calc_residuals(params, points) ** 2) | |
x_mean = np.mean(points[:, 0]) | |
y_mean = np.mean(points[:, 1]) | |
r_mean = np.mean(np.sqrt((points[:, 0] - x_mean) ** 2 + (points[:, 1] - y_mean) ** 2)) | |
init_params = [x_mean, y_mean, r_mean] | |
result = minimize(objective, init_params, args=(points,), method='L-BFGS-B') | |
x0, y0, r = result.x | |
return r, (x0, y0) | |
self.radius_middle, center_middle = fit_circle_least_squares(self.ordered_middle_np) | |
self.radius_outer_left, center_outer_left = fit_circle_least_squares(self.ordered_outer_left_np) | |
self.radius_outer_right, center_outer_right = fit_circle_least_squares(self.ordered_outer_right_np) | |
# Plot the circles on self.image_angles | |
cv2.circle(self.image_angles, (int(center_middle[0]), int(center_middle[1])), int(self.radius_middle), gc('yellow'), thickness=1) | |
cv2.circle(self.image_angles, (int(center_outer_left[0]), int(center_outer_left[1])), int(self.radius_outer_left), gc('pink'), thickness=1) | |
cv2.circle(self.image_angles, (int(center_outer_right[0]), int(center_outer_right[1])), int(self.radius_outer_right), gc('cyan'), thickness=1) | |
print('hi') | |
def calc_angle_bend(self): | |
print('hi') | |
def calc_angle_curl(self): | |
# Define the perpendicular line function | |
def wb_line_perpendicular(x): | |
return self.wb_slope_perpendicular * x + self.wb_intercept_perpendicular | |
# Calculate the slope of the line perpendicular to the given line | |
self.wb_slope_perpendicular = -1 / self.wb_slope | |
# Calculate the y-intercept of the line perpendicular to the given line | |
self.wb_intercept_perpendicular = self.inter_point[1] - self.wb_slope_perpendicular * self.inter_point[0] | |
# Line fit to first 3 points in self.ordered_middle | |
self.middle_tip_poly = np.polyfit(self.ordered_middle_np[0:3, 0], self.ordered_middle_np[0:3, 1], 1) | |
middle_tip_slope = self.middle_tip_poly[0] | |
# angle between middle_tip fit the curl perpendicular | |
theta = math.atan(abs((middle_tip_slope - self.wb_slope_perpendicular) / (1 + self.wb_slope_perpendicular*middle_tip_slope))) | |
# Convert the angle to degrees | |
self.angle_curl = math.degrees(theta) | |
print("The angle between the lines is:", self.angle_curl, "degrees") | |
# Draw the tangents at the intersection point | |
intersection_point = np.array(self.inter_point_outer_inner, dtype=int) | |
length = 50 # Length of the tangent lines | |
# Calculate the points for the tangent lines | |
curl_tangent_point1 = (intersection_point[0] - length, intersection_point[1] - length * self.wb_slope_perpendicular) | |
curl_tangent_point2 = (intersection_point[0] + length, intersection_point[1] + length * self.wb_slope_perpendicular) | |
middle_tip_tangent_point1 = (intersection_point[0] - length, intersection_point[1] - length * middle_tip_slope) | |
middle_tip_tangent_point2 = (intersection_point[0] + length, intersection_point[1] + length * middle_tip_slope) | |
# Convert the points to integers | |
curl_tangent_point1 = tuple(map(int, curl_tangent_point1)) | |
curl_tangent_point2 = tuple(map(int, curl_tangent_point2)) | |
middle_tip_tangent_point1 = tuple(map(int, middle_tip_tangent_point1)) | |
middle_tip_tangent_point2 = tuple(map(int, middle_tip_tangent_point2)) | |
# Draw the tangent lines | |
cv2.line(self.image_angles, intersection_point, curl_tangent_point1, gc('teal'), 1) | |
cv2.line(self.image_angles, intersection_point, curl_tangent_point2, gc('teal'), 1) | |
cv2.line(self.image_angles, intersection_point, middle_tip_tangent_point1, gc('teal'), 1) | |
cv2.line(self.image_angles, intersection_point, middle_tip_tangent_point2, gc('teal'), 1) | |
# Draw the arc representing the angle | |
cv2.ellipse(self.image_angles, tuple(intersection_point), (length, length), 0, 0, self.angle_curl, gc('teal'), 2) | |
cv2.ellipse(self.image_angles, tuple(intersection_point), (length, length), 180, 0, self.angle_curl, gc('teal'), 2) | |
### plot the wb_line_perpendicular | |
# Calculate the y values for the start and end points of the line | |
y_start = max(0, int(wb_line_perpendicular(0))) | |
y_end = min(self.height, int(wb_line_perpendicular(self.width))) | |
# Define the range of y values for the line | |
y_range = np.linspace(y_start, y_end, num=100, dtype=int) # You can adjust 'num' to control the number of points | |
# Draw the dotted gray line | |
for i in range(len(y_range) - 1): | |
y1, x1 = y_range[i], int((y_range[i] - self.wb_intercept_perpendicular) / self.wb_slope_perpendicular) | |
x1 = max(0, min(x1, self.width)) # Keep x1 within the bounds of the image width | |
y2, x2 = y_range[i+1], int((y_range[i+1] - self.wb_intercept_perpendicular) / self.wb_slope_perpendicular) | |
x2 = max(0, min(x2, self.width)) # Keep x2 within the bounds of the image width | |
if i % 2 == 0: # Change the value of 2 to adjust the spacing between the dots | |
cv2.line(self.image_angles, (x1, y1), (x2, y2), gc('white'), 1) | |
def calc_angle_tangent(self): | |
# Define the polynomial functions | |
def left_func(x): | |
return self.left_poly[0]*x**2 + self.left_poly[1]*x + self.left_poly[2] | |
def right_func(x): | |
return self.right_poly[0]*x**2 + self.right_poly[1]*x + self.right_poly[2] | |
# Define the difference function | |
def left_right_diff(x): | |
return left_func(x) - right_func(x) | |
# Find the x-coordinate of the intersection point | |
intersection_x = fsolve(left_right_diff, 0)[0] | |
# Calculate the y-coordinate of the intersection point on the left and right curves | |
intersection_y_left = left_func(intersection_x) | |
intersection_y_right = right_func(intersection_x) | |
# Calculate the derivatives of the polynomials at the intersection point | |
left_derivative = 2*self.left_poly[0]*intersection_x + self.left_poly[1] | |
right_derivative = 2*self.right_poly[0]*intersection_x + self.right_poly[1] | |
# Calculate the angle between the tangents to the polynomials at the intersection point | |
theta = math.atan(abs((right_derivative - left_derivative) / (1 + left_derivative*right_derivative))) | |
# Convert the angle to degrees | |
self.angle_tangent = math.degrees(theta) | |
print("The angle between the left and right polynomials at their point of intersection is:", theta, "degrees") | |
# Draw the tangents at the intersection point | |
intersection_point = np.array([int(intersection_x), int(intersection_y_left + (intersection_y_right - intersection_y_left)/2)]) | |
length = 30 # Length of the tangent lines | |
# Calculate the points for the tangent lines | |
left_tangent_point1 = (intersection_point[0] - length, intersection_point[1] - length * left_derivative) | |
left_tangent_point2 = (intersection_point[0] + length, intersection_point[1] + length * left_derivative) | |
right_tangent_point1 = (intersection_point[0] - length, intersection_point[1] - length * right_derivative) | |
right_tangent_point2 = (intersection_point[0] + length, intersection_point[1] + length * right_derivative) | |
# Convert the points to integers | |
left_tangent_point1 = tuple(map(int, left_tangent_point1)) | |
left_tangent_point2 = tuple(map(int, left_tangent_point2)) | |
right_tangent_point1 = tuple(map(int, right_tangent_point1)) | |
right_tangent_point2 = tuple(map(int, right_tangent_point2)) | |
# # Draw the tangent lines | |
# cv2.line(self.image_angles, intersection_point, left_tangent_point1, gc('yellow'), 1) | |
# cv2.line(self.image_angles, intersection_point, left_tangent_point2, gc('yellow'), 1) | |
# cv2.line(self.image_angles, intersection_point, right_tangent_point1, gc('yellow'), 1) | |
# cv2.line(self.image_angles, intersection_point, right_tangent_point2, gc('yellow'), 1) | |
# Draw the arc representing the angle | |
cv2.ellipse(self.image_angles, tuple(intersection_point), (length, length), 0, 0, self.angle_tangent, gc('yellow'), 2) | |
cv2.ellipse(self.image_angles, tuple(intersection_point), (length, length), 180, 0, self.angle_tangent, gc('yellow'), 2) | |
# self.show_angle_image() | |
# return theta | |
def group_outer_points(self): | |
# Split the points into two groups based on their position relative to the line | |
self.outer_left = [] | |
self.outer_right = [] | |
# if 'tip' in self.points_list: | |
for point in self.points_list['outer']: | |
x, y = point | |
predicted_y = self.predict_y(x) | |
if y > predicted_y: | |
self.outer_right.append(point) | |
else: | |
self.outer_left.append(point) | |
self.outer_right = np.array(self.outer_right) | |
self.outer_left = np.array(self.outer_left) | |
if (len(self.outer_right) < 3) or (len(self.outer_left) < 3): | |
self.keep_going = False | |
else: | |
# Plot `outer_left` points in pink | |
for point in self.outer_left: | |
x, y = point | |
cv2.circle(self.image, (x, y), radius=5, color=gc('pink'), thickness=-1) | |
# Plot `outer_right` points in cyan | |
for point in self.outer_right: | |
x, y = point | |
cv2.circle(self.image, (x, y), radius=5, color=gc('cyan'), thickness=-1) | |
### outer_left | |
self.outer_left = self.order_points(self.outer_left) | |
self.outer_left = self.remove_duplicate_points(self.outer_left) | |
# self.outer_left = self.check_momentum(self.outer_left, False) | |
self.order_points_plot(self.outer_left, 'outer_left', 'final') | |
self.order_points_plot(self.outer_left, 'outer_left', 'QC') | |
self.outer_left_length, self.outer_left = self.get_length_of_ordered_points(self.outer_left, 'outer_left') | |
self.has_outer_left = True | |
### outer_right | |
self.outer_right = self.order_points(self.outer_right) | |
self.outer_right = self.remove_duplicate_points(self.outer_right) | |
# self.outer_right = self.check_momentum(self.outer_right, False) | |
self.order_points_plot(self.outer_right, 'outer_right', 'final') | |
self.order_points_plot(self.outer_right, 'outer_right', 'QC') | |
self.outer_right_length, self.outer_right = self.get_length_of_ordered_points(self.outer_right, 'outer_right') | |
self.has_middle = True | |
print(f"Length outer_left - {self.outer_left_length}") | |
print(f"Length outer_right - {self.outer_right_length}") | |
self.outer_right_np = np.array(self.outer_right) | |
self.outer_left_np = np.array(self.outer_left) | |
self.ordered_middle_np = np.array(self.ordered_middle) | |
# Fit 2nd order polynomials to the line segments | |
self.left_poly = np.polyfit(self.outer_left_np[:, 0], self.outer_left_np[:, 1], 2) | |
self.right_poly = np.polyfit(self.outer_right_np[:, 0], self.outer_right_np[:, 1], 2) | |
self.middle_poly = np.polyfit(self.ordered_middle_np[:, 0], self.ordered_middle_np[:, 1], 2) | |
# Evaluate polynomial coefficients for a range of x values | |
x_range = np.linspace(0, self.width, num=100) | |
left_line = np.polyval(self.left_poly, x_range) | |
right_line = np.polyval(self.right_poly, x_range) | |
self.middle_line = np.polyval(self.middle_poly, x_range) | |
# Plot lines of fit as white lines | |
for i in range(len(x_range)-1): | |
cv2.line(self.image, (int(x_range[i]), int(left_line[i])), (int(x_range[i+1]), int(left_line[i+1])), color=gc('gray'), thickness=1) | |
cv2.line(self.image, (int(x_range[i]), int(right_line[i])), (int(x_range[i+1]), int(right_line[i+1])), color=gc('white'), thickness=1) | |
cv2.line(self.image, (int(x_range[i]), int(self.middle_line[i])), (int(x_range[i+1]), int(self.middle_line[i+1])), color=gc('white'), thickness=2) | |
# Define the polynomial functions | |
def left_func(x): | |
return self.left_poly[0]*x**2 + self.left_poly[1]*x + self.left_poly[2] | |
def right_func(x): | |
return self.right_poly[0]*x**2 + self.right_poly[1]*x + self.right_poly[2] | |
def middle_func(x): | |
return self.middle_poly[0]*x**2 + self.middle_poly[1]*x + self.middle_poly[2] | |
# Define the difference functions | |
def left_middle_diff(x): | |
return left_func(x) - middle_func(x) | |
def right_middle_diff(x): | |
return right_func(x) - middle_func(x) | |
def left_right_diff(x): | |
return left_func(x) - right_func(x) | |
# Find the intersection points | |
left_middle_intersection_x = fsolve(left_middle_diff, 0) | |
right_middle_intersection_x = fsolve(right_middle_diff, 0) | |
left_right_intersection_x = fsolve(left_right_diff, 0) | |
left_middle_intersection_y = left_func(left_middle_intersection_x)[0] | |
right_middle_intersection_y = right_func(right_middle_intersection_x)[0] | |
left_right_intersection_y = left_func(left_right_intersection_x)[0] | |
# Keep only points within the image boundaries | |
intersection_points = np.array([[left_middle_intersection_x, left_middle_intersection_y], [right_middle_intersection_x, right_middle_intersection_y], [left_right_intersection_x, left_right_intersection_y]]) | |
intersection_points = intersection_points[(intersection_points[:, 0] >= 0) & (intersection_points[:, 0] <= self.width) & (intersection_points[:, 1] >= 0) & (intersection_points[:, 1] <= self.height)] | |
if intersection_points.size == 0: | |
self.keep_going = False | |
else: | |
# Compute the average of the intersection points | |
intersection_x = np.mean(intersection_points[:, 0]) | |
intersection_y = np.mean(intersection_points[:, 1]) | |
self.inter_point = [int(intersection_x), int(intersection_y)] | |
self.inter_point_outer_inner = [int(left_right_intersection_x), int(left_right_intersection_y)] | |
# Draw intersection point on the image | |
cv2.circle(self.image, (int(intersection_x), int(intersection_y)), radius=5, color=gc('green'), thickness=-1) | |
print(f"Length outer_left - {self.outer_left_length}") | |
print(f"Length outer_right - {self.outer_right_length}") | |
print(f"Intersection point - ({int(intersection_x)}, {int(intersection_y)})") | |
# Make the first points be at the tip, last points far away at base | |
def reorder_segment(segment, inter): | |
# Convert to numpy arrays for easier manipulation | |
segment = np.array(segment) | |
inter = np.array(inter) | |
# Calculate the Euclidean distance from the INTER point to the first and last points in the segment | |
dist_first = np.linalg.norm(segment[0] - inter) | |
dist_last = np.linalg.norm(segment[-1] - inter) | |
# If the last point is closer to the INTER point than the first point, reverse the order of the segment | |
if dist_last < dist_first: | |
segment = segment[::-1] | |
return segment.tolist() | |
self.ordered_middle = reorder_segment(self.ordered_middle, self.inter_point) | |
self.outer_left = reorder_segment(self.outer_left, self.inter_point) | |
self.outer_right = reorder_segment(self.outer_right, self.inter_point) | |
self.ordered_outer_right_np = np.array(self.outer_right) | |
self.ordered_outer_left_np = np.array(self.outer_left) | |
self.ordered_middle_np = np.array(self.ordered_middle) | |
# Draw a black ring around the last point of the outer_left segment | |
self.last_point_left = self.outer_left[-1] | |
cv2.circle(self.image, (int(self.last_point_left[0]), int(self.last_point_left[1])), radius=4, color=gc('black'), thickness=2) | |
cv2.circle(self.image, (int(self.last_point_left[0]), int(self.last_point_left[1])), radius=6, color=gc('white'), thickness=2) | |
# Draw a black ring around the last point of the outer_right segment | |
self.last_point_right = self.outer_right[-1] | |
cv2.circle(self.image, (int(self.last_point_right[0]), int(self.last_point_right[1])), radius=4, color=gc('black'), thickness=2) | |
cv2.circle(self.image, (int(self.last_point_right[0]), int(self.last_point_right[1])), radius=6, color=gc('white'), thickness=2) | |
# self.show_QC_image() | |
# print('hi') | |
def split_image_by_middle(self): | |
if not self.has_middle: | |
self.keep_going = False | |
else: | |
n_fit = 2 | |
# Convert the points to a numpy array | |
points_arr = np.array(self.ordered_middle) | |
# Fit a line to the points | |
self.midvein_fit = np.polyfit(points_arr[:, 0], points_arr[:, 1], n_fit) | |
# 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**2 + self.midvein_fit[1] * x1 + self.midvein_fit[2]) | |
x2 = self.width - 1 | |
y2 = int(self.midvein_fit[0] * x2**2 + self.midvein_fit[1] * x2 + self.midvein_fit[2]) | |
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**2 + self.midvein_fit[1] * x_vals + self.midvein_fit[2] | |
# 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 predict_y(self, x): | |
return self.midvein_fit[0] * x**2 + self.midvein_fit[1] * x + self.midvein_fit[2] | |
def order_middle(self): | |
if 'middle' not in self.points_list: | |
self.keep_going = False | |
else: | |
if len(self.points_list['middle']) >= 5: | |
self.logger.debug(f"Ordered Middle - Raw list contains {len(self.points_list['middle'])} points - using momentum") | |
self.ordered_middle = self.order_points(self.points_list['middle']) | |
self.ordered_middle = self.remove_duplicate_points(self.ordered_middle) | |
self.ordered_middle = self.check_momentum(self.ordered_middle, False) | |
self.v_tip = self.find_v_tip(self.points_list['outer']) | |
# self.ordered_middle.append(self.v_tip) | |
self.order_points_plot(self.ordered_middle, 'middle', 'QC') | |
self.ordered_middle_length, self.ordered_middle = self.get_length_of_ordered_points(self.ordered_middle, 'middle') | |
self.has_middle = True | |
else: | |
self.keep_going = False | |
self.logger.debug(f"Ordered Middle - Raw list contains {len(self.points_list['middle'])} points - SKIPPING MIDDLE") | |
def v_shape_template(self, tip, scale): | |
return np.array([ | |
[tip[0] - scale, tip[1] + scale], | |
tip, | |
[tip[0] + scale, tip[1] + scale] | |
]) | |
def error_function(self, params, points): | |
tip = params[:2] | |
scale = params[2] | |
template_points = self.v_shape_template(tip, scale) | |
error = 0 | |
for p in points: | |
dist = np.min(np.linalg.norm(template_points - p, axis=1)) | |
error += dist | |
return error | |
def find_v_tip(self, points): | |
points = np.array(points) | |
initial_guess = np.mean(points, axis=0) | |
initial_scale = np.linalg.norm(np.max(points, axis=0) - np.min(points, axis=0)) / 2 | |
result = minimize( | |
self.error_function, | |
np.hstack([initial_guess, initial_scale]), | |
args=(points,), | |
method='Nelder-Mead' | |
) | |
tip = result.x[:2] | |
return tuple(map(int, tip)) | |
def show_QC_image(self): | |
if self.do_show_QC_images: | |
cv2.imshow('QC image', self.image) | |
cv2.waitKey(0) | |
def show_angle_image(self): | |
if self.do_show_QC_images: | |
cv2.imshow('Angles image', self.image_angles) | |
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 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 order_points_plot(self, points, version, QC_or_final): | |
# thk_base = 0 | |
thk_base = 16 | |
if version == 'middle': | |
# color = (0, 255, 0) | |
color = gc('green') # blue | |
thick = 1 #2 + thk_base | |
elif version == 'tip': | |
color = gc('green') | |
thick = 1 #2 + thk_base | |
elif version == 'outer': | |
color = gc('red') | |
thick = 1 #2 + thk_base | |
elif version == 'outer_left': | |
color = gc('pink') | |
thick = 1 #2 + thk_base | |
elif version == 'outer_right': | |
color = gc('cyan') | |
thick = 1 #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 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 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 distance(self, point1, point2): | |
x1, y1 = point1 | |
x2, y2 = point2 | |
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) | |
### Shortest distance | |
def order_points(self, points): | |
points = [tuple(point) for point in points] # Convert numpy.ndarray points to tuples | |
best_tour = None | |
shortest_tour_length = float('inf') | |
for start_point in points: | |
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) | |
# Calculate the length of the current tour | |
tour_length = sum(self.distance(tour[i - 1], tour[i]) for i in range(1, len(tour))) | |
# Update the best_tour if the current tour is shorter | |
if tour_length < shortest_tour_length: | |
shortest_tour_length = tour_length | |
best_tour = tour | |
return best_tour | |
### Smoothest | |
''' | |
def angle_between_points(self, p1, p2, p3): | |
v1 = np.array([p1[0] - p2[0], p1[1] - p2[1]]) | |
v2 = np.array([p3[0] - p2[0], p3[1] - p2[1]]) | |
angle = np.arccos(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))) | |
return angle | |
def order_points(self, points): | |
points = [tuple(point) for point in points] # Convert numpy.ndarray points to tuples | |
best_tour = None | |
largest_sum_angles = 0 | |
for start_point in points: | |
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) | |
# Calculate the sum of angles for the current tour | |
sum_angles = sum(self.angle_between_points(tour[i - 1], tour[i], tour[i + 1]) for i in range(1, len(tour) - 1)) | |
# Update the best_tour if the current tour has a larger sum of angles | |
if sum_angles > largest_sum_angles: | |
largest_sum_angles = sum_angles | |
best_tour = tour | |
return best_tour | |
''' | |
### ^^^ Smoothest | |
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 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_Armature': | |
self.path_image_final = os.path.join(self.Dirs.landmarks_armature_overlay_final, '.'.join([self.file_name, 'jpg'])) | |
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_Armature': | |
self.path_QC_image = os.path.join(self.Dirs.landmarks_armature_overlay_QC, '.'.join([self.file_name, 'jpg'])) | |
def setup_angle_image(self): | |
self.image_angles = cv2.imread(os.path.join(self.dir_temp, '.'.join([self.file_name, 'jpg']))) | |
if self.leaf_type == 'Landmarks_Armature': | |
self.path_angles_image = os.path.join(self.Dirs.landmarks_armature_overlay_angles, '.'.join([self.file_name, 'jpg'])) | |
def define_landmark_classes(self): | |
self.classes = { | |
'tip': 0, | |
'middle': 1, | |
'outer': 2, | |
} | |
def set_cfg_values(self): | |
self.do_show_QC_images = self.cfg['leafmachine']['landmark_detector_armature']['do_show_QC_images'] | |
self.do_save_QC_images = self.cfg['leafmachine']['landmark_detector_armature']['do_save_QC_images'] | |
self.do_show_final_images = self.cfg['leafmachine']['landmark_detector_armature']['do_show_final_images'] | |
self.do_save_final_images = self.cfg['leafmachine']['landmark_detector_armature']['do_save_final_images'] | |
def init_lists_dicts(self): | |
# Initialize all lists and dictionaries | |
self.classes = {} | |
self.points_list = [] | |
self.image = [] | |
self.ordered_middle = [] | |
self.midvein_fit = [] | |
self.midvein_fit_points = [] | |
self.outer_right = [] | |
self.outer_left = [] | |
# self.ordered_outer_left = [] | |
# self.ordered_outer_right = [] | |
self.tip = [] | |
self.apex_left = [] | |
self.apex_right = [] | |
self.apex_center = [] | |
self.base_left = [] | |
self.base_right = [] | |
self.base_center = [] | |
self.lamina_base = [] | |
self.width_left = [] | |
self.width_right = [] | |
def get_final(self): | |
self.image_final = np.hstack((self.image, self.image_angles)) | |
return self.image_final | |
def euclidean_distance(p1, p2): | |
return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) | |
def gc(color): | |
colors = { | |
'red': (0, 0, 255), | |
'green': (0, 255, 0), | |
'blue': (255, 0, 0), | |
'yellow': (0, 255, 255), | |
'pink': (255, 0, 255), | |
'cyan': (255, 255, 0), | |
'black': (0, 0, 0), | |
'white': (255, 255, 255), | |
'gray': (128, 128, 128), | |
'orange': (0, 165, 255), | |
'purple': (128, 0, 128), | |
'lightpink': (203, 192, 255), | |
'brown': (42, 42, 165), | |
'navy': (128, 0, 0), | |
'teal': (128, 128, 0), | |
} | |
return colors.get(color.lower(), (0, 0, 0)) | |