broadfield-dev commited on
Commit
86f8031
·
verified ·
1 Parent(s): 4148517

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -86
app.py CHANGED
@@ -3,7 +3,13 @@ import numpy as np
3
  import cv2
4
  from PIL import Image
5
  import matplotlib.pyplot as plt
 
 
 
 
 
6
 
 
7
  low_int = 10
8
  high_int = 100
9
  edge_thresh = 50
@@ -12,6 +18,42 @@ center_tol = 30
12
  morph_dia = 5
13
  min_rad = 70
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  def extract_frames(gif_path):
16
  """Extract frames from a GIF and return as a list of numpy arrays."""
17
  try:
@@ -30,16 +72,10 @@ def extract_frames(gif_path):
30
 
31
  def preprocess_frame(frame, lower_bound, upper_bound, morph_iterations):
32
  """Preprocess a frame: isolate mid-to-light pixels and enhance circular patterns."""
33
- # Apply Gaussian blur to reduce noise
34
  blurred = cv2.GaussianBlur(frame, (9, 9), 0)
35
-
36
- # Isolate mid-to-light pixels using user-defined intensity range
37
  mask = cv2.inRange(blurred, lower_bound, upper_bound)
38
-
39
- # Apply morphological operation to enhance circular patterns
40
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
41
  enhanced = cv2.dilate(mask, kernel, iterations=morph_iterations)
42
-
43
  return enhanced
44
 
45
  def detect_circles(frame_diff, image_center, center_tolerance, param1, param2, min_radius=20, max_radius=200):
