first commit

This commit is contained in:
admin
2026-05-20 15:05:35 +08:00
commit ac09b26253
2048 changed files with 189478 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import sys
from pathlib import Path
from typing import Set, Dict, Tuple
# <--- 新增: 导入 Pillow 库用于图像处理 ---
try:
from PIL import Image
# 禁用解压缩炸弹检查,以防万一标签图非常大且简单
Image.MAX_IMAGE_PIXELS = None
except ImportError:
print("错误: 本脚本需要 'Pillow' 库来检查图像尺寸。", file=sys.stderr)
print("请运行: pip install Pillow", file=sys.stderr)
sys.exit(1)
# --- 新增结束 ---
# 定义支持的文件扩展名
VALID_EXTENSIONS = {'.png', '.jpg', '.jpeg'}
def process_directory(path: Path,
prefix: str = "",
suffix: str = "") -> Tuple[Set[str], Dict[str, Path], Dict[Tuple[int, int], int]]: # <--- 修改:更新了返回类型
"""
处理单个目录提取所有有效文件的文件名stem并根据前缀和后缀进行规范化。
同时统计所有图像的尺寸。
返回:
normalized_stems (Set[str]): 规范化处理后的文件名集合。
normalized_to_full_path (Dict[str, Path]): 规范化文件名 -> 原始完整路径 的映射。
size_counts (Dict[Tuple[int, int], int]): (宽度, 高度) -> 数量 的映射。 # <--- 新增
"""
if not path.is_dir():
print(f"错误: 路径 '{path}' 不是一个有效的目录。", file=sys.stderr)
return set(), {}, {} # <--- 修改
normalized_stems: Set[str] = set()
normalized_to_full_path: Dict[str, Path] = {}
size_counts: Dict[Tuple[int, int], int] = {} # <--- 新增:用于统计尺寸
for file_path in path.glob('*'):
# 确保是文件,并且扩展名在我们的有效列表中
if file_path.is_file() and file_path.suffix.lower() in VALID_EXTENSIONS:
# <--- 新增: 尝试获取图像尺寸 ---
try:
# 使用 with...as... 确保文件被正确关闭
with Image.open(file_path) as img:
size: Tuple[int, int] = img.size
size_counts[size] = size_counts.get(size, 0) + 1
except Exception as e:
print(f"警告: 无法读取图像 '{file_path.name}' 的尺寸. 错误: {e}", file=sys.stderr)
# --- 尺寸获取结束 ---
original_stem = file_path.stem
normalized_stem = original_stem
# 仅当提供了前缀/后缀时才进行处理
if prefix and normalized_stem.startswith(prefix):
normalized_stem = normalized_stem[len(prefix):]
if suffix and normalized_stem.endswith(suffix):
normalized_stem = normalized_stem[:-len(suffix)]
# 检查处理后是否重名,如果重名则发出警告
if normalized_stem in normalized_to_full_path:
print(f"警告: 规范化后文件名发生冲突。")
print(f" '{file_path.name}''{normalized_to_full_path[normalized_stem].name}' 都变成了 '{normalized_stem}'")
normalized_stems.add(normalized_stem)
normalized_to_full_path[normalized_stem] = file_path
return normalized_stems, normalized_to_full_path, size_counts # <--- 修改
# <--- 新增: 打印尺寸统计的辅助函数 ---
def print_size_report(title: str, size_counts: Dict[Tuple[int, int], int]):
"""
格式化并打印尺寸统计报告。
"""
print(f"\n{title}")
if not size_counts:
print(" (未找到或无法读取任何图像文件)")
return
total_files = sum(size_counts.values())
print(f" 总共 {total_files} 个文件,分布如下:")
# 按尺寸 (宽, 高) 排序输出
for size, count in sorted(size_counts.items()):
width, height = size
print(f" - 尺寸 (宽, 高) {width}x{height}: {count} 个文件")
# --- 新增结束 ---
def main():
parser = argparse.ArgumentParser(description="比较两个文件夹中的文件名是否匹配,并可选择删除不匹配项。")
parser.add_argument("-i", "--image",
type=Path,
default="./ORI",
help="Image 文件夹路径")
parser.add_argument("-l", "--label",
type=Path,
default="./Label",
help="Label 文件夹路径")
parser.add_argument("-p", "--prefix",
type=str,
default="",
help="在 Label 文件名中要忽略的前缀")
parser.add_argument("-s", "--suffix",
type=str,
default="",
help="在 Label 文件名中要忽略的后缀")
parser.add_argument("-y", "--yes",
action="store_true",
help="自动确认删除所有不匹配的文件,跳过交互式提示。")
args = parser.parse_args()
# 1. 处理 Image 文件夹
print(f"--- 正在处理 Image 文件夹: {args.image} ---")
image_stems, image_norm_to_path, image_size_counts = process_directory(args.image) # <--- 修改
if not image_stems:
print(f"未在 Image 文件夹中找到任何 .png 或 .jpg 文件。")
# <--- 新增: 打印 Image 尺寸报告 ---
print_size_report("--- Image 文件夹尺寸统计 ---", image_size_counts)
# 2. 处理 Label 文件夹
print(f"\n--- 正在处理 Label 文件夹: {args.label} ---")
print(f"(忽略前缀: '{args.prefix}', 忽略后缀: '{args.suffix}')")
label_stems_normalized, label_norm_to_path, label_size_counts = process_directory( # <--- 修改
args.label,
args.prefix,
args.suffix
)
if not label_stems_normalized:
print(f"未在 Label 文件夹中找到任何 .png 或 .jpg 文件。")
# <--- 新增: 打印 Label 尺寸报告 ---
print_size_report("--- Label 文件夹尺寸统计 ---", label_size_counts)
# 3. 执行比较 (使用集合运算)
matching_stems = image_stems.intersection(label_stems_normalized)
extra_in_image = image_stems.difference(label_stems_normalized)
extra_in_label_normalized = label_stems_normalized.difference(image_stems)
extra_in_label_original_stems = {label_norm_to_path[stem].stem for stem in extra_in_label_normalized}
# 4. 输出结果
print("\n" + "="*30)
print(" 匹配结果报告")
print("="*30)
print(f"\n匹配的文件总数: {len(matching_stems)}")
# <--- 新增: 检查匹配项的尺寸是否一致 ---
print("\n--- 正在检查匹配文件的尺寸 ---")
mismatched_size_pairs = []
if not matching_stems:
print("(无匹配文件,跳过尺寸检查)")
else:
print(f"正在检查 {len(matching_stems)} 对文件...")
for stem in sorted(matching_stems):
try:
img_path = image_norm_to_path[stem]
lbl_path = label_norm_to_path[stem]
# 再次打开以比较尺寸
with Image.open(img_path) as img:
img_size = img.size
with Image.open(lbl_path) as lbl:
lbl_size = lbl.size
if img_size != lbl_size:
mismatched_size_pairs.append((img_path, img_size, lbl_path, lbl_size))
except Exception as e:
print(f" 错误: 无法比较 '{stem}' 的尺寸. 错误: {e}", file=sys.stderr)
if not mismatched_size_pairs:
print(f"√ 成功: 所有 {len(matching_stems)} 对匹配文件均具有相同的尺寸。")
else:
print(f"\n!!! 警告: 发现 {len(mismatched_size_pairs)} 对文件尺寸不匹配:")
for img_path, img_size, lbl_path, lbl_size in mismatched_size_pairs:
print(f" - [Image] {img_path.name} {img_size} != [Label] {lbl_path.name} {lbl_size}")
# --- 尺寸检查结束 ---
print("\n--- Image 文件夹中多余的文件 (共 {} 个) ---".format(len(extra_in_image)))
if not extra_in_image:
print("(无)")
else:
for file_stem in sorted(extra_in_image):
print(file_stem)
print("\n--- Label 文件夹中多余的文件 (共 {} 个) ---".format(len(extra_in_label_original_stems)))
print("(显示的是原始文件名,非规范化名称)")
if not extra_in_label_original_stems:
print("(无)")
else:
for file_stem in sorted(extra_in_label_original_stems):
print(file_stem)
print("\n" + "="*30)
# 5. 删除交互部分 (无修改)
if not extra_in_image and not extra_in_label_normalized:
print("\n所有文件均完美匹配,无需删除。")
# <--- 新增: 如果文件名匹配,但尺寸不匹配,也在这里提醒一下 ---
if mismatched_size_pairs:
print("注意:虽然文件名匹配,但有 {} 对文件的尺寸不一致,请检查上面的报告。".format(len(mismatched_size_pairs)))
return
# ... (后续的删除逻辑保持不变) ...
confirm_delete = False
if args.yes:
print("\n检测到 -y/--yes 参数,将自动删除不匹配的文件。")
confirm_delete = True
else:
try:
choice = input("\n警告: 是否要永久删除所有上述 '多余' 的文件? (y/n): ").strip().lower()
if choice in ['y', 'yes']:
confirm_delete = True
except (KeyboardInterrupt, EOFError):
print("\n操作被用户中断。")
confirm_delete = False
if confirm_delete:
print("\n--- 正在删除 Image 文件夹中的多余文件 ---")
deleted_count = 0
for stem in sorted(extra_in_image):
file_path = image_norm_to_path[stem]
try:
file_path.unlink()
print(f" 已删除: {file_path.name}")
deleted_count += 1
except OSError as e:
print(f" 删除失败: {file_path.name} (错误: {e})", file=sys.stderr)
print(f"--- Image 文件夹共删除 {deleted_count} 个文件 ---")
print("\n--- 正在删除 Label 文件夹中的多余文件 ---")
deleted_count = 0
for norm_stem in sorted(extra_in_label_normalized):
file_path = label_norm_to_path[norm_stem]
try:
file_path.unlink()
print(f" 已删除: {file_path.name} (原始文件名)")
deleted_count += 1
except OSError as e:
print(f" 删除失败: {file_path.name} (错误: {e})", file=sys.stderr)
print(f"--- Label 文件夹共删除 {deleted_count} 个文件 ---")
print("\n删除操作完成。")
else:
print("\n操作已取消。未删除任何文件。")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,122 @@
#!/bin/bash
usage() {
echo "Usage: $0 -i <ori_image_directory> -l <ori_label_directory> -r <stack_result_directory> [ -a <alpha> -p <prefix> -s <suffix> -h]"
echo "对image图片和label图片进行匹配-i、-l -r均不能为空-p -s默认为空"" -a默认为\"0.3\") "
echo "-i:原始image的路径-l:原始label的路径-p:前缀内容,-s:后缀内容(不用管文件后缀名)-h:帮助"
echo "e.g. bash 0_2_TOOL_stack_pics.sh -i ./ori -l ./label -r ./result_0.3透明度 -a 0.3 -p Prefix -s _label"
}
ori_image_directorys=""
ori_label_directorys=""
stack_result_directorys=""
prefix=""
suffix=""
alpha="0.3"
while getopts "hl:i:r:p:s:a:" opt; do
case $opt in
h)
usage
exit 0
;;
i)
ori_image_directorys=$OPTARG
;;
l)
ori_label_directorys=$OPTARG
;;
p)
prefix=$OPTARG
;;
s)
suffix=$OPTARG
;;
r)
stack_result_directorys=$OPTARG
;;
a)
alpha=$OPTARG
;;
*)
echo -e '\033[31m!!! Error, Illegal input !!!\033[0m'
usage
exit 1
;;
esac
done
# 判断输入地址是否为空
if [ -z "$ori_label_directorys" ] || [ -z "$ori_image_directorys" ] || [ -z "$stack_result_directorys" ]; then
echo -e "\033[31m输入地址 -i -l -z 存在空地址\033[0m"
usage
exit 1
fi
# 地址转化
ori_image_directory=$(readlink -f "$ori_image_directorys")
ori_label_directory=$(readlink -f "$ori_label_directorys")
stack_result_directory=$(readlink -f "$stack_result_directorys")
if [ -z "$ori_label_directory" ] || [ -z "$ori_image_directory" ]|| [ -z "$stack_result_directory" ]; then
echo "image、label、result存在无法解析地址程序退出"
echo -e "\033[31mori_image_directory\033[0m: $ori_image_directorys"
echo -e "\033[31mori_label_directory\033[0m: $ori_label_directorys"
echo -e "\033[31mori_label_directory\033[0m: $stack_result_directorys"
exit 1
fi
if [ ! -d "$ori_label_directory" ] || [ ! -d "$ori_image_directory" ]; then
echo "image、label两目录有一个不存在程序退出"
echo -e "\033[31mori_image_directory\033[0m: $ori_image_directory"
echo -e "\033[31mori_label_directory\033[0m: $ori_label_directorys"
exit 1
fi
# 获取当前脚本的路径和名称
script_path=$(dirname "$0")
# 将当前目录更改为脚本所在的路径
cd "$script_path"
# 激活conda环境
source /home/"$USER"/miniconda/bin/activate Deal_Data
echo -e "\033[32m_____ 0_2_TOOL_stack_pics.sh _____\033[0m"
echo -e "\033[33mimage所在文件夹为$ori_image_directory\nlable所在文件夹为$ori_label_directory\033[0m"
# 遍历label目录
for file_path in "$ori_label_directory"/*; do
# 判断是否是文件
if [[ -f "$file_path" ]]; then
file_name=$(basename "$file_path")
# 判断文件名是否符合规范
if [[ "$file_name" =~ .*\.(jpg|png|bmp|JPG|PNG|BMP) ]]; then # 判断是否有为图片
# if [[ "$file_name" =~ "$prefix".*"$suffix".*\.(jpg|png|bmp|JPG|PNG|BMP)$ ]]; then # 判断是否有满足要求的文件名
# 抽取文件名(有前缀、后缀的抽取前缀、后缀里面的,没有的返回整个)
if [ -z $prefix ];then
file_name_extract=$(echo $file_name | sed "s/"$prefix"\(.*\)"$suffix".*/\1/" | sed "s/\(.*\)\.\(jpg\|png\|bmp\|JPG\|PNG\|BMP\)$/\1/")
else
file_name_extract=$(echo $file_name | sed "s/".*$prefix"\(.*\)"$suffix".*/\1/" | sed "s/\(.*\)\.\(jpg\|png\|bmp\|JPG\|PNG\|BMP\)$/\1/")
fi
# 从label目录中看是否有此文件
file_name_other=$(ls $ori_image_directory | grep "^${file_name_extract}\.")
file_name_other=$(echo "$(echo "$file_name_other" | sed '/^$/d')" | head -n1) # 提取出文件名
# 如果另一个目录没有此文件的话
if [ -z "$file_name_other" ]; then
echo "$file_name label中对应内容未在$ori_image_directory搜索到"
# 建立相关存储文件夹
if [ ! -d "$ori_label_directory/Not_pair_pics" ]; then
mkdir -p "$ori_label_directory/Not_pair_pics" # 建立存储文件夹
fi
# 移动相关文件
cp "$ori_label_directory/$file_name" "$ori_label_directory/Not_pair_pics"
echo "$file_name" >> "$ori_label_directory/Not_pair_pics/not_pair.txt"
else # 如果另一个目录有此配对文件的话,则运行相关程序
echo "image中的$file_name_other与lable中的$file_name"
mkdir -p "$stack_result_directory"
python 0_2_stack_picture.py "$ori_image_directory/$file_name_other" "$ori_label_directory/$file_name" "$stack_result_directory" "$alpha"
echo ""
fi
fi
else
echo "$file_path不是文件"
fi
done

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import cv2, os, sys
def Stack_pic(Background_path, Overlay_path, Result_dir, alpha=0.3):
# 读取两张没有alpha通道的图片
img1 = cv2.imread(Background_path) # 底层图片
img2 = cv2.imread(Overlay_path) # 顶层图片
Result_name = os.path.splitext(os.path.basename(Background_path))[0]
# 将img2调整为与img1大小相同
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
# 将img2的透明度调整为20%
overlay_alpha = alpha
# 将img2叠加到img1上
overlay = cv2.addWeighted(img1, 1 - overlay_alpha, img2, overlay_alpha, 0)
# 保存结果
if not os.path.exists(Result_dir):
os.makedirs(Result_dir)
cv2.imwrite(os.path.join(Result_dir, Result_name+'.png'), overlay)
print("堆叠图片写入地址:", os.path.join(Result_dir, Result_name+'.png'))
if __name__ == '__main__':
Background_path = sys.argv[1] # 背景所在路径
Overlay_path = sys.argv[2] # 上层图片所在路径
Result_dir = sys.argv[3] # 结果所在目录
# 透明度默认为0.3
try:
alpha = float(sys.argv[4])
if(alpha > 1 or alpha < 0):
print("alpha 透明度输入不正确其值应该在0~1之间")
alpha = 0.3
except:
alpha = 0.3
# 进行对叠程序
Stack_pic(Background_path, Overlay_path, Result_dir, alpha)

