sid385 commited on
Commit
f429d8e
·
1 Parent(s): 73c4342

recitfied visualization

Browse files
backend/app/api/animation.py CHANGED
@@ -1,14 +1,15 @@
1
  from fastapi import APIRouter
 
2
  from app.schemas.common import ApiResponse
3
- from app.schemas.animation import AnimationSequenceResponse
 
4
 
5
  router = APIRouter()
6
 
 
7
  @router.get("/{file_id}/sequence", response_model=ApiResponse)
8
- async def get_sequence(file_id: str, start_time: str = "", end_time: str = "", fps: int = 10):
 
 
9
  # Placeholder
10
- return ApiResponse(
11
- success=True,
12
- message="Animation sequence retrieved",
13
- data=None
14
- )
 
1
  from fastapi import APIRouter
2
+
3
  from app.schemas.common import ApiResponse
4
+
5
+ # from app.schemas.animation import AnimationSequenceResponse
6
 
7
  router = APIRouter()
8
 
9
+
10
  @router.get("/{file_id}/sequence", response_model=ApiResponse)
11
+ async def get_sequence(
12
+ file_id: str, start_time: str = "", end_time: str = "", fps: int = 10
13
+ ):
14
  # Placeholder
15
+ return ApiResponse(success=True, message="Animation sequence retrieved", data=None)
 
 
 
 
backend/app/api/visualization.py CHANGED
@@ -1,12 +1,11 @@
1
  from fastapi import APIRouter, HTTPException, Query
2
- from fastapi.responses import FileResponse # 🚨 NAYA IMPORT
3
 
4
  from app.schemas.common import ApiResponse
5
  from app.services.scientific.visualization_service import VisualizationService
6
 
7
  router = APIRouter()
8
 
9
-
10
  @router.get("/{file_id}/variables", response_model=ApiResponse)
11
  async def get_variables(file_id: str):
12
  """
@@ -25,40 +24,45 @@ async def get_variables(file_id: str):
25
  )
26
 
27
 
28
- @router.get("/{file_id}/frame", response_model=ApiResponse)
29
- async def get_frame(
30
- file_id: str,
31
- variable: str = Query(..., description="The variable to extract"),
32
- time_index: int = Query(0, description="The time index to extract"),
33
  ):
34
  """
35
- Extract a 2D frame matrix for a specific variable and time index.
36
  """
37
  try:
38
- frame_data = VisualizationService.get_frame(file_id, variable, time_index)
39
  return ApiResponse(
40
  success=True,
41
- message=f"Frame data for {variable} extracted successfully.",
42
- data=frame_data.model_dump(),
43
  )
44
  except Exception as e:
45
  return ApiResponse(
46
- success=False, message=f"Failed to extract frame: {str(e)}", data=None
47
  )
48
 
49
 
50
- # 🚨 RESPONSE MODEL HATA DIYA HAI AUR FILE RESPONSE BHEJ RAHE HAIN
51
- @router.get("/{file_id}/thumbnail")
52
- async def get_thumbnail(
53
- file_id: str, variable: str = Query("C13", description="The variable to preview")
54
  ):
55
  """
56
- Generate and return a JPEG thumbnail image for the dataset preview.
 
57
  """
58
  try:
59
- thumb_path = VisualizationService.get_thumbnail_path(file_id, variable)
60
- return FileResponse(path=thumb_path, media_type="image/jpeg")
61
- except Exception as e:
62
- return ApiResponse(
63
- success=False, message=f"Failed to generate thumbnail: {str(e)}", data=None
 
 
 
64
  )
 
 
 
1
  from fastapi import APIRouter, HTTPException, Query
2
+ from fastapi.responses import StreamingResponse
3
 
4
  from app.schemas.common import ApiResponse
5
  from app.services.scientific.visualization_service import VisualizationService
6
 
7
  router = APIRouter()
8
 
 
9
  @router.get("/{file_id}/variables", response_model=ApiResponse)
10
  async def get_variables(file_id: str):
11
  """
 
