Source code for manim_pymunk.utils.img_tools

"""图片工具模块。

该模块提供将图片转换为Pymunk物理形状的工具函数,支持透明背景图和实色背景图的智能处理。
"""

import pymunk
from pymunk.autogeometry import march_soft, simplify_vertexes, convex_decomposition
from PIL import Image, ImageFilter, ImageOps
import numpy as np


[docs] def get_normalized_convex_polygons( pixel_array, base_px_width=512.0, target_cell_size=4, img_manim_w=8, img_manim_h=14.22 ): """从像素数组中提取规范化的凸多边形集合。 该函数通过marchingSquares算法和凸分解,从图片中智能提取 碰撞用的凸多边形。支持透明背景和实色背景的自动识别。 Args: pixel_array (np.ndarray): 输入图片的像素数组[H, W, C]。 base_width (float, optional): 采样基准宽度,默认为512.0。 用于控制采样精度。 target_cell_size (float, optional): 目标单元格大小,默认为4。 控制marchingSquares的网格密度。 frame_w (float, optional): Manim框架宽度,默认为8。 用于坐标映射。 frame_h (float, optional): Manim框架高度,默认为14.22。 用于坐标映射。 Returns: list: Manim坐标系中的凸多边形列表,每个多边形为顶点坐标列表。 """ # 1. 基础维度获取 orig_h, orig_w = pixel_array.shape[:2] is_rgba = pixel_array.shape[2] == 4 if len(pixel_array.shape) > 2 else False actual_base_width = min(base_px_width, orig_w) scale_factor = orig_w / actual_base_width actual_base_height = int(orig_h / scale_factor) # 2. 智能判断:这是"透明背景图"还是"带Alpha通道的实色图"? use_alpha_mask = False if is_rgba: alpha_channel = pixel_array[:, :, 3] # 计算透明像素占比:如果透明像素超过 1%,通常认为它是抠好图的透明背景 transparent_ratio = np.mean(alpha_channel < 32) if transparent_ratio > 0.1: use_alpha_mask = True # 3. 根据判断结果生成 Mask if use_alpha_mask: # --- 路径 A: 透明背景处理 --- # 直接使用 Alpha 通道,这比任何颜色分析都准 img_obj = Image.fromarray(pixel_array[:, :, 3]).convert("L") img_resized = img_obj.resize( (int(actual_base_width), actual_base_height), Image.Resampling.LANCZOS ) mask_np = np.where(np.array(img_resized) > 128, 255, 0).astype(np.uint8) else: # --- 路径 B: 实色背景处理 (保留你原有的对比度拉伸逻辑) --- img_rgb = Image.fromarray(pixel_array[:, :, :3].astype("uint8")).convert("L") img_obj = ImageOps.autocontrast(img_rgb, cutoff=0.5) img_resized = img_obj.resize( (int(actual_base_width), actual_base_height), Image.Resampling.LANCZOS ) img_np = np.array(img_resized) # 环形边缘采样逻辑 border_pixels = np.concatenate( [img_np[0, :], img_np[-1, :], img_np[:, 0], img_np[:, -1]] ) bg_color = np.median(border_pixels) bg_std = np.std(border_pixels) diff = np.abs(img_np.astype(np.int16) - bg_color) dynamic_threshold = max(10, bg_std * 3) mask_np = np.where(diff > dynamic_threshold, 255, 0).astype(np.uint8) # 4. 后处理与采样 mask = Image.fromarray(mask_np) # 闭运算:连接断裂的高光位 mask = mask.filter(ImageFilter.MaxFilter(3)).filter(ImageFilter.MinFilter(3)) def sample_func(point): """采样函数:根据坐标返回Mask值。 Args: point (tuple): (x, y)坐标。 Returns: int: 该点的Mask值(0或255)。 """ x, y = int(point[0]), int(point[1]) if 0 <= x < actual_base_width and 0 <= y < actual_base_height: return mask.getpixel((x, y)) return 0 bb = pymunk.BB(0, 0, actual_base_width - 1, actual_base_height - 1) x_samples = max(20, int(actual_base_width / target_cell_size)) y_samples = max(20, int(actual_base_height / target_cell_size)) pl_set = march_soft(bb, x_samples, y_samples, 128.0, sample_func) # 4. 顶点映射还原 pixel_polygons = [] for polyline in pl_set: simplified = simplify_vertexes(polyline, 0.4) if len(simplified) > 3: try: parts = convex_decomposition(simplified, 0.1) for part in parts: pixel_polygons.append( [(p[0] * scale_factor, p[1] * scale_factor) for p in part] ) except: continue # 坐标转换 manim_polygons = map_polygons_to_manim( pixel_polygons, img_px_w=orig_w, img_px_h=orig_h, img_manim_w=img_manim_w, img_manim_h=img_manim_h, ) return manim_polygons
[docs] def map_polygons_to_manim(polygons, img_px_w, img_px_h, img_manim_w, img_manim_h): """将像素坐标系中的多边形映射到Manim坐标系。 执行坐标系转换:从图片像素坐标转换为Manim的笛卡尔坐标系。 Args: polygons (list): 像素坐标系中的多边形列表。 img_w (float): 图片宽度(像素)。 img_h (float): 图片高度(像素)。 frame_w (float): Manim框架宽度。 frame_h (float): Manim框架高度。 Returns: list: Manim坐标系中的多边形列表。 """ manim_polygons = [] for poly in polygons: manim_vertices = [] for x, y in poly: # 执行坐标映射 m_x = (x / img_px_w - 0.5) * img_manim_w m_y = (0.5 - y / img_px_h) * img_manim_h manim_vertices.append([m_x, m_y]) manim_polygons.append(manim_vertices) return manim_polygons