File size: 12,047 Bytes
1a3a976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""
Dataset Statistics Script for Flux Identity LoRA Training

Features:
- Count total images and caption files
- List missing captions
- Resolution distribution histogram
- Average/min/max dimensions
- File format breakdown
- Caption length statistics
"""

import os
import sys
import argparse
from pathlib import Path
from collections import defaultdict, Counter
from PIL import Image
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn

console = Console()

# Supported image formats
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff', '.tif'}


def scan_files(directory: Path) -> tuple:
    """Scan directory for image and caption files."""
    images = []
    captions = []

    for f in directory.iterdir():
        if f.is_file():
            if f.suffix.lower() in IMAGE_EXTENSIONS:
                images.append(f)
            elif f.suffix.lower() == '.txt':
                captions.append(f)

    return sorted(images), sorted(captions)


def get_image_info(image_path: Path) -> dict:
    """Get information about an image."""
    try:
        with Image.open(image_path) as img:
            return {
                'path': image_path,
                'width': img.width,
                'height': img.height,
                'format': img.format,
                'mode': img.mode,
                'size_kb': image_path.stat().st_size / 1024
            }
    except Exception as e:
        return {
            'path': image_path,
            'error': str(e)
        }


def get_caption_info(caption_path: Path) -> dict:
    """Get information about a caption file."""
    try:
        content = caption_path.read_text(encoding='utf-8').strip()
        words = content.split()
        return {
            'path': caption_path,
            'length': len(content),
            'word_count': len(words),
            'content': content[:200] + '...' if len(content) > 200 else content
        }
    except Exception as e:
        return {
            'path': caption_path,
            'error': str(e)
        }


def create_histogram(values: list, bins: int = 10, width: int = 40) -> str:
    """Create a simple text histogram."""
    if not values:
        return "No data"

    min_val = min(values)
    max_val = max(values)

    if min_val == max_val:
        return f"All values: {min_val}"

    bin_size = (max_val - min_val) / bins
    histogram = defaultdict(int)

    for v in values:
        bin_idx = min(int((v - min_val) / bin_size), bins - 1)
        histogram[bin_idx] += 1

    max_count = max(histogram.values()) if histogram else 1

    lines = []
    for i in range(bins):
        bin_start = min_val + i * bin_size
        bin_end = bin_start + bin_size
        count = histogram[i]
        bar_len = int((count / max_count) * width)
        bar = 'β–ˆ' * bar_len
        lines.append(f"{bin_start:6.0f}-{bin_end:6.0f} | {bar} ({count})")

    return '\n'.join(lines)


def print_stats(images: list, captions: list, image_infos: list, caption_infos: list, directory: Path):
    """Print comprehensive dataset statistics."""

    console.print(Panel.fit(
        f"[bold blue]Dataset Statistics[/bold blue]\n[dim]{directory}[/dim]",
        border_style="blue"
    ))

    # Basic counts
    console.print("\n[bold cyan]═══ File Counts ═══[/bold cyan]")

    counts_table = Table(show_header=False)
    counts_table.add_column("Metric", style="cyan")
    counts_table.add_column("Value", style="green")

    counts_table.add_row("Total Images", str(len(images)))
    counts_table.add_row("Total Caption Files", str(len(captions)))

    # Check for matching pairs
    image_stems = {img.stem for img in images}
    caption_stems = {cap.stem for cap in captions}

    matched = image_stems & caption_stems
    images_without_captions = image_stems - caption_stems
    captions_without_images = caption_stems - image_stems

    counts_table.add_row("Matched Image-Caption Pairs", str(len(matched)))
    counts_table.add_row("Images Missing Captions", str(len(images_without_captions)))
    counts_table.add_row("Orphan Caption Files", str(len(captions_without_images)))

    console.print(counts_table)

    # Missing captions
    if images_without_captions:
        console.print("\n[bold yellow]Images Missing Captions:[/bold yellow]")
        for stem in sorted(images_without_captions)[:15]:
            console.print(f"  β€’ {stem}")
        if len(images_without_captions) > 15:
            console.print(f"  ... and {len(images_without_captions) - 15} more")

    # Image statistics
    valid_infos = [i for i in image_infos if 'error' not in i]

    if valid_infos:
        console.print("\n[bold cyan]═══ Image Dimensions ═══[/bold cyan]")

        widths = [i['width'] for i in valid_infos]
        heights = [i['height'] for i in valid_infos]
        sizes = [i['size_kb'] for i in valid_infos]

        dim_table = Table()
        dim_table.add_column("Metric", style="cyan")
        dim_table.add_column("Width", style="green")
        dim_table.add_column("Height", style="green")

        dim_table.add_row("Minimum", str(min(widths)), str(min(heights)))
        dim_table.add_row("Maximum", str(max(widths)), str(max(heights)))
        dim_table.add_row("Average", f"{sum(widths)/len(widths):.0f}", f"{sum(heights)/len(heights):.0f}")

        console.print(dim_table)

        # Resolution distribution
        console.print("\n[bold]Resolution Distribution:[/bold]")
        resolutions = Counter(f"{i['width']}x{i['height']}" for i in valid_infos)

        res_table = Table()
        res_table.add_column("Resolution", style="cyan")
        res_table.add_column("Count", style="green")
        res_table.add_column("Percentage", style="yellow")
        res_table.add_column("", style="dim")

        for res, count in resolutions.most_common(10):
            pct = (count / len(valid_infos)) * 100
            bar = 'β–ˆ' * int(pct / 2)
            res_table.add_row(res, str(count), f"{pct:.1f}%", bar)

        if len(resolutions) > 10:
            res_table.add_row("...", f"+{len(resolutions) - 10} more", "", "")

        console.print(res_table)

        # File format breakdown
        console.print("\n[bold]File Format Breakdown:[/bold]")
        formats = Counter(i['format'] for i in valid_infos)

        fmt_table = Table()
        fmt_table.add_column("Format", style="cyan")
        fmt_table.add_column("Count", style="green")
        fmt_table.add_column("Percentage", style="yellow")

        for fmt, count in formats.most_common():
            pct = (count / len(valid_infos)) * 100
            fmt_table.add_row(fmt or "Unknown", str(count), f"{pct:.1f}%")

        console.print(fmt_table)

        # File size statistics
        console.print("\n[bold]File Size Statistics:[/bold]")
        size_table = Table(show_header=False)
        size_table.add_column("Metric", style="cyan")
        size_table.add_column("Value", style="green")

        size_table.add_row("Minimum Size", f"{min(sizes):.1f} KB")
        size_table.add_row("Maximum Size", f"{max(sizes):.1f} KB")
        size_table.add_row("Average Size", f"{sum(sizes)/len(sizes):.1f} KB")
        size_table.add_row("Total Size", f"{sum(sizes)/1024:.1f} MB")

        console.print(size_table)

    # Caption statistics
    valid_captions = [c for c in caption_infos if 'error' not in c]

    if valid_captions:
        console.print("\n[bold cyan]═══ Caption Statistics ═══[/bold cyan]")

        lengths = [c['length'] for c in valid_captions]
        word_counts = [c['word_count'] for c in valid_captions]

        cap_table = Table()
        cap_table.add_column("Metric", style="cyan")
        cap_table.add_column("Characters", style="green")
        cap_table.add_column("Words", style="green")

        cap_table.add_row("Minimum", str(min(lengths)), str(min(word_counts)))
        cap_table.add_row("Maximum", str(max(lengths)), str(max(word_counts)))
        cap_table.add_row("Average", f"{sum(lengths)/len(lengths):.0f}", f"{sum(word_counts)/len(word_counts):.1f}")

        console.print(cap_table)

        # Caption length histogram
        console.print("\n[bold]Caption Length Distribution (characters):[/bold]")
        console.print(create_histogram(lengths, bins=8, width=30))

        # Sample captions
        console.print("\n[bold]Sample Captions:[/bold]")
        for cap in valid_captions[:3]:
            console.print(f"\n  [dim]{cap['path'].stem}:[/dim]")
            console.print(f"  {cap['content']}")

    # Recommendations
    console.print("\n[bold cyan]═══ Recommendations ═══[/bold cyan]")

    recommendations = []

    if images_without_captions:
        recommendations.append(f"⚠ Add captions for {len(images_without_captions)} image(s)")

    if valid_infos:
        small = sum(1 for i in valid_infos if i['width'] < 512 or i['height'] < 512)
        if small > 0:
            recommendations.append(f"⚠ {small} image(s) are smaller than 512px")

        large = sum(1 for i in valid_infos if i['width'] > 2048 or i['height'] > 2048)
        if large > 0:
            recommendations.append(f"β„Ή {large} image(s) are larger than 2048px (will be resized)")

    if valid_captions:
        short = sum(1 for c in valid_captions if c['word_count'] < 5)
        if short > 0:
            recommendations.append(f"⚠ {short} caption(s) are very short (<5 words)")

    if not recommendations:
        recommendations.append("βœ“ Dataset looks good!")

    for rec in recommendations:
        console.print(f"  {rec}")


def main():
    parser = argparse.ArgumentParser(description="Generate dataset statistics for Flux LoRA training")
    parser.add_argument(
        "directory",
        nargs="?",
        default="/workspace/flux-project/datasets/identity/images",
        help="Directory containing training images and captions"
    )
    parser.add_argument(
        "-o", "--output",
        help="Save statistics to file"
    )

    args = parser.parse_args()

    dataset_dir = Path(args.directory)

    if not dataset_dir.exists():
        console.print(f"[red]Error: Directory not found: {dataset_dir}[/red]")
        sys.exit(1)

    console.print(f"[bold]Scanning: {dataset_dir}[/bold]\n")

    # Scan files
    images, captions = scan_files(dataset_dir)

    if not images and not captions:
        console.print("[yellow]No images or captions found in directory.[/yellow]")
        sys.exit(0)

    # Gather image info
    image_infos = []
    if images:
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            console=console
        ) as progress:
            task = progress.add_task(f"Analyzing {len(images)} images...", total=len(images))

            for img in images:
                image_infos.append(get_image_info(img))
                progress.advance(task)

    # Gather caption info
    caption_infos = []
    if captions:
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            console=console
        ) as progress:
            task = progress.add_task(f"Analyzing {len(captions)} captions...", total=len(captions))

            for cap in captions:
                caption_infos.append(get_caption_info(cap))
                progress.advance(task)

    # Print statistics
    print_stats(images, captions, image_infos, caption_infos, dataset_dir)

    # Save to file if requested
    if args.output:
        with open(args.output, 'w') as f:
            f.write(f"Dataset Statistics: {dataset_dir}\n")
            f.write("=" * 50 + "\n\n")
            f.write(f"Images: {len(images)}\n")
            f.write(f"Captions: {len(captions)}\n")
            # Add more details as needed
        console.print(f"\n[dim]Statistics saved to: {args.output}[/dim]")


if __name__ == "__main__":
    main()