24
  )
25
 
26
 
27
+ @router.get("/{file_id}/bounds", response_model=ApiResponse)
28
+ async def get_map_bounds(
29
+ file_id: str,
30
+ variable: str = Query("C13", description="The variable to extract bounds for")
 
31
  ):
32
  """
33
+ Get the geographical bounding box coordinates for Leaflet Map.
34
  """
35
  try:
36
+ bounds_data = VisualizationService.get_map_bounds(file_id, variable)
37
  return ApiResponse(
38
  success=True,
39
+ message="Map bounds extracted successfully.",
40
+ data=bounds_data,
41
  )
42
  except Exception as e:
43
  return ApiResponse(
44
+ success=False, message=f"Failed to extract bounds: {str(e)}", data=None
45
  )
46
 
47
 
48
+ @router.get("/{file_id}/layer")
49
+ async def get_map_layer(
50
+ file_id: str,
51
+ variable: str = Query("C13", description="The variable to extract")
52
  ):
53
  """
54
+ Returns a fast, transparent PNG image overlay for Leaflet Map.
55
+ Frontend Leaflet will use this URL directly in L.imageOverlay().
56
  """
57
  try:
58
+ img_buffer = VisualizationService.get_map_layer_image(file_id, variable)
59
+ return StreamingResponse(
60
+ img_buffer,
61
+ media_type="image/png",
62
+ headers={
63
+ "Cache-Control": "public, max-age=86400",
64
+ "Content-Disposition": f"inline; filename={file_id}_{variable}.png"
65
+ }
66
  )
67
+ except Exception as e:
68
+ raise HTTPException(status_code=500, detail=str(e))
backend/app/schemas/animation.py CHANGED
@@ -1,7 +1,7 @@
1
- from pydantic import BaseModel
2
- from typing import List
3
- from .visualization import FrameDataResponse
4
-
5
- class AnimationSequenceResponse(BaseModel):
6
- frames: List[FrameDataResponse]
7
- total_frames: int
 
1
+ # from pydantic import BaseModel
2
+ # from typing import List
3
+ # from .visualization import FrameDataResponse
4
+ #
5
+ # class AnimationSequenceResponse(BaseModel):
6
+ # frames: List[FrameDataResponse]
7
+ # total_frames: int
backend/app/schemas/visualization.py CHANGED
@@ -1,5 +1,7 @@
 
 
1
  from pydantic import BaseModel, Field
2
- from typing import List, Optional
3
 
4
  class VariableMetadata(BaseModel):
5
  name: str = Field(..., description="Name of the scientific variable")
@@ -9,8 +11,8 @@ class VariableMetadata(BaseModel):
9
  class Config:
10
  json_schema_extra = {
11
  "example": {
12
- "name": "brightness_temperature",
13
- "shape": [24, 512, 512],
14
  "datatype": "float32"
15
  }
16
  }
@@ -23,54 +25,20 @@ class VariablesResponse(BaseModel):
23
  "example": {
24
  "variables": [
25
  {
26
- "name": "brightness_temperature",
27
- "shape": [24, 512, 512],
28
  "datatype": "float32"
29
  }
30
  ]
31
  }
32
  }
33
 
34
- class FrameStatistics(BaseModel):
35
- min: float = Field(..., description="Minimum value in the frame")
36
- max: float = Field(..., description="Maximum value in the frame")
37
- mean: float = Field(..., description="Mean value of the frame")
38
- std: float = Field(..., description="Standard deviation of the frame")
39
 
40
  class Config:
