diff --git a/.gitignore b/.gitignore index c7b10cf9b0e7e9fa4feac8c8209bad798cf3d950..a4dcaf308eb33e4e550e2c6dc4641c3c224e6a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ data .idea __pycache__ -plugins diff --git a/plugins/moldmaker.py b/plugins/moldmaker.py new file mode 100644 index 0000000000000000000000000000000000000000..4b581c2f2129d71949db8fde6c55866e77419015 --- /dev/null +++ b/plugins/moldmaker.py @@ -0,0 +1,478 @@ +import cv2 +import numpy as np +import matplotlib.pyplot as plt +import math +import plyfile +import re +import argparse + + +def parse_arguments(): + usage_text = ( + "Usage: python aruco-frame.py [options]" + ) + parser = argparse.ArgumentParser(description=usage_text) + parser.add_argument("-i", "--input", type=str, + help="Input filename.") + parser.add_argument("-o", "--output", type=str, default="", + help="Output filename (default: <filename_in>.ply).") + parser.add_argument("-d", "--dpi", type=int, default=-1, + help="Manual output DPI (default: auto).") + parser.add_argument("-s", "--show", action="store_true", + help="Show debug image.") + parser.add_argument("-v", "--verbose", action="store_true", + help="Verbose mode (default: false).") + return parser.parse_known_args() + + +def write_ply(filename, xyz, triangles, ascii=False): + n_points, _ = xyz.shape + n_faces, _ = triangles.shape + + vert_data = np.zeros((n_points,), dtype=[ + ("x", "float32"), + ("y", "float32"), + ("z", "float32")]) + vert_data["x"][:] = xyz[:, 0] + vert_data["y"][:] = xyz[:, 1] + vert_data["z"][:] = xyz[:, 2] + + face_data = np.zeros((n_faces,), dtype=[ + ('vertex_indices', 'i4', (3,)), + ]) + + face_data['vertex_indices'] = triangles + + el1 = plyfile.PlyElement.describe(vert_data, "vertex") + el2 = plyfile.PlyElement.describe(face_data, "face") + dat = plyfile.PlyData([el1, el2], text=ascii) + dat.write(filename) + + +def write_svg(filename, pts_mm): + with open(filename, "w") as f: + f.write( + '<svg width="300mm" height="300mm" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg"><polyline points="') + for pt in pts_mm: + x, y = pt + f.write(f"{x:.3f},{300 - y:.3f} ") + f.write('" fill="none" stroke="black" /></svg>') + + +def imshow(img, h_view=700, win_name="debug", waitkey=True): + dtype = img.dtype + if dtype == bool: + img = img.astype(np.uint8) * 255 + h, w = img.shape[:2] + w_view = int(h_view * w / h) + cv2.imshow(win_name, cv2.resize(img, (w_view, h_view), interpolation=cv2.INTER_AREA)) + if waitkey: + cv2.waitKey(0) + + +def find_axis(img_thresh): + img_bin = img_thresh > 0 + # img_thresh = img[:, :, 0] < 255 + + # imshow(img_thresh) + + y = np.sum(img_thresh / 255, axis=1) + + def find_first(x_bin): + return np.argmax(x_bin) + + def find_last(x_bin): + length = len(x_bin) + return (length - 1) - np.argmax(x_bin[::-1]) + + n_min = 2 + i1 = find_first(y > n_min) + i2 = find_last(y > n_min) + + j1 = (find_first(img_bin[i1, :]) + find_last(img_bin[i1, :])) // 2 + j2 = (find_first(img_bin[i2, :]) + find_last(img_bin[i2, :])) // 2 + + pt1 = np.array((j1, i1), dtype=np.int32) + pt2 = np.array((j2, i2), dtype=np.int32) + + return pt1, pt2 + + +def mm_to_px(x, dpi): + return x * dpi / 25.4 + + +def px_to_mm(x, dpi): + return x / (dpi / 25.4) + + +def threshold(img_gray, dpi, min_stroke_mm=15.0): + img_thresh = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 17, 5) + # img_thresh = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 9) + + if min_stroke_mm > 0.0: + cts, hierarchy = cv2.findContours(img_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) + + cts_noise = [] + for ct in cts: + ct_len = np.sum(np.linalg.norm(ct[1:, 0, :] - ct[:-1, 0, :], axis=1)) + ct_len_mm = px_to_mm(ct_len, dpi) + if ct_len_mm < min_stroke_mm: + cts_noise.append(ct) + + cv2.drawContours(img_thresh, cts_noise, -1, (0, 0, 0), thickness=cv2.FILLED) + + return img_thresh + + +def normalize_and_filter_section(section, k_filt=5, n_samp=256): + section -= np.mean(section, axis=0, keepdims=True) + + n = len(section) + + section_ext = np.zeros((n + 1, 2)) + section_ext[:-1, 0] = section[:, 0] + section_ext[:-1, 1] = section[:, 1] + section_ext[-1, :] = section_ext[0, :] + + k_half = (k_filt - 1) // 2 + + # filter + section_ext[:, 0] = np.convolve(np.pad(section_ext[:, 0], [k_half, k_half], "wrap"), np.ones((k_filt,)) / k_filt, + "valid") + section_ext[:, 1] = np.convolve(np.pad(section_ext[:, 1], [k_half, k_half], "wrap"), np.ones((k_filt,)) / k_filt, + "valid") + + r = np.linalg.norm(section, axis=1) + section_ext /= np.max(r) + + length = np.zeros((n + 1,)) + deltas = section_ext[1:] - section_ext[:-1] + length[1:] = np.cumsum(np.linalg.norm(deltas, axis=1)) + + length_paced = np.linspace(0, length[-1], n_samp + 1) + + section_new = np.zeros((n_samp, 2)) + + section_new[:, 0] = np.interp(length_paced, length, section_ext[:, 0])[:-1] + section_new[:, 1] = np.interp(length_paced, length, section_ext[:, 1])[:-1] + + return section_new + + +def hermite(n): + t = np.linspace(0, 1, n) + return 2 * (t ** 3) - 3 * (t ** 2) + 1 + + +def write_mesh(filename_out, f_x, f_y, section_bot, section_top, dpi): + n_vertical = len(f_x) + n_horizontal = len(section_bot) + + n_points = n_vertical * n_horizontal + + alpha = hermite(n_vertical) + + sections = [] + + for i in range(n_vertical): + xyz = np.zeros((n_horizontal, 3)) + xyz[:, 2] = f_y[i] + xyz[:, 0] = (section_bot[:, 0] * alpha[i] + section_top[:, 0] * (1 - alpha[i])) * f_x[i] + xyz[:, 1] = (section_bot[:, 1] * alpha[i] + section_top[:, 1] * (1 - alpha[i])) * f_x[i] + sections.append(xyz) + + xyz_all = np.vstack(sections) + xyz_mm = px_to_mm(xyz_all, dpi) + + n_face = 2 * (n_vertical - 1) * n_horizontal + triangles = np.zeros((n_face, 3)) + + f = 0 + for i in range(n_vertical - 1): + for j in range(n_horizontal - 1): + triangles[f, 0] = j + i * n_horizontal + triangles[f, 1] = (j + 1) + i * n_horizontal + triangles[f, 2] = (j + 1) + (i + 1) * n_horizontal + triangles[f + 1, 0] = j + i * n_horizontal + triangles[f + 1, 1] = j + (i + 1) * n_horizontal + triangles[f + 1, 2] = (j + 1) + (i + 1) * n_horizontal + + f += 2 + + triangles[f, 0] = (n_horizontal-1) + i * n_horizontal + triangles[f, 1] = 0 + i * n_horizontal + triangles[f, 2] = 0 + (i + 1) * n_horizontal + triangles[f + 1, 0] = (n_horizontal-1) + i * n_horizontal + triangles[f + 1, 1] = (n_horizontal-1) + (i + 1) * n_horizontal + triangles[f + 1, 2] = 0 + (i + 1) * n_horizontal + + f += 2 + + write_ply(filename_out, xyz_mm, triangles) + + +def bounding_box(xy): + xmin, xmax = np.min(xy[:, 0]), np.max(xy[:, 0]) + ymin, ymax = np.min(xy[:, 1]), np.max(xy[:, 1]) + b = np.array([ + (xmin, ymin), + (xmin, ymax), + (xmax, ymax), + (xmax, xmin) + ], dtype=np.int32) + return b + + +def main(): + args, _ = parse_arguments() + + filename_in = args.input + + if args.output == "": + filename_out = filename_in.replace(".png", ".ply") + else: + filename_out = args.output + + if args.dpi == -1: + m = re.match(r".+_(\d+)_DPI\.png$", filename_in) + if m is None: + raise ValueError(f"DPI not specified and missing in filename: '{filename_in}'") + dpi = int(m.group(1)) + else: + dpi = args.dpi + + show = args.show + + n_horizontal = 256 + + print(f"Reading '{filename_in}'") + + img = cv2.imread(args.input, cv2.IMREAD_UNCHANGED) + img_draw = np.copy(img) + + h, w, _ = img.shape + + img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + img_thresh = threshold(img_gray, dpi) + + imshow(img_thresh) + + pt1, pt2 = find_axis(img_thresh) + + cv2.circle(img_draw, pt1, radius=int(mm_to_px(1.5, dpi)), color=(0, 0, 255), thickness=cv2.FILLED) + cv2.circle(img_draw, pt2, radius=int(mm_to_px(1.5, dpi)), color=(0, 0, 255), thickness=cv2.FILLED) + + origin = np.array(pt2, dtype=np.float32) + y_length = np.linalg.norm(pt1 - pt2) + y_axis = (pt1 - pt2) / y_length + x_axis = np.array([-y_axis[1], y_axis[0]]) + + def to_coords(pt): + if isinstance(pt, np.ndarray) and len(pt.shape) == 2: + mat = np.array([ + x_axis, + y_axis + ]) + pt_shift = pt - origin[np.newaxis, :] + return pt_shift @ mat.T + else: + return np.array([np.dot(x_axis, pt - origin), np.dot(y_axis, pt - origin)]) + + def from_coords(pt): + if isinstance(pt, np.ndarray) and len(pt.shape) == 2: + return origin[np.newaxis, :] + x_axis[np.newaxis, :] * pt[:, 0:1] + y_axis[np.newaxis, :] * pt[:, 1:2] + else: + return origin + x_axis * pt[0] + y_axis * pt[1] + + def get_ct_length(ct): + return np.sum(np.linalg.norm(ct[1:, 0, :] - ct[:-1, 0, :], axis=1)) + + # RIGHT SIDE + img_masked = np.copy(img_thresh) + eps = mm_to_px(3.0, dpi) + cv2.fillPoly(img_masked, np.array([[ + from_coords([eps, -eps]), + from_coords([eps, y_length + eps]), + [0, 0], + [0, h] + ]], dtype=np.int32), color=(0, 0, 0)) + + cts, hierarchy = cv2.findContours(img_masked, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_TC89_KCOS) + + # cv2.drawContours(img_draw, cts, -1, lineType=cv2.LINE_8, thickness=int(mm_to_px(1, dpi)), color=(0, 255, 0)) + + k_filt = 15 + k_half = (k_filt - 1) // 2 + + cts_sorted = sorted(cts, key=lambda x: get_ct_length(x)) + + if len(cts) == 0: + print("Error: no shape found") + return + + ct = cts_sorted[-1] + + if px_to_mm(get_ct_length(ct), dpi) < 40.0: + print("Error: no shape found") + + if show: + b = bounding_box(ct[:, 0, :]) + cv2.rectangle(img_draw, + b[0], + b[2], + (0, 128, 255), int(mm_to_px(0.3, dpi))) + + xy_profile = to_coords(ct[:, 0, :]) + + # mask = (xy_profile[2:, 1] - xy_profile[:-2, 1]) > 0 + + # xy_profile = xy_profile[1:-1, :][mask, :] + xy_profile = xy_profile[1:-1, :] + + # i_start = np.argmin(xy_profile[:, 1]) + i_start = np.argmin(xy_profile[:, 0]) + i_end = np.argmax(xy_profile[:, 1]) + + if i_start > i_end: + i_start, i_end = i_end, i_start + + print(i_start) + print(i_end) + + xy_profile = xy_profile[i_start:i_end] + + # plt.figure() + # plt.plot(xy_profile[:, 0], xy_profile[:, 1]) + # plt.axis("equal") + # plt.show() + + print(xy_profile.shape) + + y_length_used = xy_profile[-1, 1] - xy_profile[0, 1] + + # resolution + n_points = int(px_to_mm(y_length_used, dpi) / 0.5) + + # EXPERIMENT + f_x_pad = np.pad(xy_profile[:, 0], [k_half, k_half], "edge") + f_x = np.convolve(f_x_pad, np.ones((k_filt,)) / k_filt, "valid") + + f_y_pad = np.pad(xy_profile[:, 1], [k_half, k_half], "edge") + f_y = np.convolve(f_y_pad, np.ones((k_filt,)) / k_filt, "valid") + + """ + f_y = np.linspace(xy_profile[0, 1], xy_profile[-1, 1], n_points) + + f_x = np.interp(f_y, xy_profile[:, 1], xy_profile[:, 0]) + + f_y -= f_y[0] + + f_x_pad = np.pad(f_x, [k_half, k_half], "edge") + f_x = np.convolve(f_x_pad, np.ones((k_filt,)) / k_filt, "valid") + """ + + # LEFT SIDE + # if show: + # imshow(img_masked, waitkey=False) + + img_masked = np.copy(img_thresh) + eps = mm_to_px(3.0, dpi) + + cv2.fillPoly(img_masked, np.array([[ + from_coords([-eps, y_length + eps]), + from_coords([-eps, -eps]), + [w, h], + [w, 0] + ]], dtype=np.int32), color=(0, 0, 0)) + + cts, hierarchy = cv2.findContours(img_masked, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_TC89_KCOS) + + # cv2.drawContours(img_draw, cts, -1, lineType=cv2.LINE_8, thickness=int(mm_to_px(1, dpi)), color=(255, 255, 0)) + + sections = [] + + for ct in cts: + ct_center = np.mean(ct[:, 0, :], axis=0) + + if ct_center[0] < 20: + continue + + xy_center = to_coords(ct_center) + if px_to_mm(abs(xy_center[0]), dpi) < 20: + continue + + ct_len = np.sum(np.linalg.norm(ct[1:, 0, :] - ct[:-1, 0, :], axis=1)) + ct_len_mm = px_to_mm(ct_len, dpi) + + if ct_len_mm < 30.0: + continue + + sections.append(to_coords(ct[:, 0, :])) + + if len(sections) == 0: + if show: + imshow(img_draw) + + theta = 2 * math.pi * np.arange(n_horizontal) / n_horizontal + section = np.zeros((n_horizontal, 2)) + section[:, 0] = np.cos(theta) + section[:, 1] = np.sin(theta) + write_mesh(filename_out, f_x, f_y, section, section, dpi) + if len(sections) == 1: + if show: + b = bounding_box(from_coords(sections[0])) + cv2.rectangle(img_draw, + b[0], + b[2], + (255, 128, 0), int(mm_to_px(0.3, dpi))) + + imshow(img_draw) + section = normalize_and_filter_section(sections[0], n_samp=n_horizontal) + + write_mesh(filename_out, f_x, f_y, section, section, dpi) + if len(sections) >= 2: + sections = sorted(sections, key=lambda x: np.mean(x[:, 1])) + + if show: + b = bounding_box(from_coords(sections[0])) + + cv2.rectangle(img_draw, + b[0], + b[2], + (255, 128, 0), int(mm_to_px(0.3, dpi))) + b = bounding_box(from_coords(sections[-1])) + cv2.rectangle(img_draw, + b[0], + b[2], + (128, 0, 255), int(mm_to_px(0.3, dpi))) + + imshow(img_draw) + + section_bot = normalize_and_filter_section(sections[0], n_samp=n_horizontal) + section_top = normalize_and_filter_section(sections[-1], n_samp=n_horizontal) + + if show: + plt.figure() + plt.subplot(1, 3, 1) + plt.plot(f_x, f_y) + plt.axis("equal") + + plt.subplot(1, 3, 2) + plt.plot(section_bot[:, 0], section_bot[:, 1], 'r') + plt.axis("equal") + + plt.subplot(1, 3, 3) + plt.plot(section_top[:, 0], section_top[:, 1], 'b') + plt.axis("equal") + + plt.show() + + write_mesh(filename_out, f_x, f_y, section_bot, section_top, dpi) + + print(f"Wrote 3D model as '{filename_out}'") + + +if __name__ == "__main__": + main()