Download StarDenoise/denoise.py from MilanMagik/AstroStarDetect: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/MilanMagik/AstroStarDetect/resolve/main/StarDenoise/denoise.py
- Command line
-
hf download hf://MilanMagik/AstroStarDetect/StarDenoise/denoise.py
-
curl -L -o denoise.py https://huggingface.co/MilanMagik/AstroStarDetect/resolve/main/StarDenoise/denoise.py
10.2 kB
| """Command-line inference demo for Cosmic Clarity AI 3.6.""" | |
| from __future__ import annotations | |
| import argparse | |
| import contextlib | |
| import math | |
| import time | |
| from pathlib import Path | |
| from typing import Iterator | |
| import cv2 | |
| import numpy as np | |
| import torch | |
| from image_io import load_image, save_image | |
| from model import load_model, model_statistics | |
| ROOT = Path(__file__).resolve().parent | |
| DEFAULT_WEIGHTS = ROOT / "models" / "deep_denoise_cnn_AI3_6.pth" | |
| def rgb_to_ycbcr(image: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]: | |
| matrix = np.array( | |
| [[0.299, 0.587, 0.114], [-0.168736, -0.331264, 0.5], [0.5, -0.418688, -0.081312]], | |
| dtype=np.float32, | |
| ) | |
| converted = image @ matrix.T | |
| return converted[..., 0], converted[..., 1] + 0.5, converted[..., 2] + 0.5 | |
| def ycbcr_to_rgb(y: np.ndarray, cb: np.ndarray, cr: np.ndarray) -> np.ndarray: | |
| converted = np.stack((y, cb - 0.5, cr - 0.5), axis=-1) | |
| matrix = np.array( | |
| [[1.0, 0.0, 1.402], [1.0, -0.344136, -0.714136], [1.0, 1.772, 0.0]], | |
| dtype=np.float32, | |
| ) | |
| return np.clip(converted @ matrix.T, 0.0, 1.0) | |
| def _mtf_stretch(image: np.ndarray, target_median: float = 0.25) -> tuple[np.ndarray, float, list[float]]: | |
| original_min = float(image.min()) | |
| stretched = image - original_min | |
| medians = [float(np.median(stretched[..., channel])) for channel in range(3)] | |
| for channel, median in enumerate(medians): | |
| if median == 0: | |
| continue | |
| x = stretched[..., channel] | |
| denominator = median * (target_median + x - 1.0) - target_median * x | |
| stretched[..., channel] = ((median - 1.0) * target_median * x) / np.where( | |
| np.abs(denominator) < 1e-12, 1e-12, denominator | |
| ) | |
| return np.clip(stretched, 0.0, 1.0), original_min, medians | |
| def _mtf_unstretch(image: np.ndarray, original_min: float, original_medians: list[float]) -> np.ndarray: | |
| result = image.copy() | |
| current_medians = [float(np.median(result[..., channel])) for channel in range(3)] | |
| for channel, current in enumerate(current_medians): | |
| target = original_medians[channel] | |
| if current == 0 or target == 0: | |
| continue | |
| x = result[..., channel] | |
| denominator = current * (target + x - 1.0) - target * x | |
| result[..., channel] = ((current - 1.0) * target * x) / np.where( | |
| np.abs(denominator) < 1e-12, 1e-12, denominator | |
| ) | |
| return np.clip(result + original_min, 0.0, 1.0) | |
| def _guided_filter(guide: np.ndarray, source: np.ndarray, radius: int, epsilon: float) -> np.ndarray: | |
| kernel = (2 * radius + 1, 2 * radius + 1) | |
| kwargs = {"ddepth": -1, "ksize": kernel, "borderType": cv2.BORDER_REFLECT} | |
| mean_i = cv2.boxFilter(guide, **kwargs) | |
| mean_p = cv2.boxFilter(source, **kwargs) | |
| corr_i = cv2.boxFilter(guide * guide, **kwargs) | |
| corr_ip = cv2.boxFilter(guide * source, **kwargs) | |
| variance_i = corr_i - mean_i * mean_i | |
| covariance_ip = corr_ip - mean_i * mean_p | |
| a = covariance_ip / (variance_i + epsilon) | |
| b = mean_p - a * mean_i | |
| return cv2.boxFilter(a, **kwargs) * guide + cv2.boxFilter(b, **kwargs) | |
| def denoise_chroma(y: np.ndarray, cb: np.ndarray, cr: np.ndarray, strength: float) -> tuple[np.ndarray, np.ndarray]: | |
| effective = float(np.clip(strength * 2.0, 0.0, 1.0)) | |
| if effective == 0: | |
| return cb, cr | |
| radius = 2 + int(round(10 * effective)) | |
| epsilon = (0.001 + 0.05 * effective) ** 2 | |
| cb_filtered = _guided_filter(y, cb, radius, epsilon) | |
| cr_filtered = _guided_filter(y, cr, radius, epsilon) | |
| return ( | |
| (1.0 - effective) * cb + effective * cb_filtered, | |
| (1.0 - effective) * cr + effective * cr_filtered, | |
| ) | |
| def _positions(length: int, tile: int, overlap: int) -> list[int]: | |
| if length <= tile: | |
| return [0] | |
| stride = tile - overlap | |
| result = list(range(0, length - tile + 1, stride)) | |
| if result[-1] != length - tile: | |
| result.append(length - tile) | |
| return result | |
| def _batches(items: list[tuple[int, int, np.ndarray]], size: int) -> Iterator[list[tuple[int, int, np.ndarray]]]: | |
| for start in range(0, len(items), size): | |
| yield items[start : start + size] | |
| def infer_channel( | |
| channel: np.ndarray, | |
| model: torch.nn.Module, | |
| device: torch.device, | |
| tile_size: int, | |
| overlap: int, | |
| batch_size: int, | |
| half_precision: bool, | |
| ) -> np.ndarray: | |
| if overlap >= tile_size: | |
| raise ValueError("overlap must be smaller than tile-size") | |
| height, width = channel.shape | |
| pad_y, pad_x = max(0, tile_size - height), max(0, tile_size - width) | |
| padded = np.pad(channel, ((0, pad_y), (0, pad_x)), mode="reflect") | |
| ph, pw = padded.shape | |
| tiles = [ | |
| (y, x, padded[y : y + tile_size, x : x + tile_size]) | |
| for y in _positions(ph, tile_size, overlap) | |
| for x in _positions(pw, tile_size, overlap) | |
| ] | |
| # A Hann window suppresses tile seams; the floor guarantees edge coverage. | |
| window_1d = np.hanning(tile_size).astype(np.float32) | |
| window = np.maximum(np.outer(window_1d, window_1d), 1e-3) | |
| accumulated = np.zeros_like(padded, dtype=np.float32) | |
| weights = np.zeros_like(padded, dtype=np.float32) | |
| use_amp = half_precision and device.type == "cuda" | |
| for batch in _batches(tiles, batch_size): | |
| array = np.stack([item[2] for item in batch]) | |
| tensor = torch.from_numpy(array[:, None]).repeat(1, 3, 1, 1).to(device) | |
| amp = torch.autocast(device_type="cuda", dtype=torch.float16) if use_amp else contextlib.nullcontext() | |
| with torch.inference_mode(), amp: | |
| prediction = model(tensor)[:, 0].float().cpu().numpy() | |
| for (y, x, _), tile_prediction in zip(batch, prediction): | |
| accumulated[y : y + tile_size, x : x + tile_size] += tile_prediction * window | |
| weights[y : y + tile_size, x : x + tile_size] += window | |
| return (accumulated / np.maximum(weights, 1e-8))[:height, :width] | |
| def denoise_array( | |
| pixels: np.ndarray, | |
| model: torch.nn.Module, | |
| device: torch.device, | |
| strength: float = 0.9, | |
| color_strength: float | None = None, | |
| mode: str = "full", | |
| tile_size: int = 256, | |
| overlap: int = 64, | |
| batch_size: int = 1, | |
| half_precision: bool = True, | |
| linear_stretch: str = "auto", | |
| ) -> np.ndarray: | |
| if not 0.0 <= strength <= 1.0: | |
| raise ValueError("strength must be in [0, 1]") | |
| if strength == 0.0: | |
| return pixels.copy() | |
| mono = pixels.shape[-1] == 1 | |
| rgb = np.repeat(pixels, 3, axis=-1) if mono else pixels[..., :3].copy() | |
| should_stretch = linear_stretch == "always" or ( | |
| linear_stretch == "auto" and float(np.median(rgb - rgb.min())) < 0.05 | |
| ) | |
| stretch_state = None | |
| if should_stretch: | |
| rgb, minimum, medians = _mtf_stretch(rgb) | |
| stretch_state = (minimum, medians) | |
| def run(channel: np.ndarray) -> np.ndarray: | |
| return infer_channel(channel, model, device, tile_size, overlap, batch_size, half_precision) | |
| if mono: | |
| denoised = ((1.0 - strength) * rgb[..., 0] + strength * run(rgb[..., 0]))[..., None] | |
| result = np.repeat(denoised, 3, axis=-1) | |
| elif mode == "separate": | |
| result = np.stack( | |
| [(1.0 - strength) * rgb[..., c] + strength * run(rgb[..., c]) for c in range(3)], | |
| axis=-1, | |
| ) | |
| elif mode in ("luminance", "full"): | |
| y, cb, cr = rgb_to_ycbcr(rgb) | |
| y = (1.0 - strength) * y + strength * run(y) | |
| if mode == "full": | |
| cb, cr = denoise_chroma(y, cb, cr, strength if color_strength is None else color_strength) | |
| result = ycbcr_to_rgb(y, cb, cr) | |
| else: | |
| raise ValueError(f"Unknown mode: {mode}") | |
| if stretch_state is not None: | |
| result = _mtf_unstretch(result, *stretch_state) | |
| return result[..., :1] if mono else result | |
| def choose_device(requested: str) -> torch.device: | |
| if requested == "auto": | |
| return torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| device = torch.device(requested) | |
| if device.type == "cuda" and not torch.cuda.is_available(): | |
| raise RuntimeError("CUDA was requested but is not available") | |
| return device | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(description="Astronomy-specific Cosmic Clarity AI 3.6 denoising demo") | |
| parser.add_argument("input", type=Path, help="Input JPG/PNG/TIFF/FITS") | |
| parser.add_argument("output", type=Path, help="Output path; extension selects format") | |
| parser.add_argument("--weights", type=Path, default=DEFAULT_WEIGHTS) | |
| parser.add_argument("--device", default="auto", help="auto, cpu, cuda, or cuda:N") | |
| parser.add_argument("--strength", type=float, default=0.9) | |
| parser.add_argument("--color-strength", type=float, default=None) | |
| parser.add_argument("--mode", choices=("luminance", "full", "separate"), default="full") | |
| parser.add_argument("--tile-size", type=int, default=256) | |
| parser.add_argument("--overlap", type=int, default=64) | |
| parser.add_argument("--batch-size", type=int, default=1) | |
| parser.add_argument("--precision", choices=("fp16", "fp32"), default="fp16") | |
| parser.add_argument("--linear-stretch", choices=("auto", "always", "never"), default="auto") | |
| return parser | |
| def main() -> None: | |
| args = build_parser().parse_args() | |
| device = choose_device(args.device) | |
| model = load_model(args.weights, device) | |
| record = load_image(args.input) | |
| started = time.perf_counter() | |
| output = denoise_array( | |
| record.pixels, | |
| model, | |
| device, | |
| strength=args.strength, | |
| color_strength=args.color_strength, | |
| mode=args.mode, | |
| tile_size=args.tile_size, | |
| overlap=args.overlap, | |
| batch_size=args.batch_size, | |
| half_precision=args.precision == "fp16", | |
| linear_stretch=args.linear_stretch, | |
| ) | |
| save_image(args.output, record, output) | |
| elapsed = time.perf_counter() - started | |
| statistics = model_statistics(model) | |
| print(f"Model: Cosmic Clarity AI 3.6 ({statistics['parameters']:,} parameters)") | |
| print(f"Device: {device}; input: {record.pixels.shape}; elapsed: {elapsed:.3f}s") | |
| print(f"Saved: {args.output.resolve()}") | |
| if __name__ == "__main__": | |
| main() | |