This tutorial demonstrates the gradient-field detection method, which provides automated horizon detection without requiring ML dependencies or manual annotation.
When to Use Gradient-Field
Best for:
Clear, well-defined horizons with strong gradients
Atmospheric limbs without complex cloud structure
Batch processing multiple images
When PyTorch/ML dependencies are unavailable
Situations where reproducibility is critical
Not ideal for:
Horizons with multiple strong edges (use manual)
Very noisy or low-contrast images (use manual)
Complex cloud structures (use segmentation or manual)
Prerequisites
Python 3.8+ with Planet Ruler installed
Clear horizon photograph
Camera configuration file or auto-config
Step 1: Basic Gradient-Field Measurement
import planet_ruler.observation as obs
from planet_ruler.uncertainty import calculate_parameter_uncertainty
# Load observation
observation = obs.LimbObservation(
image_filepath="demo/images/ISS_Earth_horizon.jpg",
fit_config="config/earth_iss_1.yaml"
)
# Gradient-field detection will do nothing (it isn't needed!)
observation.detect_limb(detection_method="gradient-field")
# Fitting is where the magic happens instead
observation.fit_limb(
loss_function="gradient_field", # Need to specify this!
minimizer='dual-annealing',
resolution_stages='auto', # Multi-resolution optimization
maxiter=3000
)
# Calculate uncertainty
result = calculate_parameter_uncertainty(
observation, "r",
scale_factor=1000,
method='auto'
)
print(f"Radius: {result['uncertainty']:.1f} km")
observation.plot()
Step 2: Understanding Gradient-Field Parameters
The gradient-field method has several configurable parameters:
# Configure gradient-field detection
observation.fit_limb(
minimizer = "dual-annealing",
loss_function="gradient_field",
image_smoothing=2.0,
kernel_smoothing=8.0,
directional_smoothing=50,
directional_decay_rate=0.10, # Directional smoothing fall-off.
prefer_direction="up" # A trick that can help if you know that the horizon is darker along the top (up) or the bottom (down)
)
Parameter Effects:
image_smoothing: Controls image smoothing _before_ gradient calculation. Helps with artifacts and other local minima.
Lower (0.5): Preserves fine details, more sensitive to noise
Higher (2.0): Smoother gradients, less noise sensitivity
kernel_smoothing: Controls image smoothing _after_ gradient calculation. Helps to homogenize noisy gradient directions.
Lower (1.0): Preserves fine details, more sensitive to noise
Higher (8.0): Smoother gradients, less noise sensitivity
directional_smoothing: Directional smoothing distance (along gradients). Helps avoid vanishing gradient in loss function.
Larger values (50): Casts a wide net to keep optimization from getting stuck. Can distort results depending on strength and how far the limb is from the edge of frame.
Smaller values (5): Slight speedup to optimization if already converging, but too low and fit may not converge to the true minimum.
directional_decay_rate: Rate of exponential decay in directional sampling
Larger values: Faster decay, emphasizes nearby pixels
Smaller values: Slower decay, includes more distant pixels
Step 3: Multi-Resolution Optimization
Gradient-field detection benefits greatly from multi-resolution optimization:
# Automatic multi-resolution (recommended)
observation.fit_limb(
minimizer="dual-annealing",
loss_function="gradient_field",
resolution_stages='auto', # Automatically generates stages
maxiter=1000
)
# Manual multi-resolution configuration
observation.fit_limb(
minimizer="dual-annealing"
loss_function="gradient_field",
resolution_stages=[4, 2, 1], # Downsample factors: 4x → 2x → 1x
maxiter=1000
)
# Single resolution (faster but may miss global optimum)
observation.fit_limb(
minimizer="dual-annealing",
loss_function="gradient_field",
resolution_stages=None, # No multi-resolution
maxiter=1000
)
Multi-Resolution Benefits:
Avoids local minima by starting coarse
Progressively refines solution
More robust convergence
Slightly slower but much more reliable
Step 4: Choosing Minimizers for Gradient-Field
Different minimizers have different characteristics:
# Differential evolution: Best for complex problems
observation.fit_limb(
minimizer='differential-evolution',
resolution_stages='auto',
popsize=15,
maxiter=1000,
seed=42
)
# Pros: Most robust, provides population for uncertainty
# Cons: Slowest
# Dual annealing: Good balance
observation.fit_limb(
minimizer='dual-annealing',
resolution_stages='auto',
maxiter=1000,
seed=42
)
# Pros: Fast, good global optimization
# Cons: No population (use Hessian for uncertainty)
# Basinhopping: Fastest
observation.fit_limb(
minimizer='basinhopping',
resolution_stages='auto',
niter=100,
seed=42
)
# Pros: Fastest optimization
# Cons: May miss global optimum, use with multi-resolution
Step 5: Visualizing Gradient-Field Results
import matplotlib.pyplot as plt
import numpy as np
# Create comprehensive visualization
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Original image
axes[0, 0].imshow(observation.image_data)
axes[0, 0].set_title("Original Image")
axes[0, 0].axis('off')
# Gradient field
observation.plot(gradient=True, ax=axes[0, 1], show=False)
axes[0, 1].set_title("Gradient Field")
# Detected limb overlay
axes[1, 0].imshow(observation.image_data)
x = np.arange(len(observation.features["limb"]))
axes[1, 0].plot(x, observation.features["limb"], 'r-', linewidth=2)
axes[1, 0].set_title("Detected Limb")
axes[1, 0].axis('off')
# Fit quality
x = np.arange(len(observation.features["limb"]))
axes[1, 1].plot(x, observation.features["limb"], 'b-',
linewidth=2, label="Detected")
# Theoretical limb
final_params = observation.init_parameter_values.copy()
final_params.update(observation.best_parameters)
theoretical = planet_ruler.geometry.limb_arc(
n_pix_x=len(observation.features["limb"]),
n_pix_y=observation.image_data.shape[0],
**final_params
)
axes[1, 1].plot(x, theoretical, 'r--',
linewidth=2, label="Fitted model")
axes[1, 1].set_title("Fit Quality")
axes[1, 1].set_xlabel("Pixel X")
axes[1, 1].set_ylabel("Pixel Y")
axes[1, 1].legend()
axes[1, 1].grid(alpha=0.3)
plt.tight_layout()
plt.show()
Step 6: Batch Processing with Gradient-Field
The gradient-field method is ideal for batch processing:
from pathlib import Path
import pandas as pd
# Process multiple images
image_dir = Path("demo/images/")
image_files = list(image_dir.glob("*_horizon_*.jpg"))
results = []
for image_file in image_files:
print(f"Processing {image_file.name}...")
try:
# Load and process
obs = obs.LimbObservation(
str(image_file),
"config/earth_iss_1.yaml"
)
# Automated detection
obs.fit_limb(
minimizer='dual-annealing',
loss_function="gradient_field",
resolution_stages='auto',
maxiter=500 # Reduce for speed
)
# Calculate uncertainty
result = calculate_parameter_uncertainty(
obs, "r", scale_factor=1000, method='auto'
)
results.append({
'file': image_file.name,
'radius_km': result['uncertainty'],
'status': 'success'
})
except Exception as e:
results.append({
'file': image_file.name,
'radius_km': None,
'status': f'failed: {str(e)}'
})
# Summary
df = pd.DataFrame(results)
print(df)
# Save results
df.to_csv("batch_results.csv", index=False)