矩形挺好匹配的,目前应该没什么问题。
test
import os
import numpy as np
import cv2
def read_image_with_chinese_path(image_path):
"""读取含中文路径的图像,返回OpenCV图像对象"""
try:
with open(image_path, 'rb') as f:
img_bytes = f.read()
img_array = np.frombuffer(img_bytes, np.uint8)
img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
return img
except Exception as e:
print(f" 读取图像失败 {image_path}:{str(e)}")
return None
def overlay_image(main_img, overlay_img, x, y):
"""图像叠加函数,含透明通道处理,返回(处理后图像, 叠加是否成功)"""
if main_img is None or overlay_img is None:
print(" 叠加失败:主图或表情图为空")
return main_img, False
# 获取图像尺寸
h, w = overlay_img.shape[:2]
main_h, main_w = main_img.shape[:2]
# 检查坐标是否在主图范围内
if x < 0 or y < 0 or x + w > main_w or y + h > main_h:
print(f" 叠加失败:坐标({x},{y})超出主图范围")
return main_img, False
# 确保主图有Alpha通道,没有则添加
if main_img.shape[2] != 4:
b, g, r = cv2.split(main_img)
a = np.ones((main_h, main_w), dtype=np.uint8) * 255 # 不透明
main_img = cv2.merge([b, g, r, a])
# 提取表情图的通道
if overlay_img.shape[2] == 4:
b_overlay, g_overlay, r_overlay, a_overlay = cv2.split(overlay_img)
alpha = a_overlay / 255.0 # 归一化Alpha通道(0-255 -> 0-1)
alpha_inv = 1.0 - alpha # 反转alpha用于主图区域
else:
b_overlay, g_overlay, r_overlay = cv2.split(overlay_img)
alpha = np.ones((h, w), dtype=np.float32)
alpha_inv = np.zeros((h, w), dtype=np.float32)
# 获取主图ROI区域
main_roi = main_img[y:y+h, x:x+w]
b_main, g_main, r_main, a_main = cv2.split(main_roi)
# 执行叠加计算(NumPy向量化操作,比循环快)
b_result = (alpha * b_overlay + alpha_inv * b_main).astype(np.uint8)
g_result = (alpha * g_overlay + alpha_inv * g_main).astype(np.uint8)
r_result = (alpha * r_overlay + alpha_inv * r_main).astype(np.uint8)
a_result = (alpha * 255 + alpha_inv * a_main).astype(np.uint8) # 叠加区域不透明
# 合并通道
roi_result = cv2.merge([b_result, g_result, r_result, a_result])
# 将结果放回主图
main_img[y:y+h, x:x+w] = roi_result
return main_img, True
def get_best_overlay_position(main_img, facial_w, facial_h):
"""计算主图的最佳叠加位置(仅计算一次)"""
main_h, main_w = main_img.shape[:2]
main_a = main_img[:, :, 3] # 提取主图Alpha通道
main_center_x, main_center_y = main_w // 2, main_h // 2 # 主图中心(用于选最近区域)
# 1. 生成透明区域二值掩码(Alpha=0→1,Alpha>0→0)
mask = cv2.inRange(main_a, 0, 0) // 255 # 归一化到0/1
# 2. 筛选能容纳表情图的透明连通块
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
min_area = facial_w * facial_h # 表情图最小容纳面积
valid_contours = []
for cnt in contours:
if cv2.contourArea(cnt) >= min_area:
x, y, cw, ch = cv2.boundingRect(cnt)
if cw >= facial_w and ch >= facial_h: # 宽高也需满足
valid_contours.append((x, y, cw, ch))
if not valid_contours:
print(f" 没有找到能容纳表情图({facial_w}x{facial_h})的透明连通块")
return None
# 3. 积分图加速候选区域搜索(O(1)判断区域透明度)
candidates = []
for (cx, cy, cw, ch) in valid_contours:
# 连通块内可容纳表情图的坐标范围
max_x = cx + cw - facial_w
max_y = cy + ch - facial_h
if max_x < cx or max_y < cy:
continue
# 生成连通块局部积分图
contour_mask = mask[cy:cy+ch, cx:cx+cw]
integral = cv2.integral(contour_mask)
# 遍历所有可能位置,判断是否完全透明
for y in range(cy, max_y + 1):
for x in range(cx, max_x + 1):
rel_x = x - cx
rel_y = y - cy
# 积分图计算窗口和:完全透明→窗口和=表情图像素总数
window_sum = integral[rel_y + facial_h, rel_x + facial_w] - \
integral[rel_y, rel_x + facial_w] - \
integral[rel_y + facial_h, rel_x] + \
integral[rel_y, rel_x]
if window_sum == facial_w * facial_h:
# 计算到主图中心的距离(选最近的位置)
region_center_x = x + facial_w // 2
region_center_y = y + facial_h // 2
distance = np.sqrt((region_center_x - main_center_x)**2 +
(region_center_y - main_center_y)** 2)
candidates.append((distance, x, y))
if not candidates:
print(f" 未找到适合表情图的透明区域")
return None
# 选择距离主图中心最近的位置
candidates.sort()
best_dist, best_x, best_y = candidates[0]
print(f" 最佳叠加位置:({best_x}, {best_y})(到主图中心距离:{best_dist:.1f})")
return best_x, best_y
def process_leaf_folder(input_leaf_path, output_leaf_path):
"""处理单个最后一级子文件夹的图像,返回(处理表情图数, 成功数, 失败清单)"""
folder_total = 0
folder_success = 0
folder_fail_list = []
# 1. 收集当前子文件夹下的PNG文件
png_files = [os.path.join(input_leaf_path, f) for f in os.listdir(input_leaf_path)
if f.lower().endswith('.png')]
if not png_files:
print(f" 子文件夹无PNG文件,跳过处理")
return (folder_total, folder_success, folder_fail_list)
# 2. 分类主图(不含"facial")和表情图(含"facial")
facial_paths = [p for p in png_files if "facial" in os.path.basename(p)]
main_paths = [p for p in png_files if "facial" not in os.path.basename(p)]
if not main_paths:
print(f" 未找到主图(文件名不含'facial'的PNG),跳过处理")
return (folder_total, folder_success, folder_fail_list)
if not facial_paths:
print(f" 未找到表情图(文件名含'facial'的PNG),跳过处理")
return (folder_total, folder_success, folder_fail_list)
# 3. 处理每个主图(复用叠加位置)
for main_idx, main_path in enumerate(main_paths, 1):
main_img = read_image_with_chinese_path(main_path)
if main_img is None:
print(f" 跳过主图 {main_idx}/{len(main_paths)}:读取失败")
continue
main_h, main_w = main_img.shape[:2]
main_name = os.path.splitext(os.path.basename(main_path))[0]
print(f"\n --- 处理主图 {main_idx}/{len(main_paths)}:{main_name}({main_w}x{main_h})---")
# 保存原始主图(用于后续表情图叠加副本)
original_main_img = main_img.copy()
# 4. 检查主图Alpha通道(无则无法计算叠加区域)
if main_img.shape[2] != 4:
print(f" 主图无Alpha通道,无法计算叠加区域,跳过")
continue
# 5. 用第一个表情图尺寸计算最佳叠加位置(仅一次)
first_facial = read_image_with_chinese_path(facial_paths[0])
if first_facial is None:
print(f" 参考表情图读取失败,无法计算叠加位置,跳过")
continue
facial_w, facial_h = first_facial.shape[1], first_facial.shape[0] # 宽、高
best_xy = get_best_overlay_position(main_img, facial_w, facial_h)
if best_xy is None:
print(f" 无法计算最佳叠加位置,跳过")
continue
best_x, best_y = best_xy
# 6. 处理所有表情图(复用最佳位置)
for facial_idx, facial_path in enumerate(facial_paths, 1):
folder_total += 1 # 累计处理表情图总数
facial_img = read_image_with_chinese_path(facial_path)
# 表情图读取失败
if facial_img is None:
err_msg = f"表情图 {facial_idx}/{len(facial_paths)} 读取失败:{facial_path}"
print(f" {err_msg}")
folder_fail_list.append(err_msg)
continue
facial_name = os.path.splitext(os.path.basename(facial_path))[0]
f_w, f_h = facial_img.shape[1], facial_img.shape[0]
print(f"\n 处理表情图 {facial_idx}/{len(facial_paths)}:{facial_name}({f_w}x{f_h})")
# 表情图尺寸一致性警告
if f_w != facial_w or f_h != facial_h:
print(f" 警告:表情图尺寸与参考图不一致,叠加位置可能偏差")
# 表情图尺寸超出主图
if f_w > main_w or f_h > main_h:
err_msg = f"表情图 {facial_idx}/{len(facial_paths)} 尺寸超出主图(表情图:{f_w}x{f_h},主图:{main_w}x{main_h}),跳过叠加:{facial_path}"
print(f" {err_msg}")
folder_fail_list.append(err_msg)
continue
# 执行叠加
temp_main = original_main_img.copy()
temp_main, overlay_success = overlay_image(temp_main, facial_img, best_x, best_y)
# 叠加失败
if not overlay_success:
err_msg = f"表情图 {facial_idx}/{len(facial_paths)} 叠加失败:{facial_path}"
print(f" {err_msg}")
folder_fail_list.append(err_msg)
continue
# 叠加成功,保存结果
output_filename = f"{facial_name}_{main_name}.png" if len(main_paths) > 1 else f"{facial_name}.png"
output_save_path = os.path.join(output_leaf_path, output_filename)
cv2.imencode('.png', temp_main)[1].tofile(output_save_path)
print(f" 叠加完成,保存至:{output_save_path}")
folder_success += 1 # 累计成功数
print(f"\n 主图 {main_name} 处理完成")
return (folder_total, folder_success, folder_fail_list)
if __name__ == "__main__":
# 初始化全局统计变量
total_facial = 0 # 总处理表情图数
success_facial = 0 # 成功处理数
fail_facial_list = [] # 失败清单
# 1. 输入根文件夹路径
input_root = input("请输入输入根文件夹地址:").strip()
output_root = input("请输入输出根文件夹地址:").strip()
# 2. 验证输入路径有效性
if not os.path.isdir(input_root):
print(f"错误:输入根文件夹 {input_root} 无效")
exit()
# 3. 创建输出根文件夹(若不存在)
os.makedirs(output_root, exist_ok=True)
# 4. 筛选所有最后一级子文件夹(叶子文件夹)
leaf_folders = []
for dirpath, dirs, _ in os.walk(input_root):
if not dirs: # dirs为空表示当前目录是最后一级
leaf_folders.append(dirpath)
if not leaf_folders:
print(f"提示:输入根文件夹 {input_root} 下无最后一级子文件夹,程序将退出")
exit()
# 5. 处理每个叶子子文件夹并累计统计
print(f"\n共找到 {len(leaf_folders)} 个最后一级子文件夹,开始处理...\n")
for leaf_idx, input_leaf in enumerate(leaf_folders, 1):
print(f"==== 处理第 {leaf_idx}/{len(leaf_folders)} 个子文件夹:{input_leaf} ====")
# 构建输出子文件夹路径(保留原目录结构)
rel_path = os.path.relpath(input_leaf, input_root)
output_leaf = os.path.join(output_root, rel_path)
os.makedirs(output_leaf, exist_ok=True)
# 处理当前文件夹并获取统计数据
folder_total, folder_success, folder_fail = process_leaf_folder(input_leaf, output_leaf)
total_facial += folder_total
success_facial += folder_success
fail_facial_list.extend(folder_fail)
print(f"子文件夹 {input_leaf} 处理完成,结果保存至 {output_leaf}\n")
# 6. 计算失败数并输出最终统计
fail_facial = total_facial - success_facial
print("=" * 60)
print("所有子文件夹处理完成!【最终统计】")
print(f"处理表情图数:{total_facial}")
print(f"成功数:{success_facial}")
print(f"失败数:{fail_facial}")
print("-" * 60)
if fail_facial > 0:
print("失败清单:")
for idx, err in enumerate(fail_facial_list, 1):
print(f" {idx}. {err}")
else:
print("失败清单:无(所有表情图均处理成功)")
print("=" * 60)
input("任意键退出");