41
  json_schema_extra = {
42
  "example": {
43
- "min": 180.3,
44
- "max": 321.7,
45
- "mean": 245.2,
46
- "std": 18.4
47
- }
48
- }
49
-
50
- class FrameDataResponse(BaseModel):
51
- file_id: str = Field(..., description="ID of the dataset file")
52
- variable: str = Field(..., description="Name of the variable")
53
- time_index: int = Field(..., description="Time index of the extracted frame")
54
- timestamp: Optional[str] = Field(None, description="ISO timestamp if available")
55
- shape: List[int] = Field(..., description="Shape of the 2D frame (height, width)")
56
- min: float = Field(..., description="Minimum value in the frame")
57
- max: float = Field(..., description="Maximum value in the frame")
58
- mean: float = Field(..., description="Mean value of the frame")
59
- std: float = Field(..., description="Standard deviation of the frame")
60
- z: List[List[float]] = Field(..., description="2D matrix of frame values")
61
-
62
- class Config:
63
- json_schema_extra = {
64
- "example": {
65
- "file_id": "abc123",
66
- "variable": "brightness_temperature",
67
- "time_index": 5,
68
- "timestamp": "2024-01-01T00:15:00Z",
69
- "shape": [512, 512],
70
- "min": 180.3,
71
- "max": 321.7,
72
- "mean": 245.2,
73
- "std": 18.4,
74
- "z": [[240.1, 241.5], [239.8, 240.2]]
75
  }
76
  }
 
1
+ from typing import List
2
+
3
  from pydantic import BaseModel, Field
4
+
5
 
6
  class VariableMetadata(BaseModel):
7
  name: str = Field(..., description="Name of the scientific variable")
 
11
  class Config:
12
  json_schema_extra = {
13
  "example": {
14
+ "name": "C13",
15
+ "shape": [512, 512],
16
  "datatype": "float32"
17
  }
18
  }
 
25
  "example": {
26
  "variables": [
27
  {
28
+ "name": "C13",
29
+ "shape": [512, 512],
30
  "datatype": "float32"
31
  }
32
  ]
33
  }
34
  }
35
 
36
+ class MapBoundsResponse(BaseModel):
37
+ bounds: List[List[float]] = Field(..., description="Geographical bounds for Leaflet map overlay [[South, West], [North, East]]")
 
 
 
38
 
39
  class Config:
