Add image_resolution_scanner.py + update README (v1.3.0)
This commit is contained in:
@@ -77,6 +77,7 @@ Messy-Little-Gadgets/
|
||||
├── directory_sync.py # Directory sync tool
|
||||
├── music_downloader.py # Music download GUI tool
|
||||
├── argon2_password_hasher.py # Argon2 password hasher
|
||||
├── image_resolution_scanner.py # Image resolution scanner
|
||||
└── README.md # This document
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Image Resolution Scanner
|
||||
============================================================
|
||||
Scans all images in a folder, extracts resolution info
|
||||
(width, height, file size, format, mode, aspect ratio),
|
||||
and exports results + statistics to a JSON file.
|
||||
|
||||
Features:
|
||||
- Multi-threaded scanning (ThreadPoolExecutor)
|
||||
- Per-image resolution & metadata extraction
|
||||
- Aggregate statistics (formats, resolutions, total pixels/size)
|
||||
- Progress reporting (every 100 images)
|
||||
- Exports to JSON
|
||||
|
||||
Usage: python image_resolution_scanner.py
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from PIL import Image
|
||||
from datetime import datetime
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ImageResolutionScanner:
|
||||
def __init__(self, folder_path, output_json="image_resolutions.json", max_workers=16):
|
||||
"""
|
||||
Initialize the image resolution scanner.
|
||||
|
||||
Args:
|
||||
folder_path: folder containing images
|
||||
output_json: output JSON file path
|
||||
max_workers: number of worker threads
|
||||
"""
|
||||
self.folder_path = Path(folder_path)
|
||||
self.output_json = output_json
|
||||
self.max_workers = max_workers
|
||||
self.results = []
|
||||
self.stats = {
|
||||
"total_images": 0,
|
||||
"processed_images": 0,
|
||||
"failed_images": 0,
|
||||
"image_formats": {},
|
||||
"resolutions": {}
|
||||
}
|
||||
self.lock = threading.Lock()
|
||||
self.supported_formats = {'.jpg'}
|
||||
|
||||
def get_image_files(self):
|
||||
"""Get all image files in the folder."""
|
||||
image_files = []
|
||||
for ext in self.supported_formats:
|
||||
image_files.extend(self.folder_path.glob(f'*{ext}'))
|
||||
image_files.extend(self.folder_path.glob(f'*{ext.upper()}'))
|
||||
return image_files
|
||||
|
||||
def process_image(self, image_path):
|
||||
"""
|
||||
Process a single image and extract resolution info.
|
||||
|
||||
Args:
|
||||
image_path: path to the image file
|
||||
|
||||
Returns:
|
||||
dict: image info including resolution metadata
|
||||
"""
|
||||
try:
|
||||
# Open image with PIL to get dimensions
|
||||
with Image.open(image_path) as img:
|
||||
width, height = img.size
|
||||
file_size = os.path.getsize(image_path) # bytes
|
||||
file_size_kb = file_size / 1024 # KB
|
||||
file_size_mb = file_size_kb / 1024 # MB
|
||||
|
||||
# Get image format
|
||||
img_format = img.format if img.format else 'Unknown'
|
||||
|
||||
# Get image mode (RGB, RGBA, L, etc.)
|
||||
img_mode = img.mode
|
||||
|
||||
result = {
|
||||
"filename": image_path.name,
|
||||
"filepath": str(image_path),
|
||||
"width": width,
|
||||
"height": height,
|
||||
"resolution": f"{width}x{height}",
|
||||
"total_pixels": width * height,
|
||||
"file_size_bytes": file_size,
|
||||
"file_size_kb": round(file_size_kb, 2),
|
||||
"file_size_mb": round(file_size_mb, 2),
|
||||
"format": img_format,
|
||||
"mode": img_mode,
|
||||
"aspect_ratio": round(width / height, 3) if height > 0 else 0
|
||||
}
|
||||
|
||||
# Update statistics (thread-safe)
|
||||
with self.lock:
|
||||
self.stats["processed_images"] += 1
|
||||
|
||||
# Count image formats
|
||||
if img_format not in self.stats["image_formats"]:
|
||||
self.stats["image_formats"][img_format] = 0
|
||||
self.stats["image_formats"][img_format] += 1
|
||||
|
||||
# Count resolutions
|
||||
resolution_key = f"{width}x{height}"
|
||||
if resolution_key not in self.stats["resolutions"]:
|
||||
self.stats["resolutions"][resolution_key] = 0
|
||||
self.stats["resolutions"][resolution_key] += 1
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
with self.lock:
|
||||
self.stats["failed_images"] += 1
|
||||
self.stats["total_images"] = len(self.image_files)
|
||||
print(f"Failed to process: {image_path.name} - {str(e)}")
|
||||
return {
|
||||
"filename": image_path.name,
|
||||
"filepath": str(image_path),
|
||||
"error": str(e),
|
||||
"status": "failed"
|
||||
}
|
||||
|
||||
def scan(self):
|
||||
"""Start scanning all images."""
|
||||
print(f"Scanning folder: {self.folder_path}")
|
||||
start_time = time.time()
|
||||
|
||||
# Get all image files
|
||||
self.image_files = self.get_image_files()
|
||||
self.stats["total_images"] = len(self.image_files)
|
||||
|
||||
if not self.image_files:
|
||||
print("No image files found!")
|
||||
return
|
||||
|
||||
print(f"Found {len(self.image_files)} image(s), processing with {self.max_workers} threads...")
|
||||
|
||||
# Process with thread pool
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
# Submit all tasks
|
||||
future_to_file = {executor.submit(self.process_image, img_path): img_path
|
||||
for img_path in self.image_files}
|
||||
|
||||
# Process completed tasks
|
||||
for idx, future in enumerate(as_completed(future_to_file), 1):
|
||||
img_path = future_to_file[future]
|
||||
try:
|
||||
result = future.result()
|
||||
if result:
|
||||
self.results.append(result)
|
||||
# Print progress every 100 images
|
||||
if idx % 100 == 0 or idx == len(self.image_files):
|
||||
processed = self.stats["processed_images"] + self.stats["failed_images"]
|
||||
print(f"Progress: {processed}/{len(self.image_files)} processed")
|
||||
except Exception as e:
|
||||
print(f"Error processing task: {img_path.name} - {str(e)}")
|
||||
with self.lock:
|
||||
self.stats["failed_images"] += 1
|
||||
|
||||
# Generate final statistics
|
||||
self.generate_statistics()
|
||||
|
||||
# Save JSON results
|
||||
self.save_to_json()
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
print(f"\nScan complete! Time elapsed: {elapsed_time:.2f} seconds")
|
||||
print(f"Processed: {self.stats['processed_images']} image(s)")
|
||||
print(f"Failed: {self.stats['failed_images']} image(s)")
|
||||
print(f"Results saved to: {self.output_json}")
|
||||
|
||||
def generate_statistics(self):
|
||||
"""Generate aggregate statistics."""
|
||||
total_pixels = sum([r.get("total_pixels", 0) for r in self.results if "total_pixels" in r])
|
||||
total_size_mb = sum([r.get("file_size_mb", 0) for r in self.results if "file_size_mb" in r])
|
||||
|
||||
self.stats.update({
|
||||
"total_pixels": total_pixels,
|
||||
"total_size_mb": round(total_size_mb, 2),
|
||||
"total_size_gb": round(total_size_mb / 1024, 2),
|
||||
"scan_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"folder_path": str(self.folder_path),
|
||||
"thread_count": self.max_workers
|
||||
})
|
||||
|
||||
def save_to_json(self):
|
||||
"""Save results to a JSON file."""
|
||||
output_data = {
|
||||
"statistics": self.stats,
|
||||
"images": self.results
|
||||
}
|
||||
|
||||
with open(self.output_json, 'w', encoding='utf-8') as f:
|
||||
json.dump(output_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"JSON file saved: {self.output_json}")
|
||||
|
||||
|
||||
def main():
|
||||
# Configuration - change these to your own paths
|
||||
folder_path = input("Enter image folder path: ").strip()
|
||||
output_json = "image_resolutions.json"
|
||||
thread_count = 16
|
||||
|
||||
# Check folder exists
|
||||
if not os.path.exists(folder_path):
|
||||
print(f"Error: folder path does not exist - {folder_path}")
|
||||
return
|
||||
|
||||
# Create the scanner and run
|
||||
scanner = ImageResolutionScanner(
|
||||
folder_path=folder_path,
|
||||
output_json=output_json,
|
||||
max_workers=thread_count
|
||||
)
|
||||
|
||||
scanner.scan()
|
||||
|
||||
print("\nProgram finished!")
|
||||
print(f"View detailed results in: {output_json}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user