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
2 changes: 1 addition & 1 deletion CONTEXT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ PiFinder is a multi-process Raspberry Pi finder/plate-solver. These contexts eac
- **SQM → Camera**: `shared_state.set_noise_floor()` feeds the minimum acceptable background used by the Camera context's background controller.
- **Positioning → Camera**: `Matches` is published on every solve attempt (success or failure) as the feedback signal for solver-driven auto-exposure.
- **Catalog ↔ Positioning**: Catalog supplies the `(RA, Dec)` target for the alignment flow that calibrates `solve_pixel` in Positioning.
- **Equipment → Catalog**: the active telescope's flip/flop flags and the active eyepiece's true field of view orient and scale the POSS/SDSS object image in `cat_images.get_display_image`.
- **Equipment → Catalog**: the active telescope's flip/flop flags and the active eyepiece's true field of view orient and scale the **object image** (one sourceless survey JPEG per object) in `cat_images.get_display_image`.
- **Positioning → Equipment**: the object-image baseline rotation combines the active telescope's flip/flop with the live solve **roll** from `shared_state` (see [ADR 0003](./docs/adr/0003-object-image-orientation.md)).
- **Battery → UI**: STATUS (and web/API) display `BatteryState` from `shared_state.battery()` — *consumption is future work; this run is plumbing + tests only*.
- **Battery → system-wide**: `hardware_detect` probes the I²C bus at startup and publishes `HardwareCapabilities` into `shared_state`; the battery monitor process only runs when `has_bq25895` is detected (rev-4). The same capabilities record is the source of truth for other rev-dependent decisions.
Expand Down
Binary file modified astro_data/pifinder_objects.db
Binary file not shown.
21 changes: 21 additions & 0 deletions docs/adr/0018-one-object-image-per-object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# One object image per object; survey source is a recorded curation directive, not a runtime branch

The device stores and displays exactly one survey JPEG per object, named without a survey suffix (`<last-digit>/<image_name>.jpg`). Which survey that image comes from — POSS/DSS, SDSS, or future sources — is a **curation decision recorded once** in `object_images.source` and made **off-device**, never derived or branched on at runtime. This replaces the prior scheme, which fetched **both** POSS and SDSS into parallel `_POSS.jpg`/`_SDSS.jpg` files per object — of which the runtime only ever displayed POSS (`DM_SDSS` was defined but never assigned).

The `source` record is both **retrospective** (where an image came from) and **prescriptive** (where to regenerate it from). A **discriminator** (human or AI — aspirational) compares candidate images across surveys and writes the winner; the dev-side **Generate** step reads `source` to (re)produce the single canonical image and publish it to the CDN. `NULL` means "not yet curated" — Generate falls back to a default policy (today: POSS). The on-device **Download** feature and the runtime **Display** path key purely on the sourceless `image_name`; they carry `source` but never act on it.

## Considered Options
- **Keep both sources per object (status quo).** Rejected: ~doubles on-device storage and download time for an SDSS file no reachable UI path displayed.
- **Per-device source choice / toggle.** Rejected: premature with no working SDSS display mode, and it can't beat dev curation that picks the best image per object — while keeping the source dimension alive in storage, UI, and CDN.
- **Record the source choice in a dev-side manifest (not shipped).** Rejected: a second artifact to keep in sync with the CDN, and provenance/attribution isn't queryable on-device. The catalog DB already assigns the (sourceless) `image_name` in `resolve_object_images()`, so the same shipped table is the natural home for the choice.
- **Record nothing; let the CDN be the only record.** Rejected: non-reproducible (re-runs may flip the choice), non-auditable, and no path to survey attribution/credit.
- **Encode the source in the filename / CDN path (sourceless name but `M31_SDSS.jpg`).** Rejected: that's the scheme we're leaving; it forces resolution and display to know the source and reintroduces per-source duplication.