View File

@@ -0,0 +1,442 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*
import os,time,sys,threading, colorsys, argparse
import asyncio, cv2, multiprocessing, random
from PIL import Image
import numpy as np
from Tool_deal_labels import edge_detection, detect_connected_regions, Tool_color_connected_array, fill_white_regions, color_connected_regions
from Tool_Classes_And_Palette import Annotate_CLASSES, Annotate_PALETTE, bg_PALETTE
def getFileList(dir,Filelist=[], ext=None, Max_layer=1, layer=0, Donot_Search=['1_边缘检测并膨胀', '2_连通区域检测', '3_分水岭算法填充']):
"""
获取文件夹及其子文件夹中文件列表
输入 dir文件夹根目录
输入 ext: 扩展名
返回: 文件路径列表
"""
newDir = dir
if os.path.isfile(dir):
if ext is None:
Filelist.append(dir)
else:
if ext in dir[-3:]:
Filelist.append(dir)
elif os.path.isdir(dir):
file_name = os.path.basename(dir)
# 判断是否在禁搜名单中
if file_name in Donot_Search:
return Filelist
for s in os.listdir(dir):
newDir=os.path.join(dir,s)
if layer <= Max_layer:
getFileList(newDir, Filelist, ext, Max_layer, layer+1)
return Filelist
class Deal_image():
def __init__(self, Annotate_CLASSES = ('肝脏','胆囊'), Annotate_PALETTE = [[255,91,0],[255,234,0]], src_label_fold = "./Label", save_pro_label_fold = "./LABEL_PNG_new", save_GT_label_fold = "./Label_Generate", GT_channel = 1, pro_append_name="_label", GT_append_name="", ori_img_folder="./ORI_PNG", res_label_folder="./Result_label", save_merge_pic_folder="./Result_merge", back_gnd_color=0, first_class_color=1, pic_type="png", Max_width = 10000, Label_Max_Search_layer=1000, save_process_pics=False, bg_PALETTE = [0,0,0]):
# 背景最好放在最后
# self.src_CLASSES = ('肝脏','胆囊','分离钳','止血海绵','肝总管','胆总管','吸引器','剪刀','止血纱布','生物夹','无损伤钳','喷洒','胆囊管','胆囊动脉','电凝','标本袋','引流管','纱布','金属钛夹','术中超声','吻合器','乳胶管','推结器','肝带','钳夹','超声刀','脂肪','双极电凝','棉球','血管阻断夹','肿瘤','针','线','韧带','胆囊静脉','背景')
# self.src_PALETTE = np.array([[255,91,0],[255,234,0],[85, 111, 181],[181, 227, 14],[72, 0, 255],[0, 155, 33],[255,0,255],[29, 32, 136],[160, 15, 95],[0,160,233],[52,184,178],[90,120,41],[255,0,0],[177,0,0],[167,24,233],[112,113,150],[0,255,0],[255,255,255],[0,255,255],[138,251,213],[136,162,196],[197,83,181],[202,202,200],[113,102,140],[66,115,82],[240,16,116],[155,132,0],[155,62,0],[146,175,236],[255,172,159],[245,161,0],[134,124,118], [0,157,142], [181,85,105], [42,8,66],[0,0,0]])
# self.src_CLASSES_NUM = np.shape(self.src_CLASSES)[0]
self.bg_PALETTE = bg_PALETTE # 背景颜色 TODO
self.Annotate_CLASSES = Annotate_CLASSES # 待分类的类
self.Annotate_PALETTE = np.array(Annotate_PALETTE) # 每一类的像素直
self.Annotate_CLASSES_NUM = np.shape(Annotate_CLASSES)[0] # 类数量
self.save_process_pics = save_process_pics # 保存中间过程图片
self.src_label_fold = src_label_fold # 原始标签图片 保存位置
self.save_pro_label_fold = save_pro_label_fold # 优化后标签图片 保存位置
self.save_GT_label_fold = save_GT_label_fold # GT标签图片 保存位置
self.ori_img_folder = ori_img_folder # 最原始手术图片 保存位置
self.res_label_folder = res_label_folder # 训练出来的label 保存位置
self.save_merge_pic_folder = save_merge_pic_folder # 融合图像保存位置
self.pro_append_name = pro_append_name # 优化后标签图片后缀
self.GT_append_name = GT_append_name # GT标签图片后缀
self.GT_channel = GT_channel # GT标签图片通道数
self.Max_width = Max_width # 最大图片宽度(匹配时候用)
self.pic_type = pic_type # 图片类型
self.back_gnd_color = back_gnd_color # 背景颜色
self.first_class_color = first_class_color # 第一类上的颜色
self.Label_Max_Search_layer=Label_Max_Search_layer # 文件夹最大搜索深度
try:
self.labellist_src = getFileList(src_label_fold, [], pic_type, self.Label_Max_Search_layer)
print('本次执行检索到ori_label图片 '+str(len(self.labellist_src))+' 张图像')
except:
self.labellist_src = None
print("没有ori_label相关文件")
try:
# print(save_pro_label_fold)
self.labellist_pro = getFileList(save_pro_label_fold, [], pic_type, self.Label_Max_Search_layer)
print('本次执行检索到pro_label图片 '+str(len(self.labellist_pro))+' 张图像')
except:
self.labellist_pro = None
print("没有pro_label相关文件")
try:
self.imglist_src = getFileList(ori_img_folder, [], pic_type, self.Label_Max_Search_layer)
self.reslist_src = getFileList(res_label_folder, [], pic_type, self.Label_Max_Search_layer)
print('本次执行检索到ori原始图片 '+str(len(self.imglist_src))+' 张图像')
print('本次执行检索到训练train_result图片 '+str(len(self.reslist_src))+' 张图像')
except:
self.imglist_src = None
self.reslist_src = None
print("没有train_result和原始图片相关文件")
# 获取单张图片各个通路信息
def get_single_pic_rgb(self, imgpath):
print(imgpath)
image = Image.open(imgpath).convert('RGB') # 转为RGB图片
# 将 RGB 色值分离
image.load()
r, g, b = image.split()
r = np.array(r)
g = np.array(g)
b = np.array(b)
return image, r, g, b
# 将单个pro图片变成GT图片
def Conver_pro_label_pic_2_GT_pic(self, imgpath, imgname):
time_start=time.time() # 记录开始时间
# 获取单张图片各个通路信息
image, r,g,b = self.get_single_pic_rgb(imgpath)
result_gt = np.ones(np.shape(image))*self.back_gnd_color # 初始化填充内容为back_gnd_color
gt_number = self.first_class_color # 第一类上色颜色确定
# PALETTE中排除掉 '背景' [0,0,0]
PALETTE_No_Bg = self.Annotate_PALETTE[~np.all(self.Annotate_PALETTE == self.bg_PALETTE, axis=1)]
# 遍历所有待识别颜色
for [Annotate_PALETTE_r, Annotate_PALETTE_g, Annotate_PALETTE_b] in PALETTE_No_Bg:
# 查找三原色匹配位置
locate_r = np.where( r == Annotate_PALETTE_r )
locate_g = np.where( g == Annotate_PALETTE_g )
locate_b = np.where( b == Annotate_PALETTE_b )
# 查找都匹配位置(交集)
# 将矩阵换一种表示形式
locate_r = np.array(locate_r[0]) * self.Max_width + np.array(locate_r[1])
locate_g = np.array(locate_g[0]) * self.Max_width + np.array(locate_g[1])
locate_b = np.array(locate_b[0]) * self.Max_width + np.array(locate_b[1])
# 用自带函数寻找匹配项
matched = np.intersect1d(np.intersect1d(locate_r, locate_g), locate_b)
matched = np.concatenate(([matched // self.Max_width], [np.mod(matched, self.Max_width)]), 0)
result_gt[matched[0],matched[1], :] = gt_number
gt_number = gt_number + 1
# 输出GT图片
if(int(self.GT_channel) == 1):
result_gt = result_gt[:,:,0]
elif(int(self.GT_channel) == 3):
result_gt = cv2.cvtColor(np.float32(result_gt), cv2.COLOR_RGB2BGR) # rgb颜色互换
else:
print("GT_channel 必须为1或3")
quit
try: # 新建文件夹
os.mkdir(self.save_GT_label_fold)
except:
print("已有"+self.save_GT_label_fold)
if imgname.lower().endswith(('.jpg', '.png')):
save_dir = os.path.join(self.save_GT_label_fold, os.path.basename(imgname).rpartition('.')[0]+self.GT_append_name+'.'+self.pic_type)
else:
save_dir = os.path.join(self.save_GT_label_fold, os.path.basename(imgname)+self.GT_append_name+'.'+self.pic_type)
cv2.imwrite(save_dir, result_gt)
print("GT图片已保存", save_dir)
time_end=time.time() # 输出结束时间
print('time cost',time_end-time_start,'s')
# 将处理好的图片转化为GT图片
def Conver_pro_label_pic_2_GT_pic_all(self):
print("\033[33m**** 进行转换将Pro_label_pic转换为GT_label_pic ****\033[0m")
print("\033[33mPro_label_pic存储位置为\033[0m", self.save_pro_label_fold)
print("\033[33mGT_label_pic生成位置为\033[0m", self.save_GT_label_fold)
try:
# print(save_pro_label_fold)
self.labellist_pro = getFileList(save_pro_label_fold, [], pic_type, self.Label_Max_Search_layer)
print('本次执行检索到pro_label图片 '+str(len(self.labellist_pro))+' 张图像')
except:
self.labellist_pro = None
print("没有pro_label相关文件")
try:
os.mkdir(self.save_GT_label_fold) # 新建存储文件夹
except:
print("已有"+self.save_GT_label_fold)
# 指定最大进程数为 3
max_processes = 20
# 创建Pool对象
pool = multiprocessing.Pool(processes=max_processes)
# 创建并启动进程
args_list1 = []
args_list2 = []
# 遍历整个文件夹
for imgpath in self.labellist_pro:
imgname = os.path.basename(imgpath).rpartition('.')[0].replace(self.pro_append_name,"")
args_list1.append(imgpath)
args_list2.append(imgname)
args_list = zip(args_list1, args_list2)
# 使用进程池并行执行任务
pool.starmap(self.Conver_pro_label_pic_2_GT_pic, args_list)
# 关闭进程池
pool.close()
pool.join()
def Conver_ori_label_pic_2_pro_pic(self, imgpath, imgname):
time_start=time.time() # 记录开始时间
# 获取单张图片各个通路信息
image = cv2.imread(imgpath)
# 1. 边缘检测并膨胀
dilated_image = edge_detection(image)
# 如果需要存储中间态图片
if(self.save_process_pics == True):
if imgname.lower().endswith(('.jpg', '.png')):
save_dir = os.path.join(self.save_pro_label_fold, '1_边缘检测并膨胀', os.path.basename(imgname).rpartition('.')[0]+self.pro_append_name+'_Edge'+'.'+self.pic_type)
else:
save_dir = os.path.join(self.save_pro_label_fold, '1_边缘检测并膨胀', os.path.basename(imgname)+self.pro_append_name+'_Edge'+'.'+self.pic_type)
cv2.imwrite(save_dir, dilated_image)
print("中间态-边缘检测并膨胀 图片已保存", save_dir)
time_end=time.time() # 输出结束时间
print('time cost',time_end-time_start,'s')
# 2. 检测连通区域
filtered_labeled_array, _ = detect_connected_regions(dilated_image)
colored_image_filtered = Tool_color_connected_array(filtered_labeled_array)
# 如果需要存储中间态图片
if(self.save_process_pics == True):
if imgname.lower().endswith(('.jpg', '.png')):
save_dir = os.path.join(self.save_pro_label_fold, '2_连通区域检测', os.path.basename(imgname).rpartition('.')[0]+self.pro_append_name+'_Region'+'.'+self.pic_type)
else:
save_dir = os.path.join(self.save_pro_label_fold, '2_连通区域检测', os.path.basename(imgname)+self.pro_append_name+'_Region'+'.'+self.pic_type)
cv2.imwrite(save_dir, colored_image_filtered)
print("中间态-连通区域检测 图片已保存", save_dir)
time_end=time.time() # 输出结束时间
print('time cost',time_end-time_start,'s')
# 3. 分水岭填充白色区域
filled_labeled_array = fill_white_regions(filtered_labeled_array)
colored_image_filled = Tool_color_connected_array(filled_labeled_array)
# 如果需要存储中间态图片
if(self.save_process_pics == True):
if imgname.lower().endswith(('.jpg', '.png')):
save_dir = os.path.join(self.save_pro_label_fold, '3_分水岭算法填充', os.path.basename(imgname).rpartition('.')[0]+self.pro_append_name+'_FillEdge'+'.'+self.pic_type)
else:
save_dir = os.path.join(self.save_pro_label_fold, '3_分水岭算法填充', os.path.basename(imgname)+self.pro_append_name+'_FillEdge'+'.'+self.pic_type)
cv2.imwrite(save_dir, colored_image_filled)
print("中间态-分水岭算法填充 图片已保存", save_dir)
time_end=time.time() # 输出结束时间
print('time cost',time_end-time_start,'s')
# 4. 对连通区域最终上色
ori_labeled_image = image
result_pro = color_connected_regions(filled_labeled_array, filtered_labeled_array, ori_labeled_image, self.Annotate_PALETTE)
if imgname.lower().endswith(('.jpg', '.png')):
save_dir = os.path.join(self.save_pro_label_fold, os.path.basename(imgname).rpartition('.')[0]+self.pro_append_name+'.'+self.pic_type)
else:
save_dir = os.path.join(self.save_pro_label_fold, os.path.basename(imgname)+self.pro_append_name+'.'+self.pic_type)
print("Pro图片已保存", save_dir)
cv2.imwrite(save_dir, result_pro)
time_end=time.time() # 输出结束时间
print('time cost',time_end-time_start,'s')
# 将原始src图片转化为处理好的pro图片
def Conver_ori_label_pic_2_pro_pic_all(self):
print("\033[33m**** 进行转换将Ori_label_pic转换为Pro_label_pic ****\033[0m")
print("\033[33mOri_label_pic存储位置为\033[0m", self.src_label_fold)
print("\033[33mPro_label_pic生成位置为\033[0m", self.save_pro_label_fold)
# 输出颜色预处理图片
try:
os.mkdir(self.save_pro_label_fold) # 新建存储文件夹
except:
print("已有"+self.save_pro_label_fold)
if(self.save_process_pics == True):
try:
os.mkdir(os.path.join(self.save_pro_label_fold, '1_边缘检测并膨胀')) # 新建存储1_边缘检测并膨胀文件夹
except:
print("已有"+os.path.join(self.save_pro_label_fold, '1_边缘检测并膨胀'))
try:
os.mkdir(os.path.join(self.save_pro_label_fold, '2_连通区域检测')) # 新建存储2_连通区域检测文件夹
except:
print("已有"+os.path.join(self.save_pro_label_fold, '2_连通区域检测'))
try:
os.mkdir(os.path.join(self.save_pro_label_fold, '3_分水岭算法填充')) # 新建存储1_边缘检测并膨胀文件夹
except:
print("已有"+os.path.join(self.save_pro_label_fold, '3_分水岭算法填充'))
# 指定最大进程数为 20多参数函数并行
max_processes = 20
# 创建Pool对象
pool = multiprocessing.Pool(processes=max_processes)
# 创建并启动进程
args_list1 = []
args_list2 = []
# 遍历整个文件夹
for imgpath in self.labellist_src:
if imgpath.lower().endswith(('.jpg', '.png')):
imgname= os.path.basename(imgpath).rpartition('.')[0].replace(self.pro_append_name,"")
else:
imgname= os.path.basename(imgpath).replace(self.pro_append_name,"")
try:
print("Processing: ", imgname, "...")
# self.Conver_ori_label_pic_2_pro_pic(imgpath, imgname)s
# args_list.append({'imgpath': imgpath, 'imgname': imgname})
args_list1.append(imgpath)
args_list2.append(imgname)
except:
os.system("echo "+imgname+" >> error_1.txt")
args_list = zip(args_list1, args_list2)
# 使用进程池并行执行任务
pool.starmap(self.Conver_ori_label_pic_2_pro_pic, args_list) # 使用starmap进行多参数并行
# 关闭进程池
pool.close()
pool.join()
# 图片堆叠
def Merge_ori_pic_and_label_pic(self, res_img_path, res_imgname):
time_start=time.time() # 记录开始时间
# 获取单张图片各个通路信息
ori_img_path = os.path.join(self.ori_img_folder, res_imgname+'.'+self.pic_type)
if not os.path.exists(ori_img_path):
print("****照片不存在:****", ori_img_path)
return -1
ori_image, ori_r, ori_g, ori_b = self.get_single_pic_rgb(ori_img_path)
res_image, res_r, res_g, res_b = self.get_single_pic_rgb(res_img_path)
merge_img = np.array(ori_image) # merge图片初始化默认图片背景为0.0.0
# 遍历所有待识别颜色
for [Annotate_PALETTE_r, Annotate_PALETTE_g, Annotate_PALETTE_b] in self.Annotate_PALETTE:
# 查找三原色匹配位置
locate_r = np.where( res_r == Annotate_PALETTE_r )
locate_g = np.where( res_g == Annotate_PALETTE_g )
locate_b = np.where( res_b == Annotate_PALETTE_b )
# 查找都匹配位置(交集)
# 将矩阵换一种表示形式
locate_r = np.array(locate_r[0]) * self.Max_width + np.array(locate_r[1])
locate_g = np.array(locate_g[0]) * self.Max_width + np.array(locate_g[1])
locate_b = np.array(locate_b[0]) * self.Max_width + np.array(locate_b[1])
# 用自带函数寻找匹配项
matched = np.intersect1d(np.intersect1d(locate_r, locate_g), locate_b)
matched = np.concatenate(([matched // self.Max_width], [np.mod(matched, self.Max_width)]), 0)
merge_img[matched[0],matched[1], 0] = Annotate_PALETTE_r
merge_img[matched[0],matched[1], 1] = Annotate_PALETTE_g
merge_img[matched[0],matched[1], 2] = Annotate_PALETTE_b
# 转成cv2形式
merge_img = cv2.cvtColor(np.float32(merge_img), cv2.COLOR_RGB2BGR)
try: # 新建文件夹
os.mkdir(self.save_merge_pic_folder)
except:
print("已有"+self.save_merge_pic_folder)
if res_imgname.lower().endswith(('.jpg', '.png')):
save_dir = os.path.join(self.save_merge_pic_folder, os.path.basename(res_imgname).rpartition('.')[0]+'.'+self.pic_type)
else:
save_dir = os.path.join(self.save_merge_pic_folder, os.path.basename(res_imgname)+'.'+self.pic_type)
cv2.imwrite(save_dir, merge_img)
print("Merge图片已保存", save_dir)
time_end=time.time() # 输出结束时间
print('time cost',time_end-time_start,'s')
# 将label图片与原图片重合
def Merge_ori_pic_and_label_pic_all(self):
# 遍历整个文件夹
for res_img_path in self.reslist_src:
if res_img_path.lower().endswith(('.jpg', '.png')):
res_imgname = os.path.basename(res_img_path).rpartition('.')[0].replace(self.pro_append_name,"")
else:
res_imgname = os.path.basename(res_img_path).replace(self.pro_append_name,"")
print("Processing: ", res_imgname, "...")
self.Merge_ori_pic_and_label_pic(res_img_path, res_imgname)
if __name__ == "__main__":
# 创建参数解析器
parser = argparse.ArgumentParser(description='Process some files.')
# 添加参数选项
parser.add_argument('-src_fold', dest='src_label_fold', default='./', help='source label folder')
parser.add_argument('-save_pro_fold', dest='save_pro_label_fold', default='./ORI_pro_label_fold', help='processed label folder')
parser.add_argument('-save_GT_fold', dest='save_GT_label_fold', default='./ORI_GT_label_fold', help='ground truth folder')
parser.add_argument('-fold_search_depth', dest='Label_Max_Search_layer', default='1000', type=int, help='Folder Search Depth')
parser.add_argument('-pro_suffix_name', dest='pro_append_name', default='_label', help='Pro file suffix')
parser.add_argument('-GT_suffix_name', dest='GT_append_name', default='', help='GT file suffix')
parser.add_argument('-GT_channel', dest='GT_channel', default='1', type=int, help='GT file channel(1 or 3)')
parser.add_argument('-back_gnd_color', dest='back_gnd_color', default='0', type=int, help='Color of "Back ground"(0 or 255)')
parser.add_argument('-first_class_color', dest='first_class_color', default='1', type=int, help='Color of "First Class"')
parser.add_argument('-pic_type', dest='pic_type', default='png', help='type of picture(Do not add ".")')
parser.add_argument('-Max_width', dest='Max_width', default='10000', type=int, help='Max width of picture')
parser.add_argument('-Rebuild_from', dest='Rebuild_from', default='label', help='Source to Rebuild Labels(label/pro)')
parser.add_argument('-Rebuild_to', dest='Rebuild_to', default='GT', help='Destination of Rebuild Labels(pro/GT)')
parser.add_argument('-save_process_pics', dest='save_process_pics', default='False', help='Save the processed pics(e.g.Gray_pics,Color_pics) in generating pro_pics')
# 解析命令行参数
args = parser.parse_args()
src_label_fold = args.src_label_fold
save_pro_label_fold = args.save_pro_label_fold
save_GT_label_fold = args.save_GT_label_fold
Label_Max_Search_layer = args.Label_Max_Search_layer
pro_append_name = args.pro_append_name
GT_append_name = args.GT_append_name
GT_channel = args.GT_channel
back_gnd_color = args.back_gnd_color
first_class_color = args.first_class_color
pic_type = args.pic_type
Max_width = args.Max_width
Rebuild_from = args.Rebuild_from
Rebuild_to = args.Rebuild_to
save_process_pics = args.save_process_pics
try: # 遍历文件深度最小为1
Label_Max_Search_layer=int(Label_Max_Search_layer)
except:
Label_Max_Search_layer=1000
try: # GT标签图片通道数
GT_channel=int(GT_channel)
except:
GT_channel=1
try: # 背景颜色背景选择0或255)
back_gnd_color=int(back_gnd_color)
except:
back_gnd_color=0
try: # 第一类上的颜色(如果背景为0,选择1)
first_class_color=int(first_class_color)
except:
first_class_color=1
try: # 最大图片宽度(匹配时候用)
Max_width=int(Max_width)
except:
Max_width=10000
if(save_process_pics.lower() == 'false'):
save_process_pics = False
elif(save_process_pics.lower() == 'true'):
save_process_pics = True
else:
save_process_pics = False
D = Deal_image(Annotate_CLASSES=Annotate_CLASSES, Annotate_PALETTE=Annotate_PALETTE, src_label_fold=src_label_fold, save_pro_label_fold=save_pro_label_fold, save_GT_label_fold=save_GT_label_fold, GT_channel=GT_channel, pro_append_name=pro_append_name, GT_append_name=GT_append_name, back_gnd_color=back_gnd_color, first_class_color=first_class_color, pic_type=pic_type, Max_width=Max_width, Label_Max_Search_layer=Label_Max_Search_layer, save_process_pics=save_process_pics, bg_PALETTE = bg_PALETTE)
# print(D.src_CLASSES_NUM)
if Rebuild_from == 'label':
# 1.先将所有原始图片转为pro图片
D.Conver_ori_label_pic_2_pro_pic_all()
pass
if Rebuild_to == 'GT':
# 2.再将pro图片转为GT图片
D.Conver_pro_label_pic_2_GT_pic_all()
pass

View File

@@ -0,0 +1,169 @@
import os
import cv2
import numpy as np
from collections import Counter, defaultdict
from PIL import Image
from Tool_Classes_And_Palette import Annotate_CLASSES, Annotate_PALETTE, bg_PALETTE
# ※ 需要修改输入输出路径 ※ #
input_dir = './ORI_GT_label_fold'
output_dir_1 = 'Data/labels/train'
output_dir_2 = 'Data/labels/val'
# ----------------------------------------------------
# ※※※ 1. 修改: 移除背景项并统一格式 ※※※
# ----------------------------------------------------
# (1) 从 Annotate_CLASSES (元组) 中移除 '背景'
if Annotate_CLASSES and Annotate_CLASSES[-1] == '背景':
Annotate_CLASSES = Annotate_CLASSES[:-1]
# (2) 从 Annotate_PALETTE (列表) 中移除背景颜色 [0,0,0]
if Annotate_PALETTE and (Annotate_PALETTE[-1] == [0, 0, 0] or Annotate_PALETTE[-1] == (0, 0, 0)):
Annotate_PALETTE.pop()
# (3) 确保调色板中的所有颜色都是元组 (Tuple),以便后续查找
Annotate_PALETTE = [tuple(color) for color in Annotate_PALETTE]
# (4) 确保 bg_PALETTE 是元组,以匹配 Counter 中的键类型
bg_PALETTE = tuple(bg_PALETTE)
# (5) 检查类别和调色板长度是否一致
if len(Annotate_CLASSES) != len(Annotate_PALETTE):
print(f"[警告] 移除背景后,类别 ({len(Annotate_CLASSES)}) 和调色板 ({len(Annotate_PALETTE)}) 长度不一致!")
else:
print(f"--- 成功移除背景项,剩余 {len(Annotate_CLASSES)} 个有效类别。 ---")
os.makedirs(output_dir_1, exist_ok=True)
os.makedirs(output_dir_2, exist_ok=True)
# 全局统计颜色频率与 class 像素频率
global_color_counter = Counter()
global_class_counter = Counter() # 将用于统计ori_label 模式)
color_class_counter = defaultdict(int) # (R,G,B) → count
color_to_old_class = {} # (R,G,B) → class_id
# *** ori_label 模式: 移除了 remap_class_dict ***
# 自动提取颜色映射(跳过背景)
def extract_color_mapping(img_path):
img = Image.open(img_path).convert('RGB')
pixels = list(img.getdata())
counter = Counter(pixels)
color_map = {}
for color, count in counter.items():
global_color_counter[color] += count
if color != bg_PALETTE and color[0] == color[1] == color[2]: # 使用元组 bg_PALETTE
class_id = color[0] - 1
if class_id >= 0:
color_map[color] = class_id
global_class_counter[class_id] += count # 使用 global_class_counter 统计
color_class_counter[color] += count
color_to_old_class[color] = class_id
return color_map
# 处理单张图片
def process_image(img_path, save_path_list):
color_to_class = extract_color_mapping(img_path)
if not color_to_class:
print(f"[跳过] {os.path.basename(img_path)} 无有效目标")
return
img = cv2.imread(img_path)
h, w = img.shape[:2]
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
lines = []
for rgb, old_class_id in color_to_class.items():
mask = np.all(img_rgb == rgb, axis=-1).astype(np.uint8) * 255
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
if contour.shape[0] < 3:
continue
norm_pts = contour.squeeze(1).astype(np.float32)
norm_pts[:, 0] /= w
norm_pts[:, 1] /= h
flat = norm_pts.flatten()
# *** ori_label 模式: 直接使用 old_class_id ***
line = f"{old_class_id} " + " ".join([f"{x:.6f}" for x in flat])
lines.append(line)
if lines:
for save_path in save_path_list:
with open(save_path, 'w') as f:
for line in lines:
f.write(line + "\n")
print(f"[✔] 转换成功: {os.path.basename(save_path)},共 {len(lines)} 个实例")
else:
print(f"[⚠] {os.path.basename(img_path)} 没有轮廓")
# 第一次遍历图像,仅提取颜色信息用于构建 class 映射
for fname in os.listdir(input_dir):
if fname.lower().endswith('.png'):
img_path = os.path.join(input_dir, fname)
extract_color_mapping(img_path)
# *** ori_label 模式: 移除了重映射 (remap) 逻辑块 ***
# (保留 class_pixel_count 用于统计)
class_pixel_count = {color_to_old_class[c]: count for c, count in color_class_counter.items()}
# 第二次遍历图像,正式处理并生成标签
for fname in os.listdir(input_dir):
if fname.lower().endswith('.png'):
img_path = os.path.join(input_dir, fname)
base_name = os.path.splitext(fname)[0]
txt_path_1 = os.path.join(output_dir_1, base_name + '.txt')
txt_path_2 = os.path.join(output_dir_2, base_name + '.txt')
process_image(img_path, [txt_path_1, txt_path_2])
# ----------------------------------------------------
# ※※※ 2. 修改: 打印详细的颜色统计 ※※※
# ----------------------------------------------------
print("\n📊 所有图像颜色统计:")
def get_label_info(class_id):
"""辅助函数:根据 class_id 获取标签名和颜色"""
if 0 <= class_id < len(Annotate_CLASSES) and 0 <= class_id < len(Annotate_PALETTE):
return Annotate_CLASSES[class_id], Annotate_PALETTE[class_id]
else:
return "未知标签", (255, 255, 255) # 返回一个默认值
for color, count in global_color_counter.most_common():
if color == bg_PALETTE: # 使用 bg_PALETTE 变量
print(f"背景颜色 {color} 出现次数: {count}")
elif color[0] == color[1] == color[2]:
class_id = color[0] - 1
label_name, label_color = get_label_info(class_id)
print(f"颜色 {color} → class {class_id} (标签: '{label_name}', 颜色: {label_color}),出现次数: {count}")
else:
# 此情况理论上不应出现,因为 extract_color_mapping 已过滤
print(f"[⚠] 非灰阶颜色 {color},出现次数: {count} (此颜色不应被处理)")
# ----------------------------------------------------
# ※※※ 3. 修改: 打印详细的类别统计 (ori_label 模式) ※※※
# ----------------------------------------------------
print("\n✅ 有效类别统计(按 原 class_id 排序 → 总像素数):")
# 按照 old_id (原类别索引) 排序输出
sorted_classes = sorted(class_pixel_count.items(), key=lambda item: item[0])
for old_id, pixel_count in sorted_classes:
label_name, label_color = get_label_info(old_id)
print(f"class {old_id} (标签: '{label_name}', 颜色: {label_color}): {pixel_count} pixels")
# ----------------------------------------------------
# ※※※ 4. 修改: 额外输出 原 class_id 到颜色的映射 (ori_label 模式) ※※※
# ----------------------------------------------------
print(f"\n🎨 找到的 原 class_id 与标签颜色 (Annotate_PALETTE) 映射表:")
# 1. 按照 old_id (0, 1, 2...) 排序并按指定格式打印
# 我们使用 class_pixel_count.keys() 来获取所有实际找到的 old_id
sorted_old_ids = sorted(class_pixel_count.keys())
for old_id in sorted_old_ids:
if 0 <= old_id < len(Annotate_PALETTE):
color_tuple = Annotate_PALETTE[old_id]
color_list = list(color_tuple)
print(f" {old_id}: {color_list}")
else:
# 预防性代码
print(f" {old_id}: [颜色未在调色板中定义]")
# ----------------------------------------------------
# ※※※ 修改结束 ※※※
# ----------------------------------------------------
print(f"\n✅ 全部图像处理完毕。标签输出目录:{output_dir_1}{output_dir_2}")

View File

@@ -0,0 +1,204 @@
import os
import cv2
import numpy as np
from collections import Counter, defaultdict
from PIL import Image
from Tool_Classes_And_Palette import Annotate_CLASSES, Annotate_PALETTE, bg_PALETTE
# ※ 需要修改输入输出路径 ※ #
input_dir = 'ORI_GT_label_fold'
output_dir_1 = 'Data/labels/train'
output_dir_2 = 'Data/labels/val'
output_GT_dir_1 = 'Data/labels_GT/train'
output_GT_dir_2 = 'Data/labels_GT/val'
# 1. --- 您提供的类别和调色板 ---
# (1) 从 Annotate_CLASSES (元组) 中移除 '背景'
if Annotate_CLASSES and Annotate_CLASSES[-1] == '背景':
Annotate_CLASSES = Annotate_CLASSES[:-1]
# (2) 从 Annotate_PALETTE (列表) 中移除背景颜色 [0,0,0]
if Annotate_PALETTE and (Annotate_PALETTE[-1] == [0, 0, 0] or Annotate_PALETTE[-1] == (0, 0, 0)):
Annotate_PALETTE.pop()
# (3) 确保调色板中的所有颜色都是元组 (Tuple),以便后续查找
Annotate_PALETTE = [tuple(color) for color in Annotate_PALETTE]
# (4) 确保 bg_PALETTE 是元组,以匹配 Counter 中的键类型
bg_PALETTE = tuple(bg_PALETTE)
# (5) 检查类别和调色板长度是否一致
if len(Annotate_CLASSES) != len(Annotate_PALETTE):
print(f"[警告] 移除背景后,类别 ({len(Annotate_CLASSES)}) 和调色板 ({len(Annotate_PALETTE)}) 长度不一致!")
else:
print(f"--- 成功移除背景项,剩余 {len(Annotate_CLASSES)} 个有效类别。 ---")
os.makedirs(output_dir_1, exist_ok=True)
os.makedirs(output_dir_2, exist_ok=True)
os.makedirs(output_GT_dir_1, exist_ok=True)
os.makedirs(output_GT_dir_2, exist_ok=True)
# 全局统计颜色频率与 class 像素频率
global_color_counter = Counter()
global_class_counter = Counter()
color_class_counter = defaultdict(int) # (R,G,B) → count
color_to_old_class = {} # (R,G,B) → class_id
remap_class_dict = {} # old_class_id → new_class_id
# 自动提取颜色映射(跳过背景)
def extract_color_mapping(img_path):
img = Image.open(img_path).convert('RGB')
pixels = list(img.getdata())
counter = Counter(pixels)
color_map = {}
for color, count in counter.items():
global_color_counter[color] += count
if color != (0, 0, 0) and color[0] == color[1] == color[2]:
class_id = color[0] - 1
if class_id >= 0:
color_map[color] = class_id
global_class_counter[class_id] += count
color_class_counter[color] += count
color_to_old_class[color] = class_id
return color_map
# --- 修改:函数签名增加了 gt_save_paths ---
def process_image(img_path, txt_save_paths, gt_save_paths):
color_to_class = extract_color_mapping(img_path)
if not color_to_class:
print(f"[跳过] {os.path.basename(img_path)} 无有效目标")
return
img = cv2.imread(img_path)
h, w = img.shape[:2]
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# --- 新增:创建新的 GT 掩码 (灰度图)0 为背景 ---
new_gt_mask = np.zeros((h, w), dtype=np.uint8)
lines = []
for rgb, old_class_id in color_to_class.items():
# --- 修改:获取 new_class_id 并填充 new_gt_mask ---
new_class_id = remap_class_dict.get(old_class_id, old_class_id)
# 创建布尔掩码
mask_bool = np.all(img_rgb == rgb, axis=-1)
# --- 新增:在 new_gt_mask 上填充新的 class_id
# (使用 new_class_id + 1因为 0 是背景)
new_gt_mask[mask_bool] = new_class_id + 1
# --- 修改:基于布尔掩码创建 255 掩码用于 findContours ---
mask_255 = mask_bool.astype(np.uint8) * 255
contours, _ = cv2.findContours(mask_255, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
if contour.shape[0] < 3:
continue
norm_pts = contour.squeeze(1).astype(np.float32)
norm_pts[:, 0] /= w
norm_pts[:, 1] /= h
flat = norm_pts.flatten()
# --- 修改:此处 new_class_id 已在循环外层定义 ---
line = f"{new_class_id} " + " ".join([f"{x:.6f}" for x in flat])
lines.append(line)
# --- 新增:保存处理后的 GT 掩码图像 ---
# 只要存在有效类别 (color_to_class 非空),就保存 GT 掩码
for save_path in gt_save_paths:
try:
# 使用 PIL 保存灰度 PNG 图像
Image.fromarray(new_gt_mask).save(save_path)
print(f"[✔] GT 掩码保存成功: {os.path.basename(save_path)}")
except Exception as e:
print(f"[✘] GT 掩码保存失败: {os.path.basename(save_path)} - {e}")
# 保存 .txt 标签文件 (仅当找到轮廓时)
if lines:
for save_path in txt_save_paths:
with open(save_path, 'w') as f:
for line in lines:
f.write(line + "\n")
print(f"[✔] 转换成功: {os.path.basename(save_path)},共 {len(lines)} 个实例")
else:
# 即使没有轮廓GT 掩码也已保存
print(f"[⚠] {os.path.basename(img_path)} 没有轮廓 (但 GT 掩码已保存)")
# 第一次遍历图像,仅提取颜色信息用于构建 class 映射
for fname in os.listdir(input_dir):
if fname.lower().endswith('.png'):
img_path = os.path.join(input_dir, fname)
extract_color_mapping(img_path)
# 构建 class_id 重映射表(按像素数从大到小排序)
class_pixel_count = {color_to_old_class[c]: count for c, count in color_class_counter.items()}
sorted_classes = sorted(class_pixel_count.items(), key=lambda x: x[1], reverse=True)
remap_class_dict = {old_cls: new_idx for new_idx, (old_cls, _) in enumerate(sorted_classes)}
# 第二次遍历图像,正式处理并生成标签
for fname in os.listdir(input_dir):
if fname.lower().endswith('.png'):
img_path = os.path.join(input_dir, fname)
base_name = os.path.splitext(fname)[0]
txt_path_1 = os.path.join(output_dir_1, base_name + '.txt')
txt_path_2 = os.path.join(output_dir_2, base_name + '.txt')
# --- 新增:定义 GT 掩码输出路径 (保存为 .png) ---
gt_path_1 = os.path.join(output_GT_dir_1, base_name + '.png')
gt_path_2 = os.path.join(output_GT_dir_2, base_name + '.png')
process_image(img_path, [txt_path_1, txt_path_2], [gt_path_1, gt_path_2])
# 打印颜色统计和类别映射
print("\n📊 所有图像颜色统计:")
def get_label_info(class_id):
"""辅助函数:根据 class_id 获取标签名和颜色"""
if 0 <= class_id < len(Annotate_CLASSES) and 0 <= class_id < len(Annotate_PALETTE):
return Annotate_CLASSES[class_id], Annotate_PALETTE[class_id]
else:
return "未知标签", (255, 255, 255) # 返回一个默认值
for color, count in global_color_counter.most_common():
if color == bg_PALETTE: # 使用 bg_PALETTE 变量
print(f"背景颜色 {color} 出现次数: {count}")
elif color[0] == color[1] == color[2]:
class_id = color[0] - 1
label_name, label_color = get_label_info(class_id)
print(f"颜色 {color} → class {class_id} (标签: '{label_name}', 颜色: {label_color}),出现次数: {count}")
else:
# 此情况理论上不应出现,因为 extract_color_mapping 已过滤
print(f"[⚠] 非灰阶颜色 {color},出现次数: {count} (此颜色不应被处理)")
# 打印详细的类别统计
print("\n✅ 有效类别统计(原 class_id → 新 class_id → 总像素数):")
# 按照 new_id (新类别索引) 排序输出
sorted_remap = sorted(remap_class_dict.items(), key=lambda item: item[1])
for old_id, new_id in sorted_remap:
label_name, label_color = get_label_info(old_id)
print(f"class {old_id} (标签: '{label_name}', 颜色: {label_color}) → {new_id}: {class_pixel_count[old_id]} pixels")
# 4. 额外输出新 class_id 到颜色的映射 ※※※
print(f"\n🎨 新 class_id 与标签颜色 (Annotate_PALETTE) 映射表:")
# 1. 创建一个 new_id -> color 的映射
new_id_to_color = {}
for old_id, new_id in remap_class_dict.items():
if 0 <= old_id < len(Annotate_PALETTE):
# 从 Annotate_PALETTE 获取原始颜色(它是一个元组)
color_tuple = Annotate_PALETTE[old_id]
# 转换为列表 [R, G, B] 以匹配您要的格式
new_id_to_color[new_id] = list(color_tuple)
else:
# 预防性代码,以防 old_id 超出范围
new_id_to_color[new_id] = [-1, -1, -1] # 表示错误/未找到
# 2. 按照 new_id (0, 1, 2...) 排序并按指定格式打印
sorted_new_ids = sorted(new_id_to_color.keys())
for new_id in sorted_new_ids:
color_list = new_id_to_color[new_id]
# 格式化输出: {id}: {color_list}
# (注意:颜色列表的格式会自然包含逗号和空格)
print(f" {new_id}: {color_list}")
print(f"\n✅ 全部图像处理完毕。")
print(f" 标签 (.txt) 输出目录: {output_dir_1}, {output_dir_2}")
print(f" GT 掩码 (.png) 输出目录: {output_GT_dir_1}, {output_GT_dir_2}")

View File

@@ -0,0 +1,20 @@
# # 胆囊标注
# Annotate_CLASSES = ('肝脏','胆囊','分离钳','止血海绵','肝总管','胆总管','吸引器','剪刀','止血纱布','生物夹','无损伤钳','喷洒','胆囊管','胆囊动脉','电凝','标本袋','引流管','纱布','金属钛夹','术中超声','吻合器','乳胶管','推结器','肝带','钳夹','超声刀','脂肪','双极电凝','棉球','血管阻断夹','肿瘤','针','线','韧带','胆囊静脉','背景') # 待分类的类
# Annotate_PALETTE = [[255,91,0],[255,234,0],[85, 111, 181],[181, 227, 14],[72, 0, 255],[0, 155, 33],[255,0,255],[29, 32, 136],[160, 15, 95],[0,160,233],[52,184,178],[90,120,41],[255,0,0],[117,0,0],[167,24,233],[112,113,150],[0,255,0],[255,255,255],[0,255,255],[138,251,213],[136,162,196],[197,83,181],[202,202,200],[113,102,140],[66,115,82],[240,16,116],[155,132,0],[155,62,0],[146,175,236],[255,172,159],[245,161,0],[134,124,118], [0,157,142], [181,85,105], [42,8,66],[0,0,0]] # 每一类的像素直
# # 甲状腺标注
# Annotate_CLASSES = ('甲状腺', '针', '双极电凝', '止血海绵', '止血纱布', '喷洒', '标本袋', '肝蒂', '神经', '线', '肝脏', '肝总管', '胆总管', '胆囊管', '引流管', '推结器', '肌肉', '肿瘤', '胆囊', '吸引器', '生物夹', '动脉', '纱布', '乳胶管', '血管阻断夹', '分离钳', '肌肉', '无损伤钳', '电凝', '金属钛夹', '吻合器', '棉球', '脂肪', '超声刀', '钳夹', '静脉', '韧带', '术中超声','背景') # 待分类的类
# Annotate_PALETTE = [[255, 148, 81], [134, 124, 118], [155, 62, 0], [181, 227, 14], [160, 15, 95], [90, 120, 41], [112, 113, 150], [133, 102, 140], [168, 162, 252], [0, 157, 142], [255, 91, 0], [72, 0, 255], [0, 155, 33], [255, 0, 0], [0, 255, 0], [202, 202, 200], [254, 141, 179], [254, 161, 0], [255, 234, 0], [255, 0, 255], [0, 160, 233], [117, 0, 0], [255, 255, 255], [197, 83, 181],[255, 172, 159], [85, 111, 181], [29, 32, 136], [52, 184, 178], [166, 24, 232], [0, 254, 254], [136, 162, 196], [146, 175, 236], [155, 132, 0], [240, 16, 116], [66, 115, 82], [42, 8, 66], [181, 85, 105], [138, 251, 213],[0,0,0]] # 每一类的像素直
# # 胃癌标注+去雾影像标注
Annotate_CLASSES = ('', '', '双极电凝', '止血海绵', '止血纱布', '喷洒', '标本袋', '肝蒂', '小肠', '线', '肝脏', '脾脏', '胆总管', '胆囊管', '引流管', '推结器', '淋巴结', '胰腺', '胆囊', '吸引器', '生物夹', '动脉', '纱布', '乳胶管', '分离钳', '超声刀', '无损伤钳', '电凝', '金属钛夹', '吻合器', '脂肪', '剪刀', '钳夹', '静脉', '韧带','背景') # 待分类的类
Annotate_PALETTE = [(237, 35, 85), (134, 124, 118), (155, 62, 0), (187, 227, 14), (160, 15, 95), (90, 120, 41), (112, 113, 150), (133, 102, 140), (110, 255, 166), (0, 157, 142), (255, 91, 0), (72, 0, 255), (0, 155, 33), (255, 0, 0), (0, 255, 0), (202, 202, 200), (201, 255, 74), (245, 161, 0), (255, 234, 0), (255, 0, 255), (0, 160, 233), (117, 0, 0), (255, 255, 255), (197, 83, 181), (85, 111, 181), (29, 32, 136), (52, 184, 178), (167, 24, 233), (0, 255, 255), (136, 162, 196), (155, 132, 0), (240, 16, 116), (66, 115, 82), (42, 8, 66), (181, 85, 105), [0,0,0]] # 每一类的像素直
# # 甲状腺标注
# Annotate_CLASSES = ('甲状旁腺', '喉返神经', '电凝', '无损伤钳', '超声刀', '分离钳', '纱布', '背景') # 待分类的类
# Annotate_PALETTE = [(238, 25, 30), (24, 124, 248), (198, 24, 248), (24, 248, 240), (248, 119, 24), (24, 248, 114), (255,255,255), [0,0,0]] # 每一类的像素值
# # 磁器械标注
# Annotate_CLASSES = ( '双极电凝', '止血海绵', '止血纱布', '喷洒', '标本袋', '肝脏', '肝总管', '胆总管', '胆囊管', '引流管', '胆囊', '磁器械', '生物夹', '胆囊动脉', '纱布', '分离钳', '剪刀', '无损伤钳','电凝', '金属钛夹', '脂肪', '背景') # 待分类的类
# Annotate_PALETTE = [ (155, 62, 0), (181, 227, 14), (160, 15, 95), (90, 120, 41), (112, 113, 150), (255, 91, 0), (72, 0, 255), (0, 155, 33), (255, 0, 0), (0, 255, 0), (255, 234, 0), (255, 0, 255), (0, 160, 233), (117, 0, 0), (255, 255, 255), (85, 111, 181), (29, 32, 136), (52, 184, 178), (167, 24, 233), (0, 255, 255), (155, 132, 0), [0,0,0]] # 每一类的像素值
# # 二分类标注
# Annotate_CLASSES = ( '特殊部位', '背景') # 待分类的类
# Annotate_PALETTE = [ [255,255,255], [0,0,0]] # 每一类的像素值
bg_PALETTE = [0,0,0] # 背景的RGB

View File

@@ -0,0 +1,167 @@
import os
import argparse
from PIL import Image
import concurrent.futures
# 确保使用的是 os.cpu_count() 来自动检测核心数
try:
DEFAULT_WORKERS = os.cpu_count() or 1
except AttributeError:
try:
import multiprocessing
DEFAULT_WORKERS = multiprocessing.cpu_count()
except (ImportError, NotImplementedError):
DEFAULT_WORKERS = 4
def _process_single_file(source_path, png_path, delete_source):
"""
工作函数处理单个文件的转换、缩放并保存为PNG。
(此函数与上一版完全相同)
"""
filename = os.path.basename(source_path)
png_filename = os.path.basename(png_path)
try:
with Image.open(source_path) as img:
width, height = img.size
min_side = min(width, height)
resize_info = ""
if min_side > 1080:
scale_factor = 1080 / min_side
new_width = int(width * scale_factor)
new_height = int(height * scale_factor)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
resize_info = f" (已缩放至 {new_width}x{new_height})"
img.save(png_path, 'PNG')
if delete_source:
os.remove(source_path)
return "success_del", filename, f"{png_filename}{resize_info}"
else:
return "success", filename, f"{png_filename}{resize_info}"
except Exception as e:
return "fail", filename, str(e)
def process_images(folder_path, delete_source=False, max_workers=DEFAULT_WORKERS):
"""
批量将指定文件夹 (及其所有子文件夹) 内的 .bmp, .jpg, .jpeg 图像
转换为 .png, 并按比例缩放 (最小边<=1080px)。
"""
if not os.path.isdir(folder_path):
print(f"错误:文件夹 '{folder_path}' 不存在或不是一个有效的目录。")
return
print(f"--- 开始递归处理文件夹: {folder_path} ---")
print(f"--- 使用最多 {max_workers} 个进程并行处理 ---")
if delete_source:
print("警告:已启用源文件删除模式。")
# --- 1. 收集所有需要处理的任务 ---
tasks = []
supported_extensions = ('.bmp', '.jpg', '.jpeg')
# <--- 更改:从 os.listdir() 切换到 os.walk() 以支持递归
print("--- 正在扫描所有子文件夹...")
for dirpath, dirnames, filenames in os.walk(folder_path):
for filename in filenames:
if filename.lower().endswith(supported_extensions):
# 构建完整的文件路径
source_path = os.path.join(dirpath, filename)
# 构建PNG输出路径 (保持在同一个子文件夹内)
base_name = os.path.splitext(filename)[0]
png_path = os.path.join(dirpath, base_name + '.png')
# 避免重复转换
if os.path.exists(png_path):
# 打印相对路径,使其更清晰
relative_path = os.path.relpath(png_path, folder_path)
print(f" [跳过] {relative_path} (目标文件已存在)")
continue
tasks.append((source_path, png_path, delete_source))
# <--- 更改结束 ---
if not tasks:
print(f"未在文件夹中找到新的 {supported_extensions} 文件。")
return
print(f"--- 发现 {len(tasks)} 个新图像文件,开始转换... ---")
converted_count = 0
failed_count = 0
# --- 2. 使用 ProcessPoolExecutor 执行任务 (此部分不变) ---
with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
future_to_source = {
executor.submit(_process_single_file, source, png, delete): source
for source, png, delete in tasks
}
# --- 3. 实时获取已完成的结果 (此部分不变) ---
for future in concurrent.futures.as_completed(future_to_source):
source_path_orig = future_to_source[future]
try:
status, name, result = future.result()
# 获取相对路径以便于阅读
relative_dir = os.path.relpath(os.path.dirname(source_path_orig), folder_path)
# 如果是根目录relative_dir 会是 ".",我们将其替换为空字符串
if relative_dir == ".":
log_name = name
else:
log_name = os.path.join(relative_dir, name)
if status == "success":
print(f" [成功] {log_name} -> {result}")
converted_count += 1
elif status == "success_del":
print(f" [成功] {log_name} -> {result} (并已删除源文件)")
converted_count += 1
elif status == "fail":
print(f" [失败] 转换 {log_name} 时出错: {result} (源文件未删除)")
failed_count += 1
except Exception as e:
print(f" [严重失败] 处理 {os.path.basename(source_path_orig)} 时进程出错: {e}")
failed_count += 1
print("--- 处理完毕 ---")
if converted_count > 0:
print(f"成功转换 {converted_count} 个文件。")
if failed_count > 0:
print(f"失败 {failed_count} 个文件。")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="将指定文件夹(及其所有子文件夹)中的所有 .bmp, .jpg, .jpeg 文件批量转换为 .png 文件 (并行加速),并按比例缩放 (最小边<=1080px)。"
)
parser.add_argument(
"folder",
type=str,
help="包含 .bmp, .jpg, .jpeg 图像的 (根) 目标文件夹路径 (例如 ./ABC)"
)
parser.add_argument(
"-d", "--delete-source",
action="store_true",
help="在成功转换为 .png 后,删除原始的 .bmp/.jpg/.jpeg 文件。"
)
parser.add_argument(
"-w", "--workers",
type=int,
default=DEFAULT_WORKERS,
help=f"指定用于转换的工作进程数 (默认: {DEFAULT_WORKERS}, 即本机CPU核心数)"
)
args = parser.parse_args()
process_images(args.folder, args.delete_source, args.workers)

View File

@@ -0,0 +1,180 @@
import cv2
import random
import numpy as np
from scipy.ndimage import label, distance_transform_edt
def skeletonize(image):
"""骨架化函数确保线条连通性并缩减为1像素宽"""
skeleton = np.zeros_like(image)
temp_image = np.copy(image)
while True:
eroded = cv2.erode(temp_image, None) # 腐蚀操作
temp_dilate = cv2.dilate(eroded, None) # 膨胀操作
temp = cv2.subtract(temp_image, temp_dilate) # 提取边缘
skeleton = cv2.bitwise_or(skeleton, temp) # 将边缘加入骨架
temp_image = np.copy(eroded)
if cv2.countNonZero(temp_image) == 0:
break
return skeleton
# 1. *** 边缘检测并膨胀 ***
def edge_detection(image):
"""对图像的各个通道进行边缘检测并进行膨胀处理"""
b_channel, g_channel, r_channel = cv2.split(image)
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
edges_b = cv2.Canny(b_channel, 100, 200)
edges_g = cv2.Canny(g_channel, 100, 200)
edges_r = cv2.Canny(r_channel, 100, 200)
edges_gray = cv2.Canny(gray_image, 100, 200)
# 合并所有边缘检测结果
edges = cv2.bitwise_or(edges_b, edges_g)
edges = cv2.bitwise_or(edges, edges_r)
edges = cv2.bitwise_or(edges, edges_gray)
# 创建膨胀核并进行膨胀操作
kernel = np.ones((3, 3), np.uint8)
dilated_image = cv2.dilate(edges, kernel, iterations=1)
return dilated_image
# 2. ** 检测连通区域 ***
def detect_connected_regions(dilated_image):
"""检测图像中的连通区域并过滤掉小区域"""
_, binary = cv2.threshold(dilated_image, 1, 255, cv2.THRESH_BINARY_INV)
binary[binary > 0] = 1 # 转换为二值图像
# 标记连通区域
structure = np.ones((3, 3), dtype=int)
labeled_array, num_features = label(binary, structure=structure)
# 清除掉小于100像素的区域
filtered_labeled_array = np.copy(labeled_array)
for label_num in range(1, num_features + 1):
area = np.sum(labeled_array == label_num)
if area < 100:
filtered_labeled_array[filtered_labeled_array == label_num] = 0
# 重新标记过滤后的连通区域
filtered_labeled_array, num_features = label(filtered_labeled_array, structure=structure)
return filtered_labeled_array, num_features # 返回过滤后的labeled_array和未过滤的labeled_array用于寻找颜色
# 3. *** 分水岭填充白色区域 ***
def fill_white_regions(filtered_labeled_array):
"""使用分水岭算法填充白色区域"""
# 准备三通道图像作为分水岭算法的输入
color_image = np.zeros((filtered_labeled_array.shape[0], filtered_labeled_array.shape[1], 3), dtype=np.uint8)
# 将过滤后的 labeled_array 转化为 32 位整型,作为分水岭的 markers
markers = np.copy(filtered_labeled_array).astype(np.int32)
markers[markers == 0] = -1 # 背景标记为 -1
# 执行分水岭算法
cv2.watershed(color_image, markers)
# 更新分水岭结果
filled_labeled_array = markers.astype(int)
filled_labeled_array[filled_labeled_array == -1] = 0
# 使用距离变换计算边缘像素0最近的非零值
non_zero_mask = filled_labeled_array != 0
distance, nearest_indices = distance_transform_edt(non_zero_mask == 0, return_indices=True)
nearest_values = filled_labeled_array[tuple(nearest_indices)]
filled_labeled_array[filled_labeled_array == 0] = nearest_values[filled_labeled_array == 0]
return filled_labeled_array
# 4. *** 对连通区域上色(使用“filtered_labeled_array”作为颜色判断给“filled_labeled_array”上色) ***
def color_connected_regions(filled_labeled_array, filtered_labeled_array, ori_labeled_image, Annotate_PALETTE):
"""根据原始图像的颜色和注解调色板给连通区域上色"""
# 初始化一个三通道的彩色图像
colored_image = np.zeros((*filled_labeled_array.shape, 3), dtype=np.uint8)
# 遍历filtered_labeled_array中的每个标签
unique_labels = np.unique(filtered_labeled_array)
for label_num in unique_labels:
if label_num == 0:
continue # 跳过背景标签
# 找到filtered_labeled_array中等于当前标签的区域
mask_filtered = (filtered_labeled_array == label_num)
# 获取ori_labeled_image中对应区域的RGB值
region_rgb_values = ori_labeled_image[mask_filtered]
if len(region_rgb_values) == 0:
continue
# 计算区域的RGB平均值
average_rgb = np.mean(region_rgb_values, axis=0)
# 找到Annotate_PALETTE中与average_rgb最接近的颜色
closest_palette_color = find_closest_palette_color(average_rgb, Annotate_PALETTE)
# 将该颜色赋给filled_labeled_array对应区域的元素
mask_filled = (filled_labeled_array == label_num)
colored_image[mask_filled] = closest_palette_color
return colored_image
# 寻找最近邻颜色
def find_closest_palette_color(average_rgb, Annotate_PALETTE):
"""根据平均RGB值找到Annotate_PALETTE中最接近的颜色"""
Annotate_PALETTE = [[color[2], color[1], color[0]] for color in Annotate_PALETTE]
average_rgb = np.array(average_rgb)
min_distance = float('inf')
closest_color = None
# 遍历调色板计算每个颜色与平均RGB的欧几里得距离
for palette_color in Annotate_PALETTE:
palette_color = np.array(palette_color)
distance = np.linalg.norm(average_rgb - palette_color) # 欧几里得距离
if distance < min_distance:
min_distance = distance
closest_color = palette_color
return closest_color
# 5. 对Array区域上色4的简化版
def Tool_color_connected_array(Array):
colored_image = np.zeros((*Array.shape, 3), dtype=np.uint8)
for label_num in range(1, np.max(Array) + 1):
color = [np.random.randint(0, 254) for _ in range(3)]
colored_image[Array == label_num] = color
return colored_image
if __name__ == '__main__':
"""超参数"""
image_path = './2023_02_03_09_13_48.00_08_04_21.Still085.png'
"""主函数,处理图像并保存结果"""
# 读取图像
image = cv2.imread(image_path)
# 1. 边缘检测并膨胀
dilated_image = edge_detection(image)
cv2.imwrite('./1_1_range_image.png', dilated_image)
# 2. 检测连通区域
filtered_labeled_array, _ = detect_connected_regions(dilated_image)
colored_image_filtered = Tool_color_connected_array(filtered_labeled_array)
cv2.imwrite('./2_colored_image_filtered.png', colored_image_filtered)
# 3. 分水岭填充白色区域
filled_labeled_array = fill_white_regions(filtered_labeled_array)
colored_image_filled = Tool_color_connected_array(filled_labeled_array)
cv2.imwrite('./3_colored_image_filled.png', colored_image_filled)
# 4. 对连通区域上色
ori_labeled_image = image
colored_image_final = color_connected_regions(filled_labeled_array, filtered_labeled_array, ori_labeled_image, Annotate_PALETTE)
cv2.imwrite('./4_color_image_Final.png', colored_image_final)
print("处理后的图片已保存。")

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import argparse
from PIL import Image
import concurrent.futures
import time
# --- 1. 复制自您脚本中的 CPU 核心数检测逻辑 ---
try:
DEFAULT_WORKERS = os.cpu_count() or 1
except AttributeError:
try:
import multiprocessing
DEFAULT_WORKERS = multiprocessing.cpu_count()
except (ImportError, NotImplementedError):
DEFAULT_WORKERS = 4
# --- 2. 定义支持的图片格式 ---
SUPPORTED_EXTENSIONS = ('.jpg', '.jpeg', '.png', '.bmp', '.webp', '.tiff', '.tif')
# --- 3. 确定Pillow的重采样过滤器 ---
try:
RESAMPLE_FILTER = Image.Resampling.LANCZOS
except AttributeError:
RESAMPLE_FILTER = Image.LANCZOS
def _process_single_image(image_path, target_min_side):
"""
工作函数:处理单个文件的缩放。(此函数与上一版完全相同)
:param image_path: 完整图片路径
:param target_min_side: 目标最小边长 (例如 1080)
:return: (状态, 原始文件名, 结果/错误信息)
"""
filename = os.path.basename(image_path)
try:
with Image.open(image_path) as img:
img_info = img.info.copy()
width, height = img.size
min_side = min(width, height)
# --- 核心逻辑:检查是否需要缩放 ---
if min_side <= target_min_side:
# 返回相对路径以便于阅读
return "skipped", image_path, f"最小边 ({min_side}px) 已 <= {target_min_side}px"
# --- 计算新尺寸 ---
scale_ratio = target_min_side / min_side
new_width = int(width * scale_ratio)
new_height = int(height * scale_ratio)
# --- 执行缩放 ---
resized_img = img.resize((new_width, new_height), RESAMPLE_FILTER)
# --- 覆盖保存 ---
resized_img.save(image_path, **img_info)
# 返回相对路径以便于阅读
return "success", image_path, f"{width}x{height} -> {new_width}x{new_height}"
except Exception as e:
# 返回相对路径以便于阅读
return "fail", image_path, str(e)
def resize_images_in_folder(folder_path, target_min_side=1080, max_workers=DEFAULT_WORKERS):
"""
*** [已更新] ***
批量将文件夹及其所有子文件夹中最小边 > target_min_side 的图片等比例缩小。
:param folder_path: 目标根文件夹路径
:param target_min_side: 目标最小边长
:param max_workers: 使用的进程数
"""
if not os.path.isdir(folder_path):
print(f"错误:文件夹 '{folder_path}' 不存在或不是一个有效的目录。")
return
print(f"--- 开始 **递归** 处理文件夹: {folder_path} ---")
print(f"--- 目标最小边: {target_min_side}px ---")
print(f"--- 使用最多 {max_workers} 个进程并行处理 ---")
start_time = time.time()
# --- 1. 收集所有需要处理的任务 (*** [核心修改] ***) ---
tasks = []
print("--- 正在递归扫描所有子文件夹... ---")
# 使用 os.walk 递归遍历
for dirpath, dirnames, filenames in os.walk(folder_path):
for filename in filenames:
# 检查文件扩展名是否在支持的列表中
if filename.lower().endswith(SUPPORTED_EXTENSIONS):
# 构造完整的文件路径
image_path = os.path.join(dirpath, filename)
tasks.append((image_path, target_min_side))
if not tasks:
print(f"未在 {folder_path} 及其子文件夹中找到支持的图片文件。")
return
print(f"--- 发现 {len(tasks)} 个图片文件,开始处理... ---")
resized_count = 0
skipped_count = 0
failed_count = 0
# 转换为相对路径,使输出更简洁
base_folder_path = os.path.abspath(folder_path)
# --- 2. 使用 ProcessPoolExecutor 执行任务 (与上一版相同) ---
with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
future_to_path = {
executor.submit(_process_single_image, path, size): path
for path, size in tasks
}
# --- 3. 实时获取已完成的结果 (与上一版相同) ---
for future in concurrent.futures.as_completed(future_to_path):
try:
# status, path_or_name, result
status, image_path, result = future.result()
# 转换为相对路径
try:
display_path = os.path.relpath(image_path, base_folder_path)
except ValueError:
display_path = image_path # 如果不在同一驱动器(Windows),则显示完整路径
if status == "success":
print(f" [成功] {display_path}: {result}")
resized_count += 1
elif status == "skipped":
print(f" [跳过] {display_path}: {result}")
skipped_count += 1
elif status == "fail":
print(f" [失败] {display_path}: {result}")
failed_count += 1
except Exception as e:
orig_path = future_to_path[future]
print(f" [严重失败] 处理 {orig_path} 时进程出错: {e}")
failed_count += 1
end_time = time.time()
print("--- 处理完毕 ---")
if resized_count > 0:
print(f"成功缩放 {resized_count} 个文件。")
if skipped_count > 0:
print(f"跳过 {skipped_count} 个文件 (无需缩放)。")
if failed_count > 0:
print(f"失败 {failed_count} 个文件。")
print(f"总耗时: {end_time - start_time:.2f} 秒。")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="*** [已更新] *** 批量 **递归** 缩放文件夹及其子文件夹中的图片,使最小边不超过指定值。"
)
# 位置参数:文件夹路径 (必需)
parser.add_argument(
"folder",
type=str,
help="包含图片的 **根** 文件夹路径 (例如 ./MyImages),将递归处理所有子文件夹。"
)
# 选项参数:目标尺寸 (可选)
parser.add_argument(
"-s", "--size",
type=int,
default=1080,
help="指定目标最小边长 (默认: 1080)。如果图片最小边已小于此值,则跳过。"
)
# 选项参数:控制进程数 (可选)
parser.add_argument(
"-w", "--workers",
type=int,
default=DEFAULT_WORKERS,
help=f"指定用于转换的工作进程数 (默认: {DEFAULT_WORKERS}, 即本机CPU核心数)"
)
args = parser.parse_args()
# 将文件夹路径、"目标尺寸" 和 "工作进程数" 传递给函数
resize_images_in_folder(args.folder, args.size, args.workers)

View File

@@ -0,0 +1,69 @@
#### 0. 准备工作 ####
# 清理旧文件
rm -r ./Label/* ./ORI/*
rm -r ./result_stack_*透明度 ./Data
rm -r ./ORI_GT_label_fold ./ORI_pro_label_fold ./__pycache__
conda activate SMP
# A. 图像移动
cp 磁辅助分割图像-有效图/* Label/ # TODO TODO 将标注后图片放入Label中 # 保证Label中图片与ORI中图片一一对应且命名相同
cp 磁辅助分割-原图/* ORI/ # TODO TODO 将原始图片放入ORI中
# B.1 修改类别颜色
vim Tool_Classes_And_Palette.py # 修改 Annotate_CLASSES、Annotate_PALETTE 以匹配标注时的类别与颜色
# B.2 将bmp、jpg图片转为png
python Tool_convert_bmp_jpg_to_png.py ./Label --delete-source
python Tool_convert_bmp_jpg_to_png.py ./ORI --delete-source
# B.3 将图片转为最大边限制为1080
python Tool_resize_pics.py ./Label # -s 1080
python Tool_resize_pics.py ./ORI # -s 1080
# C. 检测图片是否匹配
python 0_1_check_picture_pair.py # -i "./ORI" -l "./Label" -p "" -s ""
# python 0_1_check_picture_pair.py # -i "../../DataSet_Public/6_CWK_2_cfz/images/train" -l "../../DataSet_Public/6_CWK_2_cfz/labels_GT/train" -p "" -s ""
# D. 生成堆叠图片(可视化标签效果)
bash 0_2_TOOL_stack_pics.sh -i "./ORI" -l ./Label -r ./result_stack_0.3透明度 -a 0.3 -p "" -s "_label"
# E. ※下载图片,查看匹配的是否有问题※
#### 1. 批量化生成训练、测试集图片 ####
# A. 将图片转为GT图片
python 1_deal_labels.py -src_fold ./Label
# B. 新建数据最终存储文件夹
rm -r ./Data
mkdir -p Data/images/train Data/images/val
cp ORI/* Data/images/train/
cp ORI/* Data/images/val/ # 或根据需要分配训练集、验证集
mkdir -p Data/labels/train Data/labels/val
mkdir -p Data/labels_GT/train Data/labels_GT/val
# C. 生成labels 、 labels_GT图片到 Data/labels_GT/train 中
# Way 1※推荐※将类别压缩
python 2_Check_and_Gen_Txt_Label_sort_label.py
# Way 2使用原始类别
# python 2_Check_and_Gen_Txt_Label_ori_label.py # Way 2
# cp ORI_GT_label_fold/* Data/labels_GT/train/ # Way 2
# cp ORI_GT_label_fold/* Data/labels_GT/val/ # Way 2
#### 2. 纳入到Yolo训练体系 ####
# A. 移动数据
cp -r ./Data ../../DataSet_Public/8_Haze_Baidu_Plus # TODO TODO 将 Data 文件夹改名后 放入 ../../DataSet_Public 下
# B. 设置输出颜色参考
将 程序输出的信息存入 ../../Seg_All_In_One_YoloModel/dataset.yaml 的 color中作为最终输出颜色的参考
# C. 根据 Seg_All_In_One_YoloModel 中的使用手册新增数据集、进行配置、训练
修改 dataset.yaml 中 训练数据集、修改 yolo_config.py 中 EPOCHS、PATIENCE
############################## 进行训练推理(整体流程) ############################################
# 1. 批量化训练
conda activate SMP
cd ~/Desktop/Seg/Seg_All_In_One_YoloModel
bash yolo_train.sh
# 2. 复制最优模型到预测文件夹
bash ./Tool_Yolo_Copy_Best_Model.sh --pt_name "best.pt" && bash ./Tool_Yolo_Copy_Best_Model.sh --pt_name "epoch100.pt" && bash ./Tool_Yolo_Copy_Best_Model.sh --pt_name "epoch50.pt" && bash ./Tool_Yolo_Copy_Best_Model.sh --pt_name "epoch150.pt"
# 3. 批量化预测+热度图可视化
bash yolo_predict.sh --conf 0.2 --pt_name "epoch100.pt" && bash yolo_predict.sh --conf 0.2 && bash yolo_predict.sh --conf 0.2 --pt_name "epoch50.pt" && bash yolo_predict.sh --conf 0.2 --pt_name "epoch150.pt"
bash ./yolo_predict.sh --pt_name "best.pt" --heatmap_method "All" # && bash ./yolo_predict.sh --pt_name "epoch100.pt" --heatmap_method "All"
# 4. 横向对比结果
python yolo_predict_V2_compare_all.py
# 5. 打包预测结果(不包含*.pt模型文件)
cd /home/wkmgc/Desktop/Seg/BestMode_Predict_Results_DataSet_Public/
zip -r /home/wkmgc/Desktop/8_Haze_Baidu_Plus-Yolo.zip 8_Haze_Baidu_Plus*-Yolo -x "*.pt" # TODO TODO
# 6. 打包训练结果(只有.png、.jpg、.csv文件)
cd /home/wkmgc/Desktop/Seg/Hardisk/
zip -r /home/wkmgc/Desktop/8_Haze_Baidu_Plus-Yolo_train.zip 8_Haze_Baidu_Plus-Yolo -i \*.png \*.jpg \*.csv # TODO TODO