统计训练图片数量脚本中,增加导出excel功能,便于在餐厅看各个品类的数量

This commit is contained in:
2026-03-04 16:42:19 +08:00
parent dedf72f53c
commit 1ff540045d
2 changed files with 256 additions and 97 deletions
+5 -1
View File
@@ -1151,7 +1151,11 @@ class EmbeddingFoodClassifierApp:
# 提取查询图片的特征向量 # 提取查询图片的特征向量
query_embedding = self.model.extract_embedding(pil_image, normalize=True) query_embedding = self.model.extract_embedding(pil_image, normalize=True)
# print('特征向量:', query_embedding) # 打印前50位向量,用于与秤端结果对比
# print("=" * 60)
# print(f"[PC端] 模型输出向量(前50位,共{len(query_embedding)}维):")
# print(", ".join(f"{v:.6f}" for v in query_embedding[:50]))
# print("=" * 60)
query_embedding = query_embedding.reshape(1, -1).astype(np.float32) query_embedding = query_embedding.reshape(1, -1).astype(np.float32)
# 在FAISS索引中搜索最相似的k张图片 # 在FAISS索引中搜索最相似的k张图片
+219 -64
View File
@@ -4,15 +4,180 @@
import os import os
from pathlib import Path from pathlib import Path
from collections import defaultdict from collections import defaultdict
from datetime import datetime
def count_images_by_class(dataset_root: str, output_file: str = None): def export_to_excel(sorted_classes, class_counts_detail, class_counts,
total_images, total_train, total_val, total_test,
min_threshold, dataset_root: Path, output_dir: Path = None):
"""
将统计结果导出为 Excel 文件(带格式,方便粘贴到飞书)
Args:
sorted_classes: 按数量排序的类别列表
class_counts_detail: 每类的详细统计
class_counts: 每类的总数
total_images/train/val/test: 各分区汇总
min_threshold: 样本不足阈值
dataset_root: 数据集根目录(用于生成文件名)
output_dir: Excel 输出目录(默认和脚本同目录)
"""
try:
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
except ImportError:
print("❌ 未找到 openpyxl,请运行: pip install openpyxl")
return
wb = Workbook()
ws = wb.active
ws.title = "图片数量统计"
# ── 颜色 & 样式定义 ──────────────────────────────────────────
COLOR_HEADER_BG = "2563EB" # 深蓝色表头背景
COLOR_HEADER_FONT = "FFFFFF" # 白色表头字
COLOR_WARN_BG = "FEF08A" # 淡黄色:样本不足行
COLOR_SUMMARY_BG = "F1F5F9" # 浅灰:汇总行
COLOR_TOTAL_BG = "DBEAFE" # 浅蓝:总计行
def make_font(bold=False, color="000000", size=11):
return Font(bold=bold, color=color, size=size, name="微软雅黑")
def make_fill(hex_color):
return PatternFill("solid", fgColor=hex_color)
def make_border():
thin = Side(style="thin", color="CBD5E1")
return Border(left=thin, right=thin, top=thin, bottom=thin)
center = Alignment(horizontal="center", vertical="center")
left = Alignment(horizontal="left", vertical="center")
# ── 第1行:大标题 ────────────────────────────────────────────
ws.merge_cells("A1:F1")
title_cell = ws["A1"]
title_cell.value = f"📊 图片数量统计 — {dataset_root.name}"
title_cell.font = make_font(bold=True, color="1E3A5F", size=14)
title_cell.alignment = center
title_cell.fill = make_fill("EFF6FF")
ws.row_dimensions[1].height = 32
# ── 第2行:生成时间 ──────────────────────────────────────────
ws.merge_cells("A2:F2")
time_cell = ws["A2"]
time_cell.value = f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 数据集: {dataset_root}"
time_cell.font = make_font(color="64748B", size=9)
time_cell.alignment = center
time_cell.fill = make_fill("EFF6FF")
ws.row_dimensions[2].height = 18
# ── 第3行:空行分隔 ──────────────────────────────────────────
ws.row_dimensions[3].height = 8
# ── 第4行:汇总信息 ──────────────────────────────────────────
summary_labels = [
("总类别数", len(class_counts)),
("总图片数", total_images),
("训练集", total_train),
("验证集", total_val),
("测试集", total_test),
("平均每类", total_images // len(class_counts) if len(class_counts) > 0 else 0),
]
insufficient_count = sum(1 for _, cnt in sorted_classes if cnt < min_threshold)
# 汇总用两行:标签行 + 数值行
for col_idx, (label, _) in enumerate(summary_labels, start=1):
cell = ws.cell(row=4, column=col_idx, value=label)
cell.font = make_font(bold=True, color="475569", size=10)
cell.alignment = center
cell.fill = make_fill(COLOR_SUMMARY_BG)
cell.border = make_border()
ws.row_dimensions[4].height = 20
for col_idx, (_, value) in enumerate(summary_labels, start=1):
cell = ws.cell(row=5, column=col_idx, value=value)
cell.font = make_font(bold=True, size=12)
cell.alignment = center
cell.fill = make_fill(COLOR_TOTAL_BG)
cell.border = make_border()
ws.row_dimensions[5].height = 22
# ── 第6行:空行 ───────────────────────────────────────────────
ws.row_dimensions[6].height = 8
# ── 第7行:列表头 ────────────────────────────────────────────
headers = ["类别名称", "总计", "训练集", "验证集", "测试集", "状态"]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=7, column=col_idx, value=header)
cell.font = make_font(bold=True, color=COLOR_HEADER_FONT, size=11)
cell.fill = make_fill(COLOR_HEADER_BG)
cell.alignment = center
cell.border = make_border()
ws.row_dimensions[7].height = 24
# ── 数据行(从第8行开始)────────────────────────────────────
for row_offset, (class_name, count) in enumerate(sorted_classes):
row = 8 + row_offset
detail = class_counts_detail[class_name]
is_warn = count < min_threshold
status = "⚠ 样本不足" if is_warn else ""
row_data = [class_name, detail['total'], detail['train'],
detail['val'], detail['test'], status]
for col_idx, value in enumerate(row_data, start=1):
cell = ws.cell(row=row, column=col_idx, value=value)
cell.border = make_border()
cell.alignment = left if col_idx == 1 else center
cell.font = make_font(size=10)
if is_warn:
cell.fill = make_fill(COLOR_WARN_BG)
elif row_offset % 2 == 1:
cell.fill = make_fill("F8FAFC") # 斑马条纹
ws.row_dimensions[row].height = 18
# ── 最后一行:样本汇总说明 ───────────────────────────────────
last_row = 8 + len(sorted_classes)
ws.merge_cells(f"A{last_row}:F{last_row}")
note_cell = ws[f"A{last_row}"]
note_cell.value = (f"样本不足(< {min_threshold} 张): {insufficient_count} "
f"样本充足(≥ {min_threshold} 张): {len(class_counts) - insufficient_count}")
note_cell.font = make_font(bold=True, color="475569", size=10)
note_cell.alignment = center
note_cell.fill = make_fill(COLOR_SUMMARY_BG)
note_cell.border = make_border()
ws.row_dimensions[last_row].height = 22
# ── 列宽自适应 ───────────────────────────────────────────────
col_widths = [40, 8, 8, 8, 8, 12]
for col_idx, width in enumerate(col_widths, start=1):
ws.column_dimensions[get_column_letter(col_idx)].width = width
# ── 冻结表头(冻结到第8行,滚动时列头始终可见)──────────────
ws.freeze_panes = "A8"
# ── 生成文件名 & 保存 ────────────────────────────────────────
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"image_count_{dataset_root.name}_{timestamp}.xlsx"
save_dir = output_dir if output_dir else Path(__file__).parent
save_path = save_dir / filename
wb.save(save_path)
print(f"\n✅ Excel 已导出: {save_path}")
print(f" (直接打开后全选复制,粘贴到飞书云文档即可保留格式)")
return save_path
def count_images_by_class(dataset_root: str, output_file: str = None,
export_excel: bool = False):
""" """
统计每个类别的图片数量(包含train/val/test 统计每个类别的图片数量(包含train/val/test
Args: Args:
dataset_root: 数据集根目录(包含train/val/test目录) dataset_root: 数据集根目录(包含train/val/test目录)
output_file: 输出文件路径(可选,如果提供则保存到文件) output_file: 输出文件路径(可选,如果提供则保存到 txt 文件)
export_excel: 是否同时导出 Excel 文件(默认 False
""" """
dataset_root = Path(dataset_root) dataset_root = Path(dataset_root)
train_dir = dataset_root / 'train' train_dir = dataset_root / 'train'
@@ -28,62 +193,38 @@ def count_images_by_class(dataset_root: str, output_file: str = None):
print("="*80) print("="*80)
class_counts = {} class_counts = {}
class_counts_detail = {} # 详细统计:train/val/test分别的数量 class_counts_detail = {}
total_images = 0 total_images = total_train = total_val = total_test = 0
total_train = 0
total_val = 0
total_test = 0
# 获取所有类别文件夹(从train目录 # 获取所有类别(以 train 为主,兼容 val/test 独有类别
class_folders = sorted([f for f in train_dir.iterdir() if f.is_dir()])
# 也检查val和test目录中是否有train中没有的类别
all_class_names = set() all_class_names = set()
for folder in class_folders: for folder in train_dir.iterdir():
if folder.is_dir():
all_class_names.add(folder.name) all_class_names.add(folder.name)
if val_dir.exists(): if val_dir.exists():
for folder in val_dir.iterdir(): for folder in val_dir.iterdir():
if folder.is_dir(): if folder.is_dir():
all_class_names.add(folder.name) all_class_names.add(folder.name)
if test_dir.exists(): if test_dir.exists():
for folder in test_dir.iterdir(): for folder in test_dir.iterdir():
if folder.is_dir(): if folder.is_dir():
all_class_names.add(folder.name) all_class_names.add(folder.name)
# 统计每个类别的图片数量
for class_name in sorted(all_class_names):
train_count = 0
val_count = 0
test_count = 0
# 统计train目录
image_exts = {'.jpg', '.jpeg', '.png'} image_exts = {'.jpg', '.jpeg', '.png'}
train_class_dir = train_dir / class_name
if train_class_dir.exists():
train_count = sum(
1 for f in train_class_dir.iterdir()
if f.is_file() and f.suffix.lower() in image_exts
)
# 统计val目录 for class_name in sorted(all_class_names):
val_class_dir = val_dir / class_name def _count(base_dir):
if val_class_dir.exists(): d = base_dir / class_name
val_count = sum( if not d.exists():
1 for f in val_class_dir.iterdir() return 0
if f.is_file() and f.suffix.lower() in image_exts return sum(1 for f in d.iterdir()
) if f.is_file() and f.suffix.lower() in image_exts)
# 统计test目录
test_class_dir = test_dir / class_name
if test_class_dir.exists():
test_count = sum(
1 for f in test_class_dir.iterdir()
if f.is_file() and f.suffix.lower() in image_exts
)
train_count = _count(train_dir)
val_count = _count(val_dir)
test_count = _count(test_dir)
total = train_count + val_count + test_count total = train_count + val_count + test_count
class_counts[class_name] = total class_counts[class_name] = total
class_counts_detail[class_name] = { class_counts_detail[class_name] = {
'train': train_count, 'train': train_count,
@@ -96,15 +237,12 @@ def count_images_by_class(dataset_root: str, output_file: str = None):
total_val += val_count total_val += val_count
total_test += test_count total_test += test_count
# 按数量排序 # 按数量降序排列
sorted_classes = sorted(class_counts.items(), key=lambda x: x[1], reverse=True) sorted_classes = sorted(class_counts.items(), key=lambda x: x[1], reverse=True)
min_threshold = 10
insufficient_count = 0
# 修复文件开头的注释 # ── 控制台输出 ──────────────────────────────────────────────
if output_file:
# 确保sorted_classes已定义
pass
# 打印结果
print(f"\n总类别数: {len(class_counts)}") print(f"\n总类别数: {len(class_counts)}")
print(f"总图片数: {total_images} (训练集: {total_train}, 验证集: {total_val}, 测试集: {total_test})") print(f"总图片数: {total_images} (训练集: {total_train}, 验证集: {total_val}, 测试集: {total_test})")
print(f"平均每类: {total_images // len(class_counts) if len(class_counts) > 0 else 0}\n") print(f"平均每类: {total_images // len(class_counts) if len(class_counts) > 0 else 0}\n")
@@ -112,22 +250,19 @@ def count_images_by_class(dataset_root: str, output_file: str = None):
print(f"{'类别名称':<50} {'总计':<8} {'训练':<8} {'验证':<8} {'测试':<8} {'状态':<10}") print(f"{'类别名称':<50} {'总计':<8} {'训练':<8} {'验证':<8} {'测试':<8} {'状态':<10}")
print("-"*100) print("-"*100)
# 统计样本不足的类别(假设阈值是10)
min_threshold = 10
insufficient_count = 0
for class_name, count in sorted_classes: for class_name, count in sorted_classes:
detail = class_counts_detail[class_name] detail = class_counts_detail[class_name]
status = "⚠样本不足" if count < min_threshold else "" status = "⚠样本不足" if count < min_threshold else ""
if count < min_threshold: if count < min_threshold:
insufficient_count += 1 insufficient_count += 1
print(f"{class_name:<50} {detail['total']:<8} {detail['train']:<8} {detail['val']:<8} {detail['test']:<8} {status:<10}") print(f"{class_name:<50} {detail['total']:<8} {detail['train']:<8} "
f"{detail['val']:<8} {detail['test']:<8} {status:<10}")
print("-"*100) print("-"*100)
print(f"\n样本不足的类别数(<{min_threshold}张): {insufficient_count}") print(f"\n样本不足的类别数(<{min_threshold}张): {insufficient_count}")
print(f"样本充足的类别数(>={min_threshold}张): {len(class_counts) - insufficient_count}") print(f"样本充足的类别数(>={min_threshold}张): {len(class_counts) - insufficient_count}")
# 保存到文件(如果指定) # ── 可选:保存到 txt 文件 ────────────────────────────────────
if output_file: if output_file:
output_path = Path(output_file) output_path = Path(output_file)
with open(output_path, 'w', encoding='utf-8') as f: with open(output_path, 'w', encoding='utf-8') as f:
@@ -140,28 +275,48 @@ def count_images_by_class(dataset_root: str, output_file: str = None):
f.write("-"*100 + "\n") f.write("-"*100 + "\n")
f.write(f"{'类别名称':<50} {'总计':<8} {'训练':<8} {'验证':<8} {'测试':<8} {'状态':<10}\n") f.write(f"{'类别名称':<50} {'总计':<8} {'训练':<8} {'验证':<8} {'测试':<8} {'状态':<10}\n")
f.write("-"*100 + "\n") f.write("-"*100 + "\n")
for class_name, count in sorted_classes: for class_name, count in sorted_classes:
detail = class_counts_detail[class_name] detail = class_counts_detail[class_name]
status = "⚠样本不足" if count < min_threshold else "" status = "⚠样本不足" if count < min_threshold else ""
f.write(f"{class_name:<50} {detail['total']:<8} {detail['train']:<8} {detail['val']:<8} {detail['test']:<8} {status:<10}\n") f.write(f"{class_name:<50} {detail['total']:<8} {detail['train']:<8} "
f"{detail['val']:<8} {detail['test']:<8} {status:<10}\n")
f.write("-"*100 + "\n") f.write("-"*100 + "\n")
f.write(f"\n样本不足的类别数(<{min_threshold}张): {insufficient_count}\n") f.write(f"\n样本不足的类别数(<{min_threshold}张): {insufficient_count}\n")
f.write(f"样本充足的类别数(>={min_threshold}张): {len(class_counts) - insufficient_count}\n") f.write(f"样本充足的类别数(>={min_threshold}张): {len(class_counts) - insufficient_count}\n")
print(f"\n✓ 统计结果已保存到: {output_path}") print(f"\n✓ 统计结果已保存到: {output_path}")
# ── 可选:导出 Excel ─────────────────────────────────────────
if export_excel:
export_to_excel(
sorted_classes = sorted_classes,
class_counts_detail = class_counts_detail,
class_counts = class_counts,
total_images = total_images,
total_train = total_train,
total_val = total_val,
total_test = total_test,
min_threshold = min_threshold,
dataset_root = dataset_root,
)
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
parser = argparse.ArgumentParser(description='统计每个类别的图片数量') parser = argparse.ArgumentParser(description='统计每个类别的图片数量')
parser.add_argument('--dataset', type=str, parser.add_argument(
'--dataset', type=str,
default='dataset/WholeIngredientRecognition', default='dataset/WholeIngredientRecognition',
help='数据集根目录(默认: dataset/WholeIngredientRecognition') help='数据集根目录(默认: dataset/WholeIngredientRecognition'
parser.add_argument('--output', type=str, default=None, )
help='输出文件路径(可选,保存统计结果到文件)') parser.add_argument(
'--output', type=str, default=None,
help='输出 txt 文件路径(可选)'
)
parser.add_argument(
'--excel', action='store_true',
help='是否同时导出 Excel 文件(可直接复制粘贴到飞书云文档)'
)
args = parser.parse_args() args = parser.parse_args()
@@ -175,4 +330,4 @@ if __name__ == "__main__":
else: else:
dataset_path = Path(args.dataset) dataset_path = Path(args.dataset)
count_images_by_class(str(dataset_path), args.output) count_images_by_class(str(dataset_path), args.output, export_excel=args.excel)