## Consequences
- On-disk and CDN layout become `<last-digit>/<image_name>.jpg`; `resolve_image_name` drops its `source` argument; `cat_images.get_display_image` drops the source concept; `DM_SDSS` is removed from `object_details`.
- `object_images` gains a nullable `source TEXT` column, populated by Generate/the discriminator and shipped in `pifinder_objects.db`. The column is provenance + regeneration directive only — no resolution or display code branches on it.
- A one-time, idempotent rename migration under `migration_source/` converts existing `_POSS`/`_SDSS` installs to the sourceless name (POSS wins when both exist; the redundant file is deleted). Reversing this decision would mean re-curating the CDN and reintroducing source suffixes across storage, the importer, and the UI — hence "hard to reverse."
- Generation (`gen_images.py`, off-device, multi-source, source-aware) and the new on-device download are cleanly separated: download copies one curated image by `image_name`; Generate is the only place a source is read or acted on.
- **Coordinated cutover:** the CDN is re-laid-out to sourceless paths (`/<digit>/<image_name>.jpg`) in the same release that ships the rename migration and the on-device download feature. The legacy `get_images.py` is replaced by a thin CLI over the shared core; a pre-release device's old CLI requests `_POSS`/`_SDSS` URLs and will 404 against the new CDN, so it must update first. Accepted because downloading is an explicit setup action, not a runtime dependency.
- The new `source` column ships with the (git-tracked) `pifinder_objects.db` via the normal software update — there is **no** on-device DB migration; the only on-device migration is the JPEG file rename.
- Companion glossary: [`docs/ax/catalog/CONTEXT.md`](../ax/catalog/CONTEXT.md) — "Object image", "Image source", "Discriminator", "Generate", "Download", "Display".
30 changes: 30 additions & 0 deletions docs/ax/catalog/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,36 @@ _Avoid_: section header / section heading (that's the visual rendering of the la
The description text an observing list carries for one of its targets — the observing-list counterpart of a **catalog description**. So an object's description can come from a catalog *or* from an observing list, and the **composed description** shows both. Session-only, held in `CompositeObject.list_descriptions` keyed by list name (re-loading a list replaces its own entry, never duplicates). Set only on **resolved** objects; a **coordinate object** has none (its list text becomes its own catalog-side description, since it has nothing else).
_Avoid_: list note, note, comment, annotation, user description.

### Object images

**Object image**:
The single survey JPEG shown behind a sky object in object details (the POSS/DSS plate, reddened and oriented to the eyepiece view). **Exactly one per object** — there is no per-source duplication on the device. Resolved from the object's **image_name** and rendered by `cat_images.get_display_image`, scaled/oriented by the active **Equipment** (see [ADR 0003](../../adr/0003-object-image-orientation.md)).
_Avoid_: POSS image / SDSS image (those name a **source**, not the stored image), thumbnail, finder image.

**Image source** (survey):
The sky survey a curated **object image** is (re)generated from — POSS/DSS, SDSS, or future surveys. Recorded per object in `object_images.source` and authored by a **discriminator**. The record is both **retrospective** (where this image came from) and **prescriptive** (regenerate it from here): the dev-side **Generate** step reads it to (re)produce the one canonical image, and `NULL` means "not yet curated" — Generate falls back to a default policy. The source is deliberately **not** encoded in the on-disk name, the CDN path, or any resolution/display branch — the runtime carries the value but never acts on it (see [ADR 0018](../../adr/0018-one-object-image-per-object.md)).
_Avoid_: image type, image format; the historical `_POSS`/`_SDSS` filename suffix (a retired storage detail, not a live concept).

**Discriminator**:
The (human or AI) curation role that compares an object's candidate images across **image sources** and chooses the best one, writing that choice to `object_images.source`. The authoritative producer of the **prescriptive** source record; the **Generate** step is its consumer. Aspirational: both the `source` column and the discriminator are introduced by [ADR 0018](../../adr/0018-one-object-image-per-object.md), not yet built.
_Avoid_: selector, picker, classifier (it discriminates *between candidate images*, not object types).

**image_name**:
The **sourceless** filename stem for an object's image (`"M31"`), stored in the `object_images` table (one row per imaged sky object). Resolves on disk to `<last-digit>/<image_name>.jpg`. Falls back to a whitespace-stripped common **Name** when the catalog-code+sequence stem has no file.
_Avoid_: image path, image id, image key.

**Generate** (object images):
The **dev-side, off-device** step that fetches candidate cutouts from surveys, curates the single best per object, and publishes one sourceless **object image** to the CDN. The only place an **image source** is chosen. (`gen_images.py`.)
_Avoid_: fetch, build, import.

**Download** (object images):
The **on-device** step that pulls already-curated object images for in-scope objects from the CDN to local disk. Carries no source dimension — it copies whatever the CDN holds for each **image_name**. Distinct from **Generate**: download never touches a survey.
_Avoid_: fetch, sync, get (the verb is "download"; reserve "generate" for the survey-side step).

**Display** (object images):
The **runtime** step that loads the local object image and orients/crops/reddens it for the current eyepiece. Read-only; never fetches. (`cat_images.get_display_image`.)
_Avoid_: render (too generic), show.

### UI helpers

**CatalogDesignator**:
Expand Down
5 changes: 5 additions & 0 deletions docs/source/menu_map.rst
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ information or perform actions. See :ref:`user_guide:tools`.
Tools --> PnT["Place & Time"]
Tools --> Console
Tools --> SU["Software Upd"]
Tools --> DI["Download Images"]
Tools --> TM["Test Mode"]
Tools --> Exp["Experimental"]
Tools --> Power
Expand Down Expand Up @@ -340,6 +341,10 @@ Console
Software Upd
Download and install software updates over WiFi. See
:ref:`user_guide:update software`.
Download Images
Fetch catalog object images on the device for a chosen scope — All Objects,
the Current Filter, or your Observing List. Requires WiFi Client mode. See
:ref:`software:catalog image download`.
Test Mode
A demo/debug mode that solves a saved image from disk. It blocks real use at
night but lets you explore the PiFinder's features indoors.
Expand Down
25 changes: 14 additions & 11 deletions docs/source/software.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,24 +132,27 @@ Booting takes up to two minutes, but you should see the startup screen before lo
Catalog Image Download
^^^^^^^^^^^^^^^^^^^^^^

The PiFinder can display catalog object images when they're present on your SD card. These images take about 5gb of space and can take several hours or more to download, but you can cancel and resume at any time.
The PiFinder can show a survey image behind each object in Object Details when the images are present on your SD card. They take a few GB of space and the full set can take a while to fetch, but you can cancel and pick up where you left off at any time.

The :ref:`software:prebuilt release image` already includes these images and is much quicker to download as a single file from your main computer.
The :ref:`software:prebuilt release image` already bundles every image, so flashing it is by far the fastest way to get them — one download on your main computer instead of thousands of small ones on the device.

To download the catalog images, put your PiFinder in WIFI client mode so it can reach the internet, then SSH into it using the password you set up initially.
To fetch them on the device, first put the PiFinder in WiFi Client mode so it can reach the internet (see :doc:`connectivity`). Then open Tools, select Download Images, and choose what to fetch:

Once connected, type:
* **All Objects** — every catalog image.
* **Current Filter** — only the objects your active filter currently shows.
* **Observing List** — the objects on your loaded observing list.

.. code-block::
The PiFinder counts how many images are missing, estimates the download size, and checks that it can reach the image server, then offers Download or Cancel. (If WiFi isn't in Client mode the screen reads "WiFi must be client mode" — switch modes and come back.)

cd PiFinder/python
python -m PiFinder.get_images

The PiFinder checks which images are missing and starts downloading. You can monitor progress on the status bar.
While a download runs, a status line just under the title bar shows its progress on every screen, so you can keep observing while it works. Pressing **LEFT** to go back leaves the download running in the background; the Cancel action on the Download Images screen stops it. Either way, images already downloaded are kept.

Downloading is idempotent: re-running a scope skips images you already have, so there's nothing special to resume — just trigger the same scope again.

.. image:: ../../images/screenshots/Image_download_001.png
:alt: Image Download


There are 13,000+ images, so it takes a while, but you can do it across multiple sessions. The PiFinder uses whichever images you have on hand each time you observe.
.. note::
There's also a headless fallback for advanced users. With the PiFinder in
Client mode, SSH in and run ``python -m PiFinder.get_images`` from
``PiFinder/python`` to fetch every image, or add ``--catalog NGC`` to fetch a
single catalog.
6 changes: 6 additions & 0 deletions migration_source/v2.7.0.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Convert legacy per-source object images to one sourceless image per object.
# Renames <name>_POSS.jpg / <name>_SDSS.jpg to <name>.jpg (POSS wins when both
# exist; the redundant SDSS file is removed) so the device matches the
# sourceless CDN layout. Idempotent and version-gated by pifinder_post_update.sh.
# See docs/adr/0018-one-object-image-per-object.md.
python /home/pifinder/PiFinder/python/PiFinder/migrations/v2_7_0_sourceless_images.py /home/pifinder/PiFinder_data/catalog_images
8 changes: 8 additions & 0 deletions pifinder_post_update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ then
touch /home/pifinder/PiFinder_data/migrations/v2.6.0
fi

# v2.7.0
# Rename per-source object images (_POSS/_SDSS) to one sourceless image per object
if ! [ -f "/home/pifinder/PiFinder_data/migrations/v2.7.0" ]
then
source /home/pifinder/PiFinder/migration_source/v2.7.0.sh
touch /home/pifinder/PiFinder_data/migrations/v2.7.0
fi

# DONE
echo "Post Update Complete"

93 changes: 0 additions & 93 deletions python/PiFinder/audit_images.py

This file was deleted.

51 changes: 6 additions & 45 deletions python/PiFinder/cat_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
from PIL import Image, ImageChops, ImageDraw
from PiFinder import image_util
from PiFinder import utils
from PiFinder.object_image_store import resolve_image_name
import PiFinder.ui.ui_utils as ui_utils
import logging

BASE_IMAGE_PATH = f"{utils.data_dir}/catalog_images"
# On-disk layout, image-dir creation and (sourceless) name resolution now live
# in the shared object-image core (object_image_store); see ADR 0018. This
# module keeps only the runtime **Display** logic and imports resolve_image_name
# for it below.
CATALOG_PATH = f"{utils.astro_data_dir}/pifinder_objects.db"


Expand Down Expand Up @@ -195,7 +199,7 @@ def get_display_image(
roll:
degrees
"""
object_image_path = resolve_image_name(catalog_object, source="POSS")
object_image_path = resolve_image_name(catalog_object)
logger.debug("object_image_path = %s", object_image_path)
if not os.path.exists(object_image_path):
return_image = Image.new("RGB", display_class.resolution)
Expand Down Expand Up @@ -402,46 +406,3 @@ def get_display_image(
)

return return_image


def resolve_image_name(catalog_object, source):
"""
returns the image path for this object
"""

def create_image_path(image_name):
last_char = str(image_name)[-1]
image = f"{BASE_IMAGE_PATH}/{last_char}/{image_name}_{source}.jpg"
exists = os.path.exists(image)
return exists, image

# Try primary name
image_name = f"{catalog_object.catalog_code}{catalog_object.sequence}"
ok, image = create_image_path(image_name)

if ok:
catalog_object.image_name = image
return image

# Try alternatives
for name in catalog_object.names:
alt_image_name = f"{''.join(name.split())}"
ok, image = create_image_path(alt_image_name)
if ok:
catalog_object.image_name = image
return image

return ""


def create_catalog_image_dirs():
"""
Checks for and creates catalog_image dirs
"""
if not os.path.exists(BASE_IMAGE_PATH):
os.makedirs(BASE_IMAGE_PATH)

for i in range(0, 10):
_image_dir = f"{BASE_IMAGE_PATH}/{i}"
if not os.path.exists(_image_dir):
os.makedirs(_image_dir)
8 changes: 7 additions & 1 deletion python/PiFinder/catalog_imports/catalog_import_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,13 @@ def resolve_object_images():
else:
unresolved_objects.append(object_id)

# Bulk insert image objects
# Bulk insert image objects.
#
# ``source`` is intentionally left NULL here: at import time every image is
# "uncurated". The survey provenance / regeneration directive is written
# off-device by Generate / the discriminator (ADR 0018); nothing on-device
# branches on it, so the importer only needs object_id + (sourceless)
# image_name.
if image_objects_to_insert:
# Use executemany for bulk insert into the correct table
db_c.executemany(
Expand Down
Loading
Loading