40
  json_schema_extra = {
41
  "example": {
42
+ "bounds": [[8.0, 68.0], [37.0, 97.0]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
  }
backend/app/services/scientific/visualization_service.py CHANGED
@@ -1,17 +1,16 @@
 
1
  import logging
2
- import os
3
  from pathlib import Path
4
- from typing import Any, Dict, List, Optional
5
 
6
- import matplotlib.pyplot as plt
7
  import numpy as np
8
  from fastapi import HTTPException
9
  from huggingface_hub import HfFileSystem
 
10
 
11
- # 🚨 UPLOAD_DIR is completely removed. Using HF configs and Temp Storage.
12
  from app.core.config import HF_BUCKET_ID, HF_TOKEN, TEMP_STORAGE_DIR
13
- from app.schemas.visualization import (FrameDataResponse, FrameStatistics,
14
- VariableMetadata, VariablesResponse)
15
  from app.services.scientific.metadata_service import MetadataService
16
 
17
  logger = logging.getLogger(__name__)
@@ -19,7 +18,6 @@ logger = logging.getLogger(__name__)
19
  # Initialize the Hugging Face File System globally for this service
20
  fs = HfFileSystem(token=HF_TOKEN)
21
 
22
-
23
  class VisualizationService:
24
 
25
  @staticmethod
@@ -96,130 +94,94 @@ class VisualizationService:
96
  raise HTTPException(status_code=400, detail="Invalid Variable")
97
 
98
  @staticmethod
99
- def validate_time_index(time_index: int):
100
- if time_index < 0:
101
- logger.error(f"Invalid time index requested: {time_index}")
102
- raise HTTPException(status_code=400, detail="Invalid Time Index")
103
-
104
- @staticmethod
105
- def compute_statistics(frame: np.ndarray) -> FrameStatistics:
106
- valid_data = frame[~np.isnan(frame)]
107
- if valid_data.size == 0:
108
- return FrameStatistics(min=0.0, max=0.0, mean=0.0, std=0.0)
109
-
110
- return FrameStatistics(
111
- min=float(np.min(valid_data)),
112
- max=float(np.max(valid_data)),
113
- mean=float(np.mean(valid_data)),
114
- std=float(np.std(valid_data)),
115
- )
116
-
117
- @staticmethod
118
- def get_frame(file_id: str, variable: str, time_index: int) -> FrameDataResponse:
119
  file_path = VisualizationService._get_file_path(file_id)
120
- logger.info(f"Dataset opened: {file_id}")
121
-
122
  parser = None
123
  try:
124
  parser = MetadataService.get_parser(file_path)
125
  parser.load_dataset(file_path)
126
-
127
  VisualizationService.validate_variable(parser, variable)
128
- VisualizationService.validate_time_index(time_index)
129
-
130
- try:
131
- frame = parser.extract_time_slice(variable, time_index)
132
- logger.info("Frame extracted successfully")
133
- except Exception as e:
134
- logger.error(f"Time slice extraction failed: {str(e)}")
135
- raise HTTPException(status_code=400, detail="Invalid Time Index")
136
-
137
- if len(frame.shape) != 2:
138
- raise HTTPException(
139
- status_code=400, detail="Frame is not 2D after slicing"
140
- )
141
-
142
- timestamp = parser.extract_timestamp(time_index)
143
- stats = VisualizationService.compute_statistics(frame)
144
-
145
- frame_small = frame[::10, ::10]
146
-
147
- frame_clean = np.where(
148
- np.isnan(frame_small) | np.isinf(frame_small), -9999.0, frame_small
149
- ).tolist()
150
 
151
- response = FrameDataResponse(
152
- file_id=file_id,
153
- variable=variable,
154
- time_index=time_index,
155
- timestamp=timestamp,
156
- shape=list(frame_small.shape),
157
- min=stats.min,
158
- max=stats.max,
159
- mean=stats.mean,
160
- std=stats.std,
161
- z=frame_clean,
162
- )
 
163
 
164
- logger.info("Visualization request completed")
165
- return response
166
-
167
- except HTTPException:
168
- raise
169
  except Exception as e:
170
- logger.error(f"Visualization request failed: {str(e)}")
171
- raise HTTPException(status_code=500, detail="Dataset Read Failure")
 
172
  finally:
173
  if parser is not None:
174
  parser.close()
175
 
176
  @staticmethod
177
- def get_thumbnail_path(file_id: str, variable: str) -> str:
178
  """
179
- File se data nikal kar JPEG image banata hai, aur serverless cache me path return karta hai.
 
180
  """
181
- # 🚨 Cache logic updated for Serverless TEMP_STORAGE_DIR
182
- target_dir = Path(TEMP_STORAGE_DIR) / file_id
183
- target_dir.mkdir(parents=True, exist_ok=True)
184
-
185
- thumb_path = target_dir / f"thumb_{variable}.jpg"
186
-
187
- # Agar thumbnail pehle se bani hui hai (Cache), toh seedha path do
188
- if thumb_path.exists():
189
- return str(thumb_path)
190
-
191
- # File path fetch karo (Yeh check karega ki file locally cached hai ya HF se lani hai)
192
  file_path = VisualizationService._get_file_path(file_id)
193
-
194
  parser = None
195
  try:
196
  parser = MetadataService.get_parser(file_path)
197
  parser.load_dataset(file_path)
198
  VisualizationService.validate_variable(parser, variable)
199
-
200
- # Data nikalo
201
  frame = parser.extract_time_slice(variable, 0)
202
 
203
- # Downsample for Speed (fast processing)
204
- frame_small = frame[::5, ::5]
205
-
206
- # Image banao aur serverless disk cache par save karo
207
- plt.figure(figsize=(8, 8))
208
- plt.imshow(frame_small, cmap="gray", vmin=90, vmax=313)
209
- plt.axis("off")
210
-
211
- plt.savefig(
212
- thumb_path, bbox_inches="tight", pad_inches=0, format="jpg", dpi=100
213
- )
214
- plt.close()
215
-
216
- return str(thumb_path)
217
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  except HTTPException:
219
  raise
220
  except Exception as e:
221
- logger.error(f"Thumbnail generation failed: {str(e)}")
222
- raise HTTPException(status_code=500, detail="Failed to generate thumbnail")
223
  finally:
224
  if parser is not None:
225
  parser.close()
 
1
+ import io
2
  import logging
 
3
  from pathlib import Path
4
+ from typing import Dict, List
5
 
 
6
  import numpy as np
7
  from fastapi import HTTPException
8
  from huggingface_hub import HfFileSystem
9
+ from PIL import Image
10
 
11
+ # UPLOAD_DIR is replaced with HF configs and Temp Storage.
12
  from app.core.config import HF_BUCKET_ID, HF_TOKEN, TEMP_STORAGE_DIR
13
+ from app.schemas.visualization import VariableMetadata, VariablesResponse
 
14
  from app.services.scientific.metadata_service import MetadataService
15
 
16
  logger = logging.getLogger(__name__)
 
18
  # Initialize the Hugging Face File System globally for this service
19
  fs = HfFileSystem(token=HF_TOKEN)
20
 
 
21
  class VisualizationService:
22
 
23
  @staticmethod
 
94
  raise HTTPException(status_code=400, detail="Invalid Variable")
95
 
96
  @staticmethod
97
+ def get_map_bounds(file_id: str, variable: str) -> Dict[str, List[float]]:
98
+ """
99
+ Extracts the geographical bounding box of the satellite data.
100
+ Returns format for Leaflet: [[South, West], [North, East]]
101
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  file_path = VisualizationService._get_file_path(file_id)
 
 
103
  parser = None
104
  try:
105
  parser = MetadataService.get_parser(file_path)
106
  parser.load_dataset(file_path)
 
107
  VisualizationService.validate_variable(parser, variable)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
+ # SatPy scene area se bounds nikalna
110
+ area = parser.scene[variable].attrs.get('area')
111
+ if not area:
112
+ # Default bounds for India/Subcontinent as fallback
113
+ return {"bounds": [[8.0, 68.0], [37.0, 97.0]]}
114
+
115
+ lons, lats = area.get_lonlats()
116
+ south, north = float(np.nanmin(lats)), float(np.nanmax(lats))
117
+ west, east = float(np.nanmin(lons)), float(np.nanmax(lons))
118
+
119
+ return {
120
+ "bounds": [[south, west], [north, east]]
121
+ }
122
 
 
 
 
 
 
123
  except Exception as e:
124
+ logger.error(f"Failed to extract bounds: {str(e)}")
125
+ # Fallback bounds if projection logic fails initially
126
+ return {"bounds": [[8.0, 68.0], [37.0, 97.0]]}
127
  finally:
128
  if parser is not None:
129
  parser.close()
130
 
131
  @staticmethod
132
+ def get_map_layer_image(file_id: str, variable: str) -> io.BytesIO:
133
  """
134
+ Converts the 2D scientific array into a transparent PNG for Map Overlay.
135
+ Replaces both get_frame and get_thumbnail_path to be faster.
136
  """
 
 
 
 
 
 
 
 
 
 
 
137
  file_path = VisualizationService._get_file_path(file_id)
 
138
  parser = None
139
  try:
140
  parser = MetadataService.get_parser(file_path)
141
  parser.load_dataset(file_path)
142
  VisualizationService.validate_variable(parser, variable)
143
+
144
+ # Extract 2D array matrix (Time index 0)
145
  frame = parser.extract_time_slice(variable, 0)
146
 
147
+ # Downsample optimization for extremely fast web renders
148
+ frame = frame[::2, ::2]
149
+
150
+ # 1. Identify valid data vs empty space/NaNs
151
+ valid_mask = ~np.isnan(frame) & ~np.isinf(frame)
152
+ frame_clean = np.where(valid_mask, frame, 90.0)
153
+
154
+ # 2. Normalize Brightness Temperature to 0-255 range
155
+ MIN_BT = 90.0
156
+ MAX_BT = 313.0
157
+ frame_norm = np.clip((frame_clean - MIN_BT) / (MAX_BT - MIN_BT) * 255, 0, 255).astype(np.uint8)
158
+
159
+ # 3. Create RGBA image matrix (A stands for Alpha/Transparency)
160
+ rgba_img = np.zeros((frame_norm.shape[0], frame_norm.shape[1], 4), dtype=np.uint8)
161
+
162
+ # Grayscale mapping
163
+ rgba_img[..., 0] = frame_norm # Red
164
+ rgba_img[..., 1] = frame_norm # Green
165
+ rgba_img[..., 2] = frame_norm # Blue
166
+
167
+ # 4. Make NaNs and empty space completely transparent (0 opacity)
168
+ rgba_img[..., 3] = np.where(valid_mask, 255, 0)
169
+
170
+ # 5. Convert array to PIL Image
171
+ img = Image.fromarray(rgba_img, mode="RGBA")
172
+
173
+ # 6. Save to in-memory byte buffer
174
+ img_byte_arr = io.BytesIO()
175
+ img.save(img_byte_arr, format='PNG', optimize=True)
176
+ img_byte_arr.seek(0)
177
+
178
+ return img_byte_arr
179
+
180
  except HTTPException:
181
  raise
182
  except Exception as e:
183
+ logger.error(f"Image layer generation failed: {str(e)}")
184
+ raise HTTPException(status_code=500, detail="Failed to generate map layer")
185
  finally:
186
  if parser is not None:
187
  parser.close()
backend/pyproject.toml CHANGED
@@ -18,7 +18,8 @@ dependencies = [
18
  "satpy>=0.60.0",
19
  "scikit-image>=0.26.0",
20
  "uvicorn>=0.49.0",
21
- "scikit-image"
 
22
  ]
23
  [tool.pyright]
24
  venvPath = "~/Projects/satellite_video_feed/"
 
18
  "satpy>=0.60.0",
19
  "scikit-image>=0.26.0",
20
  "uvicorn>=0.49.0",
21
+ "scikit-image",
22
+ "pillow>=12.2.0",
23
  ]
24
  [tool.pyright]
25
  venvPath = "~/Projects/satellite_video_feed/"
uv.lock CHANGED
@@ -72,6 +72,7 @@ dependencies = [
72
  { name = "matplotlib" },
73
  { name = "netcdf4" },
74
  { name = "onnxruntime" },
 
75
  { name = "python-multipart" },
76
  { name = "satpy" },
77
  { name = "scikit-image" },
@@ -89,8 +90,10 @@ requires-dist = [
89
  { name = "matplotlib", specifier = ">=3.11.0" },
90
  { name = "netcdf4", specifier = ">=1.7.4" },
91
  { name = "onnxruntime", specifier = ">=1.27.0" },
 
92
  { name = "python-multipart", specifier = ">=0.0.32" },
93
  { name = "satpy", specifier = ">=0.60.0" },
 
94
  { name = "scikit-image", specifier = ">=0.26.0" },
95
  { name = "uvicorn", specifier = ">=0.49.0" },
96
  ]
 
72
  { name = "matplotlib" },
73
  { name = "netcdf4" },
74
  { name = "onnxruntime" },
75
+ { name = "pillow" },
76
  { name = "python-multipart" },
77
  { name = "satpy" },
78
  { name = "scikit-image" },
 
90
  { name = "matplotlib", specifier = ">=3.11.0" },
91
  { name = "netcdf4", specifier = ">=1.7.4" },
92
  { name = "onnxruntime", specifier = ">=1.27.0" },
93
+ { name = "pillow", specifier = ">=12.2.0" },
94
  { name = "python-multipart", specifier = ">=0.0.32" },
95
  { name = "satpy", specifier = ">=0.60.0" },
96
+ { name = "scikit-image" },
97
  { name = "scikit-image", specifier = ">=0.26.0" },
98
  { name = "uvicorn", specifier = ">=0.49.0" },
99
  ]