saann commited on
Commit
4bb6657
·
0 Parent(s):

SAR Image Colorization — U-Net FastAPI

Browse files
Files changed (6) hide show
  1. Dockerfile +16 -0
  2. README.md +14 -0
  3. main.py +117 -0
  4. requirements.txt +7 -0
  5. static/.gitkeep +0 -0
  6. templates/index.html +253 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY --chown=user . .
13
+
14
+ RUN mkdir -p static
15
+
16
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SAR Image Colorization
2
+
3
+ Convert grayscale Sentinel-1 SAR radar images to optical color using U-Net deep learning.
4
+
5
+ ## Model
6
+ - Architecture: U-Net (encoder-decoder with skip connections)
7
+ - Input: 256x256 grayscale SAR image
8
+ - Output: 256x256 RGB colorized image
9
+ - Dataset: 16,000 paired Sentinel-1/Sentinel-2 images
10
+
11
+ ## Tech Stack
12
+ - TensorFlow 2.16 + U-Net
13
+ - FastAPI + Uvicorn
14
+ - Deployed on Hugging Face Spaces
main.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.templating import Jinja2Templates
4
+ from fastapi.requests import Request
5
+ from fastapi.responses import JSONResponse
6
+ import numpy as np
7
+ from PIL import Image
8
+ import io
9
+ import os
10
+ import base64
11
+ import tensorflow as tf
12
+ from tensorflow.keras import layers, Model
13
+
14
+ app = FastAPI(title="SAR Image Colorization")
15
+
16
+ app.mount("/static", StaticFiles(directory="static"), name="static")
17
+ templates = Jinja2Templates(directory="templates")
18
+
19
+ IMG_SIZE = (256, 256)
20
+ MODEL_PATH = "sar_model.weights.h5"
21
+ model = None
22
+
23
+ def build_unet(input_shape=(256, 256, 1)):
24
+ inputs = layers.Input(input_shape)
25
+ def conv_block(x, filters):
26
+ x = layers.Conv2D(filters, 3, padding="same", activation="relu")(x)
27
+ x = layers.BatchNormalization()(x)
28
+ x = layers.Conv2D(filters, 3, padding="same", activation="relu")(x)
29
+ x = layers.BatchNormalization()(x)
30
+ return x
31
+ def encoder_block(x, filters):
32
+ skip = conv_block(x, filters)
33
+ pool = layers.MaxPooling2D(2)(skip)
34
+ return skip, pool
35
+ def decoder_block(x, skip, filters):
36
+ x = layers.Conv2DTranspose(filters, 2, strides=2, padding="same")(x)
37
+ x = layers.Concatenate()([x, skip])
38
+ x = conv_block(x, filters)
39
+ return x
40
+ s1, p1 = encoder_block(inputs, 32)
41
+ s2, p2 = encoder_block(p1, 64)
42
+ s3, p3 = encoder_block(p2, 128)
43
+ s4, p4 = encoder_block(p3, 256)
44
+ b = conv_block(p4, 512)
45
+ d1 = decoder_block(b, s4, 256)
46
+ d2 = decoder_block(d1, s3, 128)
47
+ d3 = decoder_block(d2, s2, 64)
48
+ d4 = decoder_block(d3, s1, 32)
49
+ outputs = layers.Conv2D(3, 1, activation="tanh")(d4)
50
+ return Model(inputs, outputs)
51
+
52
+ @app.on_event("startup")
53
+ async def load_model():
54
+ global model
55
+ if os.path.exists(MODEL_PATH):
56
+ model = build_unet()
57
+ model(tf.zeros((1, 256, 256, 1))) # build model
58
+ model.load_weights(MODEL_PATH)
59
+ print("✅ SAR model loaded successfully")
60
+ else:
61
+ print("⚠️ Model not found — demo mode")
62
+
63
+ def preprocess(image_bytes):
64
+ img = Image.open(io.BytesIO(image_bytes)).convert("L")
65
+ # Use LANCZOS for high quality resize
66
+ img = img.resize(IMG_SIZE, Image.LANCZOS)
67
+ arr = np.array(img, dtype=np.float32) / 127.5 - 1.0
68
+ return np.expand_dims(arr[..., np.newaxis], 0)
69
+
70
+ def to_base64(arr_uint8):
71
+ img = Image.fromarray(arr_uint8)
72
+ buf = io.BytesIO()
73
+ img.save(buf, format="PNG")
74
+ return base64.b64encode(buf.getvalue()).decode()
75
+
76
+ @app.get("/")
77
+ async def home(request: Request):
78
+ return templates.TemplateResponse("index.html", {"request": request})
79
+
80
+ @app.post("/colorize")
81
+ async def colorize(file: UploadFile = File(...)):
82
+ if not file.content_type.startswith("image/"):
83
+ raise HTTPException(status_code=400, detail="File must be an image.")
84
+ contents = await file.read()
85
+ if len(contents) > 10 * 1024 * 1024:
86
+ raise HTTPException(status_code=400, detail="Image too large. Max 10MB.")
87
+ try:
88
+ inp = preprocess(contents)
89
+ except Exception:
90
+ raise HTTPException(status_code=400, detail="Could not process image.")
91
+
92
+ if model is None:
93
+ # Demo mode
94
+ import random
95
+ dummy = np.random.randint(50, 200, (256, 256, 3), dtype=np.uint8)
96
+ dummy[:,:,0] = np.clip(dummy[:,:,0], 30, 100)
97
+ dummy[:,:,1] = np.clip(dummy[:,:,1], 80, 180)
98
+ dummy[:,:,2] = np.clip(dummy[:,:,2], 30, 100)
99
+ pred_b64 = to_base64(dummy)
100
+ else:
101
+ pred = model.predict(inp, verbose=0)[0]
102
+ pred_uint8 = ((pred + 1) * 127.5).clip(0, 255).astype(np.uint8)
103
+ pred_b64 = to_base64(pred_uint8)
104
+
105
+ # Also return input as base64 for display
106
+ inp_disp = ((inp[0,:,:,0] + 1) * 127.5).clip(0, 255).astype(np.uint8)
107
+ inp_b64 = to_base64(inp_disp)
108
+
109
+ return JSONResponse({
110
+ "success": True,
111
+ "input_b64": inp_b64,
112
+ "output_b64": pred_b64
113
+ })
114
+
115
+ @app.get("/health")
116
+ async def health():
117
+ return {"status": "ok", "model_loaded": model is not None}
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ python-multipart==0.0.6
4
+ jinja2==3.1.2
5
+ Pillow==10.1.0
6
+ numpy==1.26.4
7
+ tensorflow==2.16.1
static/.gitkeep ADDED
File without changes
templates/index.html ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SAR Image Colorization</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { background: #0d1117; color: #e6edf3; font-family: Arial, sans-serif; min-height: 100vh; }
10
+
11
+ header {
12
+ background: linear-gradient(135deg, #1a2a4a, #0d1117);
13
+ padding: 24px 40px;
14
+ border-bottom: 1px solid #30363d;
15
+ }
16
+ header h1 { font-size: 28px; color: #58a6ff; }
17
+ header p { color: #8b949e; font-size: 14px; margin-top: 4px; }
18
+ .tag { display: inline-block; background: #1f6feb33; color: #58a6ff; border: 1px solid #1f6feb; border-radius: 20px; padding: 2px 12px; font-size: 12px; margin-top: 8px; margin-right: 6px; }
19
+
20
+ .container { max-width: 1100px; margin: 40px auto; padding: 0 24px; }
21
+
22
+ .upload-section {
23
+ background: #161b22;
24
+ border: 2px dashed #30363d;
25
+ border-radius: 12px;
26
+ padding: 40px;
27
+ text-align: center;
28
+ cursor: pointer;
29
+ transition: border-color 0.3s;
30
+ margin-bottom: 32px;
31
+ }
32
+ .upload-section:hover { border-color: #58a6ff; }
33
+ .upload-section.dragover { border-color: #58a6ff; background: #1f2937; }
34
+ .upload-icon { font-size: 48px; margin-bottom: 12px; }
35
+ .upload-section h2 { font-size: 20px; margin-bottom: 8px; }
36
+ .upload-section p { color: #8b949e; font-size: 14px; }
37
+
38
+ input[type="file"] { display: none; }
39
+
40
+ .btn {
41
+ background: linear-gradient(135deg, #1f6feb, #58a6ff);
42
+ color: white;
43
+ border: none;
44
+ padding: 12px 32px;
45
+ border-radius: 8px;
46
+ font-size: 16px;
47
+ cursor: pointer;
48
+ margin-top: 16px;
49
+ transition: opacity 0.2s;
50
+ }
51
+ .btn:hover { opacity: 0.85; }
52
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
53
+
54
+ .result-section { display: none; }
55
+ .result-section h2 { font-size: 22px; margin-bottom: 20px; color: #58a6ff; }
56
+
57
+ .images-grid {
58
+ display: grid;
59
+ grid-template-columns: 1fr 60px 1fr;
60
+ gap: 0;
61
+ align-items: center;
62
+ margin-bottom: 32px;
63
+ }
64
+ .image-card {
65
+ background: #161b22;
66
+ border: 1px solid #30363d;
67
+ border-radius: 12px;
68
+ overflow: hidden;
69
+ }
70
+ .image-card .label {
71
+ padding: 12px 16px;
72
+ font-size: 13px;
73
+ font-weight: bold;
74
+ border-bottom: 1px solid #30363d;
75
+ }
76
+ .image-card.input .label { color: #8b949e; }
77
+ .image-card.output .label { color: #3fb950; }
78
+ .image-card img {
79
+ width: 100%;
80
+ display: block;
81
+ image-rendering: pixelated;
82
+ }
83
+
84
+ .arrow {
85
+ text-align: center;
86
+ font-size: 36px;
87
+ color: #58a6ff;
88
+ }
89
+
90
+ .loading {
91
+ display: none;
92
+ text-align: center;
93
+ padding: 40px;
94
+ }
95
+ .spinner {
96
+ width: 48px; height: 48px;
97
+ border: 4px solid #30363d;
98
+ border-top-color: #58a6ff;
99
+ border-radius: 50%;
100
+ animation: spin 0.8s linear infinite;
101
+ margin: 0 auto 16px;
102
+ }
103
+ @keyframes spin { to { transform: rotate(360deg); } }
104
+
105
+ .how-it-works {
106
+ background: #161b22;
107
+ border: 1px solid #30363d;
108
+ border-radius: 12px;
109
+ padding: 24px;
110
+ margin-top: 32px;
111
+ }
112
+ .how-it-works h3 { color: #58a6ff; margin-bottom: 16px; }
113
+ .steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
114
+ .step { text-align: center; }
115
+ .step-num { width: 36px; height: 36px; border-radius: 50%; background: #1f6feb; color: white; font-weight: bold; display: flex; align-items: center; justify-content: center; margin: 0 auto 8px; }
116
+ .step p { font-size: 13px; color: #8b949e; }
117
+
118
+ footer {
119
+ text-align: center;
120
+ padding: 24px;
121
+ color: #8b949e;
122
+ font-size: 13px;
123
+ border-top: 1px solid #30363d;
124
+ margin-top: 48px;
125
+ }
126
+ </style>
127
+ </head>
128
+ <body>
129
+
130
+ <header>
131
+ <h1>🛰️ SAR Image Colorization</h1>
132
+ <p>Convert grayscale Sentinel-1 SAR radar images to optical color using U-Net deep learning</p>
133
+ <span class="tag">U-Net</span>
134
+ <span class="tag">Sentinel-1 → Sentinel-2</span>
135
+ <span class="tag">Image-to-Image Translation</span>
136
+ </header>
137
+
138
+ <div class="container">
139
+
140
+ <div class="upload-section" id="dropZone" onclick="document.getElementById('fileInput').click()">
141
+ <div class="upload-icon">🛰️</div>
142
+ <h2>Upload SAR Image</h2>
143
+ <p>Drag & drop a Sentinel-1 grayscale image or click to browse</p>
144
+ <p style="margin-top:8px; font-size:12px; color:#6e7681;">Supports PNG, JPG • Max 10MB</p>
145
+ <button class="btn" onclick="event.stopPropagation(); document.getElementById('fileInput').click()">Choose Image</button>
146
+ <input type="file" id="fileInput" accept="image/*">
147
+ </div>
148
+
149
+ <div class="loading" id="loading">
150
+ <div class="spinner"></div>
151
+ <p style="color:#8b949e;">Colorizing your SAR image...</p>
152
+ </div>
153
+
154
+ <div class="result-section" id="resultSection">
155
+ <h2>✅ Colorization Result</h2>
156
+ <div class="images-grid">
157
+ <div class="image-card input">
158
+ <div class="label">📡 SAR Input (Grayscale)</div>
159
+ <img id="inputImg" src="" alt="Input">
160
+ </div>
161
+ <div class="arrow">→</div>
162
+ <div class="image-card output">
163
+ <div class="label">🌍 Colorized Output (Predicted)</div>
164
+ <img id="outputImg" src="" alt="Output">
165
+ </div>
166
+ </div>
167
+ <button class="btn" onclick="resetApp()">Try Another Image</button>
168
+ </div>
169
+
170
+ <div class="how-it-works">
171
+ <h3>How It Works</h3>
172
+ <div class="steps">
173
+ <div class="step">
174
+ <div class="step-num">1</div>
175
+ <p>Upload a grayscale SAR radar image from Sentinel-1</p>
176
+ </div>
177
+ <div class="step">
178
+ <div class="step-num">2</div>
179
+ <p>U-Net encoder extracts spatial features at multiple scales</p>
180
+ </div>
181
+ <div class="step">
182
+ <div class="step-num">3</div>
183
+ <p>Decoder reconstructs image with predicted RGB colors</p>
184
+ </div>
185
+ <div class="step">
186
+ <div class="step-num">4</div>
187
+ <p>Output resembles what a Sentinel-2 optical satellite would see</p>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ </div>
193
+
194
+ <footer>
195
+ Built by Sanjay S &nbsp;|&nbsp; U-Net · TensorFlow · FastAPI &nbsp;|&nbsp;
196
+ <a href="https://github.com/sanjay5656/sar-colorization" style="color:#58a6ff;">GitHub</a>
197
+ </footer>
198
+
199
+ <script>
200
+ const dropZone = document.getElementById("dropZone");
201
+ const fileInput = document.getElementById("fileInput");
202
+ const loading = document.getElementById("loading");
203
+ const resultSec = document.getElementById("resultSection");
204
+ const inputImg = document.getElementById("inputImg");
205
+ const outputImg = document.getElementById("outputImg");
206
+
207
+ fileInput.addEventListener("change", e => {
208
+ if (e.target.files[0]) handleFile(e.target.files[0]);
209
+ });
210
+
211
+ dropZone.addEventListener("dragover", e => {
212
+ e.preventDefault();
213
+ dropZone.classList.add("dragover");
214
+ });
215
+ dropZone.addEventListener("dragleave", () => dropZone.classList.remove("dragover"));
216
+ dropZone.addEventListener("drop", e => {
217
+ e.preventDefault();
218
+ dropZone.classList.remove("dragover");
219
+ if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
220
+ });
221
+
222
+ async function handleFile(file) {
223
+ dropZone.style.display = "none";
224
+ loading.style.display = "block";
225
+ resultSec.style.display = "none";
226
+
227
+ const formData = new FormData();
228
+ formData.append("file", file);
229
+
230
+ try {
231
+ const res = await fetch("/colorize", { method: "POST", body: formData });
232
+ const data = await res.json();
233
+ if (data.success) {
234
+ inputImg.src = "data:image/png;base64," + data.input_b64;
235
+ outputImg.src = "data:image/png;base64," + data.output_b64;
236
+ loading.style.display = "none";
237
+ resultSec.style.display = "block";
238
+ }
239
+ } catch (err) {
240
+ alert("Error: " + err.message);
241
+ resetApp();
242
+ }
243
+ }
244
+
245
+ function resetApp() {
246
+ dropZone.style.display = "block";
247
+ loading.style.display = "none";
248
+ resultSec.style.display = "none";
249
+ fileInput.value = "";
250
+ }
251
+ </script>
252
+ </body>
253
+ </html>