@@ -47,17 +83,15 @@ def detect_circles(frame_diff, image_center, center_tolerance, param1, param2, m
47
  circles = cv2.HoughCircles(
48
  frame_diff,
49
  cv2.HOUGH_GRADIENT,
50
- dp=1.5, # Resolution for better detection
51
- minDist=100, # Prevent overlapping circles
52
- param1=param1, # User-defined edge threshold
53
- param2=param2, # User-defined accumulator threshold
54
  minRadius=min_radius,
55
  maxRadius=max_radius
56
  )
57
-
58
  if circles is not None:
59
  circles = np.round(circles[0, :]).astype("int")
60
- # Filter circles: only keep those centered near the image center
61
  filtered_circles = []
62
  for (x, y, r) in circles:
63
  if (abs(x - image_center[0]) < center_tolerance and
@@ -66,54 +100,50 @@ def detect_circles(frame_diff, image_center, center_tolerance, param1, param2, m
66
  return filtered_circles if filtered_circles else None
67
  return None
68
 
69
- def analyze_gif(gif_file, lower_bound, upper_bound, param1, param2, center_tolerance, morph_iterations, min_rad):
70
- """Analyze a GIF for concentric circles, highlighting growing series."""
71
- try:
72
- # Handle Gradio file input
73
- gif_path = gif_file.name if hasattr(gif_file, 'name') else gif_file
74
-
75
- # Extract frames
76
- frames, error = extract_frames(gif_path)
77
- if error:
78
- return error, []
 
79
 
80
- if len(frames) < 2:
81
- return "GIF must have at least 2 frames for analysis.", []
 
 
 
82
 
83
- # Determine the image center (Sun's position)
84
  height, width = frames[0].shape
85
- image_center = (width // 2, height // 2) # Assume Sun is at the center
86
-
87
- # Initialize results
88
- all_circle_data = [] # Store all detected circles
89
  min_radius = int(min_rad)
90
- max_radius = min(height, width) // 2 # Limit max radius to half the image size
91
 
92
  # Process frames and detect circles
 
93
  for i in range(len(frames) - 1):
94
  frame1 = preprocess_frame(frames[i], lower_bound, upper_bound, morph_iterations)
95
  frame2 = preprocess_frame(frames[i + 1], lower_bound, upper_bound, morph_iterations)
96
-
97
- # Compute absolute difference between consecutive frames
98
  frame_diff = cv2.absdiff(frame2, frame1)
99
- # Enhance contrast for the difference image
100
  frame_diff = cv2.convertScaleAbs(frame_diff, alpha=3.0, beta=0)
101
-
102
- # Detect circles centered at the Sun
103
  circles = detect_circles(frame_diff, image_center, center_tolerance, param1, param2, min_radius, max_radius)
104
-
105
  if circles:
106
- # Take the largest circle (most prominent CME feature)
107
- largest_circle = max(circles, key=lambda c: c[2]) # Sort by radius
108
  x, y, r = largest_circle
109
  all_circle_data.append({
110
  "frame": i + 1,
111
  "center": (x, y),
112
  "radius": r,
113
- "output_frame": frames[i + 1] # Store the frame for visualization
114
  })
115
 
116
- # Find the longest series of frames where the circle is growing
117
  growing_circle_data = []
118
  current_series = []
119
  if all_circle_data:
@@ -122,75 +152,141 @@ def analyze_gif(gif_file, lower_bound, upper_bound, param1, param2, center_toler
122
  if all_circle_data[i]["radius"] > current_series[-1]["radius"]:
123
  current_series.append(all_circle_data[i])
124
  else:
125
- # If the radius doesn't increase, check if the current series is the longest
126
  if len(current_series) > len(growing_circle_data):
127
  growing_circle_data = current_series.copy()
128
  current_series = [all_circle_data[i]]
129
-
130
- # Check the last series
131
  if len(current_series) > len(growing_circle_data):
132
  growing_circle_data = current_series.copy()
133
 
134
- # Mark frames that are part of the growing series
135
  growing_frames = set(c["frame"] for c in growing_circle_data)
136
-
137
- # Generate output frames and report
138
  results = []
139
- report = "Analysis Report (as of 08:07 PM PDT, May 24, 2025):\n"
140
-
141
- # Report all detected circles
142
- if all_circle_data:
143
- report += f"\nAll Frames with Detected Circles ({len(all_circle_data)} frames):\n"
 
 
 
 
 
 
 
 
144
  for c in all_circle_data:
145
- # Visualize the frame with detected circle (green)
146
  output_frame = cv2.cvtColor(c["output_frame"], cv2.COLOR_GRAY2RGB)
147
- cv2.circle(output_frame, (c["center"][0], c["center"][1]), c["radius"], (0, 255, 0), 2)
148
-
149
- # If the frame is part of the growing series, add a red circle
150
  if c["frame"] in growing_frames:
151
- cv2.circle(output_frame, (c["center"][0], c["center"][1]), c["radius"] + 2, (255, 0, 0), 2)
152
-
153
- # Convert to PIL Image for Gradio
154
- output_frame = Image.fromarray(output_frame)
155
- results.append(output_frame)
 
 
 
 
 
 
 
156
 
 
 
 
 
157
  report += f"Frame {c['frame']}: Center at {c['center']}, Radius {c['radius']} pixels\n"
158
  else:
159
  report += "No circles detected.\n"
160
 
161
- # Report the growing series
162
  if growing_circle_data:
163
  report += f"\nSeries of Frames with Growing Circles ({len(growing_circle_data)} frames):\n"
164
  for c in growing_circle_data:
165
  report += f"Frame {c['frame']}: Center at {c['center']}, Radius {c['radius']} pixels\n"
166
- report += "\nConclusion: Growing concentric circles of mid-to-light pixels detected, indicative of a potential Earth-directed CME."
167
  else:
168
  report += "\nNo growing concentric circles detected. CME may not be Earth-directed."
169
 
170
- return report, results
 
 
 
 
 
 
 
171
  except Exception as e:
172
- return f"Error during analysis: {str(e)}", []
173
-
174
- # Gradio interface with user controls
175
- iface = gr.Interface(
176
- fn=analyze_gif,
177
- inputs=[
178
- gr.File(label="Upload Solar GIF", file_types=[".gif"]),
179
- gr.Slider(minimum=0, maximum=255, value=low_int, step=1, label="Lower Intensity Bound (0-255)"),
180
- gr.Slider(minimum=0, maximum=255, value=high_int, step=1, label="Upper Intensity Bound (0-255)"),
181
- gr.Slider(minimum=10, maximum=200, value=edge_thresh, step=1, label="Hough Param1 (Edge Threshold)"),
182
- gr.Slider(minimum=1, maximum=50, value=accum_thresh, step=1, label="Hough Param2 (Accumulator Threshold)"),
183
- gr.Slider(minimum=10, maximum=100, value=center_tol, step=1, label="Center Tolerance (Pixels)"),
184
- gr.Slider(minimum=1, maximum=5, value=morph_dia, step=1, label="Morphological Dilation Iterations"),
185
- gr.Slider(minimum=1, maximum=100, value=min_rad, step=1, label="Minimum Circle Radius")
186
- ],
187
- outputs=[
188
- gr.Textbox(label="Analysis Report"),
189
- gr.Gallery(label="Frames with Detected Circles (Green: Detected, Red: Growing Series)")
190
- ],
191
- title="Solar CME Detection",
192
- description="Upload a GIF of solar images to detect concentric circles of mid-to-light pixels. All detected circles are shown in green, and the series of growing circles (indicating an Earth-directed CME) are highlighted in red. Adjust the sliders to fine-tune detection."
193
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  if __name__ == "__main__":
196
- iface.launch()
 
3
  import cv2
4
  from PIL import Image
5
  import matplotlib.pyplot as plt
6
+ import requests
7
+ from datetime import datetime, timedelta
8
+ import io
9
+ import os
10
+ from urllib.parse import urljoin
11
 
12
+ # Default parameters
13
  low_int = 10
14
  high_int = 100
15
  edge_thresh = 50
 
18
  morph_dia = 5
19
  min_rad = 70
20
 
21
+ def fetch_sdo_images(start_date, end_date, ident="0171", size="1024", tool="hmiigr"):
22
+ """Fetch SDO images from NASA URL for a given date range."""
23
+ try:
24
+ start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
25
+ end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
26
+ if start > end:
27
+ return None, "Start date must be before end date."
28
+
29
+ base_url = "https://sdo.gsfc.nasa.gov/assets/img/browse/"
30
+ frames = []
31
+ current = start
32
+ while current <= end:
33
+ # Format URL: https://sdo.gsfc.nasa.gov/assets/img/browse/YEAR/MONTH/DAY/DATE_IDENT_SIZE_TOOL.jpg
34
+ date_str = current.strftime("%Y%m%d_%H%M%S")
35
+ year, month, day = current.strftime("%Y"), current.strftime("%m"), current.strftime("%d")
36
+ url = urljoin(base_url, f"{year}/{month}/{day}/{date_str}_{ident}_{size}_{tool}.jpg")
37
+
38
+ # Fetch image
39
+ try:
40
+ response = requests.get(url, timeout=5)
41
+ if response.status_code == 200:
42
+ img = Image.open(io.BytesIO(response.content)).convert('L') # Convert to grayscale
43
+ frames.append(np.array(img))
44
+ else:
45
+ print(f"Failed to fetch {url}: Status {response.status_code}")
46
+ except Exception as e:
47
+ print(f"Error fetching {url}: {str(e)}")
48
+
49
+ current += timedelta(minutes=12) # SDO images are typically 12 minutes apart
50
+
51
+ if not frames:
52
+ return None, "No images found in the specified date range."
53
+ return frames, None
54
+ except Exception as e:
55
+ return None, f"Error fetching images: {str(e)}"
56
+
57
  def extract_frames(gif_path):
58
  """Extract frames from a GIF and return as a list of numpy arrays."""
59
  try:
 
72
 
73
  def preprocess_frame(frame, lower_bound, upper_bound, morph_iterations):
74
  """Preprocess a frame: isolate mid-to-light pixels and enhance circular patterns."""
 
75
  blurred = cv2.GaussianBlur(frame, (9, 9), 0)
 
 
76
  mask = cv2.inRange(blurred, lower_bound, upper_bound)
 
 
77
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
78
  enhanced = cv2.dilate(mask, kernel, iterations=morph_iterations)
 
79
  return enhanced
80
 
81
  def detect_circles(frame_diff, image_center, center_tolerance, param1, param2, min_radius=20, max_radius=200):
 
83
  circles = cv2.HoughCircles(
84
  frame_diff,
85
  cv2.HOUGH_GRADIENT,
86
+ dp=1.5,
87
+ minDist=100,
88
+ param1=param1,
89
+ param2=param2,
90
  minRadius=min_radius,
91
  maxRadius=max_radius
92
  )
 
93
  if circles is not None:
94
  circles = np.round(circles[0, :]).astype("int")
 
95
  filtered_circles = []
96
  for (x, y, r) in circles:
97
  if (abs(x - image_center[0]) < center_tolerance and
 
100
  return filtered_circles if filtered_circles else None
101
  return None
102
 
103
+ def create_gif(frames, output_path, duration=0.5):
104
+ """Create a GIF from a list of frames."""
105
+ pil_frames = [Image.fromarray(frame) for frame in frames]
106
+ pil_frames[0].save(
107
+ output_path,
108
+ save_all=True,
109
+ append_images=pil_frames[1:],
110
+ duration=int(duration * 1000), # Duration in milliseconds
111
+ loop=0
112
+ )
113
+ return output_path
114
 
115
+ def analyze_images(frames, lower_bound, upper_bound, param1, param2, center_tolerance, morph_iterations, min_rad, display_mode):
116
+ """Analyze frames for concentric circles, highlighting growing series."""
117
+ try:
118
+ if not frames or len(frames) < 2:
119
+ return "At least 2 frames are required for analysis.", [], None
120
 
121
+ # Determine image center
122
  height, width = frames[0].shape
123
+ image_center = (width // 2, height // 2)
 
 
 
124
  min_radius = int(min_rad)
125
+ max_radius = min(height, width) // 2
126
 
127
  # Process frames and detect circles
128
+ all_circle_data = []
129
  for i in range(len(frames) - 1):
130
  frame1 = preprocess_frame(frames[i], lower_bound, upper_bound, morph_iterations)
131
  frame2 = preprocess_frame(frames[i + 1], lower_bound, upper_bound, morph_iterations)
 
 
132
  frame_diff = cv2.absdiff(frame2, frame1)
 
133
  frame_diff = cv2.convertScaleAbs(frame_diff, alpha=3.0, beta=0)
 
 
134
  circles = detect_circles(frame_diff, image_center, center_tolerance, param1, param2, min_radius, max_radius)
135
+
136
  if circles:
137
+ largest_circle = max(circles, key=lambda c: c[2])
 
138
  x, y, r = largest_circle
139
  all_circle_data.append({
140
  "frame": i + 1,
141
  "center": (x, y),
142
  "radius": r,
143
+ "output_frame": frames[i + 1]
144
  })
145
 
146
+ # Find growing series
147
  growing_circle_data = []
148
  current_series = []
149
  if all_circle_data:
 
152
  if all_circle_data[i]["radius"] > current_series[-1]["radius"]:
153
  current_series.append(all_circle_data[i])
154
  else:
 
155
  if len(current_series) > len(growing_circle_data):
156
  growing_circle_data = current_series.copy()
157
  current_series = [all_circle_data[i]]
 
 
158
  if len(current_series) > len(growing_circle_data):
159
  growing_circle_data = current_series.copy()
160
 
 
161
  growing_frames = set(c["frame"] for c in growing_circle_data)
 
 
162
  results = []
163
+ report = f"Analysis Report (as of {datetime.now().strftime('%I:%M %p PDT, %B %d, %Y')}):\n"
164
+
165
+ # Prepare output based on display mode
166
+ if display_mode == "All Frames":
167
+ for i, frame in enumerate(frames):
168
+ output_frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB)
169
+ if i + 1 in growing_frames:
170
+ for c in all_circle_data:
171
+ if c["frame"] == i + 1:
172
+ cv2.circle(output_frame, c["center"], c["radius"], (0, 255, 0), 2)
173
+ cv2.circle(output_frame, c["center"], c["radius"] + 2, (255, 0, 0), 2)
174
+ results.append(Image.fromarray(output_frame))
175
+ elif display_mode == "Detected Frames":
176
  for c in all_circle_data:
 
177
  output_frame = cv2.cvtColor(c["output_frame"], cv2.COLOR_GRAY2RGB)
178
+ cv2.circle(output_frame, c["center"], c["radius"], (0, 255, 0), 2)
 
 
179
  if c["frame"] in growing_frames:
180
+ cv2.circle(output_frame, c["center"], c["radius"] + 2, (255, 0, 0), 2)
181
+ results.append(Image.fromarray(output_frame))
182
+ elif display_mode == "Both (Detected Replaces Original)":
183
+ for i, frame in enumerate(frames):
184
+ output_frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB)
185
+ if i + 1 in growing_frames:
186
+ for c in all_circle_data:
187
+ if c["frame"] == i + 1:
188
+ output_frame = cv2.cvtColor(c["output_frame"], cv2.COLOR_GRAY2RGB)
189
+ cv2.circle(output_frame, c["center"], c["radius"], (0, 255, 0), 2)
190
+ cv2.circle(output_frame, c["center"], c["radius"] + 2, (255, 0, 0), 2)
191
+ results.append(Image.fromarray(output_frame))
192
 
193
+ # Generate report
194
+ if all_circle_data:
195
+ report += f"\nAll Frames with Detected Circles ({len(all_circle_data)} frames):\n"
196
+ for c in all_circle_data:
197
  report += f"Frame {c['frame']}: Center at {c['center']}, Radius {c['radius']} pixels\n"
198
  else:
199
  report += "No circles detected.\n"
200
 
 
201
  if growing_circle_data:
202
  report += f"\nSeries of Frames with Growing Circles ({len(growing_circle_data)} frames):\n"
203
  for c in growing_circle_data:
204
  report += f"Frame {c['frame']}: Center at {c['center']}, Radius {c['radius']} pixels\n"
205
+ report += "\nConclusion: Growing concentric circles detected, indicative of a potential Earth-directed CME."
206
  else:
207
  report += "\nNo growing concentric circles detected. CME may not be Earth-directed."
208
 
209
+ # Create GIF if results exist
210
+ gif_path = None
211
+ if results:
212
+ gif_frames = [np.array(img) for img in results]
213
+ gif_path = "output.gif"
214
+ create_gif(gif_frames, gif_path)
215
+
216
+ return report, results, gif_path
217
  except Exception as e:
218
+ return f"Error during analysis: {str(e)}", [], None
219
+
220
+ def process_input(gif_file, start_date, end_date, ident, size, tool, lower_bound, upper_bound, param1, param2, center_tolerance, morph_iterations, min_rad, display_mode):
221
+ """Process either uploaded GIF or fetched SDO images."""
222
+ if gif_file:
223
+ frames, error = extract_frames(gif_file.name)
224
+ if error:
225
+ return error, [], None
226
+ else:
227
+ frames, error = fetch_sdo_images(start_date, end_date, ident, size, tool)
228
+ if error:
229
+ return error, [], None
230
+
231
+ # Preview first frame if available
232
+ preview = Image.fromarray(frames[0]) if frames else None
233
+
234
+ # Analyze frames
235
+ report, results, gif_path = analyze_images(
236
+ frames, lower_bound, upper_bound, param1, param2, center_tolerance, morph_iterations, min_rad, display_mode
237
+ )
238
+
239
+ return report, results, gif_path, preview
240
+
241
+ # Gradio Blocks interface
242
+ with gr.Blocks(title="Solar CME Detection") as demo:
243
+ gr.Markdown("""
244
+ # Solar CME Detection
245
+ Upload a GIF or specify a date range to fetch SDO images and detect concentric circles indicative of coronal mass ejections (CMEs).
246
+ Green circles mark detected features; red circles highlight growing series (potential Earth-directed CMEs).
247
+ """)
248
+
249
+ with gr.Row():
250
+ with gr.Column():
251
+ gr.Markdown("### Input Options")
252
+ gif_input = gr.File(label="Upload Solar GIF (optional)", file_types=[".gif"])
253
+ start_date = gr.Textbox(label="Start Date (YYYY-MM-DD HH:MM:SS)", value="2025-05-24 00:00:00")
254
+ end_date = gr.Textbox(label="End Date (YYYY-MM-DD HH:MM:SS)", value="2025-05-24 23:59:59")
255
+ ident = gr.Textbox(label="Image Identifier", value="0171")
256
+ size = gr.Textbox(label="Image Size", value="1024")
257
+ tool = gr.Textbox(label="Instrument", value="hmiigr")
258
+
259
+ gr.Markdown("### Analysis Parameters")
260
+ lower_bound = gr.Slider(minimum=0, maximum=255, value=low_int, step=1, label="Lower Intensity Bound (0-255)")
261
+ upper_bound = gr.Slider(minimum=0, maximum=255, value=high_int, step=1, label="Upper Intensity Bound (0-255)")
262
+ param1 = gr.Slider(minimum=10, maximum=200, value=edge_thresh, step=1, label="Hough Param1 (Edge Threshold)")
263
+ param2 = gr.Slider(minimum=1, maximum=50, value=accum_thresh, step=1, label="Hough Param2 (Accumulator Threshold)")
264
+ center_tolerance = gr.Slider(minimum=10, maximum=100, value=center_tol, step=1, label="Center Tolerance (Pixels)")
265
+ morph_iterations = gr.Slider(minimum=1, maximum=5, value=morph_dia, step=1, label="Morphological Dilation Iterations")
266
+ min_rad = gr.Slider(minimum=1, maximum=100, value=min_rad, step=1, label="Minimum Circle Radius")
267
+ display_mode = gr.Dropdown(
268
+ choices=["All Frames", "Detected Frames", "Both (Detected Replaces Original)"],
269
+ value="Detected Frames",
270
+ label="Display Mode"
271
+ )
272
+
273
+ analyze_button = gr.Button("Analyze")
274
+
275
+ with gr.Column():
276
+ gr.Markdown("### Outputs")
277
+ report = gr.Textbox(label="Analysis Report", lines=10)
278
+ preview = gr.Image(label="Input Preview (First Frame)")
279
+ gallery = gr.Gallery(label="Frames with Detected Circles (Green: Detected, Red: Growing Series)")
280
+ gif_output = gr.File(label="Download Resulting GIF")
281
+
282
+ analyze_button.click(
283
+ fn=process_input,
284
+ inputs=[
285
+ gif_input, start_date, end_date, ident, size, tool,
286
+ lower_bound, upper_bound, param1, param2, center_tolerance, morph_iterations, min_rad, display_mode
287
+ ],
288
+ outputs=[report, gallery, gif_output, preview]
289
+ )
290
 
291
  if __name__ == "__main__":
292
+ demo.launch()