""" Утилиты для обработки изображений: нормализация, преобразование цветов и т.д. """ import numpy as np from typing import Optional from pathlib import Path # Пытаемся импортировать доступные библиотеки для JP2 JP2_AVAILABLE = False JP2_METHOD = None JP2_ERROR = None # Приоритет: imagecodecs > PIL > glymur try: import imagecodecs import tifffile JP2_AVAILABLE = True JP2_METHOD = "imagecodecs" print("✓ Используется imagecodecs для JP2 (рекомендуемый метод)") except ImportError as e: JP2_ERROR = str(e) try: from PIL import Image JP2_AVAILABLE = True JP2_METHOD = "pil" print("✓ Используется PIL для JP2 (ограниченная поддержка)") except ImportError: try: import glymur from glymur import Jp2k JP2_AVAILABLE = True JP2_METHOD = "glymur" print("✓ Используется Glymur для JP2") except ImportError: print("✗ Нет доступных библиотек для JP2") print(" Установите: pip install imagecodecs") class ImageProcessor: """Обработчик изображений для солнечных снимков""" @staticmethod def load_jp2(filepath: str) -> Optional[np.ndarray]: """ Загружает JP2 файл и возвращает numpy array Args: filepath: Путь к JP2 файлу Returns: numpy array с изображением (нормализованный 8-бит) """ if not JP2_AVAILABLE: print(f"Ошибка: Нет доступных библиотек для JP2. Файл: {filepath}") print(f" Причина: {JP2_ERROR}") return None try: img_data = None if JP2_METHOD == "imagecodecs": # Метод через imagecodecs (работает без внешнего OpenJPEG) import imagecodecs with open(filepath, 'rb') as f: data = f.read() img_data = imagecodecs.jpeg2k_decode(data) elif JP2_METHOD == "pil": # Метод через PIL (базовая поддержка) from PIL import Image with Image.open(filepath) as img: img_data = np.array(img) elif JP2_METHOD == "glymur": # Метод через Glymur from glymur import Jp2k jp2 = Jp2k(filepath) img_data = jp2[:] if img_data is None: print(f"Не удалось загрузить: {filepath}") return None # Нормализуем 16-битные данные в 8-бит if img_data.dtype == np.uint16: img_data = ImageProcessor.percent_normalize(img_data) elif img_data.dtype == np.uint8: pass # уже 8-бит else: # Конвертируем в uint8 if img_data.max() > 0: img_data = ((img_data - img_data.min()) / (img_data.max() - img_data.min()) * 255).astype(np.uint8) else: img_data = np.zeros_like(img_data, dtype=np.uint8) # Если изображение черно-белое, дублируем в RGB if len(img_data.shape) == 2: img_data = np.stack([img_data] * 3, axis=2) elif len(img_data.shape) == 3 and img_data.shape[2] == 1: img_data = np.concatenate([img_data] * 3, axis=2) return img_data except Exception as e: print(f"Ошибка загрузки JP2 ({JP2_METHOD}): {e}") return None @staticmethod def percent_normalize(img_16bit: np.ndarray, low_percent: float = 0.5, high_percent: float = 99.5) -> np.ndarray: """ Процентная нормализация для астрономических изображений Args: img_16bit: 16-битное изображение low_percent: Нижний процент отсечения high_percent: Верхний процент отсечения Returns: 8-битное нормализованное изображение """ # Вычисляем перцентили low_val = np.percentile(img_16bit, low_percent) high_val = np.percentile(img_16bit, high_percent) # Защита от одинаковых значений if high_val <= low_val: high_val = low_val + 1 # Клиппируем и нормализуем img_clipped = np.clip(img_16bit, low_val, high_val) img_normalized = ((img_clipped - low_val) / (high_val - low_val) * 255).astype(np.uint8) return img_normalized @staticmethod def apply_color_map(img_8bit: np.ndarray, color_map: str = "default") -> np.ndarray: """ Применяет цветовую карту к черно-белому изображению Args: img_8bit: 8-битное черно-белое изображение color_map: Название цветовой карты Returns: RGB изображение """ # Встроенные цветовые карты для разных спектров color_maps = { "AIA 335": "hot", # золотистый "AIA 304": "magma", # красный "AIA 193": "plasma", # пурпурный "AIA 171": "viridis", # зеленый "AIA 211": "inferno", # оранжевый "AIA 131": "cool", # синий "LASCO C2": "gray", # серый "default": "gray" } # Выбираем цветовую карту cmap_name = color_maps.get(color_map, color_maps["default"]) # Если нет matplotlib, используем простую цветовую карту try: import matplotlib.pyplot as plt cmap = plt.get_cmap(cmap_name) colored = (cmap(img_8bit / 255.0)[:, :, :3] * 255).astype(np.uint8) return colored except ImportError: # Упрощенная цветовая карта (красный-зеленый-синий) if cmap_name == "hot": r = img_8bit g = np.clip((img_8bit.astype(int) - 85) * 3, 0, 255).astype(np.uint8) b = np.clip((img_8bit.astype(int) - 170) * 3, 0, 255).astype(np.uint8) return np.stack([r, g, b], axis=2) elif cmap_name == "viridis": r = np.clip(img_8bit * 0.2, 0, 255).astype(np.uint8) g = np.clip(img_8bit * 0.6, 0, 255).astype(np.uint8) b = np.clip(img_8bit * 0.9, 0, 255).astype(np.uint8) return np.stack([r, g, b], axis=2) else: return np.stack([img_8bit] * 3, axis=2) @staticmethod def composite_layers(layers_data: list, opacities: list) -> np.ndarray: """ Композитинг нескольких слоев изображений Args: layers_data: Список numpy массивов изображений opacities: Список прозрачностей (0-1) Returns: Скомпозированное изображение """ if not layers_data: return None # Приводим все к одному размеру min_h = min(img.shape[0] for img in layers_data) min_w = min(img.shape[1] for img in layers_data) resized_layers = [] for img in layers_data: if img.shape[0] != min_h or img.shape[1] != min_w: from PIL import Image pil_img = Image.fromarray(img) pil_img = pil_img.resize((min_w, min_h), Image.Resampling.LANCZOS) resized_layers.append(np.array(pil_img).astype(float)) else: resized_layers.append(img.astype(float)) # Композитим result = resized_layers[0] * opacities[0] for i in range(1, len(resized_layers)): result += resized_layers[i] * opacities[i] # Нормализуем result = np.clip(result, 0, 255).astype(np.uint8) return result @staticmethod def create_test_image(width: int = 1024, height: int = 1024) -> np.ndarray: """ Создает тестовое изображение Солнца для отладки Args: width: Ширина изображения height: Высота изображения Returns: RGB изображение """ # Создаем черный фон img = np.zeros((height, width, 3), dtype=np.uint8) # Рисуем круг Солнца y, x = np.ogrid[:height, :width] center_y, center_x = height // 2, width // 2 radius = min(height, width) // 3 mask = (x - center_x)**2 + (y - center_y)**2 <= radius**2 # Градиент для Солнца distances = np.sqrt((x - center_x)**2 + (y - center_y)**2) gradient = 1 - (distances / radius) gradient = np.clip(gradient, 0, 1) # Цвета для разных каналов img[mask, 0] = (200 * gradient[mask]).astype(np.uint8) # Red img[mask, 1] = (150 * gradient[mask]).astype(np.uint8) # Green img[mask, 2] = (80 * gradient[mask]).astype(np.uint8) # Blue # Добавляем солнечные пятна spots = [ (center_x - 100, center_y - 50, 30), (center_x + 80, center_y + 40, 25), (center_x - 30, center_y + 90, 20), (center_x + 120, center_y - 80, 15), ] for sx, sy, r in spots: spot_mask = (x - sx)**2 + (y - sy)**2 <= r**2 img[spot_mask] = [50, 30, 20] return img