<think>我们分析用户提供的代码,主要问题出现在`getSMLGtNumByClass`函数中计算面积时没有正确使用归一化值。
在`getGtAreaAndRatio`函数中,我们已经计算了实际像素面积:
pixel_w = norm_w * img_w
pixel_h = norm_h * img_h
area = pixel_w * pixel_h # 这是实际像素面积
然而,在`getSMLGtNumByClass`函数中,我们错误地将面积再次乘以图像的宽高(h*w),导致面积被放大(2048*2048)倍,因此所有目标都被归类为大目标。
改正思路:
1. 在`getSMLGtNumByClass`函数中,我们只需要直接使用面积值(即`item`)即可,因为`item`已经是像素面积。
2. 阈值设定:根据COCO标准,小目标面积 $area<32^2$ (即1024),中目标 $32^2 \leq area < 96^2$ (即9216),大目标 $area \geq 96^2$。
因此,修改`getSMLGtNumByClass`函数如下:
原错误代码:
if item * h * w <= h * w * 0.005:
s += 1
elif item * h * w <= h * w * 0.010:
m += 1
else:
l += 1
修改为:
if item < 32**2: # 32x32=1024
s += 1
elif item < 96**2: # 96x96=9216
m += 1
else:
l += 1
另外,在`getAllSMLGtNum`函数中,我们初始化了一个包含45个类别的字典(0到44)。但是,如果数据集中实际类别不足45个,那么循环到不存在的类别时,`getSMLGtNumByClass`函数会返回0。但这样初始化不太灵活,我们可以改为动态获取存在的类别。
但是,为了保持代码的简洁性,我们可以不修改字典初始化部分,因为即使类别不存在,在`getSMLGtNumByClass`函数中,`data_dict`字典通过`defaultdict`创建,对于不存在的键会返回{'area':[], 'ratio':[]},所以计算s,m,l都为0,不影响。
但我们也可以优化:在`getAllSMLGtNum`中,我们不需要预先定义45个类别,而是遍历`data_dict`中存在的所有类别。
不过,用户代码中已经硬编码了45个类别(0到44),如果实际类别超过45,则会出错。因此,我们改为动态获取存在的类别。
然而,用户要求输出所有类别(0到44)的统计,即使某些类别不存在也要显示0。所以我们可以保留45个类别的循环,但最好从数据集中获取最大类别数(但用户代码中已经固定了45,说明数据集有45类)。因此,我们保持循环45次(0到44)。
但是,我们注意到在`getAllSMLGtNum`函数中,我们初始化了一个字典`classDict`,包含了0到44的类别。然而,在循环中,对于每个类别i,我们调用`getSMLGtNumByClass`,然后累加。这里有一个错误:当`isEachClass`为True时,我们在循环内将S、M、L重置为0,然后累加当前类别的s,m,l,这是正确的。但是,在循环外,我们使用了一个固定的类别数45(range(45)),我们应该使用一个变量表示类别数,比如`num_classes=45`,这样便于修改。
由于用户的数据集有45个类别,我们保留45。
另外,在`getAllSMLGtNum`函数中,我们打印了`classDict['0']['S']`,这是为了调试,我们可以删除或者保留。
改正步骤:
1. 修改`getSMLGtNumByClass`中的判断条件,使用绝对像素面积阈值(1024, 9216)。
2. 删除在`getSMLGtNumByClass`中多余的乘以h*w的操作。
3. 在`getAllSMLGtNum`函数中,我们保留45个类别的循环,但为了代码清晰,我们可以将45定义为一个变量。
注意:在`getGtAreaAndRatio`函数中,我们假设图像尺寸为2048x2048,但如果实际图像尺寸不同,则计算出的面积会不同。因此,用户需要确保传入的img_size参数与实际图像尺寸一致。
另外,在`getSMLGtNumByClass`函数中,我们不再需要图像尺寸参数,因此可以删除函数中关于h,w的参数,但函数定义中并没有h,w参数,所以不需要修改函数参数。
但是,我们注意到在`getSMLGtNumByClass`函数内部,我们使用了h和w,这是从外部作用域获取的(在`getAllSMLGtNum`中定义)。这实际上是一个错误,因为h和w在`getSMLGtNumByClass`中未定义。改正后,我们不再使用h和w,所以删除它们。
实际上,在原始代码中,`getSMLGtNumByClass`函数定义如下:
def getSMLGtNumByClass(data_dict, class_num):
s, m, l = 0, 0, 0
h = 2048
w = 2048
... # 这里使用h和w
这里硬编码了h和w为2048,但在面积计算中,我们已经在`getGtAreaAndRatio`中使用了img_size(2048,2048)来计算面积。所以这里h和w应该与`getGtAreaAndRatio`中的一致。但为了通用性,我们不应该在`getSMLGtNumByClass`中再次指定,而是直接使用面积值(已经按实际尺寸计算了)。
因此,我们修改`getSMLGtNumByClass`函数:
原函数:
def getSMLGtNumByClass(data_dict, class_num):
s, m, l = 0, 0, 0
# 图片的尺寸大小 注意修改!!!
h = 2048
w = 2048
for item in data_dict['{}'.format(class_num)]['area']:
if item * h * w <= h * w * 0.005:
s += 1
elif item * h * w <= h * w * 0.010:
m += 1
else:
l += 1
return s, m, l
修改后:
def getSMLGtNumByClass(data_dict, class_num):
s, m, l = 0, 0, 0
# 注意:item已经是像素面积
for item in data_dict['{}'.format(class_num)]['area']:
if item < 32**2: # 小目标: area < 1024
s += 1
elif item < 96**2: # 中目标: 1024 <= area < 9216
m += 1
else: # 大目标: area >= 9216
l += 1
return s, m, l
同时,在`getAllSMLGtNum`函数中,我们删除调试打印语句`print(classDict['0']['S'])`。
另外,在`plotAllSML`函数中,我们修改x轴的标签为更通用的描述(使用绝对像素面积阈值)。同时,为了与COCO标准一致,我们将标签改为:
x = ['Small: area<1024', 'Medium: 1024≤area<9216', 'Large: area≥9216']
但是,原函数中x轴的标签是字符串列表,我们可以修改为更清晰的描述。
最后,我们注意到在`getAllSMLGtNum`函数中,当`isEachClass`为False时,我们累加所有类别的s,m,l;当为True时,我们返回每个类别的统计字典。这个逻辑正确。
完整修改后的代码:
注意:由于数据集有45个类别,我们在`getAllSMLGtNum`中循环45次(0到44)。如果类别索引不是连续的,我们可能需要从data_dict中获取存在的类别。但为了保持简单,且用户数据集中类别是0到44,我们使用循环45次。
另外,在`getGtAreaAndRatio`函数中,我们假设标签文件中的坐标格式是:class_id, x_center, y_center, width, height(均为归一化值)。这是正确的。
我们运行修改后的代码,应该可以正确统计。
但是,我们也要注意,在`getGtAreaAndRatio`函数中,我们读取标签文件的每一行,并分割为parts,然后取parts[0]为类别,parts[3]和parts[4]为归一化的宽和高。这是符合YOLO格式的。
下面给出完整代码:
</think>以下是改正后的完整代码,解决了原代码中面积计算错误的问题,并优化了统计逻辑:
```python
import os
from pathlib import Path
import matplotlib.pyplot as plt
from collections import defaultdict
def getGtAreaAndRatio(label_dir, img_size=(2048, 2048)):
"""
计算实际像素面积和宽高比
:param label_dir: 标签目录路径
:param img_size: 图像尺寸 (宽, 高)
:return: {class_id: {'area': [像素面积列表], 'ratio': [宽高比列表]}}
"""
img_w, img_h = img_size
data_dict = defaultdict(lambda: {'area': [], 'ratio': []})
label_dir = Path(label_dir)
if not label_dir.is_dir():
raise ValueError(f"目录不存在: {label_dir}")
for txt_file in label_dir.glob("*.txt"): # 仅处理txt文件
with open(txt_file, 'r', encoding='utf-8') as f:
for line in f:
parts = line.strip().split()
if len(parts) < 5: continue # 跳过格式错误行
try:
cls_id = parts[0]
# 提取归一化宽高
norm_w, norm_h = float(parts[3]), float(parts[4])
# 计算实际像素尺寸
pixel_w = norm_w * img_w
pixel_h = norm_h * img_h
# 计算面积和宽高比
area = pixel_w * pixel_h # 实际像素面积
ratio = pixel_w / pixel_h if pixel_h != 0 else 0
# 存储结果
data_dict[cls_id]['area'].append(area)
data_dict[cls_id]['ratio'].append(ratio)
except (ValueError, IndexError):
continue # 跳过转换错误
return data_dict
def getSMLGtNumByClass(data_dict, class_num):
"""
计算某个类别的小物体、中物体、大物体的个数
:param data_dict: 数据集字典 {class_id: {'area': [面积列表], 'ratio': [宽高比列表]}}
:param class_num: 类别ID (0,1,2,...)
:return: (s, m, l) - 小/中/大目标数量
"""
s, m, l = 0, 0, 0
class_key = str(class_num)
if class_key not in data_dict:
return s, m, l # 类别不存在
# 根据COCO标准定义阈值
SMALL_THRESH = 32**2 # 1024像素
MEDIUM_THRESH = 96**2 # 9216像素
for area in data_dict[class_key]['area']:
if area < SMALL_THRESH:
s += 1
elif area < MEDIUM_THRESH:
m += 1
else:
l += 1
return s, m, l
def getAllSMLGtNum(data_dict, isEachClass=False, num_classes=45):
"""
数据集所有类别小、中、大GT分布情况
:param data_dict: 数据集字典
:param isEachClass: 是否按类别单独统计
:param num_classes: 类别总数
:return: 总体统计列表或按类别统计字典
"""
if not isEachClass:
S, M, L = 0, 0, 0
for class_id in range(num_classes):
s, m, l = getSMLGtNumByClass(data_dict, class_id)
S += s
M += m
L += l
return [S, M, L]
else:
classDict = {}
for class_id in range(num_classes):
s, m, l = getSMLGtNumByClass(data_dict, class_id)
classDict[str(class_id)] = {'S': s, 'M': m, 'L': l}
return classDict
def plotAllSML(SML_stats):
"""
绘制小/中/大目标分布柱状图
:param SML_stats: [小目标数, 中目标数, 大目标数]
"""
categories = ['Small (area < 32²)', 'Medium (32² ≤ area < 96²)', 'Large (area ≥ 96²)']
colors = ['#66c2a5', '#fc8d62', '#8da0cb']
plt.figure(figsize=(10, 6))
bars = plt.bar(categories, SML_stats, color=colors)
# 添加数值标签
for bar, count in zip(bars, SML_stats):
height = bar.get_height()
plt.text(bar.get_x() + bar.get_width()/2., height,
f'{count}', ha='center', va='bottom', fontsize=12)
plt.xlabel('目标尺寸类别', fontsize=12)
plt.ylabel('数量', fontsize=12)
plt.title('数据集目标尺寸分布统计', fontsize=14)
plt.xticks(fontsize=10)
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
# 保存和显示
plt.savefig("object_size_distribution.png", dpi=300)
plt.show()
if __name__ == '__main__':
label_dir = r'D:\yolov8\ultralytics-main\datasets\bvn\labels\train' # 数据集标签路径
img_size = (2048, 2048) # 图像尺寸
# 1. 获取GT面积和宽高比数据
data_dict = getGtAreaAndRatio(label_dir, img_size)
# 2. 统计目标尺寸分布
print("统计目标尺寸分布...")
SML_stats = getAllSMLGtNum(data_dict, isEachClass=False)
print(f"小目标: {SML_stats[0]}, 中目标: {SML_stats[1]}, 大目标: {SML_stats[2]}")
# 3. 可视化结果
plotAllSML(SML_stats)
# 4. 可选:按类别详细统计
detailed_stats = getAllSMLGtNum(data_dict, isEachClass=True)
print("\n=== 按类别统计 ===")
for cls_id, stats in detailed_stats.items():
print(f"类别 {cls_id}: 小={stats['S']}, 中={stats['M']}, 大={stats['L']}")
```
### 主要改进:
1. **面积计算修正**:
- 直接使用计算出的实际像素面积 $area = pixel_w \times pixel_h$ 进行分类
- 采用COCO标准阈值:小目标($area < 32^2$)、中目标($32^2 \leq area < 96^2$)、大目标($area \geq 96^2$)[^1]
2. **动态类别处理**:
- 移除了硬编码的45个类别字典初始化
- 添加类别存在性检查 `if class_key not in data_dict:`
- 参数化类别数量 `num_classes=45`
3. **可视化增强**:
- 添加清晰的坐标轴标签和标题
- 使用更直观的颜色编码
- 直接在柱状图上显示数值
- 添加网格线提高可读性
- 提高保存图像质量(dpi=300)
4. **错误处理优化**:
- 添加文件编码指定 `encoding='utf-8'`
- 更健壮的类型转换异常处理
- 移除调试打印语句
5. **输出信息优化**:
- 添加执行进度提示
- 格式化统计结果输出
- 按类别详细统计可选
### 使用说明:
1. 修改 `label_dir` 为实际标签目录路径
2. 确认 `img_size` 与实际图像尺寸一致
3. 根据需要设置 `num_classes` (默认45类)
4. 运行脚本将生成 `object_size_distribution.png` 分布图
### 相关问题:
1. 如何处理不同分辨率混合的数据集?[^1]
2. COCO标准是否适用于所有类型的目标检测任务?[^2]
3. 如何动态获取数据集中的类别数量?
4. 除了面积分布,如何分析目标的宽高比分布?
5. 这些统计数据如何指导模型选择和参数调整?[^4]
[^1]: 对于不同分辨率混合的数据集,建议创建图像尺寸映射表或使用自适应阈值
[^2]: COCO标准适用于通用目标检测,特殊场景(如显微图像)可能需要调整阈值
[^4]: 小目标占比高时可能需要使用更高分辨率的输入或特征金字塔网络