File size: 17,465 Bytes
3c3b0ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
import os
import json
import shutil
import random
from glob import glob
from sklearn.model_selection import train_test_split
from tqdm import tqdm  # For progress bars

# --- 1. 配置参数 ---
data_root = r'D:\不会编程\CVPR\class_project\Data\data'  # 包含501个文件夹的根目录,原始数据文件路径
char_dict_path = r'D:\不会编程\CVPR\class_project\char_dict.json'# 字符映射文件
output_dir = r'D:\不会编程\CVPR\class_project\yolo_hanzi_dataset'  # 输出 YOLO 格式数据集的目录名

# 划分比例
val_size = 0.15  # 验证集比例
test_size = 0.15  # 测试集比例 (相对于原始总数)
# train_size = 1.0 - val_size - test_size

# 边界框假设 (覆盖整个图像)
# <class_index> <x_center_norm> <y_center_norm> <width_norm> <height_norm>
bbox_annotation = "0.5 0.5 1.0 1.0" # x_center, y_center, width, height (归一化)

# 随机种子,确保结果可复现
random_seed = 42
random.seed(random_seed)

# --- 2. 加载字符映射 ---
print("加载 Char_dict.json...")
try:
    with open(char_dict_path, 'r', encoding='utf-8') as f: # 尝试UTF-8
        char_dict_raw = json.load(f)
except UnicodeDecodeError:
    try:
        with open(char_dict_path, 'r', encoding='ansi') as f: # 如果UTF-8失败,尝试ANSI (GBK/CP936等)
            char_dict_raw = json.load(f)
            print("警告: Char_dict.json 使用 ANSI 编码读取,建议转换为 UTF-8。")
    except Exception as e:
        print(f"错误: 无法读取 Char_dict.json。请确保文件存在且编码正确 (UTF-8 或 ANSI)。错误信息: {e}")
        exit()
except FileNotFoundError:
    print(f"错误: 找不到 Char_dict.json 文件,路径: {char_dict_path}")
    exit()
except json.JSONDecodeError as e:
    print(f"错误: Char_dict.json 文件格式无效。错误信息: {e}")
    exit()





print(f"从 JSON 文件加载了 {len(char_dict_raw)} 个原始映射条目。")

# --- 扫描数据文件夹,获取实际存在的类别键 ---
print(f"扫描数据目录 '{data_root}' 以确定实际存在的类别...")
actual_unicode_keys = set()
try:
    data_folders = [d for d in os.listdir(data_root) if os.path.isdir(os.path.join(data_root, d))]
    print(f"找到 {len(data_folders)} 个文件夹。")#获取实际存在的类别
    for folder_name in data_folders:
        try:
            # 尝试将文件夹名 (如 "00699") 转为整数再转为字符串 (如 "699")
            key = str(int(folder_name))
            if key in char_dict_raw: # 检查这个键是否在原始 JSON 映射中存在
                actual_unicode_keys.add(key)
        except ValueError:
            # 忽略非数字命名的文件夹
            pass
except FileNotFoundError:
     print(f"错误:无法访问指定的 data_root 目录: {data_root}。")
     exit()
except Exception as e:
    print(f"扫描数据目录时发生错误: {e}")
    exit()

if not actual_unicode_keys:
    print(f"错误:在数据目录 '{data_root}' 中没有找到任何与 JSON 文件中的键匹配的文件夹。")
    exit()

print(f"根据文件夹名称,确定了 {len(actual_unicode_keys)} 个实际存在的有效类别键。")

# ---基于实际存在的键来创建映射 ---
# 对实际存在的键进行排序,以确保 class_id 分配顺序固定
sorted_actual_keys = sorted(list(actual_unicode_keys), key=lambda x: int(x)) # 按数字大小排序

unicode_to_classid = {}#
classid_to_char = {}#实际存在的类别数量 
num_classes = 0#类别的数量

print("正在创建过滤后的类别映射...")
for i, unicode_key in enumerate(sorted_actual_keys):
    if unicode_key in char_dict_raw:
        # 获取原始字符,并清理末尾的 \u0000 (空字符)
        original_char = char_dict_raw[unicode_key]
        cleaned_char = original_char.replace('\u0000', '') # 移除空字符

        # 跳过空的类别名称 (来自 key "0": "")
        if not cleaned_char:
            print(f"警告:键 '{unicode_key}' 对应的字符为空,将跳过此类别。")
            continue

        class_id = num_classes # 分配从 0 开始的连续 ID
        unicode_to_classid[unicode_key] = class_id
        classid_to_char[class_id] = cleaned_char
        num_classes += 1 # 只有成功添加了才增加类别计数
    else:
        #理论上这里不应该发生,因为 actual_unicode_keys 已经是过滤过的
        print(f"内部错误:键 '{unicode_key}' 未在 char_dict_raw 中找到,尽管它在 actual_unicode_keys 中。")

