| """Annotation consistency analysis: detect labeling style variations. |
| =================================================================== |
| Checks: |
| 1. Box tightness distribution (multi-modal = different annotator styles) |
| 2. Cross-camera annotation bias |
| 3. Potential labeling errors (outlier boxes) |
| 4. Train/val distribution comparison |
| Runs on CPU. |
| """ |
| import sys, os, json |
| import numpy as np |
| from collections import defaultdict |
| from tqdm import tqdm |
|
|
| PROJECT_DIR = '/home/user/goat' |
| os.chdir(PROJECT_DIR) |
| sys.path.insert(0, PROJECT_DIR) |
|
|
|
|
| def main(): |
| train_dir = 'Data/Detection_dataset/labels/train' |
| val_dir = 'Data/Detection_dataset/labels/val' |
| train_img = 'Data/Detection_dataset/images/train' |
| val_img = 'Data/Detection_dataset/images/val' |
|
|
| def analyze(dir_path, img_path, label): |
| areas_px = []; ratios = []; box_counts = [] |
| cam_stats = defaultdict(list) |
| tiny_boxes = [] |
|
|
| for f in tqdm(sorted(os.listdir(dir_path)), desc=label): |
| if not f.endswith('.txt'): continue |
| img_f = f.replace('.txt', '.jpg') |
| img_exists = os.path.exists(os.path.join(img_path, img_f)) |
| cam = img_f.split('_2025')[0] if '_2025' in img_f else 'unknown' |
|
|
| n = 0 |
| with open(os.path.join(dir_path, f)) as fh: |
| for line in fh: |
| p = line.strip().split() |
| if len(p) < 5: continue |
| w, h = float(p[3]), float(p[4]) |
| area = w * h |
| areas_px.append(area * 3200 * 1800) |
| if h > 0: ratios.append((w*3200)/(h*1800)) |
| cam_stats[cam].append({'w': w, 'h': h, 'area': area}) |
| n += 1 |
| |
| if w < 0.003 or h < 0.003: |
| tiny_boxes.append((f, w, h)) |
| box_counts.append(n) |
|
|
| return { |
| 'areas': np.array(areas_px), |
| 'ratios': np.array(ratios), |
| 'box_counts': np.array(box_counts), |
| 'cam_stats': {cam: {'n': len(v), 'mean_w': float(np.mean([x['w'] for x in v])), |
| 'std_w': float(np.std([x['w'] for x in v])), |
| 'mean_h': float(np.mean([x['h'] for x in v])), |
| 'std_h': float(np.std([x['h'] for x in v]))} |
| for cam, v in cam_stats.items()}, |
| 'tiny_boxes': tiny_boxes, |
| 'n_labels': len(areas_px), |
| } |
|
|
| train_s = analyze(train_dir, train_img, 'Train') |
| val_s = analyze(val_dir, val_img, 'Val') |
|
|
| |
| from scipy import stats as scipy_stats |
| try: |
| train_w = train_s['areas'] ** 0.5 |
| kde = scipy_stats.gaussian_kde(train_w[:5000]) |
| x = np.linspace(np.percentile(train_w, 1), np.percentile(train_w, 99), 100) |
| peaks = [] |
| y = kde(x) |
| for i in range(1, len(y)-1): |
| if y[i] > y[i-1] and y[i] > y[i+1] and y[i] > y.max()*0.3: |
| peaks.append(float(x[i])) |
| multimodal = len(peaks) > 1 |
| except: |
| multimodal = False |
| peaks = [] |
|
|
| print('='*60) |
| print('ANNOTATION QUALITY REPORT') |
| print('='*60) |
| print(f'\nTrain labels: {train_s["n_labels"]} boxes in {len(train_s["box_counts"])} files') |
| print(f'Val labels: {val_s["n_labels"]} boxes in {len(val_s["box_counts"])} files') |
| print(f'Median boxes/img — Train: {np.median(train_s["box_counts"]):.0f} Val: {np.median(val_s["box_counts"]):.0f}') |
|
|
| |
| print('\nPer-camera box statistics (Train):') |
| print(f'{"Camera":12s} {"Boxes":>6s} {"mean W":>8s} {"std W":>8s} {"mean H":>8s} {"std H":>8s}') |
| print('-'*55) |
| for cam in sorted(train_s['cam_stats']): |
| s = train_s['cam_stats'][cam] |
| print(f'{cam:12s} {s["n"]:6d} {s["mean_w"]:8.4f} {s["std_w"]:8.4f} {s["mean_h"]:8.4f} {s["std_h"]:8.4f}') |
|
|
| |
| print(f'\nTrain/Val distribution comparison:') |
| print(f' Train median size: {np.median(train_s["areas"]):.0f} px²') |
| print(f' Val median size: {np.median(val_s["areas"]):.0f} px²') |
| size_diff = np.median(val_s['areas'])/np.median(train_s['areas']) - 1 |
| print(f' Difference: {size_diff*100:+.1f}%') |
| if abs(size_diff) > 0.05: |
| print(f' ⚠ Distribution mismatch > 5% — val may not represent train!') |
|
|
| if multimodal: |
| print(f'\nMultimodal size distribution detected! ({len(peaks)} peaks)') |
| print(f' Possible multiple annotator styles. Peak sizes: {[f"{p:.0f}" for p in peaks]} px') |
| else: |
| print(f'\nSize distribution appears unimodal — consistent annotation style.') |
|
|
| if train_s['tiny_boxes']: |
| print(f'\n⚠ {len(train_s["tiny_boxes"])} suspiciously tiny boxes found:') |
| for f, w, h in train_s['tiny_boxes'][:5]: |
| print(f' {f}: w={w:.4f}, h={h:.4f}') |
|
|
| with open('logs/annotation_quality.json', 'w') as f: |
| json.dump({ |
| 'multimodal': multimodal, |
| 'peaks': peaks, |
| 'train_val_diff': round(size_diff, 4), |
| 'cam_stats': {cam: {k: round(v, 4) if isinstance(v, float) else v for k, v in s.items()} |
| for cam, s in train_s['cam_stats'].items()}, |
| }, f, indent=2) |
| print('\nSaved to logs/annotation_quality.json') |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|