用opencv按照atlas识别插槽像素块并直接改为透明。
不过有些问题就是如果插槽有多个像素块只会透明化最大的被透明区域包围的像素块,不过用来去掉背景黑框马赛克什么的都没什么问题。
测试魔物娘和贤者效果都很不错,不过需要允许边界接触。
透明化指定插槽
import os
import cv2
import numpy as np
from pathlib import Path
import sys
import shutil
import threading
from multiprocessing import cpu_count
def parse_atlas_file_bounds(atlas_path, slot_name):
"""解析含bounds的atlas文件,支持带空格的插槽名称"""
slot_info = None
with open(atlas_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
i = 0
current_texture = None
while i < len(lines):
line = lines[i].strip()
# 识别纹理文件
if line.endswith(('.png', '.jpg')):
current_texture = line
i += 1
continue
# 跳过元数据行
if line.startswith(('size:', 'filter:', 'pma:', 'scale:')):
i += 1
continue
# 匹配插槽名称(支持带空格的名称)
if not line.startswith(('bounds:', 'offsets:', 'rotate:')):
# 检查是否为目标插槽
if line == slot_name:
slot_info = {'name': line, 'texture': current_texture}
i += 1
# 解析属性行
while i < len(lines):
prop_line = lines[i].strip()
if not prop_line or prop_line.endswith(('.png', '.jpg')) or \
not prop_line.startswith(('bounds:', 'offsets:', 'rotate:')):
break
# 处理属性行
prop_name, prop_value = prop_line.split(':', 1)
slot_info[prop_name.strip()] = prop_value.strip()
i += 1
break
i += 1
# 确保返回一致的格式
if slot_info and 'bounds' in slot_info:
x, y, width, height = map(int, slot_info['bounds'].split(','))
slot_info['xy'] = f"{x},{y}"
slot_info['size'] = f"{width},{height}"
return slot_info
def parse_atlas_file_xy(atlas_path, slot_name):
"""解析含xy的atlas文件,默认去除空格,无冒号行视为插槽名"""
slot_info = None
with open(atlas_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
i = 0
current_texture = None
while i < len(lines):
line = lines[i].strip()
# 识别纹理文件(.png/.jpg结尾)
if line.endswith(('.png', '.jpg')):
current_texture = line
i += 1
continue
# 匹配插槽名称
if ':' not in line:
if line == slot_name:
slot_info = {'name': line, 'texture': current_texture}
i += 1
# 解析属性行
while i < len(lines):
prop_line = lines[i].strip()
if ':' not in prop_line:
break
prop_name, prop_value = prop_line.split(': ', 1)
slot_info[prop_name] = prop_value
i += 1
break
i += 1
return slot_info
def find_largest_non_transparent_block(image, rect, exclude_boundary=True):
"""定位区域内最大非透明块(基于Alpha通道)"""
try:
x, y, w, h = rect
# 确保坐标在图像范围内
h_img, w_img = image.shape[:2]
x = max(0, min(x, w_img - 1))
y = max(0, min(y, h_img - 1))
w = min(w, w_img - x)
h = min(h, h_img - y)
if w <= 0 or h <= 0:
print(f"无效的矩形区域: {rect}")
return None
region = image[y:y+h, x:x+w]
# 确保Alpha通道存在
if region.shape[2] < 4:
region = cv2.cvtColor(region, cv2.COLOR_BGR2BGRA)
alpha = region[:, :, 3]
# 进行连通组件分析
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(alpha, connectivity=8)
if num_labels <= 1: # 只有背景
return None
valid_components = []
for i in range(1, num_labels):
if exclude_boundary:
left = stats[i, cv2.CC_STAT_LEFT]
top = stats[i, cv2.CC_STAT_TOP]
width = stats[i, cv2.CC_STAT_WIDTH]
height = stats[i, cv2.CC_STAT_HEIGHT]
if left > 0 and top > 0 and left + width < w and top + height < h:
valid_components.append(i)
else:
valid_components.append(i)
if not valid_components:
return None
areas = [stats[idx, cv2.CC_STAT_AREA] for idx in valid_components]
largest_idx = valid_components[np.argmax(areas)]
largest_mask = (labels == largest_idx).astype(np.uint8) * 255
return (x, y, largest_mask)
except Exception as e:
print(f"查找非透明块时出错: {e}")
return None
def make_block_transparent(image, block_info):
"""将指定区域设为透明(处理Alpha通道)"""
if not block_info:
return image
try:
x, y, mask = block_info
h, w = mask.shape
# 确保Alpha通道存在
if image.shape[2] < 4:
image = cv2.cvtColor(image, cv2.COLOR_BGR2BGRA)
alpha = image[y:y+h, x:x+w, 3]
alpha[mask > 0] = 0 # 只将非透明像素的Alpha通道置0
return image
except Exception as e:
print(f"透明化处理时出错: {e}")
return image
def read_image_secure(path):
"""兼容中文路径的图像读取(双保险方案)"""
try:
# 二进制流解码(彻底解决编码问题)
with open(path, 'rb') as f:
img_array = np.asarray(bytearray(f.read()), dtype=np.uint8)
return cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
except Exception as e:
print(f"图像读取失败: {e} | 路径: {path}")
return None
def save_image_secure(image, path):
"""兼容中文路径的图像保存"""
try:
_, buffer = cv2.imencode(Path(path).suffix, image)
with open(path, 'wb') as f:
f.write(buffer)
return True
except Exception as e:
print(f"保存失败: {e}")
return False
def process_atlas_file(atlas_path, slot_names, overwrite, parse_mode, exclude_boundary=True):
"""处理单个atlas文件(核心逻辑)"""
print(f"开始处理: {atlas_path}")
processed_slots = 0
textures_processed = set() # 记录已处理的纹理文件
# 按纹理文件分组处理插槽
texture_slot_map = {} # 纹理路径 -> [插槽信息列表]
for slot_name in slot_names:
if parse_mode == 'bounds':
slot_info = parse_atlas_file_bounds(atlas_path, slot_name)
else:
slot_info = parse_atlas_file_xy(atlas_path, slot_name)
if not slot_info:
print(f"插槽 {slot_name} 未找到!")
continue
# 检查必要的属性
if parse_mode == 'bounds' and 'bounds' not in slot_info:
print(f"插槽 {slot_name} 缺少 bounds 属性!")
continue
elif parse_mode == 'xy' and ('xy' not in slot_info or 'size' not in slot_info):
print(f"插槽 {slot_name} 缺少 xy 或 size 属性!")
continue
# 拼接纹理路径
atlas_dir = Path(atlas_path).parent
texture_name = slot_info.get('texture')
if not texture_name:
print("纹理文件名解析失败!")
continue
texture_path = atlas_dir / texture_name
if not texture_path.exists():
print(f"纹理文件缺失: {texture_path}")
continue
# 将插槽信息添加到对应纹理的列表中
if texture_path not in texture_slot_map:
texture_slot_map[texture_path] = []
texture_slot_map[texture_path].append(slot_info)
# 处理每个纹理文件
for texture_path, slot_info_list in texture_slot_map.items():
print(f"处理纹理: {texture_path}")
# 读取图像
image = read_image_secure(texture_path)
if image is None:
continue
# 处理该纹理上的所有插槽
for slot_info in slot_info_list:
slot_name = slot_info.get('name', '未知插槽')
print(f" 处理插槽: {slot_name}")
# 提取关键参数
try:
if parse_mode == 'bounds':
bounds = slot_info.get('bounds')
if bounds:
x, y, width, height = map(int, bounds.split(','))
else:
print(" 插槽坐标/尺寸解析失败!")
continue
else:
x = int(slot_info.get('xy').split(',')[0]) if slot_info.get('xy') else None
y = int(slot_info.get('xy').split(',')[1]) if slot_info.get('xy') else None
width = int(slot_info.get('size').split(',')[0]) if slot_info.get('size') else None
height = int(slot_info.get('size').split(',')[1]) if slot_info.get('size') else None
if None in (x, y, width, height):
print(" 插槽坐标/尺寸解析失败!")
continue
# 处理旋转 - 修改部分
rotated = slot_info.get('rotate', 'false').lower() == 'true'
rotation_angle = int(slot_info.get('rotate', '0')) if slot_info.get('rotate').isdigit() else 0
# 获取原始图像尺寸
h_orig, w_orig = image.shape[:2]
# 根据旋转角度和rotated参数调整插槽矩形
if rotation_angle in [90, 270] or (rotation_angle == 0 and rotated):
# 进行长宽互换
width, height = height, width
# 定位最大非透明块
block_info = find_largest_non_transparent_block(image, (x, y, width, height), exclude_boundary)
if not block_info:
print(" 未检测到有效非透明区域!")
continue
# 透明化处理
image = make_block_transparent(image, block_info)
processed_slots += 1
except Exception as e:
print(f"处理插槽 {slot_name} 时出错: {e}")
continue
# 处理覆盖选项
if overwrite:
output_path = texture_path
else:
backup_path = texture_path.with_name(f"{texture_path.stem}_back{texture_path.suffix}")
# 只有在备份文件不存在时才创建备份
if not backup_path.exists():
shutil.copy2(texture_path, backup_path)
output_path = texture_path
# 保存结果
if save_image_secure(image, output_path):
textures_processed.add(texture_path)
print(f" 纹理处理完成: {output_path}")
if processed_slots == 0:
print("没有成功处理任何插槽!")
return False
print(f"处理完成!共处理 {processed_slots} 个插槽,涉及 {len(textures_processed)} 个纹理文件")
return True
def process_atlas_folder(folder_path, slot_names, overwrite, parse_mode, exclude_boundary=True):
"""递归处理文件夹内所有atlas文件"""
print(f"开始处理文件夹: {folder_path}")
folder = Path(folder_path)
if not folder.is_dir():
print("无效文件夹路径!")
return False
atlas_paths = list(folder.rglob('*.atlas'))
total = len(atlas_paths)
processed = 0
def worker(atlas_path):
nonlocal processed
try:
if process_atlas_file(str(atlas_path), slot_names, overwrite, parse_mode, exclude_boundary):
processed += 1
except Exception as e:
print(f"处理文件 {atlas_path} 时出错: {e}")
num_threads = cpu_count()
threads = []
for i in range(0, total, num_threads):
for j in range(i, min(i + num_threads, total)):
thread = threading.Thread(target=worker, args=(atlas_paths[j],))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
threads = []
print(f"\n统计结果: 共处理 {total} 个atlas文件,成功 {processed} 个")
return processed > 0
def main():
"""交互入口(重点处理Windows中文路径编码)"""
print("=== Atlas插槽透明处理工具 ===")
input_path = None
while True:
# 选择解析模式
parse_mode = input("请选择解析模式 (bounds/xy): ").strip().lower()
while parse_mode not in ['bounds', 'xy']:
print("无效的选择,请输入 'bounds' 或 'xy'。")
parse_mode = input("请选择解析模式 (bounds/xy): ").strip().lower()
# 输入路径处理
if not input_path:
input_str = input("请输入atlas文件/文件夹路径: ").strip()
# Windows控制台编码修复(GBK转UTF-8)
if sys.platform == "win32":
input_str = input_str.encode(sys.stdin.encoding or 'gbk', errors='replace').decode('utf-8', errors='replace')
input_path = input_str
# 校验路径存在性
if not Path(input_path).exists():
print("路径不存在,请重新输入!")
input_path = None
continue
# 输入插槽名称,支持多个插槽用分号分隔
slot_names_input = input("请输入插槽名称(多个插槽用分号分隔): ").strip()
slot_names = [name.strip() for name in slot_names_input.split(';') if name.strip()]
if not slot_names:
print("插槽名称不能为空!")
continue
print(f"将处理以下插槽: {', '.join(slot_names)}")
# 统一选择是否覆盖原文件
overwrite = input("是否覆盖原文件? (y/n): ").strip().lower() == 'y'
# 选择是否排除与边界接触的像素块
exclude_boundary_input = input("是否排除与边界接触的像素块? (y/n): ").strip().lower()
exclude_boundary = exclude_boundary_input == 'y'
# 分发处理逻辑
try:
if Path(input_path).is_file() and input_path.endswith('.atlas'):
process_atlas_file(input_path, slot_names, overwrite, parse_mode, exclude_boundary)
elif Path(input_path).is_dir():
process_atlas_folder(input_path, slot_names, overwrite, parse_mode, exclude_boundary)
else:
print("路径类型无效(需为.atlas文件或文件夹)!")
input_path = None
continue
except Exception as e:
print(f"处理过程中发生错误: {e}")
# 询问是否继续
choice = input("\n是否继续处理? (y/n): ").strip().lower()
if choice != 'y':
print("程序正常退出。")
break
# 询问是否保留当前路径
path_choice = input("是否保留当前路径? (y/n): ").strip().lower()
if path_choice != 'y':
input_path = None
if __name__ == "__main__":
main()