if num_classes == 0:
    print("错误:过滤后没有有效的类别。请检查文件夹名称、JSON内容和脚本逻辑。")
    exit()

print(f"最终确定使用 {num_classes} 个有效类别进行处理。")

print("有效类别映射 (部分示例):")
# 打印前几个和后几个有效类别,检查是否正确
example_keys = list(unicode_to_classid.keys())
for i in range(min(5, num_classes)):
    key = example_keys[i]
    class_id = unicode_to_classid[key]
    char = classid_to_char[class_id]
    print(f"  Class ID: {class_id}, Key: {key}, Character: '{char}'")
if num_classes > 10:
    print("  ...")
    for i in range(max(5, num_classes - 5), num_classes):
        key = example_keys[i]
        class_id = unicode_to_classid[key]
        char = classid_to_char[class_id]
        print(f"  Class ID: {class_id}, Key: {key}, Character: '{char}'")

# --- 第 3 步扫描图像文件等)应该基于过滤后的 unicode_to_classid 工作 ---
# 



# --- 3. 发现所有图像文件 ---
print("扫描图像文件...")
# all_image_paths: 存储所有找到的图像文件路径
# all_labels: 存储每个图像对应的类别ID (与all_image_paths一一对应)
all_image_paths = []
all_labels = []

# 支持常见的图像格式 (用于glob模式匹配)
image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.gif']

# --- 3.1 扫描数据目录 ---
print(f"正在扫描根目录: {data_root}")
# found_folders: 统计找到的文件夹总数
# processed_folders: 统计成功处理(有对应类别)的文件夹数
found_folders = 0
processed_folders = 0

# 显式列出data_root下的内容,帮助调试和验证
# 注意: 这里会列出所有文件和文件夹,但只有符合数字命名规则的文件夹会被处理
try:
    root_contents = os.listdir(data_root)
    print(f"在 {data_root} 中找到 {len(root_contents)} 个项目 (文件/文件夹)")
    # print(f"前10个项目: {root_contents[:10]}") # 如果需要可以取消注释看具体名称
except FileNotFoundError:
    print(f"错误:无法访问指定的 data_root 目录: {data_root}。请确保路径正确且程序有权限访问。")
    exit()
except Exception as e:
    print(f"访问目录 {data_root} 时发生未知错误: {e}")
    exit()


# 遍历根目录下的所有项目(文件/文件夹)
# 使用tqdm显示进度条,desc参数设置进度条描述文本
for item_name in tqdm(root_contents, desc="扫描项目"):
    item_path = os.path.join(data_root, item_name)

    if os.path.isdir(item_path):
        found_folders += 1
        # 文件夹名称,例如 "00699"
        unicode_index_folder_padded = item_name

        try:
            # 尝试将带前导零的文件夹名称转换为整数,再转回普通字符串(去除前导零)
            # 例如 "00699" -> 699 -> "699"
            unicode_index_int = int(unicode_index_folder_padded)
            unicode_index_key = str(unicode_index_int)

            # 使用转换后的 key (如 "699") 在字典中查找
            if unicode_index_key in unicode_to_classid:
                processed_folders += 1
                class_id = unicode_to_classid[unicode_index_key]
                image_count_in_folder = 0
                for ext in image_extensions:
                    # 使用 glob 查找当前文件夹下所有匹配扩展名的图片
                    # 注意这里使用 item_path
                    folder_images = glob(os.path.join(item_path, ext))
                    for img_path in folder_images:
                        all_image_paths.append(img_path)
                        all_labels.append(class_id)
                        image_count_in_folder += 1


            else:
                # 如果转换后的 key 仍然找不到,显示警告 (只显示前几个)
                if processed_folders < 5 and found_folders <= 20: # 控制警告数量
                     print(f"警告: 从文件夹名称 '{unicode_index_folder_padded}' 导出的键 '{unicode_index_key}' 在 Char_dict.json 中没有对应的条目,将跳过此文件夹。")

        except ValueError:
            # 如果文件夹名称不是纯数字 (例如可能是 .git, Thumbs.db 或其他非数据文件夹),则忽略
            if found_folders <= 10: # 只显示前几个非数字文件夹警告
                print(f"信息: 跳过非预期格式的文件夹/文件: '{unicode_index_folder_padded}'")
        except Exception as e:
             print(f"处理文件夹 {item_name} 时发生错误: {e}")



