nakas commited on
Commit
b1d5591
·
1 Parent(s): f41ef02

Fix smoke plume artifacts: realistic HMS thresholds, light smoothing/morphology for polygons, proper contour grid axes, and legend sync

Browse files
Files changed (1) hide show
  1. app.py +53 -18
app.py CHANGED
@@ -185,7 +185,7 @@ class FoliumSmokeRenderer:
185
  <div style="margin: 5px 0;">
186
  <span style="display: inline-block; width: 20px; height: 15px;
187
  background-color: #E8E8E8; border: 1px solid #CCC; margin-right: 5px;"></span>
188
- <span style="font-size: 12px;">Light (0.5-15 µg/m³)</span>
189
  </div>
190
  <div style="margin: 5px 0;">
191
  <span style="display: inline-block; width: 20px; height: 15px;
@@ -272,12 +272,12 @@ class HMSSmokePolygonGenerator:
272
  """Generate HMS-style smoke plume polygons from HRRR-Smoke data"""
273
 
274
  def __init__(self):
275
- # HMS operational smoke detection categories (post-2022 update)
276
- # HMS analysts use visual detection from GOES 1km imagery, not direct PM2.5 thresholds
277
  self.density_categories = {
278
- 'light': {'min': 1, 'max': 2, 'color': '#FFFF88', 'description': 'Light smoke (category 1)'},
279
- 'medium': {'min': 2, 'max': 3, 'color': '#FFB366', 'description': 'Medium smoke (category 2)'},
280
- 'heavy': {'min': 3, 'max': 999, 'color': '#FF6666', 'description': 'Heavy smoke (category 3)'}
281
  }
282
 
283
  # HMS visibility-based smoke detection thresholds (primary detection method)
@@ -496,7 +496,7 @@ class HMSSmokePolygonGenerator:
496
  return classifications
497
 
498
  def extract_smoke_polygons(self, lat2d, lon2d, smoke_values, min_area=0.01, is_hms_categorical=False):
499
- """Extract smoke plume polygons from gridded data - optimized for speed"""
500
  polygons = []
501
 
502
  if not HMS_LIBS_AVAILABLE:
@@ -510,8 +510,14 @@ class HMSSmokePolygonGenerator:
510
  classifications = smoke_values.astype(int)
511
  print(f"Using HMS categorical data directly: {np.unique(classifications)}")
512
  else:
513
- # Classify smoke densities from concentration values
514
- classifications = self.classify_smoke_density(smoke_values)
 
 
 
 
 
 
515
 
516
  # Process each density category
517
  for category_name, category_info in self.density_categories.items():
@@ -528,9 +534,30 @@ class HMSSmokePolygonGenerator:
528
  if not np.any(category_mask):
529
  continue
530
 
531
- # Skip morphological operations for speed - use raw mask
532
- # smoothed_mask = binary_dilation(category_mask, iterations=1)
533
- smoothed_mask = category_mask
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
  # Find contours with simpler algorithm
536
  try:
@@ -544,8 +571,8 @@ class HMSSmokePolygonGenerator:
544
  if len(contour) < 4: # Need at least 4 points for polygon
545
  continue
546
 
547
- # Simplify contour for speed - use smaller step for light smoke to preserve more detail
548
- step = 2 if category_name == 'light' else 2
549
  simplified_contour = contour[::step]
550
 
551
  # Convert pixel coordinates to lat/lon
@@ -1587,11 +1614,19 @@ class HRRRSmokeApp:
1587
  # Try contour plotting
1588
  try:
1589
  opacity = 0.6 if polygons else 1.0 # Reduce opacity if polygons are shown
 
 
 
 
 
 
 
