Download StarDenoise/image_io.py from MilanMagik/AstroStarDetect: direct link, hf CLI and curl.
- Browser
- Download file 4.24 kB
-
https://huggingface.co/MilanMagik/AstroStarDetect/resolve/main/StarDenoise/image_io.py
- Command line
-
hf download hf://MilanMagik/AstroStarDetect/StarDenoise/image_io.py
-
curl -L -o image_io.py https://huggingface.co/MilanMagik/AstroStarDetect/resolve/main/StarDenoise/image_io.py
4.24 kB
| """Image I/O with FITS metadata and dynamic-range preservation.""" | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any | |
| import numpy as np | |
| import tifffile | |
| from astropy.io import fits | |
| from PIL import Image | |
| FITS_EXTENSIONS = {".fit", ".fits", ".fts"} | |
| TIFF_EXTENSIONS = {".tif", ".tiff"} | |
| class ImageRecord: | |
| pixels: np.ndarray # HWC float32, normalized to [0, 1] | |
| offset: float | |
| scale: float | |
| original_dtype: np.dtype | |
| original_layout: str | |
| header: Any = None | |
| def _to_hwc(array: np.ndarray) -> tuple[np.ndarray, str]: | |
| if array.ndim == 2: | |
| return array[..., None], "hw" | |
| if array.ndim != 3: | |
| raise ValueError(f"Only mono/RGB 2-D images are supported, got shape {array.shape}") | |
| if array.shape[-1] in (1, 3, 4): | |
| return array[..., :3], "hwc" | |
| if array.shape[0] in (1, 3, 4): | |
| return np.moveaxis(array[:3], 0, -1), "chw" | |
| raise ValueError(f"Cannot determine channel axis for shape {array.shape}") | |
| def _normalize(array: np.ndarray) -> tuple[np.ndarray, float, float]: | |
| finite = np.isfinite(array) | |
| if not finite.any(): | |
| raise ValueError("Image contains no finite pixels") | |
| clean = np.nan_to_num(array, copy=True) | |
| if np.issubdtype(array.dtype, np.integer): | |
| info = np.iinfo(array.dtype) | |
| offset = float(max(0, info.min)) | |
| scale = float(info.max - offset) | |
| else: | |
| minimum = float(np.min(clean[finite])) | |
| maximum = float(np.max(clean[finite])) | |
| if minimum >= 0.0 and maximum <= 1.0: | |
| offset, scale = 0.0, 1.0 | |
| else: | |
| offset, scale = minimum, maximum - minimum | |
| if scale <= 0: | |
| scale = 1.0 | |
| normalized = np.clip((clean.astype(np.float32) - offset) / scale, 0.0, 1.0) | |
| return normalized, offset, scale | |
| def load_image(path: str | Path) -> ImageRecord: | |
| path = Path(path) | |
| extension = path.suffix.lower() | |
| header = None | |
| if extension in FITS_EXTENSIONS: | |
| with fits.open(path, memmap=False) as hdul: | |
| index = next((i for i, hdu in enumerate(hdul) if hdu.data is not None), None) | |
| if index is None: | |
| raise ValueError(f"No image HDU in {path}") | |
| array = np.asarray(hdul[index].data) | |
| header = hdul[index].header.copy() | |
| elif extension in TIFF_EXTENSIONS: | |
| array = np.asarray(tifffile.imread(path)) | |
| else: | |
| with Image.open(path) as image: | |
| if image.mode not in ("L", "I;16", "I", "F"): | |
| image = image.convert("RGB") | |
| array = np.asarray(image) | |
| original_dtype = array.dtype | |
| hwc, layout = _to_hwc(array) | |
| pixels, offset, scale = _normalize(hwc) | |
| return ImageRecord(pixels, offset, scale, original_dtype, layout, header) | |
| def _restore_layout(array: np.ndarray, layout: str) -> np.ndarray: | |
| if layout == "hw": | |
| return array[..., 0] | |
| if layout == "chw": | |
| return np.moveaxis(array, -1, 0) | |
| return array | |
| def _restore_range(record: ImageRecord, pixels: np.ndarray) -> np.ndarray: | |
| restored = np.clip(pixels, 0.0, 1.0) * record.scale + record.offset | |
| if np.issubdtype(record.original_dtype, np.integer): | |
| info = np.iinfo(record.original_dtype) | |
| restored = np.clip(np.rint(restored), info.min, info.max).astype(record.original_dtype) | |
| else: | |
| restored = restored.astype(record.original_dtype) | |
| return _restore_layout(restored, record.original_layout) | |
| def save_image(path: str | Path, record: ImageRecord, pixels: np.ndarray) -> None: | |
| path = Path(path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| extension = path.suffix.lower() | |
| restored = _restore_range(record, pixels) | |
| if extension in FITS_EXTENSIONS: | |
| fits.PrimaryHDU(restored, header=record.header).writeto(path, overwrite=True) | |
| elif extension in TIFF_EXTENSIONS: | |
| tifffile.imwrite(path, restored) | |
| else: | |
| # JPEG/PNG demo output is display-oriented. Preserve mono when applicable. | |
| display = np.clip(pixels * 255.0 + 0.5, 0, 255).astype(np.uint8) | |
| if record.original_layout == "hw": | |
| display = display[..., 0] | |
| Image.fromarray(display).save(path, quality=95) | |