print(f"总共扫描了 {found_folders} 个文件夹。")
print(f"其中 {processed_folders} 个文件夹的名称与 Char_dict.json 中的键成功匹配。")


if not all_image_paths:
    print(f"错误: 在目录 '{data_root}' 下及其与 JSON 匹配的子文件夹中,没有找到任何支持的图像文件。")
    print(f"请再次检查:")
    print(f"1. 路径 '{data_root}' 是否正确?")
    print(f"2. 匹配的文件夹 (如 {processed_folders} 个) 中是否真的包含 {', '.join(image_extensions)} 格式的图片文件?")
    print(f"3. 文件权限是否允许读取?")
    exit()

print(f"总共找到 {len(all_image_paths)} 个图像文件。")








# --- 4. 划分数据集 ---
print("划分数据集 (Train/Validation/Test)...")
# 使用sklearn的train_test_split进行分层抽样划分,确保每个集合中的类别分布均衡
# 先分出测试集,再从剩余数据中分出验证集
# 确保标签列表是整数类型,并且图像和标签列表长度一致
assert len(all_image_paths) == len(all_labels)
labels_int = [int(label) for label in all_labels] # 确保是整数

# 第一次划分: 先分出测试集
# test_size: 测试集占总数据的比例
# stratify: 按标签分层抽样,保持类别分布
train_val_paths, test_paths, train_val_labels, test_labels = train_test_split(
    all_image_paths,
    labels_int,
    test_size=test_size,
    random_state=random_seed,
    stratify=labels_int # 确保测试集中类别分布与总体相似
)

# 第二次划分: 从剩余数据(train_val)中分出验证集
# 计算验证集在剩余数据中的相对比例(relative_val_size)
# 例如: 总数据1000,test_size=0.15 → 测试集150,剩余850
# val_size=0.15 → 验证集应该是150/850 ≈ 0.1765
relative_val_size = val_size / (1.0 - test_size)

train_paths, val_paths, train_labels, val_labels = train_test_split(
    train_val_paths,
    train_val_labels,
    test_size=relative_val_size,
    random_state=random_seed, # 使用相同的随机种子确保可复现性(如果需要拆分独立)
    stratify=train_val_labels # 确保验证集中类别分布与 (train+val) 相似
)

print(f"划分结果:")
print(f"  训练集: {len(train_paths)} 张图片")
print(f"  验证集: {len(val_paths)} 张图片")
print(f"  测试集: {len(test_paths)} 张图片")

# --- 5. 创建 YOLO 目录结构并处理文件 ---
print(f"创建 YOLO 格式数据集到 '{output_dir}'...")

# 定义数据集划分后的路径和标签
# 使用字典存储三个数据集(train/val/test)的路径和对应标签
sets = {
    'train': (train_paths, train_labels),
    'val': (val_paths, val_labels),
    'test': (test_paths, test_labels)
}

if os.path.exists(output_dir):
    print(f"警告: 输出目录 '{output_dir}' 已存在,将清空并重新创建。")
    shutil.rmtree(output_dir)

# 创建目录结构
os.makedirs(os.path.join(output_dir, 'images', 'train'), exist_ok=True)
os.makedirs(os.path.join(output_dir, 'labels', 'train'), exist_ok=True)
os.makedirs(os.path.join(output_dir, 'images', 'val'), exist_ok=True)
os.makedirs(os.path.join(output_dir, 'labels', 'val'), exist_ok=True)
os.makedirs(os.path.join(output_dir, 'images', 'test'), exist_ok=True)
os.makedirs(os.path.join(output_dir, 'labels', 'test'), exist_ok=True)

