**How to decide between Manual Annotation, Gradient-Field, and ML Segmentation** Planet Ruler offers three distinct methods for horizon detection, each with different trade-offs. This guide helps you choose the best method for your specific use case. Quick Decision Tree ------------------- .. mermaid:: flowchart TD Start([Start]) --> Q1{Is horizon
obstructed?} Q1 -->|YES| Q2{GPU Available?} Q1 -->|NO| Q3{Smooth horizon line?} Q2 -->|YES| ML1[ML Segmentation:
Interactive Mode] Q2 -->|NO| Manual1[Manual Annotation:
Click around obstructions] Q3 -->|YES| Q4{Prefer automated?} Q3 -->|NO| Q2 Q4 -->|YES| Q5{GPU Available?} Q4 -->|NO| Manual2[Manual Annotation:
Fast & foolproof] Q5 -->|YES| ML2[ML Segmentation:
Automatic mode] Q5 -->|NO| Gradient1[Gradient-Field:
Fast, lightweight] style Manual1 fill:#90EE90 style Manual2 fill:#90EE90 style Gradient1 fill:#87CEEB style ML1 fill:#FFB6C1 style ML2 fill:#FFB6C1 Method Overview --------------- Comparison Table ~~~~~~~~~~~~~~~~ .. list-table:: Detection & Fitting Method Comparison :header-rows: 1 :widths: 20 20 20 20 20 * - Feature - Manual Annotation - Gradient-Field - ML Segmentation - Sagitta * - **Setup Time** - Instant (built-in) - Instant (built-in) - 5-10 min (first time model download) - Instant (built-in) * - **Processing Time** - 30-120 sec (user-dependent) - 15-60 sec (automated) - 30-300 sec (model inference) - <5 sec * - **Dependencies** - None (tkinter only) - None (scipy only) - PyTorch + SAM (~2GB) - None * - **Memory Usage** - <100 MB - <200 MB - 2-4 GB - <100 MB * - **Accuracy** - Highest (user-controlled) - Good (clear horizons) - Variable (depends on scene) - Lower (best as first stage) * - **Robustness** - Works everywhere - Needs clear edges - Handles complexity - Needs detected limb * - **Reproducibility** - Low (user variation) - High (deterministic) - High (deterministic) - High (deterministic) * - **Batch Processing** - Not practical - Excellent - Good (if GPU available) - Excellent (as stage 1) * - **Best Used As** - Standalone or stage 2 - Standalone - Standalone - Stage 1 warm-start Method 1: Manual Annotation --------------------------- **Best for:** First-time users, educational settings, challenging images How It Works ~~~~~~~~~~~~~ Manual annotation uses an interactive GUI where you click points along the horizon. .. image:: images/manual_earth_native.png :alt: Screenshot of manual annotation :width: 800px It also lets you stretch the image vertically to exaggerate curvature and enhance accuracy. .. image:: images/manual_earth.png :alt: Screenshot of manual annotation using image stretch :width: 800px **Strengths:** | ✅ **Universal applicability** - Works with any image that has a visible horizon | ✅ **No dependencies** - Works immediately after installing Planet Ruler | ✅ **Educational** - Students learn by actively identifying the horizon | ✅ **Handles complexity** - Clouds, haze, wing, terrain? You decide what's horizon **Limitations:** | ❌ **User-dependent** - Different people get slightly different results | ❌ **Time-consuming** - Takes 30-120 seconds per image | ❌ **Not batch-friendly** - Must manually process each image | ❌ **Requires practice** - Takes a few tries to get good at point placement When to Use ~~~~~~~~~~~ Use manual annotation when: * You're analyzing 1-5 images * Image quality is poor (scratched windows, haze, clouds) * The horizon is ambiguous, obstructed, and/or complex * You want hands-on learning * You need to work immediately without dependencies Example Usage ~~~~~~~~~~~~~ .. code-block:: python import planet_ruler as pr # Load observation obs = pr.LimbObservation("image.jpg", "config.yaml") # Manual annotation (opens GUI) obs.detect_limb(detection_method="manual") # Fit annotated points obs.fit_arc(max_iter=1000) .. tip:: **Best practices for clicking:** * Cover as much horizontal area as you can * Click 10-20 points (more isn't always better) * Concentrate points where curvature is higher * Zoom in or use Stretch for precision * Right click (undo) or clear points to undo bad placements Visual Examples ~~~~~~~~~~~~~~~ .. list-table:: :widths: 50 50 :class: borderless * - .. figure:: ../demo/images/2013-08-05_22-42-14_Wikimania.jpg :width: 100% Raw image - .. figure:: images/manual_earth_native.png :width: 100% :height: 247px Human-Annotated * - .. figure:: images/manual_earth_fitted.png :width: 100% Planet radius fitted - .. figure:: images/manual_earth_residuals.png :width: 100% :height: 247px Fit residuals **Example 1: Clear Horizon** .. figure:: ../demo/images/PIA21341.jpg :width: 100% Manual annotation goes quickly with clear horizons. **Example 2: Obstructions** .. figure:: ../demo/images/iss064e002941.jpg :width: 100% User can avoid obstructions that can be tricky for automated methods. **Example 3: Complex Scene** .. figure:: images/plane-wing-airplane-aerial-fe58830ec52da51eed2a92d1a94e1e04.jpg :width: 100% Anything besides a human would struggle with this. Method 2: Gradient-Field Detection ---------------------------------- **Best for:** Batch processing, clear horizons, reproducible workflows How It Works ~~~~~~~~~~~~ Gradient-field detection skips explicit horizon detection entirely. Instead, it optimizes parameters directly on the image using brightness gradients perpendicular to the predicted horizon. .. figure:: images/good_gradient.png :width: 100% A 'good' horizon is one with high brightness gradient (flux) traversing its boundary. The method uses multi-resolution optimization (coarse → fine) to avoid local minima. **Strengths:** | ✅ **Fully automated** - No user interaction require | ✅ **Lightweight** - No ML models, low memory usage | ✅ **Reproducible** - Same image → same result every time | ✅ **Fast** - Processes images in around a minute | ✅ **Batch-friendly** - Perfect for processing hundreds of images | ✅ **Multi-resolution** - Robust to initialization **Limitations:** | ❌ **Needs clear edges** - Struggles with diffuse or gradual horizons | ❌ **Sensitive to obstruction** - Horizon obsturctions can confuse it | ❌ **No visual feedback** - You don't see the detected horizon until after fitting | ❌ **Parameter tuning** - May need to adjust smoothing parameters When to Use ~~~~~~~~~~~ Use gradient-field when: * You're batch processing many images (10+) * Horizons are sharp and well-defined * You want reproducible results * You don't have time for manual annotation * You want lightweight processing (no GPU needed) * Images are clean with minimal obstruction Example Usage ~~~~~~~~~~~~~ .. code-block:: python import planet_ruler as pr # Load observation obs = pr.LimbObservation("image.jpg", "config.yaml") # Gradient-field optimization (no detection step!) obs.fit_gradient( resolution_stages='auto', # Multi-resolution: 0.25 → 0.5 → 1.0 image_smoothing=2.0, # Remove high-freq artifacts kernel_smoothing=8.0, # Smooth gradient field minimizer='dual-annealing', minimizer_preset='balanced', max_iter=1000 ) # Note: No detect_limb() call needed! .. tip:: **Tuning parameters:** * Increase ``image_smoothing`` (2.0 → 4.0) for noisy images * Increase ``kernel_smoothing`` (8.0 → 16.0) for hazy horizons * Use ``prefer_direction="up"`` if above the horizon is darker than below * More resolution stages (e.g., [8,4,2,1]) or ``minimizer_preset='robust'`` for difficult cases Visual Examples ~~~~~~~~~~~~~~~ **Inside the Process** .. list-table:: :widths: 50 50 :class: borderless * - .. figure:: ../demo/images/50644513538_56228a2027_o.jpg :width: 100% Raw image - .. figure:: images/gradient_field_simple.png :width: 100% :height: 183px Gradient field * - .. figure:: images/fitted_gradient.png :width: 100% Planet radius fitted - .. figure:: images/good_gradient.png :width: 100% :height: 183px "Flux" through fitted radius **Example 1: ISS Earth Photo** .. figure:: images/good_gradient.png :width: 100% Caption: Gradient-field works perfectly on clean spacecraft imagery. **Example 2: New Horizons Photo** .. figure:: ../demo/images/PIA19948.jpg Caption: Hazy atmospheric boundary detected accurately. Multi-resolution helps. **Example 3: Failure Case** .. figure:: images/bad_gradient.png :width: 100% In this case it may have been better to go with manual annotation... Performance Notes ~~~~~~~~~~~~~~~~~ .. code-block:: text Typical timing (Intel i7, 2000x1500 image): Resolution stages [4, 2, 1]: - Stage 1 (500x375): 8 sec - Stage 2 (1000x750): 12 sec - Stage 3 (2000x1500): 20 sec Total: ~40 seconds Memory usage: <200 MB Method 3: ML Segmentation ------------------------- **Best for:** Complex scenes, when you have GPU + PyTorch installed How It Works ~~~~~~~~~~~~ ML segmentation such as Meta's Segment Anything Model (SAM) can be used to automatically detect the planetary body. In automatic mode (interactive=False), the model assumes the two largest masks are the planet and sky and labels their boundary as the horizon. .. list-table:: :widths: 33 33 33 :class: borderless * - .. figure:: ../demo/images/50644513538_56228a2027_o.jpg :width: 100% :height: 190px Original - .. figure:: images/segmented_earth.png :width: 100% :height: 190px Segmented image - .. figure:: images/segment_extracted_limb.png :width: 100% :height: 190px Detected limb When set to interactive, however, the user is allowed to validate which masks belong to the sky and planet (or which to exclude) before the horizon is determined. This can help with obscuring objects like airplane wings or clouds. Note this method still isn't foolproof -- stay tuned for updates! .. list-table:: :widths: 33 33 33 :class: borderless * - .. figure:: ../demo/images/2013-08-05_22-42-14_Wikimania.jpg :width: 100% :height: 190px Original - .. figure:: images/manual_segment.png :width: 100% :height: 190px User Mask Annotation - .. figure:: images/segmented_after_manual.png :width: 100% :height: 190px Detected limb **Strengths:** | ✅ **Handles complexity** - Can work with clouds, terrain, atmospheric layers | ✅ **Fully automated** - Can run with zero user interaction | ✅ **Semantic understanding** - "Knows" what a planet looks like | ✅ **Human-in-the-loop ready** - Can leverage user annotations for increased accuracy | ✅ **Reproducible** - Deterministic results **Limitations:** | ❌ **Heavy dependencies** - Requires PyTorch + SAM (~2GB model) | ❌ **Slow** - 30-300 seconds per image (CPU) or 5-20 seconds (GPU) | ❌ **Memory hungry** - Needs 2-4 GB RAM | ❌ **First-time setup** - Model download takes 5-10 minutes | ❌ **Not always accurate** - Can misidentify horizon with complex scenes | ❌ **Black box** - Hard to understand why it makes certain decisions When to Use ~~~~~~~~~~~ Use ML segmentation when: * You have PyTorch and GPU available * Scenes are complex (clouds, haze, terrain) * You want to avoid manual clicking * You're willing to accept occasional failures * Images have clear color/brightness differences at horizon * You're processing a moderate number of images (5-50) Example Usage ~~~~~~~~~~~~~ .. code-block:: python import planet_ruler as pr # First time only: model will auto-download (~2GB) # This takes 5-10 minutes on first use # Load observation obs = pr.LimbObservation("image.jpg", "config.yaml") # ML segmentation obs.detect_limb(detection_method="segmentation") # Always inspect the result! obs.plot() # If detection looks good, proceed obs.fit_arc(max_iter=1000) .. warning:: **Always visually inspect** ML segmentation results before fitting! The model can occasionally misidentify features as the horizon. If the detection looks wrong, use interactive mode or manual annotation instead. Installation ~~~~~~~~~~~~ .. code-block:: bash # Install PyTorch (CPU version) pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # Install Segment Anything Model pip install segment-anything # For GPU support (faster, requires CUDA) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 Method 4: Sagitta (Arc-Height) Estimation ----------------------------------------- **Best for:** Quick radius estimates, warm-starting a subsequent arc fit How It Works ~~~~~~~~~~~~ The sagitta method estimates the planetary radius directly from the vertical "sag" of the horizon arc — the pixel distance between the highest and lowest points of the detected limb. It runs a fast 2-D optimizer over curvature and tilt and does not need differential evolution, making it much faster than a full arc fit. Because it updates the parameter bounds automatically, it is especially useful as a **first stage** that narrows the search space for a subsequent ``fit_arc`` or ``fit_gradient`` call. **Strengths:** | ✅ **Very fast** — seconds rather than minutes | ✅ **No minimizer hyperparameters** — works out of the box | ✅ **Warm-starts downstream stages** — automatically tightens bounds **Limitations:** | ❌ **Requires a detected limb** — needs ``detect_limb()`` first | ❌ **Less precise than arc fit** — use as a first stage, not final answer When to Use ~~~~~~~~~~~ Use the sagitta method when: * You want a fast sanity-check radius before committing to a full fit * You want to warm-start a slower arc or gradient-field optimization * You are processing many images and speed is critical Example Usage ~~~~~~~~~~~~~ .. code-block:: python import planet_ruler as pr obs = pr.LimbObservation("image.jpg", "config.yaml") obs.detect_limb(detection_method="manual") # Stand-alone sagitta estimate (fast) obs.fit_sagitta() print(f"Quick radius estimate: {obs.best_parameters['r']/1000:.0f} km") # Or chain sagitta → arc for speed + accuracy (recommended combo) obs.fit_limb(stages=[ {"method": "sagitta"}, {"method": "arc", "minimizer": "differential-evolution", "minimizer_preset": "balanced"}, ]) print(f"Final radius: {obs.best_parameters['r']/1000:.0f} km") .. tip:: The sagitta → arc chain is the recommended default workflow for manual annotation in 2.0. Sagitta quickly finds a good starting radius and tightens the parameter bounds; the arc fitter then refines it precisely. Combining Methods ----------------- Best Practices Workflow ~~~~~~~~~~~~~~~~~~~~~~~ For critical measurements, use multiple methods and compare: .. code-block:: python import planet_ruler as pr from planet_ruler.uncertainty import calculate_parameter_uncertainty results = {} # Manual annotation → arc fit print("\nTrying manual method...") obs = pr.LimbObservation("image.jpg", "config.yaml") obs.detect_limb(detection_method='manual') obs.fit_arc() results['manual'] = obs # Gradient-field: no detection step needed print("\nTrying gradient method...") obs = pr.LimbObservation("image.jpg", "config.yaml") obs.fit_gradient(resolution_stages='auto') results['gradient'] = obs # ML segmentation → arc fit print("\nTrying ML segmentation method...") obs = pr.LimbObservation("image.jpg", "config.yaml") obs.detect_limb(detection_method='segmentation') obs.fit_arc() results['ml'] = obs # Compare results print("\nMethod comparison:") radii = {} for name, obs in results.items(): radius_result = calculate_parameter_uncertainty( obs, "r", scale_factor=1000, method='auto' ) radii[name] = radius_result['value'] print(f" {name}: {radius_result['value']:.1f} km") # Check consistency import numpy as np values = list(radii.values()) print(f"\nSpread: {np.max(values) - np.min(values):.1f} km") print(f"Mean: {np.mean(values):.1f} km") print(f"Std: {np.std(values):.1f} km") Troubleshooting Decision Guide ------------------------------ If Your Results Look Wrong ~~~~~~~~~~~~~~~~~~~~~~~~~~ **Problem:** Manual annotation gives inconsistent results * **Solution:** Click points with more care * **Solution:** Use zoom/stretch features for precision * **Solution:** Try gradient-field for comparison **Problem:** Gradient-field result is way off * **Check:** Is horizon clearly visible and sharp? * **Check:** Are there clouds or haze at horizon level? * **Solution:** Increase smoothing parameters (image_smoothing=4.0) * **Solution:** Add more resolution stages [8,4,2,1] * **Fallback:** Use manual annotation **Problem:** ML segmentation detects wrong features * **Check:** Visually inspect with ``obs.plot()`` before fitting * **Solution:** Try interactive mode to refine masks * **Solution:** Increase smoothing after detection * **Fallback:** Use manual annotation (always reliable) Summary ------- **Choose Manual Annotation if:** * You want maximum accuracy * You're analyzing 1-10 images * You're teaching or learning * Image quality is poor * You can spare 1-2 minutes per image **Choose Gradient-Field if:** * You're batch processing many images * Horizons are clean and sharp * You want reproducible results * You don't have GPU/PyTorch * Speed is important **Choose ML Segmentation if:** * You have PyTorch + GPU installed * Scenes are complex but horizon is visible * You want to experiment with AI methods * You're willing to visually inspect results * You have time for model download (first time) **Use Sagitta as Stage 1 when:** * You want a fast warm-start before a slower arc fit * You need a quick sanity-check radius estimate * You are batch processing and want to reduce full-fit time **When in doubt:** Start with manual annotation followed by a sagitta → arc staged fit (``obs.fit_limb(stages=[{"method": "sagitta"}, {"method": "arc"}])``). This is the recommended default workflow in 2.0. Next Steps ---------- * Try :doc:`tutorial_airplane` for a complete walkthrough with manual annotation * See :doc:`tutorial_gradient_field` for gradient-field examples * Check :doc:`examples` for real-world comparisons * Read :doc:`api` for advanced configuration options