1590
  fig.add_trace(go.Contour(
1591
- x=lon2d.flatten(),
1592
- y=lat2d.flatten(),
1593
- z=z2d.flatten(),
1594
  colorscale=colorscale,
 
1595
  showscale=True,
1596
  opacity=opacity,
1597
  colorbar=dict(
@@ -2438,4 +2473,4 @@ with gr.Blocks(title="HRRR Smoke Forecast", theme=gr.themes.Soft()) as app:
2438
  """)
2439
 
2440
  if __name__ == "__main__":
2441
- app.launch(server_name="0.0.0.0", server_port=7860)
 
185
  <div style="margin: 5px 0;">
186
  <span style="display: inline-block; width: 20px; height: 15px;
187
  background-color: #E8E8E8; border: 1px solid #CCC; margin-right: 5px;"></span>
188
+ <span style="font-size: 12px;">Light (3–15 µg/m³)</span>
189
  </div>
190
  <div style="margin: 5px 0;">
191
  <span style="display: inline-block; width: 20px; height: 15px;
 
272
  """Generate HMS-style smoke plume polygons from HRRR-Smoke data"""
273
 
274
  def __init__(self):
275
+ # HMS-style category thresholds expressed in µg/m³ (approximate, for visualization)
276
+ # These align with the legend and produce HMS-like plumes
277
  self.density_categories = {
278
+ 'light': {'min': 3, 'max': 15, 'color': '#FFFF88', 'description': 'Light smoke (3–15 µg/m³)'},
279
+ 'medium': {'min': 15, 'max': 35, 'color': '#FFB366', 'description': 'Medium smoke (15–35 µg/m³)'},
280
+ 'heavy': {'min': 35, 'max': 9999, 'color': '#FF6666', 'description': 'Heavy smoke (35+ µg/m³)'}
281
  }
282
 
283
  # HMS visibility-based smoke detection thresholds (primary detection method)
 
496
  return classifications
497
 
498
  def extract_smoke_polygons(self, lat2d, lon2d, smoke_values, min_area=0.01, is_hms_categorical=False):
499
+ """Extract smoke plume polygons from gridded data with light smoothing for HMS-like shapes"""
500
  polygons = []
501
 
502
  if not HMS_LIBS_AVAILABLE:
 
510
  classifications = smoke_values.astype(int)
511
  print(f"Using HMS categorical data directly: {np.unique(classifications)}")
512
  else:
513
+ # Lightly smooth the continuous field before categorization to avoid speckle
514
+ try:
515
+ from scipy import ndimage
516
+ smoothed = ndimage.gaussian_filter(smoke_values.astype(float), sigma=1.0)
517
+ except Exception:
518
+ smoothed = smoke_values
519
+ # Classify smoke densities from concentration values (µg/m³)
520
+ classifications = self.classify_smoke_density(smoothed)
521
 
522
  # Process each density category
523
  for category_name, category_info in self.density_categories.items():
 
534
  if not np.any(category_mask):
535
  continue
536
 
537
+ # Apply minimal morphology to reduce tiny blocky artifacts
538
+ try:
539
+ from scipy import ndimage
540
+ # Remove isolated noise, close small gaps
541
+ smoothed_mask = ndimage.binary_opening(category_mask, iterations=1)
542
+ smoothed_mask = ndimage.binary_closing(smoothed_mask, iterations=1)
543
+ # Light dilation to smooth edges
544
+ smoothed_mask = ndimage.binary_dilation(smoothed_mask, iterations=1)
545
+ except Exception:
546
+ smoothed_mask = category_mask
547
+
548
+ # Remove very small components (size threshold by category)
549
+ try:
550
+ from scipy import ndimage
551
+ labeled, num = ndimage.label(smoothed_mask)
552
+ sizes = np.bincount(labeled.ravel())
553
+ # Heavier categories can retain smaller features; light should be larger
554
+ min_pixels = {1: 80, 2: 50, 3: 25}.get(category_id, 40)
555
+ remove = sizes < min_pixels
556
+ remove_idx = np.nonzero(remove)[0]
557
+ if len(remove_idx) > 0:
558
+ smoothed_mask[np.isin(labeled, remove_idx)] = False
559
+ except Exception:
560
+ pass
561
 
562
  # Find contours with simpler algorithm
563
  try:
 
571
  if len(contour) < 4: # Need at least 4 points for polygon
572
  continue
573
 
574
+ # Simplify contour for speed while retaining shape
575
+ step = 2 if category_name == 'heavy' else 3
576
  simplified_contour = contour[::step]
577
 
578
  # Convert pixel coordinates to lat/lon
 
1614
  # Try contour plotting
1615
  try:
1616
  opacity = 0.6 if polygons else 1.0 # Reduce opacity if polygons are shown
1617
+ # Use 2D z with 1D axes for rectilinear grids (avoids jagged artifacts)
1618
+ if lat2d.ndim == 2 and lon2d.ndim == 2:
1619
+ x_axis = lon2d[0, :]
1620
+ y_axis = lat2d[:, 0]
1621
+ else:
1622
+ x_axis = lon2d
1623
+ y_axis = lat2d
1624
  fig.add_trace(go.Contour(
1625
+ x=x_axis,
1626
+ y=y_axis,
1627
+ z=z2d,
1628
  colorscale=colorscale,
1629
+ contours=dict(coloring='heatmap'),
1630
  showscale=True,
1631
  opacity=opacity,
1632
  colorbar=dict(
 
2473
  """)
2474
 
2475
  if __name__ == "__main__":
2476
+ app.launch(server_name="0.0.0.0", server_port=7860)