# --- 5.1 处理文件:复制图像并创建标注文件 ---
# 对每个数据集(train/val/test)分别处理:
# 1. 复制图像文件到目标目录
# 2. 创建对应的YOLO格式标注文件(.txt)
# 3. 使用唯一文件名避免冲突(父文件夹名+原文件名)
for set_name, (paths, labels) in sets.items():
    print(f"处理 {set_name} 集...")
    image_dir = os.path.join(output_dir, 'images', set_name)
    label_dir = os.path.join(output_dir, 'labels', set_name)

    for img_path, class_id in tqdm(zip(paths, labels), total=len(paths), desc=f"  拷贝和生成标注 ({set_name})"):
        # 文件名处理:
        # 1. 获取原始文件名(带扩展名)
        base_filename = os.path.basename(img_path)
        # 2. 获取文件名(不带扩展名)
        filename_no_ext = os.path.splitext(base_filename)[0]

        # --- 生成唯一文件名 ---
        # 由于不同文件夹下的图片可能有相同的文件名(如都叫 001.jpg),
        # 需要确保目标目录中的文件名唯一。
        # 解决方案: 使用"父文件夹名_原文件名"作为新文件名
        # 例如: "00699_001.jpg" 和 "00700_001.jpg" 可以共存
        parent_folder_name = os.path.basename(os.path.dirname(img_path))
        unique_filename_no_ext = f"{parent_folder_name}_{filename_no_ext}"
        unique_base_filename = f"{unique_filename_no_ext}{os.path.splitext(base_filename)[1]}"

        # 目标图像路径
        dest_img_path = os.path.join(image_dir, unique_base_filename)
        # 目标标注文件路径
        dest_label_path = os.path.join(label_dir, f"{unique_filename_no_ext}.txt")

        # 1. 复制图像文件
        try:
             shutil.copy2(img_path, dest_img_path) # copy2 保留元数据
        except Exception as e:
            print(f"\n错误: 无法复制文件 {img_path}{dest_img_path}. 错误: {e}")
            continue # 跳过这个文件

        # 2. 创建并写入标注文件 (使用假设的边界框)
        annotation_line = f"{class_id} {bbox_annotation}\n"
        try:
            with open(dest_label_path, 'w', encoding='utf-8') as f_label:
                f_label.write(annotation_line)
        except Exception as e:
            print(f"\n错误: 无法写入标注文件 {dest_label_path}. 错误: {e}")
            # 如果写入失败,最好也把对应的图片删除,避免数据不一致
            if os.path.exists(dest_img_path):
                os.remove(dest_img_path)

# --- 6. 生成 dataset.yaml 文件 ---
# YOLO训练需要此配置文件,包含:
# - 数据集路径
# - 训练/验证/测试集路径
# - 类别数量和名称列表
print("生成 dataset.yaml...")

# 获取排序后的类别名称列表
class_names = [classid_to_char[i] for i in range(num_classes)]

# 创建 YAML 内容
# 路径可以是相对路径(相对于你运行训练脚本的位置)或绝对路径
# 这里使用相对路径,假设会在 yolo_hanzi_dataset 的上一级目录运行训练
yaml_content = f"""

# YOLOv11 dataset configuration file



# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]

path: {os.path.abspath(output_dir)}  # dataset root dir (绝对路径通常更可靠)

train: images/train  # train images (relative to 'path')

val: images/val  # val images (relative to 'path')

test: images/test # test images (optional)



# Classes

nc: {num_classes}  # number of classes

names: {json.dumps(class_names, ensure_ascii=False)} # Use json.dumps for proper list formatting and handling non-ASCII characters



""" # ensure_ascii=False 很重要,用于正确显示汉字

yaml_path = os.path.join(output_dir, 'dataset.yaml')
try:
    with open(yaml_path, 'w', encoding='utf-8') as f_yaml:
        f_yaml.write(yaml_content)
except Exception as e:
    print(f"错误: 无法写入 dataset.yaml 文件到 {yaml_path}. 错误: {e}")

print("-" * 30)
print("数据集准备完成!")
print(f"YOLO 格式的数据集已生成在: {os.path.abspath(output_dir)}")
print(f"配置文件为: {os.path.abspath(yaml_path)}")
print("-" * 30)
print("重要提示:")
print("1. 每个图像只包含一个居中的汉字,并为其生成了覆盖整个图像的边界框。")
print("2. 检查 `dataset.yaml` 文件中的路径是否正确,特别是 `path` 字段。根据你的训练环境可能需要调整。")
print("3. 检查生成的 `labels` 文件夹中的 .txt 文件内容是否符合预期格式。")
print("4. 在开始训练前,建议随机抽查几个图片及其对应的标注文件,确保它们正确对应。")
print("5. 确保 `Char_dict.json` 中的汉字编码与你的系统或训练环境兼容。YAML 文件已使用 UTF-8 编码保存。")
print("-" * 30)