225 lines
No EOL
8.6 KiB
Python
225 lines
No EOL
8.6 KiB
Python
"""
|
||
Парсер для извлечения FITS-метаданных из JP2 файлов
|
||
"""
|
||
|
||
import xml.etree.ElementTree as ET
|
||
from typing import Dict, Optional
|
||
import struct
|
||
|
||
|
||
class MetadataParser:
|
||
"""Парсер FITS-метаданных из JP2 файлов"""
|
||
|
||
@staticmethod
|
||
def extract_metadata(filepath: str) -> Optional[Dict[str, str]]:
|
||
"""
|
||
Извлекает FITS-метаданные из JP2 файла
|
||
|
||
Args:
|
||
filepath: Путь к JP2 файлу
|
||
|
||
Returns:
|
||
Словарь с метаданными или None
|
||
"""
|
||
try:
|
||
# Пытаемся прочитать JP2 файл как бинарный и найти XML
|
||
with open(filepath, 'rb') as f:
|
||
data = f.read()
|
||
|
||
# Ищем XML данные в файле (JP2 может содержать XML в специальных боксах)
|
||
# Простой способ: ищем теги XML
|
||
xml_start = data.find(b'<?xml')
|
||
if xml_start == -1:
|
||
xml_start = data.find(b'<fits')
|
||
|
||
if xml_start != -1:
|
||
# Ищем конец XML
|
||
xml_end = data.find(b'>', xml_start + 100) # Примерный поиск
|
||
# Находим закрывающий тег
|
||
if xml_end != -1:
|
||
# Расширяем поиск до полного XML
|
||
depth = 1
|
||
pos = xml_start
|
||
while depth > 0 and pos < len(data):
|
||
pos += 1
|
||
if pos >= len(data):
|
||
break
|
||
if data[pos:pos+2] == b'</':
|
||
depth -= 1
|
||
elif data[pos:pos+1] == b'<':
|
||
depth += 1
|
||
|
||
if pos < len(data):
|
||
xml_data = data[xml_start:pos+1].decode('utf-8', errors='ignore')
|
||
return MetadataParser._parse_fits_xml(xml_data)
|
||
|
||
# Если не нашли XML, пытаемся извлечь из текстовых блоков
|
||
# Ищем ASCII текст
|
||
text = data.decode('utf-8', errors='ignore')
|
||
|
||
# Ищем FITS-подобные ключи
|
||
metadata = {}
|
||
fits_keys = ['TELESCOP', 'INSTRUME', 'WAVELNTH', 'DATE-OBS', 'EXPTIME',
|
||
'CRPIX1', 'CRPIX2', 'CDELT1', 'CDELT2', 'NAXIS1', 'NAXIS2']
|
||
|
||
for key in fits_keys:
|
||
# Ищем ключ в тексте
|
||
pattern = f'{key}='
|
||
start = text.find(pattern)
|
||
if start != -1:
|
||
# Находим значение
|
||
value_start = start + len(pattern)
|
||
# Ищем конец строки или следующий ключ
|
||
value_end = text.find('/', value_start)
|
||
if value_end == -1:
|
||
value_end = text.find('\n', value_start)
|
||
if value_end == -1:
|
||
value_end = text.find(' ', value_start + 20)
|
||
|
||
value = text[value_start:value_end].strip().strip("'\"")
|
||
if value:
|
||
metadata[key] = value
|
||
|
||
if metadata:
|
||
return MetadataParser._format_metadata(metadata)
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"Ошибка извлечения метаданных: {e}")
|
||
return None
|
||
|
||
@staticmethod
|
||
def _parse_fits_xml(xml_content: str) -> Dict[str, str]:
|
||
"""
|
||
Парсит XML с FITS-заголовком
|
||
|
||
Args:
|
||
xml_content: XML строка
|
||
|
||
Returns:
|
||
Словарь с параметрами FITS
|
||
"""
|
||
metadata = {}
|
||
|
||
try:
|
||
root = ET.fromstring(xml_content)
|
||
|
||
# Ищем секцию fits или FITS
|
||
fits_section = root.find('.//fits') or root.find('.//FITS')
|
||
|
||
if fits_section is not None:
|
||
for child in fits_section:
|
||
# Извлекаем ключ и значение
|
||
key = child.tag
|
||
value = child.text if child.text else ""
|
||
|
||
# Убираем namespace если есть
|
||
if '}' in key:
|
||
key = key.split('}')[-1]
|
||
|
||
metadata[key.upper()] = value.strip()
|
||
|
||
# Если не нашли fits секцию, ищем другие возможные места
|
||
if not metadata:
|
||
for child in root.iter():
|
||
if child.tag.endswith('keyword'):
|
||
key = child.get('name', '')
|
||
value = child.text if child.text else ''
|
||
if key:
|
||
metadata[key.upper()] = value.strip()
|
||
|
||
# Форматируем некоторые ключи для удобства чтения
|
||
metadata = MetadataParser._format_metadata(metadata)
|
||
|
||
except Exception as e:
|
||
print(f"Ошибка парсинга XML: {e}")
|
||
|
||
return metadata
|
||
|
||
@staticmethod
|
||
def _format_metadata(metadata: Dict[str, str]) -> Dict[str, str]:
|
||
"""
|
||
Форматирует метаданные для удобного отображения
|
||
|
||
Args:
|
||
metadata: Сырые метаданные
|
||
|
||
Returns:
|
||
Отформатированные метаданные
|
||
"""
|
||
formatted = {}
|
||
|
||
# Переименовываем некоторые ключи для понятности
|
||
key_mapping = {
|
||
'TELESCOP': 'Телескоп',
|
||
'INSTRUME': 'Инструмент',
|
||
'DETECTOR': 'Детектор',
|
||
'WAVELNTH': 'Длина волны',
|
||
'WAVEUNIT': 'Единица длины волны',
|
||
'DATE-OBS': 'Дата наблюдения',
|
||
'DATE-BEG': 'Начало экспозиции',
|
||
'DATE-END': 'Конец экспозиции',
|
||
'EXPTIME': 'Время экспозиции (сек)',
|
||
'CRPIX1': 'Центр X (пикс)',
|
||
'CRPIX2': 'Центр Y (пикс)',
|
||
'CDELT1': 'Шаг пикселя X (arcsec)',
|
||
'CDELT2': 'Шаг пикселя Y (arcsec)',
|
||
'CROTA2': 'Угол поворота (град)',
|
||
'NAXIS1': 'Ширина (пикс)',
|
||
'NAXIS2': 'Высота (пикс)',
|
||
'BITPIX': 'Бит на пиксель',
|
||
'BZERO': 'Смещение данных',
|
||
'BSCALE': 'Масштаб данных',
|
||
'IMG_TYPE': 'Тип изображения',
|
||
'QUALITY': 'Качество',
|
||
'LEVEL': 'Уровень обработки',
|
||
'STATUS': 'Статус',
|
||
'OBSRVTRY': 'Обсерватория'
|
||
}
|
||
|
||
for key, value in metadata.items():
|
||
# Используем переименованный ключ или оригинал
|
||
display_key = key_mapping.get(key, key)
|
||
formatted[display_key] = value
|
||
|
||
return formatted
|
||
|
||
@staticmethod
|
||
def extract_from_jp2_box(filepath: str) -> Optional[Dict]:
|
||
"""
|
||
Альтернативный метод: извлечение метаданных через чтение JP2 боксов
|
||
Не требует glymur, читает файл напрямую
|
||
"""
|
||
try:
|
||
with open(filepath, 'rb') as f:
|
||
data = f.read()
|
||
|
||
# JP2 signature
|
||
if data[0:4] != b'\x00\x00\x00\x0c':
|
||
return None
|
||
|
||
# Ищем XML бокс (box type 'xml ')
|
||
offset = 0
|
||
while offset < len(data) - 8:
|
||
box_len = struct.unpack('>I', data[offset:offset+4])[0]
|
||
box_type = data[offset+4:offset+8]
|
||
|
||
if box_type == b'xml ' or box_type == b'XML ':
|
||
# Нашли XML бокс
|
||
xml_data = data[offset+8:offset+box_len]
|
||
try:
|
||
xml_str = xml_data.decode('utf-8', errors='ignore')
|
||
return MetadataParser._parse_fits_xml(xml_str)
|
||
except:
|
||
pass
|
||
|
||
if box_len == 0:
|
||
break
|
||
offset += box_len
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"Ошибка чтения JP2 боксов: {e}")
|
||
return None |