201 lines
7.6 KiB
Python
201 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Command-line interface for pixel sorting."""
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
|
|
from pixelsort import pixelsort, analyze, INTERVAL_FUNCTIONS, SORTING_FUNCTIONS
|
|
|
|
|
|
def print_analysis(stats):
|
|
"""Pretty print analysis results."""
|
|
brightness_stats = stats['brightness_stats']
|
|
pixel_int_stats = stats['pixel_int_stats']
|
|
suggestions = stats['suggestions']
|
|
|
|
print("=" * 60)
|
|
print("BRIGHTNESS ANALYSIS (0-255 scale)")
|
|
print("=" * 60)
|
|
print(f" Min: {brightness_stats['min']:.1f}")
|
|
print(f" Max: {brightness_stats['max']:.1f}")
|
|
print(f" Mean: {brightness_stats['mean']:.1f}")
|
|
print(f" Median: {brightness_stats['median']:.1f}")
|
|
print()
|
|
print(" Percentiles:")
|
|
print(f" 10%: {brightness_stats['p10']:.1f}")
|
|
print(f" 25%: {brightness_stats['p25']:.1f}")
|
|
print(f" 50%: {brightness_stats['median']:.1f} (median)")
|
|
print(f" 75%: {brightness_stats['p75']:.1f}")
|
|
print(f" 90%: {brightness_stats['p90']:.1f}")
|
|
print()
|
|
|
|
print("=" * 60)
|
|
print("PIXEL INTEGER ANALYSIS (signed 32-bit)")
|
|
print("=" * 60)
|
|
print(f" Min: {pixel_int_stats['min']:,}")
|
|
print(f" Max: {pixel_int_stats['max']:,}")
|
|
print(f" Median: {pixel_int_stats['median']:,}")
|
|
print()
|
|
print(" Percentiles:")
|
|
print(f" 25%: {pixel_int_stats['p25']:,}")
|
|
print(f" 50%: {pixel_int_stats['median']:,} (median)")
|
|
print(f" 75%: {pixel_int_stats['p75']:,}")
|
|
print()
|
|
|
|
print("=" * 60)
|
|
print("SUGGESTED THRESHOLDS")
|
|
print("=" * 60)
|
|
print()
|
|
print("For --interval bright (sorts pixels above threshold):")
|
|
print(f" Subtle (top 25%): --bright-value {suggestions['bright']['subtle']}")
|
|
print(f" Moderate (top 50%): --bright-value {suggestions['bright']['moderate']}")
|
|
print(f" Aggressive (top 75%): --bright-value {suggestions['bright']['aggressive']}")
|
|
print()
|
|
print("For --interval dark (sorts pixels below threshold):")
|
|
print(f" Subtle (bottom 25%): --dark-value {suggestions['dark']['subtle']}")
|
|
print(f" Moderate (bottom 50%): --dark-value {suggestions['dark']['moderate']}")
|
|
print(f" Aggressive (bottom 75%): --dark-value {suggestions['dark']['aggressive']}")
|
|
print()
|
|
print("For --interval white (sorts pixels above threshold):")
|
|
print(f" Subtle (top 25%): --white-value {suggestions['white']['subtle']}")
|
|
print(f" Moderate (top 50%): --white-value {suggestions['white']['moderate']}")
|
|
print(f" Aggressive (top 75%): --white-value {suggestions['white']['aggressive']}")
|
|
print()
|
|
print("For --interval black (sorts pixels below threshold):")
|
|
print(f" Subtle (bottom 25%): --black-value {suggestions['black']['subtle']}")
|
|
print(f" Moderate (bottom 50%): --black-value {suggestions['black']['moderate']}")
|
|
print(f" Aggressive (bottom 75%): --black-value {suggestions['black']['aggressive']}")
|
|
print()
|
|
print("=" * 60)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Pixel sorting with ASDF and extended modes',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
# ASDF-style modes (classic)
|
|
python main.py input.jpg --interval bright
|
|
python main.py input.jpg --interval dark --dark-value 50
|
|
|
|
# New interval modes
|
|
python main.py input.jpg --interval random --char-length 100
|
|
python main.py input.jpg --interval edges
|
|
python main.py input.jpg --interval waves --char-length 75
|
|
|
|
# Different sorting functions
|
|
python main.py input.jpg --interval bright --sort hue
|
|
python main.py input.jpg --interval threshold --sort saturation
|
|
|
|
# Angle and randomness
|
|
python main.py input.jpg --interval bright --angle 45
|
|
python main.py input.jpg --interval random --randomness 20
|
|
|
|
# Analyze image first
|
|
python main.py input.jpg --analyze
|
|
"""
|
|
)
|
|
|
|
parser.add_argument('input', type=str, help='Input image path')
|
|
parser.add_argument('--analyze', action='store_true',
|
|
help='Analyze image and suggest thresholds (does not sort)')
|
|
|
|
# Interval mode
|
|
parser.add_argument('-i', '--interval', type=str, default='bright',
|
|
choices=list(INTERVAL_FUNCTIONS.keys()),
|
|
help='Interval function (default: bright)')
|
|
|
|
# Sorting function
|
|
parser.add_argument('-s', '--sort', type=str, default='value',
|
|
choices=list(SORTING_FUNCTIONS.keys()),
|
|
help='Sorting function (default: value)')
|
|
|
|
# Output
|
|
parser.add_argument('-o', '--output', type=str, help='Output image path')
|
|
|
|
# Parameters
|
|
parser.add_argument('-a', '--angle', type=float, default=0,
|
|
help='Angle in degrees (default: 0)')
|
|
parser.add_argument('-r', '--randomness', type=float, default=0,
|
|
help='Randomness percentage 0-100 (default: 0)')
|
|
parser.add_argument('-c', '--char-length', type=int, default=50,
|
|
help='Characteristic length for random/waves (default: 50)')
|
|
parser.add_argument('-l', '--loops', type=int, default=1,
|
|
help='Number of sorting passes (default: 1)')
|
|
|
|
# Threshold values
|
|
parser.add_argument('--lower-threshold', type=float, default=0.25,
|
|
help='Lower threshold 0-1 for threshold mode (default: 0.25)')
|
|
parser.add_argument('--upper-threshold', type=float, default=0.8,
|
|
help='Upper threshold 0-1 for threshold mode (default: 0.8)')
|
|
parser.add_argument('--white-value', type=int, default=-12345678,
|
|
help='White threshold for white mode (default: -12345678)')
|
|
parser.add_argument('--black-value', type=int, default=-3456789,
|
|
help='Black threshold for black mode (default: -3456789)')
|
|
parser.add_argument('--bright-value', type=int, default=127,
|
|
help='Brightness threshold for bright mode (default: 127)')
|
|
parser.add_argument('--dark-value', type=int, default=223,
|
|
help='Darkness threshold for dark mode (default: 223)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Load image
|
|
input_path = Path(args.input)
|
|
if not input_path.exists():
|
|
print(f"Error: Input file '{args.input}' not found")
|
|
return 1
|
|
|
|
print(f"Loading image: {args.input}")
|
|
img = Image.open(input_path)
|
|
|
|
# If analyze mode, run analysis and exit
|
|
if args.analyze:
|
|
stats = analyze(img)
|
|
print_analysis(stats)
|
|
return 0
|
|
|
|
# Determine output path
|
|
if args.output:
|
|
output_path = Path(args.output)
|
|
else:
|
|
output_path = input_path.with_name(f"{input_path.stem}_{args.interval}_{args.sort}.png")
|
|
|
|
# Sort the image
|
|
print(f"Sorting with interval={args.interval}, sorting={args.sort}")
|
|
if args.angle != 0:
|
|
print(f" Angle: {args.angle}°")
|
|
if args.randomness > 0:
|
|
print(f" Randomness: {args.randomness}%")
|
|
if args.loops > 1:
|
|
print(f" Loops: {args.loops}")
|
|
|
|
result = pixelsort(
|
|
img,
|
|
interval=args.interval,
|
|
sorting=args.sort,
|
|
angle=args.angle,
|
|
randomness=args.randomness,
|
|
char_length=args.char_length,
|
|
loops=args.loops,
|
|
lower_threshold=args.lower_threshold,
|
|
upper_threshold=args.upper_threshold,
|
|
white_value=args.white_value,
|
|
black_value=args.black_value,
|
|
bright_value=args.bright_value,
|
|
dark_value=args.dark_value,
|
|
)
|
|
|
|
# Save output
|
|
print(f"Saving to: {output_path}")
|
|
result.save(output_path)
|
|
print("Done!")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|