ptpd-calibration-studio / examples /comprehensive_demo.py
ianshank's picture
fix: Address PR review feedback - improve logging, fix tests, remove unused code
0a485c6
#!/usr/bin/env python3
"""
Comprehensive Demonstration of Pt/Pd Calibration Studio
This script demonstrates all major features of the platinum/palladium
calibration system. Run this to see the full capabilities in action.
Usage:
python examples/comprehensive_demo.py
"""
import sys
import tempfile
from pathlib import Path
import numpy as np
from PIL import Image
# Add project to path if needed
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
def create_synthetic_step_tablet(num_patches: int = 21) -> Image.Image:
"""Create a synthetic step tablet image for demonstration."""
width, height = num_patches * 20, 100
patch_width = width // num_patches
img = np.zeros((height, width), dtype=np.uint8)
for i in range(num_patches):
# Create realistic density progression
value = 255 - int(255 * (i / (num_patches - 1)) ** 0.85)
x_start = i * patch_width
x_end = (i + 1) * patch_width
img[:, x_start:x_end] = value
# Add border
full_img = np.full((height + 40, width + 40, 3), 250, dtype=np.uint8)
full_img[20:height + 20, 20:width + 20, 0] = img
full_img[20:height + 20, 20:width + 20, 1] = img
full_img[20:height + 20, 20:width + 20, 2] = img
return Image.fromarray(full_img)
def create_test_image() -> Image.Image:
"""Create a test grayscale image for processing demos."""
arr = np.linspace(30, 220, 256).reshape(16, 16).astype(np.uint8)
arr = np.repeat(np.repeat(arr, 8, axis=0), 8, axis=1)
return Image.fromarray(arr, mode="L")
def demo_separator(title: str):
"""Print a visual separator for demo sections."""
print("\n" + "=" * 70)
print(f" {title}")
print("=" * 70 + "\n")
# =============================================================================
# DEMO 1: Step Tablet Reading and Density Extraction
# =============================================================================
def demo_step_tablet_reading():
"""Demonstrate step tablet reading capabilities."""
demo_separator("Step Tablet Reading & Density Extraction")
from ptpd_calibration.detection import StepTabletReader
# Create synthetic step tablet
step_tablet = create_synthetic_step_tablet(21)
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
step_tablet.save(f.name)
# Read step tablet
reader = StepTabletReader()
result = reader.read(f.name)
print(f"Step tablet analysis complete!")
print(f" - Patches detected: {result.extraction.num_patches}")
print(f" - Quality score: {result.extraction.overall_quality:.2%}")
densities = result.extraction.get_densities()
if densities:
print(f" - Dmin (paper white): {min(densities):.3f}")
print(f" - Dmax (maximum black): {max(densities):.3f}")
print(f" - Density range: {max(densities) - min(densities):.3f}")
if result.extraction.warnings:
print(f" - Warnings: {', '.join(result.extraction.warnings)}")
return result
# =============================================================================
# DEMO 2: Curve Generation
# =============================================================================
def demo_curve_generation(extraction_result=None):
"""Demonstrate curve generation capabilities."""
demo_separator("Curve Generation")
from ptpd_calibration.curves import CurveGenerator
from ptpd_calibration.core.types import CurveType
generator = CurveGenerator()
if extraction_result and extraction_result.extraction:
# Generate from extraction
curve = generator.generate_from_extraction(
extraction_result.extraction,
curve_type=CurveType.LINEAR,
name="Demo Calibration Curve",
paper_type="Arches Platine",
)
else:
# Generate from sample densities
from ptpd_calibration.core.models import CurveData
curve = CurveData(
name="Demo Calibration Curve",
input_values=[i / 20.0 for i in range(21)],
output_values=[min(1.0, (i / 20.0) ** 1.1) for i in range(21)],
)
print(f"Generated curve: {curve.name}")
print(f" - Input points: {len(curve.input_values)}")
print(f" - Output points: {len(curve.output_values)}")
print(f" - Input range: {curve.input_values[0]:.2f} - {curve.input_values[-1]:.2f}")
print(f" - Output range: {curve.output_values[0]:.2f} - {curve.output_values[-1]:.2f}")
return curve
# =============================================================================
# DEMO 3: Auto-Linearization
# =============================================================================
def demo_auto_linearization():
"""Demonstrate auto-linearization algorithms."""
demo_separator("Auto-Linearization")
from ptpd_calibration.curves import AutoLinearizer, LinearizationMethod, TargetResponse
# Sample density measurements from a step wedge
densities = [0.08, 0.15, 0.28, 0.45, 0.68, 0.95, 1.25, 1.48, 1.60]
linearizer = AutoLinearizer()
print("Testing different linearization methods:\n")
for method in [
LinearizationMethod.DIRECT_INVERSION,
LinearizationMethod.SPLINE_FIT,
LinearizationMethod.POLYNOMIAL_FIT,
]:
result = linearizer.linearize(
densities,
method=method,
curve_name=f"Test {method.value}",
)
print(f" {method.value}:")
print(f" - Residual error: {result.residual_error:.4f}")
print(f" - Max deviation: {result.max_deviation:.4f}")
print("\nTesting different target responses:\n")
for target in [TargetResponse.LINEAR, TargetResponse.GAMMA_22]:
result = linearizer.linearize(
densities,
target=target,
curve_name=f"Test {target.value}",
)
print(f" {target.value}: Generated {len(result.curve.output_values)} point curve")
# =============================================================================
# DEMO 4: Chemistry Calculator
# =============================================================================
def demo_chemistry_calculator():
"""Demonstrate chemistry calculation."""
demo_separator("Chemistry Calculator")
from ptpd_calibration.chemistry import ChemistryCalculator
calculator = ChemistryCalculator()
# Calculate for standard 8x10 print
recipe = calculator.calculate(
width_inches=8.0,
height_inches=10.0,
platinum_ratio=0.5, # 50% Pt / 50% Pd
)
print("Chemistry Recipe for 8x10\" print (50% Pt / 50% Pd):\n")
print(f" Coating area: {recipe.coating_area_sq_inches:.1f} sq inches")
print(f"\nSolution drops:")
print(f" - Ferric Oxalate: {recipe.ferric_oxalate_drops:.0f} drops")
print(f" - Platinum: {recipe.platinum_drops:.0f} drops")
print(f" - Palladium: {recipe.palladium_drops:.0f} drops")
print(f" - Na2 (contrast): {recipe.na2_drops:.0f} drops")
print(f" - Total: {recipe.total_drops:.0f} drops")
print(f"\nIn milliliters:")
print(f" - Total solution: {recipe.total_ml:.2f} ml")
# Show standard sizes
print("\nStandard print sizes available:")
for name, (w, h) in ChemistryCalculator.get_standard_sizes().items():
print(f" - {name}: {w}\" x {h}\"")
# =============================================================================
# DEMO 5: Exposure Calculator
# =============================================================================
def demo_exposure_calculator():
"""Demonstrate exposure calculation."""
demo_separator("Exposure Calculator")
from ptpd_calibration.exposure import ExposureCalculator, ExposureSettings, LightSource
# Set up base exposure settings
settings = ExposureSettings(
base_exposure_minutes=10.0,
base_negative_density=1.6,
light_source=LightSource.BL_FLUORESCENT,
platinum_ratio=0.5,
)
calculator = ExposureCalculator(settings)
# Calculate exposure for different negative densities
print("Exposure calculations for different negative densities:\n")
print(f" Base: {settings.base_exposure_minutes} min at density {settings.base_negative_density}")
print()
for density in [1.4, 1.6, 1.8, 2.0]:
result = calculator.calculate(negative_density=density)
print(f" Density {density}: {result.format_time()}")
# Generate test strip
print("\nTest strip exposures (centered on 10 min, 0.5 stop increments):")
test_times = calculator.calculate_test_strip(
center_exposure=10.0,
steps=5,
increment_stops=0.5,
)
for i, time in enumerate(test_times, 1):
print(f" Strip {i}: {time:.1f} min")
# Show available light sources
print("\nSupported light sources:")
for source_name, multiplier in calculator.get_light_sources()[:5]:
print(f" - {source_name}")
# =============================================================================
# DEMO 6: Zone System Analysis
# =============================================================================
def demo_zone_system():
"""Demonstrate zone system analysis."""
demo_separator("Zone System Analysis")
from ptpd_calibration.zones import ZoneMapper, Zone
# Create test image
test_image = create_test_image()
mapper = ZoneMapper()
analysis = mapper.analyze_image(test_image)
print(f"Zone System Analysis:\n")
print(f" Development recommendation: {analysis.development_adjustment}")
print(f"\nZone distribution:")
for zone in Zone:
count = analysis.zone_histogram.get(zone, 0)
bar = "β–ˆ" * int(count * 50)
print(f" Zone {zone.value:>2}: {bar} ({count:.1%})")
# =============================================================================
# DEMO 7: Histogram Analysis
# =============================================================================
def demo_histogram_analysis():
"""Demonstrate histogram analysis."""
demo_separator("Histogram Analysis")
from ptpd_calibration.imaging import HistogramAnalyzer
test_image = create_test_image()
analyzer = HistogramAnalyzer()
result = analyzer.analyze(test_image)
print(f"Histogram Analysis:\n")
print(f" Image size: {result.image_size}")
print(f" Mean: {result.stats.mean:.1f}")
print(f" Median: {result.stats.median:.1f}")
print(f" Std dev: {result.stats.std_dev:.1f}")
print(f" Dynamic range: {result.stats.dynamic_range:.0f} levels")
print(f" Brightness: {result.stats.brightness:.2f}")
print(f" Contrast: {result.stats.contrast:.2f}")
if result.stats.highlight_clipping_percent > 0:
print(f" Highlight clipping: {result.stats.highlight_clipping_percent:.1f}%")
if result.stats.shadow_clipping_percent > 0:
print(f" Shadow clipping: {result.stats.shadow_clipping_percent:.1f}%")
# =============================================================================
# DEMO 8: Soft Proofing
# =============================================================================
def demo_soft_proofing():
"""Demonstrate soft proofing simulation."""
demo_separator("Soft Proofing")
from ptpd_calibration.proofing import SoftProofer, ProofSettings, PaperSimulation
test_image = create_test_image()
print("Soft proofing on different papers:\n")
for paper in [
PaperSimulation.ARCHES_PLATINE,
PaperSimulation.BERGGER_COT320,
PaperSimulation.STONEHENGE,
]:
settings = ProofSettings.from_paper_preset(paper)
proofer = SoftProofer(settings)
result = proofer.proof(test_image)
print(f" {paper.value}:")
print(f" - Dmax: {settings.paper_dmax}")
print(f" - Output size: {result.image.size}")
print(f" - Notes: {result.notes[0] if result.notes else 'None'}")
# =============================================================================
# DEMO 9: Paper Profiles
# =============================================================================
def demo_paper_profiles():
"""Demonstrate paper profiles database."""
demo_separator("Paper Profiles Database")
from ptpd_calibration.papers import PaperDatabase
db = PaperDatabase()
papers = db.list_papers()
print(f"Available paper profiles ({len(papers)} papers):\n")
for paper in papers[:6]: # Show first 6
print(f" {paper.name}")
if paper.characteristics:
print(f" - Dmax: {paper.characteristics.typical_dmax}")
print(f" - Sizing: {paper.characteristics.sizing}")
print(f" - Surface: {paper.characteristics.surface}")
# =============================================================================
# DEMO 10: Digital Negative Creation
# =============================================================================
def demo_digital_negative():
"""Demonstrate digital negative creation."""
demo_separator("Digital Negative Creation")
from ptpd_calibration.imaging import ImageProcessor
from ptpd_calibration.core.models import CurveData
test_image = create_test_image()
# Create a simple correction curve
curve = CurveData(
name="Demo Curve",
input_values=[i / 10.0 for i in range(11)],
output_values=[min(1.0, (i / 10.0) ** 1.1) for i in range(11)],
)
processor = ImageProcessor()
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
test_image.save(f.name)
# Preview curve effect
original, processed = processor.preview_curve_effect(f.name, curve)
print(f"Curve preview generated:")
print(f" - Original: {original.size}")
print(f" - Processed: {processed.size}")
# Create digital negative
result = processor.create_digital_negative(
f.name,
curve=curve,
invert=True,
)
print(f"\nDigital negative created:")
print(f" - Size: {result.image.size}")
print(f" - Mode: {result.image.mode}")
print(f" - Inverted: Yes")
# =============================================================================
# DEMO 11: Session Logging
# =============================================================================
def demo_session_logging():
"""Demonstrate print session logging."""
demo_separator("Print Session Logging")
from ptpd_calibration.session import SessionLogger, PrintRecord
from ptpd_calibration.session.logger import ChemistryUsed, PrintResult
with tempfile.TemporaryDirectory() as tmp_dir:
logger = SessionLogger(storage_dir=Path(tmp_dir) / "sessions")
# Start a session
session = logger.start_session("Demo Print Session")
# Log some prints
for i, (paper, rating) in enumerate([
("Arches Platine", PrintResult.EXCELLENT),
("Bergger COT320", PrintResult.GOOD),
("Stonehenge", PrintResult.ACCEPTABLE),
]):
record = PrintRecord(
image_name=f"Test Image {i+1}",
paper_type=paper,
exposure_time_minutes=10.0 + i * 2,
chemistry=ChemistryUsed(
ferric_oxalate_drops=12.0,
platinum_drops=6.0,
palladium_drops=6.0,
),
result=rating,
)
logger.log_print(record)
# Get statistics
current = logger.get_current_session()
stats = current.get_statistics()
print(f"Session: {session.name}")
print(f"\nSession statistics:")
print(f" - Total prints: {stats['total_prints']}")
print(f" - Success rate: {stats['success_rate']}")
print(f" - Avg exposure: {stats['avg_exposure_minutes']:.1f} min")
print(f" - Papers used: {', '.join(stats['papers_used'])}")
# =============================================================================
# DEMO 12: Curve Export Formats
# =============================================================================
def demo_curve_export():
"""Demonstrate curve export to different formats."""
demo_separator("Curve Export Formats")
from ptpd_calibration.curves import save_curve
from ptpd_calibration.core.models import CurveData
curve = CurveData(
name="Demo Export Curve",
input_values=[i / 255.0 for i in range(256)],
output_values=[min(1.0, (i / 255.0) ** 1.1) for i in range(256)],
paper_type="Arches Platine",
chemistry_notes="50% Pt / 50% Pd",
)
print("Supported export formats:\n")
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
for fmt in ["qtr", "csv", "json"]:
export_path = tmp_path / f"demo_curve.{fmt}"
save_curve(curve, export_path, format=fmt)
size = export_path.stat().st_size
print(f" - {fmt.upper()}: {size} bytes")
# Show preview
content = export_path.read_text()[:200]
print(f" Preview: {content[:100]}...")
# =============================================================================
# MAIN DEMO RUNNER
# =============================================================================
def run_all_demos():
"""Run all demonstration functions."""
print("\n" + "β–ˆ" * 70)
print("β–ˆ" + " " * 68 + "β–ˆ")
print("β–ˆ" + " PLATINUM/PALLADIUM CALIBRATION STUDIO - COMPREHENSIVE DEMO ".center(68) + "β–ˆ")
print("β–ˆ" + " " * 68 + "β–ˆ")
print("β–ˆ" * 70)
demos = [
("Step Tablet Reading", demo_step_tablet_reading),
("Curve Generation", demo_curve_generation),
("Auto-Linearization", demo_auto_linearization),
("Chemistry Calculator", demo_chemistry_calculator),
("Exposure Calculator", demo_exposure_calculator),
("Zone System Analysis", demo_zone_system),
("Histogram Analysis", demo_histogram_analysis),
("Soft Proofing", demo_soft_proofing),
("Paper Profiles", demo_paper_profiles),
("Digital Negative Creation", demo_digital_negative),
("Session Logging", demo_session_logging),
("Curve Export Formats", demo_curve_export),
]
results = {}
last_result = None
for name, demo_func in demos:
try:
if name == "Curve Generation" and last_result and hasattr(last_result, "extraction"):
result = demo_func(last_result)
else:
result = demo_func()
if name == "Step Tablet Reading":
last_result = result
results[name] = "βœ“ Success"
except Exception as e:
results[name] = f"βœ— Error: {e}"
print(f"\nError in {name}: {e}")
# Summary
demo_separator("DEMO SUMMARY")
print("Demo Results:\n")
for name, status in results.items():
print(f" {name}: {status}")
successes = sum(1 for s in results.values() if s.startswith("βœ“"))
print(f"\n Total: {successes}/{len(demos)} demos completed successfully")
print("\n" + "=" * 70)
print(" Demo complete! For more information, see README.md")
print("=" * 70 + "\n")
if __name__ == "__main__":
run_all_demos()