Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions .claude/settings.local.json

This file was deleted.

13 changes: 12 additions & 1 deletion python/PiFinder/auto_exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,17 @@ def _analyze_image_viability(self, image: Image.Image) -> tuple:
# Calculate metrics
mean = float(np.mean(img_array))
std = float(np.std(img_array))
saturated = np.sum(img_array > 250)

# Determine saturation threshold based on image bit depth
# 8-bit images: 0-255 range, saturate at ~250 (98% of max)
# 16-bit images: 0-65535 range, saturate at ~64000 (98% of max)
img_max = np.max(img_array)
if img_max > 1000: # Likely 16-bit (range 0-65535)
saturation_threshold = 64000 # 98% of 16-bit range
else: # 8-bit (range 0-255)
saturation_threshold = 250 # 98% of 8-bit range

saturated = np.sum(img_array > saturation_threshold)
saturation_pct = (saturated / img_array.size) * 100

# Viability criteria from test_find_min_exposure.py
Expand All @@ -489,6 +499,7 @@ def _analyze_image_viability(self, image: Image.Image) -> tuple:
"mean": mean,
"std": std,
"saturation_pct": saturation_pct,
"saturation_threshold": saturation_threshold,
"has_signal": has_signal,
"has_structure": has_structure,
"not_saturated": not_saturated,
Expand Down
39 changes: 33 additions & 6 deletions python/PiFinder/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,43 @@ def update_sqm_dual_pipeline(
if raw_array is not None:
raw_array = np.asarray(raw_array, dtype=np.float32)

# Calculate raw SQM
# Scale centroids and apertures to match raw image size
# Processed image is 512x512, raw image is larger (e.g., 1088x1088 for IMX296)
raw_height, raw_width = raw_array.shape
scale_factor = raw_width / 512.0

logger.info(
f"Raw SQM scaling: image={raw_width}x{raw_height}, "
f"scale={scale_factor:.2f}x, n_centroids={len(centroids)}"
)

# Scale centroids (y, x) coordinates
centroids_raw = [(y * scale_factor, x * scale_factor) for y, x in centroids]

# Scale aperture radii proportionally
aperture_radius_raw = int(aperture_radius * scale_factor)
annulus_inner_radius_raw = int(annulus_inner_radius * scale_factor)
annulus_outer_radius_raw = int(annulus_outer_radius * scale_factor)

logger.info(
f"Raw SQM apertures: r={aperture_radius_raw}, "
f"annulus={annulus_inner_radius_raw}-{annulus_outer_radius_raw}"
)

# Scale solution FOV to match raw image (FOV is same, but pixel scale changes)
solution_raw = solution.copy()
# FOV in degrees stays the same, SQM calc will recalculate arcsec/pixel from image size

# Calculate raw SQM with scaled parameters
sqm_value_raw, _ = sqm_calculator_raw.calculate(
centroids=centroids,
solution=solution,
centroids=centroids_raw,
solution=solution_raw,
image=raw_array,
exposure_sec=exposure_sec,
altitude_deg=altitude_deg,
aperture_radius=aperture_radius,
annulus_inner_radius=annulus_inner_radius,
annulus_outer_radius=annulus_outer_radius,
aperture_radius=aperture_radius_raw,
annulus_inner_radius=annulus_inner_radius_raw,
annulus_outer_radius=annulus_outer_radius_raw,
)

except Exception as e:
Expand Down
30 changes: 22 additions & 8 deletions python/PiFinder/sqm/sqm.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,25 @@ def __init__(
else:
logger.info("SQM initialized with manual pedestal mode")

def _calc_field_parameters(self, fov_degrees: float) -> None:
"""Calculate field of view parameters."""
def _calc_field_parameters(self, fov_degrees: float, image_shape: tuple) -> None:
"""Calculate field of view parameters based on actual image size.

Args:
fov_degrees: Field of view in degrees
image_shape: (height, width) of the image being analyzed
"""
self.fov_degrees = fov_degrees
self.field_arcsec_squared = (fov_degrees * 3600) ** 2
self.pixels_total = 512**2
# Use actual image dimensions, not hardcoded 512x512
height, width = image_shape
self.pixels_total = height * width
self.arcsec_squared_per_pixel = self.field_arcsec_squared / self.pixels_total

logger.info(
f"SQM field params: {width}x{height} pixels, FOV={fov_degrees:.2f}°, "
f"scale={np.sqrt(self.arcsec_squared_per_pixel):.2f}\"/px"
)

def _calculate_background(
self, image: np.ndarray, centroids: np.ndarray, exclusion_radius: int
) -> float:
Expand Down Expand Up @@ -380,7 +392,7 @@ def calculate(
return None, {}

fov_estimate = solution["FOV"]
self._calc_field_parameters(fov_estimate)
self._calc_field_parameters(fov_estimate, image.shape)

# Validate solution has matched stars
if "matched_centroids" not in solution or "matched_stars" not in solution:
Expand Down Expand Up @@ -584,10 +596,12 @@ def calculate(
"star_mzeros": mzeros,
}

logger.debug(
f"SQM: mzero={mzero:.2f}±{np.std(valid_mzeros_for_stats):.2f}, "
f"bg={background_flux_density:.6f} ADU/arcsec², pedestal={pedestal:.2f}, "
f"raw={sqm_raw:.2f}, extinction={extinction_correction:.2f}, final={sqm_final:.2f}"
logger.info(
f"SQM calc: mzero={mzero:.2f}±{np.std(valid_mzeros_for_stats):.2f}, "
f"bg_px={background_per_pixel:.1f}, pedestal={pedestal:.1f}, "
f"bg_corr={background_corrected:.1f}, "
f"bg_density={background_flux_density:.2f} ADU/arcsec², "
f"raw={sqm_raw:.2f}, ext={extinction_correction:.2f}, final={sqm_final:.2f}"
)

return sqm_final, details
Expand Down
172 changes: 165 additions & 7 deletions python/PiFinder/ui/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ def __init__(
# anim timer stuff
self.last_update_time = time.time()

# Rotating info: alternates between constellation and SQM value
self.rotating_mode = "constellation" # "constellation" or "sqm"
self.rotating_last_switch = time.time()
self.rotating_switch_interval = 3.0 # Switch every 3 seconds
self.rotating_animation_progress = 1.0 # 0.0 = transitioning, 1.0 = stable
self.rotating_animation_speed = 0.15 # Quick fade: ~7 frames at 30fps

def active(self):
"""
Called when a module becomes active
Expand Down Expand Up @@ -196,6 +203,159 @@ def message(self, message, timeout: float = 2, size=(5, 44, 123, 84)):

self.ui_state.set_message_timeout(timeout + time.time())

def _update_rotating_state(self):
"""
Update rotating info state. Handles timing and animation progress.
"""
# Check if it's time to switch modes
current_time = time.time()
if current_time - self.rotating_last_switch >= self.rotating_switch_interval:
# Switch mode
if self.rotating_mode == "constellation":
self.rotating_mode = "sqm"
else:
self.rotating_mode = "constellation"
self.rotating_last_switch = current_time
self.rotating_animation_progress = 0.0 # Start fade transition

# Animate if in progress
if self.rotating_animation_progress < 1.0:
self.rotating_animation_progress = min(
1.0, self.rotating_animation_progress + self.rotating_animation_speed
)

def _get_rotating_content(self):
"""
Get current and previous content for rotating display.
Returns: (current_text, previous_text)
"""
# Get content to display based on current mode
if self.rotating_mode == "constellation":
# Get constellation from solution
solution = self.shared_state.solution()
if solution and solution.get("constellation"):
current_text = solution["constellation"]
else:
current_text = "---"
# Previous was SQM
sqm_state = self.shared_state.sqm()
if sqm_state and sqm_state.value:
previous_text = f"{sqm_state.value:.1f}"
else:
previous_text = "---"
else:
# Get SQM value (1 decimal place)
sqm_state = self.shared_state.sqm()
if sqm_state and sqm_state.value:
current_text = f"{sqm_state.value:.1f}"
else:
current_text = "---"
# Previous was constellation
solution = self.shared_state.solution()
if solution and solution.get("constellation"):
previous_text = solution["constellation"]
else:
previous_text = "---"

return current_text, previous_text

def _draw_titlebar_rotating_info(self, x, y, fg):
"""
Draw rotating constellation/SQM in title bar with quick cross-fade.

Args:
x: X coordinate for text
y: Y coordinate for text
fg: Foreground color value (already from colors.get())
"""
self._update_rotating_state()
current_text, previous_text = self._get_rotating_content()

# Quick cross-fade: fade out old, fade in new
# Progress: 0.0 = show previous, 1.0 = show current
# Use brightness values 0-64 for title bar

if self.rotating_animation_progress < 1.0:
# During transition: fade out, then fade in (not overlapping)
# Title bar background is gray (64), text is black (0)
# Fade out: 0 → 64 (black to gray = invisible)
# Fade in: 64 → 0 (gray to black = visible)

if self.rotating_animation_progress < 0.5:
# First half: fade out previous (black → gray)
brightness = int(64 * self.rotating_animation_progress * 2)
self.draw.text(
(x, y),
previous_text,
font=self.fonts.bold.font,
fill=self.colors.get(brightness),
)
else:
# Second half: fade in current (gray → black)
brightness = int(64 * (1.0 - (self.rotating_animation_progress - 0.5) * 2))
self.draw.text(
(x, y),
current_text,
font=self.fonts.bold.font,
fill=self.colors.get(brightness),
)
else:
# Stable: show current in black for visibility
self.draw.text(
(x, y),
current_text,
font=self.fonts.bold.font,
fill=self.colors.get(0),
)

def draw_rotating_info(self, x=10, y=92, font=None):
"""
Draw rotating display that alternates between constellation and SQM value.
Uses quick cross-fade transition.

Args:
x: X coordinate for text
y: Y coordinate for text
font: Font to use (defaults to self.fonts.bold.font)
"""
if font is None:
font = self.fonts.bold.font

self._update_rotating_state()
current_text, previous_text = self._get_rotating_content()

# Sequential fade: fade out, then fade in (not overlapping)
if self.rotating_animation_progress < 1.0:
# Progress 0.0-0.5: fade out previous (255 → 0)
# Progress 0.5-1.0: fade in current (0 → 255)

if self.rotating_animation_progress < 0.5:
# First half: fade out previous
brightness = int(255 * (1.0 - self.rotating_animation_progress * 2))
self.draw.text(
(x, y),
previous_text,
font=font,
fill=self.colors.get(brightness),
)
else:
# Second half: fade in current
brightness = int(255 * (self.rotating_animation_progress - 0.5) * 2)
self.draw.text(
(x, y),
current_text,
font=font,
fill=self.colors.get(brightness),
)
else:
# Stable: show current at full brightness
self.draw.text(
(x, y),
current_text,
font=font,
fill=self.colors.get(255),
)

def screen_update(self, title_bar=True, button_hints=True) -> None:
"""
called to trigger UI updates
Expand Down Expand Up @@ -267,13 +427,11 @@ def screen_update(self, title_bar=True, button_hints=True) -> None:
)

if len(self.title) < 9:
# draw the constellation
constellation = solution["constellation"]
self.draw.text(
(self.display_class.resX * 0.54, 1),
constellation,
font=self.fonts.bold.font,
fill=fg if self._unmoved else self.colors.get(32),
# Draw rotating constellation/SQM wheel (replaces static constellation)
self._draw_titlebar_rotating_info(
x=int(self.display_class.resX * 0.54),
y=1,
fg=fg if self._unmoved else self.colors.get(32),
)
else:
# no solve yet....
Expand Down
Loading
Loading