File size: 4,650 Bytes
f343f06
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""
Per-modality preprocessing for satellite imagery.

Handles different channel counts:
- Optical RGB: 3 channels (R, G, B)
- SAR: 2 channels (VV, VH)
- Multispectral: 12 channels (Sentinel-2 bands)
"""

import torch
import torch.nn.functional as F
from torchvision import transforms
from PIL import Image
import numpy as np


# ImageNet normalization for RGB
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]

# Sentinel-2 band statistics (approximate)
SENTINEL2_MEAN = [1353.0, 1117.0, 1042.0, 947.0, 1199.0, 1645.0, 1849.0, 1793.0, 1859.0, 1008.0, 1593.0, 1064.0]
SENTINEL2_STD = [235.0, 309.0, 392.0, 597.0, 490.0, 625.0, 736.0, 755.0, 846.0, 487.0, 561.0, 459.0]

# SAR statistics (approximate, in dB)
SAR_MEAN = [-12.0, -18.0]
SAR_STD = [5.0, 5.0]


def get_optical_transform(size: int = 224) -> transforms.Compose:
    """Get transforms for optical RGB images."""
    return transforms.Compose([
        transforms.Resize(size),
        transforms.CenterCrop(size),
        transforms.ToTensor(),
        transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)
    ])


def get_sar_transform(size: int = 224) -> transforms.Compose:
    """Get transforms for SAR images (VV/VH channels)."""
    return transforms.Compose([
        transforms.Resize(size),
        transforms.CenterCrop(size),
        transforms.ToTensor(),
        transforms.Normalize(mean=SAR_MEAN, std=SAR_STD)
    ])


def get_multispectral_transform(size: int = 224) -> transforms.Compose:
    """Get transforms for multispectral images (12 channels)."""
    return transforms.Compose([
        transforms.Resize(size),
        transforms.CenterCrop(size),
        transforms.ToTensor(),
        transforms.Normalize(mean=SENTINEL2_MEAN, std=SENTINEL2_STD)
    ])


def preprocess_image(
    image: Image.Image,
    modality: str,
    size: int = 224
) -> torch.Tensor:
    """
    Preprocess image based on modality.
    
    Args:
        image: Input PIL image
        modality: "optical", "sar", or "multispectral"
        size: Output image size
        
    Returns:
        Preprocessed tensor
    """
    # Handle channel mismatch before applying transform
    if modality == "sar":
        # SAR expects 2 channels, but PIL images are typically 3 channels
        # Convert to numpy, take first 2 channels, convert back
        img_array = np.array(image)
        if img_array.shape[-1] == 3:
            img_array = img_array[..., :2]
        image = Image.fromarray(img_array)
        transform = get_sar_transform(size)
    elif modality == "optical":
        transform = get_optical_transform(size)
    elif modality == "multispectral":
        transform = get_multispectral_transform(size)
    else:
        raise ValueError(f"Unknown modality: {modality}")
    
    return transform(image)


def handle_channels(
    image: np.ndarray,
    target_channels: int,
    modality: str
) -> np.ndarray:
    """
    Handle channel mismatch for different modalities.
    
    Args:
        image: Input image array (H, W, C)
        target_channels: Expected number of channels
        modality: Modality type
        
    Returns:
        Image with correct number of channels
    """
    current_channels = image.shape[-1] if len(image.shape) == 3 else 1
    
    if current_channels == target_channels:
        return image
    
    # ponytail: simple channel handling, not perfect but works for v1
    if modality == "optical" and current_channels >= 3:
        # Take first 3 channels (RGB)
        return image[..., :3]
    elif modality == "sar" and current_channels >= 2:
        # Take first 2 channels (VV, VH)
        return image[..., :2]
    elif modality == "multispectral":
        if current_channels < target_channels:
            # Pad with zeros
            padding = np.zeros((*image.shape[:-1], target_channels - current_channels))
            return np.concatenate([image, padding], axis=-1)
        else:
            # Take first 12 channels
            return image[..., :target_channels]
    
    return image


# Self-check
if __name__ == "__main__":
    # Create dummy images for testing
    dummy_rgb = Image.fromarray(np.random.randint(0, 255, (256, 256, 3), dtype=np.uint8))
    dummy_sar = Image.fromarray(np.random.randint(0, 255, (256, 256, 2), dtype=np.uint8))
    
    # Test preprocessing
    optical_tensor = preprocess_image(dummy_rgb, "optical")
    sar_tensor = preprocess_image(dummy_sar, "sar")
    
    print(f"Optical shape: {optical_tensor.shape}")  # Should be [3, 224, 224]
    print(f"SAR shape: {sar_tensor.shape}")  # Should be [2, 224, 224]
    
    print("Preprocessing test passed!")