| import json |
| import random |
| from collections import defaultdict |
| from sklearn.model_selection import train_test_split |
| from pathlib import Path |
|
|
|
|
| def create_product_aware_splits(metadata_path, output_dir, |
| val_ratio=0.15, test_ratio=0.15, |
| min_images_per_decade=50, source_filter=None): |
| """ |
| Create splits ensuring: |
| 1. All images of same product stay together |
| 2. Balanced decades in each split |
| 3. Stratified by classification if possible |
| """ |
|
|
| |
| with open(metadata_path, 'r', encoding='utf-8') as f: |
| data = json.load(f) |
| |
| if source_filter == 'mobile': |
| |
| filtered_data = [] |
| for entry in data: |
| if entry.get('source') == 'https://www.mobilephonemuseum.com/': |
| |
| if 'img_0' in entry.get('id', '') or entry.get('image_index') == 0: |
| filtered_data.append(entry) |
| |
| data = filtered_data |
| print(f"Mobile Phone Museum first images (img_0): {len(data)}") |
| |
| if len(data) == 0: |
| print("No Mobile Phone Museum entries found!") |
| return |
| elif source_filter == 'phone_and_calculator': |
| |
| filtered_data = [] |
| for entry in data: |
| source = entry.get('source', '') |
| |
| if source in ['https://www.mobilephonemuseum.com/', 'http://www.datamath.org/']: |
| |
| if 'img_0' in entry.get('id', '') or entry.get('image_index') == 0: |
| filtered_data.append(entry) |
| |
| data = filtered_data |
| print(f"Mobile Phone Museum + Calculator Museum first images (img_0): {len(data)}") |
| |
| |
| products = defaultdict(list) |
| for entry in data: |
| products[entry['product_id']].append(entry) |
|
|
| |
| product_info = {} |
| decade_products = defaultdict(list) |
|
|
| for product_id, images in products.items(): |
| |
| first_image = images[0] |
| product_info[product_id] = { |
| 'decade': first_image['decade'], |
| 'classification': first_image['classification'], |
| 'country': first_image['country'], |
| 'images': images |
| } |
| decade_products[first_image['decade']].append(product_id) |
|
|
| |
| print("\nProducts per decade:") |
| for decade, product_ids in sorted(decade_products.items()): |
| image_count = sum(len(products[pid]) for pid in product_ids) |
| print(f" {decade}: {len(product_ids)} products, {image_count} images") |
|
|
| |
| train_products = [] |
| val_products = [] |
| test_products = [] |
|
|
| |
| for decade, product_ids in decade_products.items(): |
| if len(product_ids) < 3: |
| |
| train_products.extend(product_ids) |
| print(f"Warning: {decade} has only {len(product_ids)} products, all going to train") |
| else: |
| |
| train_val_ids, test_ids = train_test_split( |
| product_ids, |
| test_size=test_ratio, |
| random_state=42 |
| ) |
|
|
| |
| train_ids, val_ids = train_test_split( |
| train_val_ids, |
| test_size=val_ratio / (1 - test_ratio), |
| random_state=42 |
| ) |
|
|
| train_products.extend(train_ids) |
| val_products.extend(val_ids) |
| test_products.extend(test_ids) |
|
|
| |
| train_data = [] |
| val_data = [] |
| test_data = [] |
|
|
| for pid in train_products: |
| train_data.extend(product_info[pid]['images']) |
| for pid in val_products: |
| val_data.extend(product_info[pid]['images']) |
| for pid in test_products: |
| test_data.extend(product_info[pid]['images']) |
|
|
| |
| random.shuffle(train_data) |
| random.shuffle(val_data) |
| random.shuffle(test_data) |
|
|
| |
| splits = { |
| 'train': train_data, |
| 'val': val_data, |
| 'test': test_data |
| } |
|
|
| Path(output_dir).mkdir(parents=True, exist_ok=True) |
|
|
| for split_name, split_data in splits.items(): |
| |
| with open(Path(output_dir) / f'{split_name}.json', 'w') as f: |
| json.dump(split_data, f, indent=2) |
|
|
| |
| urls = [d['url'] for d in split_data] |
| with open(Path(output_dir) / f'{split_name}_urls.txt', 'w') as f: |
| f.write('\n'.join(urls)) |
|
|
| |
| print(f"\n=== Split Statistics ===") |
| print(f"Train: {len(train_products)} products, {len(train_data)} images") |
| print(f"Val: {len(val_products)} products, {len(val_data)} images") |
| print(f"Test: {len(test_products)} products, {len(test_data)} images") |
|
|
| |
| for split_name, split_data in splits.items(): |
| print(f"\n{split_name.capitalize()} split decades:") |
| decade_counts = defaultdict(int) |
| for entry in split_data: |
| decade_counts[entry['decade']] += 1 |
| for decade in sorted(decade_counts.keys()): |
| print(f" {decade}: {decade_counts[decade]} images") |
|
|
| |
| summary = { |
| 'train_products': len(train_products), |
| 'val_products': len(val_products), |
| 'test_products': len(test_products), |
| 'train_images': len(train_data), |
| 'val_images': len(val_data), |
| 'test_images': len(test_data), |
| 'splits_by_decade': {} |
| } |
|
|
| for split_name, split_data in splits.items(): |
| decade_counts = defaultdict(int) |
| for entry in split_data: |
| decade_counts[entry['decade']] += 1 |
| summary['splits_by_decade'][split_name] = dict(decade_counts) |
|
|
| with open(Path(output_dir) / 'split_summary.json', 'w') as f: |
| json.dump(summary, f, indent=2) |
|
|
|
|
| if __name__ == "__main__": |
| project_root = Path(__file__).parent.parent |
| |
| create_product_aware_splits( |
| str(project_root / 'data/metadata/processed_metadata.json'), |
| str(project_root / 'data/splits'), |
| val_ratio=0.15, |
| test_ratio=0.15, |
| source_filter='phone_and_calculator' |
| ) |