import cv2 import numpy as np import os import time from PIL import Image from pltreader import PltReader from concurrent.futures import ThreadPoolExecutor from shapely import affinity from scipy.optimize import linear_sum_assignment def get_single_size_info(filepath, size_num, english_name): with open(filepath, 'r') as f: reader = PltReader(f) output = reader.get_output(tolerance=10) # 整幅输出 full_sheet_path = english_name + '_full_sheet.png' cv2.imwrite(full_sheet_path, output.debug_full_sheet(scale_factor=72/1016)) #cv2.imwrite(full_sheet_path, output.debug_full_sheet(scale_factor=0.1)) print(f"已保存整幅裁片图像到: {full_sheet_path}") # 获取单片输出信息 return output.get_single_size_info(size_num=size_num) def get_principal_angle(polygon): '''计算主轴角度''' coords = np.array(polygon.exterior.coords) # 计算协方差矩阵 cov = np.cov(coords.T) # 特征值分解 eig_vals, eig_vecs = np.linalg.eig(cov) # 找到最大特征值对应的特征向量 principal_vec = eig_vecs[:, np.argmax(eig_vals)] # 计算角度 angle = np.degrees(np.arctan2(principal_vec[1], principal_vec[0])) return angle def get_different_size_match_result(clusters): base_cluster = [piece for piece in clusters[0] if piece["parent"] == None] result = {piece["index"]:[] for piece in base_cluster} for size_index in range(1, len(clusters)): compare_cluster = [piece for piece in clusters[size_index] if piece["parent"] == None] assert len(base_cluster) == len(compare_cluster) # 初始化成本矩阵 cost_matrix = np.zeros((len(base_cluster), len(base_cluster))) # 初始化旋转角度矩阵,表示每个裁片的最佳旋转角度 rotation_matrix = np.zeros((len(base_cluster), len(base_cluster))) # 把每个裁片对应上 for i in range(len(base_cluster)): for j in range(len(compare_cluster)): poly1 = base_cluster[i]["data"] poly2 = compare_cluster[j]["data"] # 质心对齐 poly1 = affinity.translate(poly1, xoff=-poly1.centroid.x, yoff=-poly1.centroid.y) poly2 = affinity.translate(poly2, xoff=-poly2.centroid.x, yoff=-poly2.centroid.y) # 缩放到相同面积 target_area = 10000 scale_factor1 = (target_area / poly1.area) ** 0.5 scale_factor2 = (target_area / poly2.area) ** 0.5 # 绕原点缩放 poly1 = affinity.scale(poly1, xfact=scale_factor1, yfact=scale_factor1, origin=(0, 0)) poly2 = affinity.scale(poly2, xfact=scale_factor2, yfact=scale_factor2, origin=(0, 0)) candidates = [0, 90, 180, 270] dist_min = float('inf') best_angle = 0 for angle in candidates: rotated_back = affinity.rotate(poly2, angle, origin='centroid') dist = poly1.hausdorff_distance(rotated_back) if dist < dist_min: dist_min = dist best_angle = angle cost_matrix[i, j] = dist_min rotation_matrix[i, j] = best_angle row_ind, col_ind = linear_sum_assignment(cost_matrix) for i, j in zip(row_ind, col_ind): result[base_cluster[i]['index']].append((compare_cluster[j]['index'], cost_matrix[i, j], rotation_matrix[i, j])) for base_index, matches in result.items(): print(f"{base_index} <->", end=" ") for match in matches: print(f"{match[0]} (d={match[1]:.2f} r={match[2]}°)", end="; ") print() print("\n") return result def export_pieces_by_size_group(filepath, size_labels, output_dir="output", dpi=150, rotation=0): """ 按尺码和组号导出裁片PNG Args: filepath: PLT文件路径 size_labels: 尺码标签列表,如 ["S", "M", "L", "XL", "2XL", "3XL", "4XL"] output_dir: 输出目录 dpi: 输出分辨率 (DPI) rotation: 旋转角度,可选值: 0 - 不旋转 90 - 顺时针旋转90° -90 - 逆时针旋转90° 180 - 旋转180° 输出文件命名: S-1.png, M-1.png, L-1.png, ... """ start_time = time.time() # 根据 DPI 计算 scale_factor (1016 绘图仪单位 = 1 英寸) scale_factor = dpi / 1016 size_num = len(size_labels) print(f"==========开始读取PLT文件: {filepath}==========") with open(filepath, 'r') as f: reader = PltReader(f) output = reader.get_output(tolerance=10) # 获取尺码聚类信息 clusters = output.get_single_size_info(size_num=size_num) # 获取不同尺码间的匹配关系 print("==========开始匹配不同尺码的相同裁片==========") base_cluster = [piece for piece in clusters[0] if piece["parent"] == None] match_result = {piece["index"]: [piece["index"]] for piece in base_cluster} # 基准尺码的index作为key for size_index in range(1, len(clusters)): compare_cluster = [piece for piece in clusters[size_index] if piece["parent"] == None] # 初始化成本矩阵 cost_matrix = np.zeros((len(base_cluster), len(compare_cluster))) for i in range(len(base_cluster)): for j in range(len(compare_cluster)): poly1 = base_cluster[i]["data"] poly2 = compare_cluster[j]["data"] # 质心对齐 poly1 = affinity.translate(poly1, xoff=-poly1.centroid.x, yoff=-poly1.centroid.y) poly2 = affinity.translate(poly2, xoff=-poly2.centroid.x, yoff=-poly2.centroid.y) # 缩放到相同面积 target_area = 10000 scale_factor1 = (target_area / poly1.area) ** 0.5 scale_factor2 = (target_area / poly2.area) ** 0.5 poly1 = affinity.scale(poly1, xfact=scale_factor1, yfact=scale_factor1, origin=(0, 0)) poly2 = affinity.scale(poly2, xfact=scale_factor2, yfact=scale_factor2, origin=(0, 0)) # 尝试不同旋转角度 dist_min = float('inf') for angle in [0, 90, 180, 270]: rotated = affinity.rotate(poly2, angle, origin='centroid') dist = poly1.hausdorff_distance(rotated) if dist < dist_min: dist_min = dist cost_matrix[i, j] = dist_min # 匈牙利算法匹配 row_ind, col_ind = linear_sum_assignment(cost_matrix) for i, j in zip(row_ind, col_ind): base_index = base_cluster[i]['index'] matched_index = compare_cluster[j]['index'] match_result[base_index].append(matched_index) # 创建输出目录 os.makedirs(output_dir, exist_ok=True) # 构建 index -> node 的映射(包含所有节点,不只是parent为None的) all_nodes = {node["index"]: node for node in output.nodes} # 导出图片 rotation_desc = {0: "不旋转", 90: "顺时针90°", -90: "逆时针90°", 180: "旋转180°"}.get(rotation, f"旋转{rotation}°") print(f"==========开始导出裁片图片到: {output_dir}/ ({rotation_desc})==========") group_id = 1 for base_index, matched_indices in match_result.items(): print(f"组 {group_id}: ", end="") for size_idx, piece_index in enumerate(matched_indices): size_label = size_labels[size_idx] # 找到对应的node(包括其子节点) node = all_nodes[piece_index] nodes_to_draw = [node] + node['child'] # 绘制图像 img = output._draw_nodes(nodes_to_draw, scale_factor, show_id=False) # OpenCV 的 BGRA 转 Pillow 的 RGBA img_rgba = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA) pil_img = Image.fromarray(img_rgba) # 应用旋转 if rotation == 90: pil_img = pil_img.rotate(-90, expand=True) # Pillow的rotate是逆时针,所以用负值实现顺时针 elif rotation == -90: pil_img = pil_img.rotate(90, expand=True) # 逆时针90° elif rotation == 180: pil_img = pil_img.rotate(180, expand=True) # 保存文件(写入 DPI 元数据) filename = f"{size_label}-{group_id}.png" filepath_out = os.path.join(output_dir, filename) pil_img.save(filepath_out, dpi=(dpi, dpi)) print(f"{filename}", end=" ") print() group_id += 1 print(f"\n导出完成!共 {group_id - 1} 组裁片,每组 {len(size_labels)} 个尺码") print(f"文件保存在: {output_dir}/") # ========== 输出每个尺码的裁片中心坐标 ========== SAFETY_MARGIN = 10 # 安全边距 10cm print("\n" + "=" * 70) print(f"裁片中心坐标(单位: cm,原点: 左下角,安全边距: {SAFETY_MARGIN}cm,{rotation_desc})") print("=" * 70) # PLT单位转厘米的系数 (1016单位 = 1英寸 = 2.54cm) plt_to_cm = 2.54 / 1016 # 按尺码分组输出坐标 for size_idx, size_label in enumerate(size_labels): # 收集该尺码下所有裁片 size_pieces = [] for base_index, matched_indices in match_result.items(): piece_index = matched_indices[size_idx] node = all_nodes[piece_index] polygon = node["data"] size_pieces.append({ "group_id": list(match_result.keys()).index(base_index) + 1, "polygon": polygon, "centroid": polygon.centroid }) # 计算该尺码的边界框(用于重置原点) all_polygons = [p["polygon"] for p in size_pieces] from shapely.geometry import MultiPolygon as ShapelyMultiPolygon multi_poly = ShapelyMultiPolygon(all_polygons) min_x, min_y, max_x, max_y = multi_poly.bounds # 原始画布尺寸(厘米) orig_width_cm = (max_x - min_x) * plt_to_cm orig_height_cm = (max_y - min_y) * plt_to_cm # 根据旋转调整画布宽高 if rotation in [90, -90]: width_cm, height_cm = orig_height_cm, orig_width_cm # 宽高互换 else: width_cm, height_cm = orig_width_cm, orig_height_cm # 含安全边距的画布尺寸 total_width_cm = width_cm + 2 * SAFETY_MARGIN total_height_cm = height_cm + 2 * SAFETY_MARGIN print(f"\n【尺码 {size_label}】") print(f" 裁片区域尺寸: 宽 {width_cm:.2f} cm, 高 {height_cm:.2f} cm") print(f" 含安全边距尺寸: 宽 {total_width_cm:.2f} cm, 高 {total_height_cm:.2f} cm") print("-" * 70) print(f" {'裁片名称':<10} {'原始中心坐标':<22} {'含安全边距坐标':<22} {'裁片尺寸'}") print("-" * 70) for piece in size_pieces: # 原始坐标(以左下角为原点,Y轴向上) orig_center_x = (piece["centroid"].x - min_x) * plt_to_cm orig_center_y = (piece["centroid"].y - min_y) * plt_to_cm # 原始裁片尺寸 piece_bounds = piece["polygon"].bounds orig_piece_width = (piece_bounds[2] - piece_bounds[0]) * plt_to_cm orig_piece_height = (piece_bounds[3] - piece_bounds[1]) * plt_to_cm # 根据旋转变换坐标 if rotation == 90: # 顺时针90° center_x = orig_center_y center_y = orig_width_cm - orig_center_x piece_width, piece_height = orig_piece_height, orig_piece_width elif rotation == -90: # 逆时针90° center_x = orig_height_cm - orig_center_y center_y = orig_center_x piece_width, piece_height = orig_piece_height, orig_piece_width elif rotation == 180: # 旋转180° center_x = orig_width_cm - orig_center_x center_y = orig_height_cm - orig_center_y piece_width, piece_height = orig_piece_width, orig_piece_height else: # 不旋转 center_x, center_y = orig_center_x, orig_center_y piece_width, piece_height = orig_piece_width, orig_piece_height # 含安全边距的坐标(加上10cm偏移) safe_x = center_x + SAFETY_MARGIN safe_y = center_y + SAFETY_MARGIN # 裁片名称与PNG文件名一致 piece_name = f"{size_label}-{piece['group_id']}" print(f" {piece_name:<10} ({center_x:7.2f}, {center_y:7.2f}) cm ({safe_x:7.2f}, {safe_y:7.2f}) cm {piece_width:.2f} x {piece_height:.2f} cm") print("\n" + "=" * 70) # 计算并显示处理时间 elapsed_time = time.time() - start_time if elapsed_time >= 60: minutes = int(elapsed_time // 60) seconds = elapsed_time % 60 print(f"处理耗时: {minutes}分{seconds:.2f}秒") else: print(f"处理耗时: {elapsed_time:.2f}秒") def get_contour_match_result(standard_clusters, rotated_clusters): for standard_cluster, rotated_cluster in zip(standard_clusters, rotated_clusters): # 仅保留父节点 standard_cluster = [piece for piece in standard_cluster if piece["parent"] == None] rotated_cluster = [piece for piece in rotated_cluster if piece["parent"] == None] assert len(standard_cluster) == len(rotated_cluster) # 初始化成本矩阵 cost_matrix = np.zeros((len(standard_cluster), len(standard_cluster))) # 初始化旋转角度矩阵,表示每个裁片的最佳旋转角度 rotation_matrix = np.zeros((len(standard_cluster), len(standard_cluster))) # 把旋转后的每个裁片和标准的裁片对应上 for i in range(len(standard_cluster)): for j in range(len(rotated_cluster)): poly1 = standard_cluster[i]["data"] poly2 = rotated_cluster[j]["data"] #poly1 = poly1.simplify(0) #poly2 = poly2.simplify(0) # 质心对齐 poly1 = affinity.translate(poly1, xoff=-poly1.centroid.x, yoff=-poly1.centroid.y) poly2 = affinity.translate(poly2, xoff=-poly2.centroid.x, yoff=-poly2.centroid.y) # 计算主轴角度 #angle1 = get_principal_angle(poly1) #angle2 = get_principal_angle(poly2) # 计算角度差 #angle_diff = angle1 - angle2 #candidates = [angle_diff, angle_diff + 90, angle_diff + 180, angle_diff + 270] candidates = [0, 90, 180, 270] dist_min = float('inf') best_angle = 0 for angle in candidates: rotated_back = affinity.rotate(poly2, angle, origin='centroid') dist = poly1.hausdorff_distance(rotated_back) if dist < dist_min: dist_min = dist best_angle = angle cost_matrix[i, j] = dist_min rotation_matrix[i, j] = best_angle row_ind, col_ind = linear_sum_assignment(cost_matrix) print("\n标准裁片与旋转后裁片的最佳匹配结果:") for i, j in zip(row_ind, col_ind): print(f"标准裁片ID: {standard_cluster[i]['index']}, 旋转后裁片ID: {rotated_cluster[j]['index']}, 距离: {cost_matrix[i, j]:.4f}, 最佳旋转角度: {rotation_matrix[i, j]}度") # 打印成本矩阵 #print(cost_matrix) def main(): STANDARD_FILE_PATH = "默认方案-POLO衫长袖-A料-标准.plt" ROTATED_FILE_PATH = "默认方案-POLO衫长袖-A料-已旋转.plt" SIZE_NUM = 5 # ========== 新功能:按尺码分组导出裁片 ========== # 尺码标签列表(根据你的实际情况修改) SIZE_LABELS = ["S", "M", "L", "XL", "2XL"] # 导出标准文件的裁片(按 S-1, M-1, L-1... 命名) export_pieces_by_size_group( filepath=STANDARD_FILE_PATH, size_labels=SIZE_LABELS, output_dir="standard_pieces", dpi=150, # 输出分辨率,Photoshop 会正确识别 rotation=90 # 顺时针旋转90° ) # 如果需要导出旋转后文件的裁片,取消下面的注释 # export_pieces_by_size_group( # filepath=ROTATED_FILE_PATH, # size_labels=SIZE_LABELS, # output_dir="rotated_pieces", # scale_factor=72/1016 # ) # ========== 以下是原有的匹配功能 ========== # print("==========开始读取轮廓数据==========") # with ThreadPoolExecutor(max_workers=2) as executor: # standard_future = executor.submit(get_single_size_info, STANDARD_FILE_PATH, SIZE_NUM, "standard") # rotated_future = executor.submit(get_single_size_info, ROTATED_FILE_PATH, SIZE_NUM, "rotated") # # standard_clusters = standard_future.result() # rotated_clusters = rotated_future.result() # # print("==========开始获取两个文件的轮廓匹配结果==========") # get_contour_match_result(standard_clusters, rotated_clusters) # # print("==========开始获取一个文件内不同尺寸的匹配结果==========") # print(f"{STANDARD_FILE_PATH}:") # get_different_size_match_result(standard_clusters) # print(f"{ROTATED_FILE_PATH}:") # get_different_size_match_result(rotated_clusters) if __name__ == "__main__": main()