HelioParser/utils/image_processor.py
2026-06-10 17:33:12 +03:00

267 lines
No EOL
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Утилиты для обработки изображений: нормализация, преобразование цветов и т.д.
"""
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