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
Binary file modified astro_data/pifinder_objects.db
Binary file not shown.
2 changes: 2 additions & 0 deletions default_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"chart_dso": 128,
"chart_reticle": 128,
"chart_constellations": 64,
"image_nsew": true,
"image_bbox": true,
"solve_pixel": [256, 256],
"gps_type": "ublox",
"gps_baud_rate": 9600,
Expand Down
131 changes: 131 additions & 0 deletions python/PiFinder/cat_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
to handle catalog image loading
"""

import math
import os
from PIL import Image, ImageChops, ImageDraw
from PiFinder import image_util
Expand All @@ -19,6 +20,55 @@
logger = logging.getLogger("Catalog.Images")


def cardinal_vectors(image_rotate, fx=1, fy=1):
"""Return (nx, ny), (ex, ey) unit vectors for North and East.

image_rotate: degrees the POSS image was rotated (180 + roll).
fx, fy: -1 to mirror that axis (flip/flop), +1 otherwise.
"""
theta = math.radians(image_rotate)
n = (fx * math.sin(theta), fy * -math.cos(theta))
e = (-fx * math.cos(theta), -fy * math.sin(theta))
return n, e


def size_overlay_points(extents, pa, image_rotate, px_per_arcsec, cx, cy, fx=1, fy=1):
"""Compute outline points for the size overlay.

