SXXXXXXX_GeoElevation/image_processor.py
VALLONGOL 2661f5bc42 add 2D and 3D view for tile
add 2D view for tile mosaik
2025-04-14 13:20:07 +02:00

289 lines
10 KiB
Python

# image_processor.py
import os
import math
import logging
from typing import Optional, List, Dict, Tuple
try:
from PIL import Image, ImageDraw, ImageFont
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
# Definisci classi dummy se PIL non c'è, per evitare errori altrove
class Image:
pass
class ImageDraw:
pass
class ImageFont:
pass
logging.error(
"Pillow library (PIL) not found. Image processing features will be disabled."
)
# Costanti per disegno (possono essere spostate in config se necessario)
TILE_TEXT_COLOR = "white"
TILE_TEXT_BG_COLOR = "black" # Sfondo opaco
PLACEHOLDER_COLOR = (128, 128, 128) # Grigio per placeholder RGB
TILE_BORDER_COLOR = "red"
TILE_BORDER_WIDTH = 2 # Spessore bordo griglia
# Carica font (una sola volta)
DEFAULT_FONT = None
if PIL_AVAILABLE:
try:
# Prova diversi font comuni
font_paths = [
"arial.ttf",
"LiberationSans-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
for font_path in font_paths:
try:
DEFAULT_FONT = ImageFont.truetype(
font_path, 12
) # Aumenta dimensione font
logging.info(f"Loaded font: {font_path}")
break
except IOError:
continue # Prova il prossimo
if DEFAULT_FONT is None:
DEFAULT_FONT = ImageFont.load_default()
logging.warning("Common system fonts not found, using default PIL font.")
except Exception as e:
logging.error(f"Error loading font: {e}")
DEFAULT_FONT = ImageFont.load_default() if PIL_AVAILABLE else None
def add_overlay_info(
image: Optional[Image.Image], tile_name: str, source_text: str = "NASADEM"
) -> Optional[Image.Image]:
"""
Draws tile name and source information onto a PIL Image object.
Args:
image (Optional[Image.Image]): The PIL Image object to draw on. If None, does nothing.
tile_name (str): The base name of the tile (e.g., 'n45e007').
source_text (str): Text indicating the data source.
Returns:
Optional[Image.Image]: The modified image object, or None if input was None or PIL unavailable.
"""
if not PIL_AVAILABLE or image is None:
return None
try:
# Assicurati sia modificabile (RGB o RGBA)
if image.mode not in ["RGB", "RGBA"]:
image = image.convert("RGB")
draw = ImageDraw.Draw(image)
text = f"{tile_name.upper()}\nSource: {source_text}"
font = DEFAULT_FONT if DEFAULT_FONT else ImageFont.load_default()
# Calcola dimensioni testo per posizionamento
try:
# textbbox è più preciso ma richiede Pillow >= 8.0.0
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
except AttributeError:
# Fallback per versioni Pillow più vecchie
text_width, text_height = draw.textsize(text, font=font)
margin = 5
text_x = image.width - text_width - margin
text_y = image.height - text_height - margin
# Disegna sfondo e testo
bg_coords = [
text_x - 2,
text_y - 2,
text_x + text_width + 2,
text_y + text_height + 2,
]
draw.rectangle(bg_coords, fill=TILE_TEXT_BG_COLOR)
draw.text((text_x, text_y), text, fill=TILE_TEXT_COLOR, font=font)
return image
except Exception as e:
logging.error(
f"Error adding overlay to image for {tile_name}: {e}", exc_info=True
)
return image # Restituisci immagine originale in caso di errore
def load_prepare_single_browse(
image_path: Optional[str], tile_name: str
) -> Optional[Image.Image]:
"""
Loads a browse image from path, adds overlay info.
Args:
image_path (Optional[str]): Path to the browse image file.
tile_name (str): Base name of the tile (e.g., 'n45e007').
Returns:
Optional[Image.Image]: Prepared PIL Image object or None if loading/processing fails.
"""
if not PIL_AVAILABLE or not image_path or not os.path.exists(image_path):
return None
try:
logging.debug(f"Loading browse image: {image_path}")
img = Image.open(image_path)
img_with_overlay = add_overlay_info(img, tile_name)
return img_with_overlay
except Exception as e:
logging.error(
f"Failed to load or prepare browse image {image_path}: {e}", exc_info=True
)
return None
def create_composite_area_image(
tile_info_list: List[Dict], target_tile_size: Optional[Tuple[int, int]] = None
) -> Optional[Image.Image]:
"""
Creates a composite image by mosaicking available browse images for an area.
Args:
tile_info_list (List[Dict]): List of dictionaries from ElevationManager.get_area_tile_info.
target_tile_size (Optional[Tuple[int, int]]): If provided, resize each tile to this size.
Otherwise, uses max dimensions found.
Returns:
Optional[Image.Image]: Composite PIL Image object or None if no tiles or error.
"""
if not PIL_AVAILABLE or not tile_info_list:
logging.warning(
"Cannot create composite image: PIL unavailable or no tile info provided."
)
return None
logging.info(f"Creating composite image for {len(tile_info_list)} tile area.")
# Estrai coordinate min/max per determinare la griglia
lats = [info["latitude_coord"] for info in tile_info_list]
lons = [info["longitude_coord"] for info in tile_info_list]
if not lats or not lons:
return None # Lista vuota?
min_lat, max_lat = min(lats), max(lats)
min_lon, max_lon = min(lons), max(lons)
num_lat_tiles = max_lat - min_lat + 1
num_lon_tiles = max_lon - min_lon + 1
# Carica immagini e determina dimensioni massime (o usa target)
tile_images: Dict[Tuple[int, int], Optional[Image.Image]] = {}
max_w, max_h = 0, 0
loaded_image_count = 0
for info in tile_info_list:
lat, lon = info["latitude_coord"], info["longitude_coord"]
key = (lat, lon)
img_path = info.get("browse_image_path")
img = None
if img_path and os.path.exists(img_path):
try:
img = Image.open(img_path).convert("RGB") # Carica come RGB
tile_images[key] = img
if not target_tile_size: # Se non forziamo dimensione, trova max
max_w = max(max_w, img.width)
max_h = max(max_h, img.height)
loaded_image_count += 1
except Exception as e:
logging.warning(f"Could not load browse image {img_path}: {e}")
tile_images[key] = None # Segna come non caricata
else:
tile_images[key] = None # Non disponibile
if loaded_image_count == 0:
logging.warning("No browse images available for the specified area.")
# Potremmo restituire None o un'immagine placeholder unica? Per ora None.
return None
# Imposta dimensione cella: target o massima trovata
tile_w = target_tile_size[0] if target_tile_size else max_w
tile_h = target_tile_size[1] if target_tile_size else max_h
if tile_w <= 0 or tile_h <= 0:
logging.error(
"Invalid target tile size or no valid images found to determine size."
)
return None # Dimensione non valida
# Crea immagine composita
total_width = num_lon_tiles * tile_w
total_height = num_lat_tiles * tile_h
logging.info(
f"Creating composite canvas: {total_width}x{total_height} ({num_lon_tiles}x{num_lat_tiles} tiles of {tile_w}x{tile_h})"
)
composite_img = Image.new("RGB", (total_width, total_height), PLACEHOLDER_COLOR)
draw = ImageDraw.Draw(composite_img)
# Incolla tile, disegna griglia e testo
for r, lat_coord in enumerate(
range(max_lat, min_lat - 1, -1)
): # Y cresce verso il basso (Nord in alto)
for c, lon_coord in enumerate(
range(min_lon, max_lon + 1)
): # X cresce verso destra (Ovest a sinistra)
key = (lat_coord, lon_coord)
img = tile_images.get(key)
paste_x = c * tile_w
paste_y = r * tile_h
if img:
# Ridimensiona se necessario
if img.width != tile_w or img.height != tile_h:
img = img.resize((tile_w, tile_h), Image.Resampling.LANCZOS)
composite_img.paste(img, (paste_x, paste_y))
# Disegna bordo
draw.rectangle(
[paste_x, paste_y, paste_x + tile_w - 1, paste_y + tile_h - 1],
outline=TILE_BORDER_COLOR,
width=TILE_BORDER_WIDTH,
)
# Aggiungi etichetta (riutilizza logica)
tile_name = f"{'N' if lat_coord >= 0 else 'S'}{abs(lat_coord):02d}{'E' if lon_coord >= 0 else 'W'}{abs(lon_coord):03d}"
add_overlay_info(
composite_img.crop(
(paste_x, paste_y, paste_x + tile_w, paste_y + tile_h)
), # Disegna su un crop temporaneo? No, direttamente sulla composita
tile_name,
source_text="",
) # Passa empty source? O solo nome tile?
# Disegna nome tile direttamente sulla composita
font = DEFAULT_FONT if DEFAULT_FONT else ImageFont.load_default()
text = tile_name
try:
text_bbox = draw.textbbox((0, 0), text, font=font)
text_w = text_bbox[2] - text_bbox[0]
text_h = text_bbox[3] - text_bbox[1]
except AttributeError:
text_w, text_h = draw.textsize(text, font=font) # Fallback
margin = 3
text_pos_x = paste_x + tile_w - text_w - margin
text_pos_y = paste_y + tile_h - text_h - margin
bg_coords = [
text_pos_x - 1,
text_pos_y - 1,
text_pos_x + text_w + 1,
text_pos_y + text_h + 1,
]
draw.rectangle(bg_coords, fill=TILE_TEXT_BG_COLOR)
draw.text((text_pos_x, text_pos_y), text, fill=TILE_TEXT_COLOR, font=font)
logging.info("Composite image created successfully.")
return composite_img