import cv2 import numpy as np def intersect_lines(line_a, line_b): """Intersect two lines given as (x1, y1, x2, y2), or None if parallel.""" x1, y1, x2, y2 = line_a x3, y3, x4, y4 = line_b p1 = np.array([x1, y1, 1.0]) p2 = np.array([x2, y2, 1.0]) p3 = np.array([x3, y3, 1.0]) p4 = np.array([x4, y4, 1.0]) l1 = np.cross(p1, p2) l2 = np.cross(p3, p4) x = np.cross(l1, l2) if abs(x[2]) < 1e-8: return None return x[0] / x[2], x[1] / x[2] def build_point_map(landmarks): """Index a list of landmark dicts (each with a 'name' key) by name.""" return {lm["name"]: lm for lm in landmarks} def set_midpoint(pt_map, name, a, b): """Store the midpoint of two named points in pt_map under `name`.""" if (a in pt_map) and (b in pt_map): pt_map[name] = { "orig_x": (pt_map[a]["orig_x"] + pt_map[b]["orig_x"]) / 2, "orig_y": (pt_map[a]["orig_y"] + pt_map[b]["orig_y"]) / 2, } return pt_map def get_angle(pt_map, p1, p2, p3): """Angle in degrees at vertex p2, between rays p2->p1 and p2->p3.""" if (p1 not in pt_map) or (p2 not in pt_map) or (p3 not in pt_map): return None v21 = np.array([ pt_map[p1]["orig_x"] - pt_map[p2]["orig_x"], pt_map[p1]["orig_y"] - pt_map[p2]["orig_y"], ]) v23 = np.array([ pt_map[p3]["orig_x"] - pt_map[p2]["orig_x"], pt_map[p3]["orig_y"] - pt_map[p2]["orig_y"], ]) dot_product = np.dot(v21, v23) cross_product_z = abs(v21[0] * v23[1] - v21[1] * v23[0]) angle_radians = np.arctan2(cross_product_z, dot_product) angle_degrees = np.degrees(angle_radians) if angle_degrees < 0: angle_degrees += 360 return angle_degrees def plot_line(image, pt_map, a, b, color, skip_display=False): """Draw a line between two named points in pt_map; returns (image, line).""" if (a not in pt_map) or (b not in pt_map): return image, None px1, py1 = pt_map[a]["orig_x"], pt_map[a]["orig_y"] px2, py2 = pt_map[b]["orig_x"], pt_map[b]["orig_y"] if not skip_display: cv2.line( image, (int(round(px1)), int(round(py1))), (int(round(px2)), int(round(py2))), color, 2, ) return image, [px1, py1, px2, py2] def plot_angle(image, pt_map, a, b, c, color): """Draw the angle arc at vertex B formed by A-B and B-C.""" if (a not in pt_map) or (b not in pt_map) or (c not in pt_map): return image xA, yA = pt_map[a]["orig_x"], pt_map[a]["orig_y"] xB, yB = pt_map[b]["orig_x"], pt_map[b]["orig_y"] xC, yC = pt_map[c]["orig_x"], pt_map[c]["orig_y"] m_ba = (yB - yA) / (xB - xA) b_ba = yB - m_ba * xB ba = np.linspace(xB, xA, 10) nx_a = ba[1] ny_a = m_ba * nx_a + b_ba pts = np.array([[ [round(nx_a), round(ny_a)], [round(xB), round(yB)], [round(xC), round(yC)], ]]).astype(int) return cv2.polylines(image, [pts], isClosed=True, color=color, thickness=2) def plot_landmarks(image, landmarks, color=(255, 255, 0), angle_color=(255, 0, 0)): """Draw landmark points plus the derived axes/joint-lines/angles used for CPAK measurement on top of image (modified in place, also returned). """ pt_map = {} for pt in landmarks: pt_map[pt["name"]] = pt if pt["name"] in ("FR", "FL"): continue cv2.circle(image, (int(pt["orig_x"]), int(pt["orig_y"])), 5, color, -1) # FHR-FR, LFR-MFR LTR-MTR [FTR]-AR # FHL-FL, LFL-MFL LTL-MTL [FTL]-AL # FTR = (LTR+MTR)/2 # FTL = (LTL+MTL)/2 # Right femur: mechanical axis (FHR-FR) x joint line (LFR-MFR) -> FFR image, femoral_axis_r = plot_line(image, pt_map, "FHR", "FR", color, skip_display=True) image, joint_line_distal_femur_r = plot_line(image, pt_map, "LFR", "MFR", color) if femoral_axis_r is not None and joint_line_distal_femur_r is not None: intersection = intersect_lines(femoral_axis_r, joint_line_distal_femur_r) if intersection is not None: pt_map["FFR"] = {"orig_x": intersection[0], "orig_y": intersection[1]} image, _ = plot_line(image, pt_map, "FHR", "FFR", color) # Right tibia: joint line (LTR-MTR), midpoint FTR -> ankle center (AR) image, _ = plot_line(image, pt_map, "LTR", "MTR", color) set_midpoint(pt_map, "FTR", "LTR", "MTR") image, _ = plot_line(image, pt_map, "FTR", "AR", color) image = plot_angle(image, pt_map, "FHR", "FFR", "LFR", angle_color) image = plot_angle(image, pt_map, "AR", "FTR", "MTR", angle_color) # Left femur image, femoral_axis_l = plot_line(image, pt_map, "FHL", "FL", color, skip_display=True) image, joint_line_distal_femur_l = plot_line(image, pt_map, "LFL", "MFL", color) if femoral_axis_l is not None and joint_line_distal_femur_l is not None: intersection = intersect_lines(femoral_axis_l, joint_line_distal_femur_l) if intersection is not None: pt_map["FFL"] = {"orig_x": intersection[0], "orig_y": intersection[1]} image, _ = plot_line(image, pt_map, "FHL", "FFL", color) # Left tibia image, _ = plot_line(image, pt_map, "LTL", "MTL", color) set_midpoint(pt_map, "FTL", "LTL", "MTL") image, _ = plot_line(image, pt_map, "FTL", "AL", color) image = plot_angle(image, pt_map, "FHL", "FFL", "LFL", angle_color) image = plot_angle(image, pt_map, "AL", "FTL", "MTL", angle_color) return image