Returns a list of (x, y) tuples.
For 1 extent returns None (caller should use native ellipse).
"""
if not extents or len(extents) == 1:
return None

theta = math.radians(image_rotate - pa - 90)
cos_t = math.cos(theta)
sin_t = math.sin(theta)

points = []
if len(extents) == 2:
rx = extents[0] * px_per_arcsec / 2
ry = extents[1] * px_per_arcsec / 2
for i in range(36):
t = 2 * math.pi * i / 36
x = rx * math.cos(t)
y = ry * math.sin(t)
points.append(
(cx + fx * (x * cos_t - y * sin_t), cy + fy * (x * sin_t + y * cos_t))
)
else:
step = 2 * math.pi / len(extents)
for i, ext in enumerate(extents):
angle = i * step - math.pi / 2
r = ext * px_per_arcsec / 2
x = r * math.cos(angle)
y = r * math.sin(angle)
points.append(
(cx + fx * (x * cos_t - y * sin_t), cy + fy * (x * sin_t + y * cos_t))
)
return points


def get_display_image(
catalog_object,
eyepiece_text,
Expand All @@ -27,6 +77,9 @@ def get_display_image(
display_class,
burn_in=True,
magnification=None,
telescope=None,
show_nsew=True,
show_bbox=True,
):
"""
Returns a 128x128 image buffer for
Expand All @@ -37,6 +90,8 @@ def get_display_image(
roll:
degrees
"""
flip = telescope.flip_image if telescope else False
flop = telescope.flop_image if telescope else False

object_image_path = resolve_image_name(catalog_object, source="POSS")
logger.debug("object_image_path = %s", object_image_path)
Expand All @@ -59,6 +114,10 @@ def get_display_image(
image_rotate += roll

return_image = return_image.rotate(image_rotate)
if flip:
return_image = return_image.transpose(Image.FLIP_LEFT_RIGHT)
if flop:
return_image = return_image.transpose(Image.FLIP_TOP_BOTTOM)

# FOV
fov_size = int(1024 * fov / 2)
Expand Down Expand Up @@ -98,6 +157,78 @@ def get_display_image(
width=1,
)

cx = display_class.fov_res / 2
cy = display_class.fov_res / 2
fx = -1 if flip else 1
fy = -1 if flop else 1

# NSEW cardinal labels — show only 2: topmost and leftmost
if show_nsew:
(nx, ny), (ex, ey) = cardinal_vectors(image_rotate, fx, fy)
label_font = display_class.fonts.base
label_color = display_class.colors.get(64)
r_label = display_class.fov_res / 2 - 2
top_limit = display_class.titlebar_height
bottom_limit = display_class.fov_res - label_font.height * 2

candidates = [
("N", nx, ny),
("S", -nx, -ny),
("E", ex, ey),
("W", -ex, -ey),
]
by_top = sorted(candidates, key=lambda c: c[2])
by_left = sorted(candidates, key=lambda c: c[1])
chosen = {by_top[0][0]: by_top[0]}
# pick leftmost that isn't already chosen
for c in by_left:
if c[0] not in chosen:
chosen[c[0]] = c
break

for label, dx, dy in chosen.values():
lx = cx + dx * r_label - label_font.width / 2
ly = cy + dy * r_label - label_font.height / 2
lx = max(0, min(lx, display_class.fov_res - label_font.width))
ly = max(top_limit, min(ly, bottom_limit))
ui_utils.shadow_outline_text(
ri_draw,
(lx, ly),
label,
font=label_font,
align="left",
fill=label_color,
shadow_color=display_class.colors.get(0),
outline=1,
)

# Size overlay
extents = catalog_object.size.extents
if show_bbox and extents and fov > 0:
px_per_arcsec = display_class.fov_res / (fov * 3600)
overlay_color = display_class.colors.get(100)

if len(extents) == 1:
r = extents[0] * px_per_arcsec / 2
ri_draw.ellipse(
[cx - r, cy - r, cx + r, cy + r],
outline=overlay_color,
width=1,
)
else:
points = size_overlay_points(
extents,
catalog_object.size.position_angle,
image_rotate,
px_per_arcsec,
cx,
cy,
fx,
fy,
)
if points:
ri_draw.polygon(points, outline=overlay_color)

# Pad out image if needed
if display_class.fov_res != display_class.resX:
pad_image = Image.new("RGB", display_class.resolution)
Expand Down
6 changes: 2 additions & 4 deletions python/PiFinder/catalog_imports/bright_stars_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from tqdm import tqdm

import PiFinder.utils as utils
from PiFinder.composite_object import MagnitudeObject
from PiFinder.composite_object import MagnitudeObject, SizeObject
from PiFinder.calc_utils import ra_to_deg, dec_to_deg
from .catalog_import_utils import (
NewCatalogObject,
Expand Down Expand Up @@ -45,8 +45,7 @@ def load_bright_stars():
sequence = int(dfs[0])

logging.debug(f"---------------> Bright Stars {sequence=} <---------------")
size = ""
# const = dfs[2].strip()
size = SizeObject([])
desc = ""

ra_h = int(dfs[3])
Expand All @@ -58,7 +57,6 @@ def load_bright_stars():
dec_deg = dec_to_deg(dec_d, dec_m, 0)

mag = MagnitudeObject([float(dfs[7].strip())])
# const = dfs[8]

new_object = NewCatalogObject(
object_type=obj_type,
Expand Down
3 changes: 2 additions & 1 deletion python/PiFinder/catalog_imports/caldwell_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
insert_catalog,
insert_catalog_max_sequence,
add_space_after_prefix,
parse_arcmin_size,
)

# Import shared database object
Expand Down Expand Up @@ -46,7 +47,7 @@ def load_caldwell():
mag = MagnitudeObject([])
else:
mag = MagnitudeObject([float(mag)])
size = dfs[5][5:].strip()
size = parse_arcmin_size(dfs[5][5:].strip())
ra_h = int(dfs[6])
ra_m = float(dfs[7])
ra_deg = ra_to_deg(ra_h, ra_m, 0)
Expand Down
22 changes: 19 additions & 3 deletions python/PiFinder/catalog_imports/catalog_import_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from dataclasses import dataclass, field
from tqdm import tqdm

from PiFinder.composite_object import MagnitudeObject
from PiFinder.composite_object import MagnitudeObject, SizeObject
from PiFinder.ui.ui_utils import normalize
from PiFinder import calc_utils
from PiFinder.db.objects_db import ObjectsDatabase
Expand All @@ -30,7 +30,7 @@ class NewCatalogObject:
dec: float
mag: MagnitudeObject
object_id: int = 0
size: str = ""
size: SizeObject = field(default_factory=lambda: SizeObject([]))
description: str = ""
aka_names: list[str] = field(default_factory=list)
surface_brightness: float = 0.0
Expand Down Expand Up @@ -76,7 +76,7 @@ def insert(self, find_object_id=True):
self.ra,
self.dec,
self.constellation,
self.size,
self.size.to_json(),
self.mag.to_json(),
self.surface_brightness,
)
Expand Down Expand Up @@ -158,6 +158,22 @@ def get_object_id(self, object_name: str):
return result


def parse_arcmin_size(raw: str) -> SizeObject:
"""Parse a size string assumed to be in arcminutes. Handles 'NxM' format."""
if not raw:
return SizeObject([])
parts = raw.lower().replace("x", " ").split()
values = []
for p in parts:
try:
values.append(float(p))
except ValueError:
logging.warning("Non-numeric size token %r in %r", p, raw)
if not values:
return SizeObject([])
return SizeObject.from_arcmin(*values)


def safe_convert_to_float(x):
"""Convert to float, filtering out non-numeric values"""
try:
Expand Down
10 changes: 4 additions & 6 deletions python/PiFinder/catalog_imports/harris_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import numpy as np
import numpy.typing as npt
import PiFinder.utils as utils
from PiFinder.composite_object import MagnitudeObject
from PiFinder.composite_object import MagnitudeObject, SizeObject
from PiFinder.calc_utils import ra_to_deg, dec_to_deg
from .catalog_import_utils import (
delete_catalog_from_database,
Expand Down Expand Up @@ -286,15 +286,13 @@ def create_cluster_object(entry: npt.NDArray, seq: int) -> Dict[str, Any]:
logging.debug(f" Magnitude: None (invalid value: {mag_value})")

# Size - use half-mass radius (Rh) in arcminutes
# Format using utils.format_size_value to match other catalogs
rh = entry["Rh"].item()
if is_valid_value(rh):
# Convert to string, removing unnecessary decimals
result["size"] = utils.format_size_value(rh)
result["size"] = SizeObject.from_arcmin(float(rh))
if VERBOSE:
logging.debug(f" Size (half-mass radius): {result['size']} arcmin")
logging.debug(f" Size (half-mass radius): {rh} arcmin")
else:
result["size"] = ""
result["size"] = SizeObject([])
if VERBOSE:
logging.debug(f" Size: None (invalid Rh value: {rh})")

Expand Down
8 changes: 6 additions & 2 deletions python/PiFinder/catalog_imports/herschel_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ def load_herschel400():
f"---------------> Herschel 400 {sequence=} <---------------"
)

object_id = objects_db.get_catalog_object_by_sequence(
result = objects_db.get_catalog_object_by_sequence(
"NGC", NGC_sequence
)["id"]
)
if result is None:
logging.warning("NGC %s not found, skipping H%d", NGC_sequence, sequence)
continue
object_id = result["id"]
objects_db.insert_name(object_id, h_name, catalog)
objects_db.insert_catalog_object(object_id, catalog, sequence, h_desc)
conn.commit()
Expand Down
8 changes: 8 additions & 0 deletions python/PiFinder/catalog_imports/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def main():
objects_db, _ = init_shared_database()

logging.info("creating catalog tables")
conn, _ = objects_db.get_conn_cursor()
conn.execute("PRAGMA journal_mode = WAL")
objects_db.destroy_tables()
objects_db.create_tables()

Expand Down Expand Up @@ -121,6 +123,12 @@ def main():
resolve_object_images()
print_database()

# Finalize: checkpoint WAL and switch to DELETE mode so the .db is
# self-contained (no -wal/-shm sidecars needed at runtime).
logging.info("Finalizing database...")
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
conn.execute("PRAGMA journal_mode = DELETE")


if __name__ == "__main__":
main()
10 changes: 5 additions & 5 deletions python/PiFinder/catalog_imports/post_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# Import shared database object
from .database import objects_db
from .catalog_import_utils import NewCatalogObject
from PiFinder.composite_object import MagnitudeObject
from PiFinder.composite_object import MagnitudeObject, SizeObject
import PiFinder.utils as utils


Expand Down Expand Up @@ -123,7 +123,7 @@ def add_missing_messier_objects():
ra=185.552, # 12h 22m 12.5272s in degrees
dec=58.083, # +58° 4′ 58.549″ in degrees
mag=MagnitudeObject([9.9]), # Average of components A (9.64) and B (10.11)
size="0.1'",
size=SizeObject.from_arcmin(0.1),
description="Winnecke 4 double star",
aka_names=m40_aka_names,
)
Expand All @@ -143,7 +143,7 @@ def add_missing_messier_objects():
ra=56.85, # 03h 47m 24s in degrees
dec=24.117, # +24° 07′ 00″ in degrees
mag=MagnitudeObject([1.6]),
size="120'", # 2° = 120 arcminutes
size=SizeObject.from_degrees(2.0), # 2°
description="Pleiades open cluster",
aka_names=m45_aka_names,
)
Expand All @@ -163,7 +163,7 @@ def add_missing_messier_objects():
ra=274.6, # 18h 18m 24s in degrees
dec=-18.4, # -18° 24′ 00″ in degrees
mag=MagnitudeObject([4.6]), # Visual magnitude of the brightest part
size="90'", # About 1.5 degrees
size=SizeObject.from_degrees(1.5), # ~1.5°
description="Sagittarius Star Cloud",
aka_names=m24_aka_names,
)
Expand All @@ -183,7 +183,7 @@ def add_missing_messier_objects():
ra=226.623, # 15h 06m 29.5s in degrees
dec=55.763, # +55° 45′ 48″ in degrees
mag=MagnitudeObject([10.7]),
size="5.2'x2.3'",
size=SizeObject.from_arcmin(5.2, 2.3),
description="Spindle Galaxy (controversial Messier object)",
aka_names=m102_aka_names,
)
Expand Down
Loading
Loading