Spaces:
Sleeping
Sleeping
import gradio as gr | |
import requests | |
import json | |
import pandas as pd | |
from datetime import datetime, timedelta | |
import matplotlib.pyplot as plt | |
import numpy as np | |
import io | |
import base64 | |
from PIL import Image, ImageDraw, ImageFilter | |
import re | |
import cv2 | |
# Complete NIWA Snow and Ice Network Stations | |
SNOW_STATIONS = { | |
"Mahanga EWS": { | |
"name": "Mahanga Electronic Weather Station", | |
"location": "Mount Mahanga, Tasman", | |
"elevation": "1940m", "years": "2009-present", | |
"lat": -41.56, "lon": 172.27, | |
"image_url": "https://webstatic.niwa.co.nz/snow-plots/mahanga-ews-snow-depth-web.png" | |
}, | |
"Mueller Hut EWS": { | |
"name": "Mueller Hut Electronic Weather Station", | |
"location": "Aoraki/Mount Cook National Park", | |
"elevation": "1818m", "years": "2010-present", | |
"lat": -43.69, "lon": 170.11, | |
"image_url": "https://webstatic.niwa.co.nz/snow-plots/mueller-hut-ews-snow-depth-web.png" | |
}, | |
"Mt Potts EWS": { | |
"name": "Mt Potts Electronic Weather Station", | |
"location": "Canterbury (highest elevation)", | |
"elevation": "2128m", "years": "2012-present", | |
"lat": -43.53, "lon": 171.17, | |
"image_url": "https://webstatic.niwa.co.nz/snow-plots/mt-potts-ews-snow-depth-web.png" | |
}, | |
"Upper Rakaia EWS": { | |
"name": "Upper Rakaia Electronic Weather Station", | |
"location": "Jollie Range", "elevation": "1752m", "years": "2010-present", | |
"lat": -43.43, "lon": 171.29, | |
"image_url": "https://webstatic.niwa.co.nz/snow-plots/upper-rakaia-ews-snow-depth-web.png" | |
}, | |
"Albert Burn EWS": { | |
"name": "Albert Burn Electronic Weather Station", | |
"location": "Mt Aspiring region", "elevation": "1280m", "years": "2012-present", | |
"lat": -44.58, "lon": 169.13, | |
"image_url": "https://webstatic.niwa.co.nz/snow-plots/albert-burn-ews-snow-depth-web.png" | |
} | |
} | |
def extract_snow_data_from_chart(image): | |
"""Advanced chart data extraction targeting green data lines (current vs previous season)""" | |
try: | |
if image is None: | |
return None, "No image provided" | |
# Convert PIL to numpy array | |
img_array = np.array(image) | |
# Convert to different color spaces for analysis | |
gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) | |
hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV) | |
height, width = gray.shape | |
# 1. Detect chart boundaries and axes | |
edges = cv2.Canny(gray, 50, 150) | |
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=100, minLineLength=100, maxLineGap=10) | |
chart_bounds = {"left": 0, "right": width, "top": 0, "bottom": height} | |
if lines is not None: | |
# Find potential axis lines (long horizontal/vertical lines) | |
h_lines = [line for line in lines if abs(line[0][1] - line[0][3]) < 10] # Horizontal | |
v_lines = [line for line in lines if abs(line[0][0] - line[0][2]) < 10] # Vertical | |
if h_lines: | |
chart_bounds["bottom"] = max([line[0][1] for line in h_lines]) | |
if v_lines: | |
chart_bounds["left"] = min([line[0][0] for line in v_lines]) | |
# 2. Green color detection for snow data lines | |
# Define green color ranges in HSV (Hue-Saturation-Value) | |
# Dark green (current season): deeper, more saturated green | |
# Light green (previous season): lighter, less saturated green | |
# Dark green mask (current season) - Tuned for the specific dark green color shown | |
# This green appears to be in the 60-80 hue range with high saturation | |
dark_green_lower = np.array([55, 150, 80]) # More specific range for the dark green shown | |
dark_green_upper = np.array([75, 255, 200]) # Narrower hue range, higher saturation | |
dark_green_mask = cv2.inRange(hsv, dark_green_lower, dark_green_upper) | |
# Light green mask (previous season) - Lighter, less saturated version | |
light_green_lower = np.array([50, 60, 150]) # Broader hue, lower saturation for light green | |
light_green_upper = np.array([80, 180, 255]) # Higher brightness for lighter green | |
light_green_mask = cv2.inRange(hsv, light_green_lower, light_green_upper) | |
# 3. Extract data points along chart width for both seasons | |
chart_start = chart_bounds["left"] + 50 # Offset from y-axis | |
chart_end = chart_bounds["right"] - 50 | |
chart_width = chart_end - chart_start | |
# Sample points across the chart | |
num_samples = min(60, chart_width // 8) | |
x_positions = np.linspace(chart_start, chart_end, num_samples, dtype=int) | |
# Data storage for both seasons | |
current_season_data = [] # Dark green | |
previous_season_data = [] # Light green | |
dates = [] | |
# For each x position, find green data lines | |
for i, x in enumerate(x_positions): | |
if x < width: | |
# Extract column for analysis | |
column_region = slice(chart_bounds["top"], chart_bounds["bottom"]) | |
# Check for dark green pixels (current season) in this column | |
dark_green_column = dark_green_mask[column_region, x] | |
dark_green_pixels = np.where(dark_green_column > 0)[0] | |
# Check for light green pixels (previous season) in this column | |
light_green_column = light_green_mask[column_region, x] | |
light_green_pixels = np.where(light_green_column > 0)[0] | |
# Convert pixel positions to snow depth estimates | |
chart_height = chart_bounds["bottom"] - chart_bounds["top"] | |
# Current season (dark green) data | |
if len(dark_green_pixels) > 0: | |
# Get the most prominent dark green point (assume lowest = highest snow depth) | |
data_y = dark_green_pixels[0] + chart_bounds["top"] # First occurrence (top of snow) | |
relative_position = (chart_bounds["bottom"] - data_y) / chart_height | |
estimated_depth = relative_position * 350 # cm (assuming 0-350cm scale) | |
current_season_data.append(max(0, estimated_depth)) | |
else: | |
current_season_data.append(None) # No data point found | |
# Previous season (light green) data | |
if len(light_green_pixels) > 0: | |
# Get the most prominent light green point | |
data_y = light_green_pixels[0] + chart_bounds["top"] | |
relative_position = (chart_bounds["bottom"] - data_y) / chart_height | |
estimated_depth = relative_position * 350 # cm | |
previous_season_data.append(max(0, estimated_depth)) | |
else: | |
previous_season_data.append(None) # No data point found | |
# Estimate date (assume snow season: May to November, ~6 months) | |
date_fraction = i / (num_samples - 1) | |
# Start from May 1st of current year, progress through season | |
season_start = datetime(datetime.now().year, 5, 1) # May 1st | |
days_into_season = int(date_fraction * 200) # ~200 days of snow season | |
estimated_date = season_start + timedelta(days=days_into_season) | |
dates.append(estimated_date.strftime('%Y-%m-%d')) | |
# 4. Process and analyze extracted data | |
# Filter out None values and get valid data points | |
current_valid = [(dates[i], val) for i, val in enumerate(current_season_data) if val is not None] | |
previous_valid = [(dates[i], val) for i, val in enumerate(previous_season_data) if val is not None] | |
# Create data tables | |
current_table = pd.DataFrame({ | |
'Date': [item[0] for item in current_valid[-15:]], # Last 15 points | |
'Current_Season_Depth_cm': [round(item[1], 1) for item in current_valid[-15:]] | |
}) if current_valid else pd.DataFrame() | |
previous_table = pd.DataFrame({ | |
'Date': [item[0] for item in previous_valid[-15:]], # Last 15 points | |
'Previous_Season_Depth_cm': [round(item[1], 1) for item in previous_valid[-15:]] | |
}) if previous_valid else pd.DataFrame() | |
# Calculate statistics | |
current_stats = {} | |
previous_stats = {} | |
if current_valid: | |
current_values = [item[1] for item in current_valid] | |
current_stats = { | |
'current_depth': current_values[-1] if current_values else 0, | |
'max_depth': max(current_values), | |
'avg_depth': np.mean(current_values), | |
'data_points': len(current_values) | |
} | |
if previous_valid: | |
previous_values = [item[1] for item in previous_valid] | |
previous_stats = { | |
'max_depth': max(previous_values), | |
'avg_depth': np.mean(previous_values), | |
'data_points': len(previous_values) | |
} | |
# Create comprehensive analysis | |
analysis = f""" | |
**Snow Depth Data Extraction - Season Comparison:** | |
## π’ CURRENT SEASON (Dark Green Line): | |
""" | |
if current_stats: | |
analysis += f"""- **Current snow depth**: ~{current_stats['current_depth']:.1f} cm | |
- **Season maximum**: ~{current_stats['max_depth']:.1f} cm | |
- **Season average**: ~{current_stats['avg_depth']:.1f} cm | |
- **Data points found**: {current_stats['data_points']} | |
**Recent Current Season Data:** | |
{current_table.to_string(index=False) if not current_table.empty else "No current season data detected"} | |
""" | |
else: | |
analysis += "- β No current season data detected (dark green line not found)\n" | |
analysis += f""" | |
## π’ PREVIOUS SEASON (Light Green Line): | |
""" | |
if previous_stats: | |
analysis += f"""- **Previous season maximum**: ~{previous_stats['max_depth']:.1f} cm | |
- **Previous season average**: ~{previous_stats['avg_depth']:.1f} cm | |
- **Data points found**: {previous_stats['data_points']} | |
**Recent Previous Season Data:** | |
{previous_table.to_string(index=False) if not previous_table.empty else "No previous season data detected"} | |
""" | |
else: | |
analysis += "- β No previous season data detected (light green line not found)\n" | |
# Season comparison | |
if current_stats and previous_stats: | |
max_diff = current_stats['max_depth'] - previous_stats['max_depth'] | |
avg_diff = current_stats['avg_depth'] - previous_stats['avg_depth'] | |
analysis += f""" | |
## π SEASON COMPARISON: | |
- **Max depth difference**: {max_diff:+.1f} cm (current vs previous) | |
- **Average depth difference**: {avg_diff:+.1f} cm (current vs previous) | |
- **Trend**: {"Higher snow levels this season" if max_diff > 0 else "Lower snow levels this season" if max_diff < 0 else "Similar snow levels"} | |
""" | |
analysis += f""" | |
## π TECHNICAL DETAILS: | |
- **Image size**: {width}x{height} pixels | |
- **Chart boundaries**: {chart_bounds} | |
- **Dark green pixels found**: {np.sum(dark_green_mask)} pixels | |
- **Light green pixels found**: {np.sum(light_green_mask)} pixels | |
- **Color detection**: HSV analysis calibrated to NIWA chart colors | |
- **Current season detection**: Tuned for specific dark green (HSV: 55-75, 150-255, 80-200) | |
- **Previous season detection**: Tuned for light green (HSV: 50-80, 60-180, 150-255) | |
**β οΈ Important Notes:** | |
- Green line detection tuned to specific NIWA chart colors | |
- Dark green HSV range: [55-75, 150-255, 80-200] (current season) | |
- Light green HSV range: [50-80, 60-180, 150-255] (previous season) | |
- Estimated snow season: May-November | |
- Y-axis scale assumed: 0-350cm | |
- Accuracy depends on chart image quality and color consistency | |
**β Best Used For:** | |
- Comparing current vs previous season trends | |
- Identifying seasonal patterns and anomalies | |
- Quick assessment of relative snow conditions | |
""" | |
return { | |
'current_season': current_stats, | |
'previous_season': previous_stats, | |
'current_table': current_table, | |
'previous_table': previous_table, | |
'chart_bounds': chart_bounds, | |
'color_detection': { | |
'dark_green_pixels': int(np.sum(dark_green_mask)), | |
'light_green_pixels': int(np.sum(light_green_mask)) | |
} | |
}, analysis | |
except Exception as e: | |
return None, f"β Chart analysis failed: {str(e)}" | |
def fetch_and_analyze_station(station_key): | |
"""Fetch image and extract data for a specific station""" | |
try: | |
station = SNOW_STATIONS[station_key] | |
# Fetch image | |
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} | |
response = requests.get(station["image_url"], headers=headers, timeout=15) | |
if response.status_code == 200: | |
image = Image.open(io.BytesIO(response.content)) | |
# Extract data | |
extracted_data, analysis = extract_snow_data_from_chart(image) | |
# Create comprehensive station info | |
info = f""" | |
## {station['name']} | |
**Location:** {station['location']} ({station['lat']}, {station['lon']}) | |
**Elevation:** {station['elevation']} | |
**Data Period:** {station['years']} | |
**Extracted Data Analysis:** | |
{analysis} | |
**Source:** NIWA Snow & Ice Network | |
**Image URL:** {station['image_url']} | |
""" | |
return image, info, extracted_data, "β Successfully analyzed station data" | |
else: | |
return None, f"β Failed to fetch image (HTTP {response.status_code})", None, "Connection failed" | |
except Exception as e: | |
return None, f"β Error: {str(e)}", None, "Analysis failed" | |
def try_alternative_nz_weather_apis(): | |
"""Test alternative weather data sources for New Zealand""" | |
results = [] | |
# Test coordinates for major NZ snow areas | |
test_locations = [ | |
{"name": "Mount Cook area", "lat": -43.69, "lon": 170.11}, | |
{"name": "Canterbury high country", "lat": -43.53, "lon": 171.17}, | |
{"name": "Tasman mountains", "lat": -41.56, "lon": 172.27} | |
] | |
apis_to_test = [ | |
{ | |
"name": "OpenWeatherMap", | |
"url_template": "https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid=demo", | |
"has_snow": True | |
}, | |
{ | |
"name": "WeatherAPI", | |
"url_template": "http://api.weatherapi.com/v1/current.json?key=demo&q={lat},{lon}", | |
"has_snow": True | |
}, | |
{ | |
"name": "Visual Crossing", | |
"url_template": "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/{lat},{lon}?key=demo", | |
"has_snow": True | |
} | |
] | |
for api in apis_to_test: | |
try: | |
test_loc = test_locations[0] # Test with Mount Cook area | |
url = api["url_template"].format(lat=test_loc["lat"], lon=test_loc["lon"]) | |
response = requests.get(url, timeout=5) | |
if response.status_code == 200: | |
results.append(f"β {api['name']}: API responds (may need valid key)") | |
try: | |
data = response.json() | |
if 'snow' in str(data).lower(): | |
results.append(f" βοΈ Contains snow data fields") | |
except: | |
pass | |
elif response.status_code == 401: | |
results.append(f"π {api['name']}: API key required") | |
elif response.status_code == 403: | |
results.append(f"π« {api['name']}: Access forbidden") | |
else: | |
results.append(f"β {api['name']}: HTTP {response.status_code}") | |
except Exception as e: | |
results.append(f"β {api['name']}: {str(e)[:50]}...") | |
# Add recommendations | |
results.append("\n**Recommendations for Real Data:**") | |
results.append("1. OpenWeatherMap: Free tier includes snow data") | |
results.append("2. WeatherAPI: Good NZ coverage with snow fields") | |
results.append("3. Visual Crossing: Historical snow data available") | |
results.append("4. MetService (NZ): Local weather service APIs") | |
return "\n".join(results) | |
def analyze_all_stations(): | |
"""Get data from all stations and create summary""" | |
all_data = {} | |
images = [] | |
for station_key in SNOW_STATIONS.keys(): | |
try: | |
image, info, extracted_data, status = fetch_and_analyze_station(station_key) | |
if image and extracted_data: | |
all_data[station_key] = extracted_data | |
images.append((image, f"{SNOW_STATIONS[station_key]['name']} ({extracted_data['estimated_current_depth']:.1f}cm)")) | |
except: | |
continue | |
# Create summary comparison | |
summary = "**Snow Depth Comparison (Estimated from Charts):**\n\n" | |
for station_key, data in all_data.items(): | |
station = SNOW_STATIONS[station_key] | |
summary += f"- **{station['name']}** ({station['elevation']}): ~{data['estimated_current_depth']:.1f}cm\n" | |
summary += f"\n**Analysis completed:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" | |
summary += "\n\nβ οΈ These are rough estimates from image analysis. For accurate data, use NIWA DataHub with proper authentication." | |
return images, summary | |
# Create the Gradio Interface | |
with gr.Blocks(title="NZ Snow Data - Chart Extraction & Alternatives", theme=gr.themes.Soft()) as app: | |
gr.Markdown(""" | |
# ποΈ New Zealand Snow Data: Chart Extraction & Alternatives | |
**Since NIWA APIs require complex authentication (email + 2FA), this app focuses on practical solutions:** | |
1. **π Advanced Chart Data Extraction** - Computer vision analysis of snow depth charts | |
2. **π Alternative Data Sources** - Other weather APIs with NZ coverage | |
3. **π Direct Data Discovery** - Finding downloadable datasets | |
""") | |
with gr.Tab("π Chart Data Extraction"): | |
gr.Markdown(""" | |
### Extract Real Data from Snow Depth Charts | |
Uses computer vision to analyze NIWA snow depth charts and extract approximate numerical values. | |
""") | |
with gr.Row(): | |
station_dropdown = gr.Dropdown( | |
choices=list(SNOW_STATIONS.keys()), | |
value="Mueller Hut EWS", | |
label="Select Snow Station", | |
info="Station for detailed analysis" | |
) | |
analyze_btn = gr.Button("π Analyze Chart Data", variant="primary") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
chart_image = gr.Image(label="Snow Depth Chart", height=500) | |
with gr.Column(scale=1): | |
extracted_info = gr.Markdown(label="Extracted Data Analysis") | |
analysis_status = gr.Textbox(label="Analysis Status", interactive=False) | |
# Hidden component to store extracted data | |
extracted_data_store = gr.JSON(visible=False) | |
with gr.Tab("πΊοΈ All Stations Summary"): | |
gr.Markdown("### Compare All Stations") | |
analyze_all_btn = gr.Button("π Analyze All Stations", variant="primary", size="lg") | |
with gr.Row(): | |
all_images = gr.Gallery(label="All Station Charts with Estimates", columns=2, height=500) | |
stations_summary = gr.Markdown(label="Snow Depth Summary") | |
with gr.Tab("π Alternative Data Sources"): | |
gr.Markdown(""" | |
### Test Alternative Weather APIs | |
Find other data sources that provide New Zealand snow and weather data. | |
""") | |
test_alternatives_btn = gr.Button("π Test Alternative APIs", variant="secondary") | |
alternative_results = gr.Textbox(label="Alternative API Results", lines=15, interactive=False) | |
gr.Markdown(""" | |
### Recommended Data Sources: | |
**For Programming/Research:** | |
- **OpenWeatherMap**: Free tier, has snow fields for NZ coordinates | |
- **WeatherAPI.com**: Good New Zealand coverage, snow depth data | |
- **Visual Crossing**: Historical weather data including snow | |
**For Real-Time Monitoring:** | |
- **MetService NZ**: Official New Zealand weather service | |
- **NIWA Weather**: Real-time weather data (separate from DataHub) | |
- **Local Council APIs**: Regional weather monitoring systems | |
""") | |
with gr.Tab("π‘ Data Access Solutions"): | |
gr.Markdown(""" | |
## π― Practical Solutions for Snow Data Access | |
### Option 1: Chart Extraction (This App) β | |
**What it does:** | |
- Computer vision analysis of NIWA snow depth charts | |
- Extracts approximate numerical values and trends | |
- Provides rough current snow depth estimates | |
**Accuracy:** Moderate (Β±20-30cm) but useful for trends | |
**Use cases:** Quick assessments, relative comparisons, proof-of-concept | |
### Option 2: NIWA DataHub (Requires Account) π | |
**Steps:** | |
1. Register at https://data.niwa.co.nz/ (email + 2FA) | |
2. Log in via web interface | |
3. Browse "Climate station data" or "Snow & Ice Network" | |
4. Download CSV files manually | |
5. For API access: Generate Personal Access Token after login | |
**Accuracy:** High (research-grade) | |
**Use cases:** Research, official reports, detailed analysis | |
### Option 3: Alternative APIs β‘ | |
**Recommended:** | |
- **OpenWeatherMap** (free tier): Snow data for NZ coordinates | |
- **WeatherAPI.com**: Comprehensive NZ weather including snow | |
- **Visual Crossing**: Historical snow data with API access | |
**Accuracy:** Good for general weather, limited for alpine specifics | |
**Use cases:** General weather apps, regional snow estimates | |
### Option 4: Direct NIWA Contact π§ | |
**For serious research:** | |
- Email NIWA data team directly | |
- Request specific dataset access | |
- Negotiate API access for commercial/research use | |
- Get real-time data feeds | |
### Option 5: Web Scraping (Advanced) π€ | |
**Automated chart analysis:** | |
- Schedule regular image downloads | |
- Batch process multiple stations | |
- Track trends over time | |
- Store extracted data in database | |
## π Recommended Approach: | |
1. **Start with this app** for immediate estimates | |
2. **Register at NIWA DataHub** for accurate historical data | |
3. **Use alternative APIs** for general weather context | |
4. **Contact NIWA directly** for research-grade real-time access | |
""") | |
# Event handlers | |
analyze_btn.click( | |
fn=fetch_and_analyze_station, | |
inputs=[station_dropdown], | |
outputs=[chart_image, extracted_info, extracted_data_store, analysis_status] | |
) | |
analyze_all_btn.click( | |
fn=analyze_all_stations, | |
outputs=[all_images, stations_summary] | |
) | |
test_alternatives_btn.click( | |
fn=try_alternative_nz_weather_apis, | |
outputs=[alternative_results] | |
) | |
# Launch for HuggingFace Spaces | |
if __name__ == "__main__": | |
app.launch() | |
# Enhanced requirements.txt: | |
""" | |
gradio>=4.0.0 | |
requests>=2.25.0 | |
pandas>=1.3.0 | |
matplotlib>=3.5.0 | |
Pillow>=8.0.0 | |
numpy>=1.21.0 | |
opencv-python>=4.5.0 | |
""" | |
# Practical README.md: | |
""" | |
--- | |
title: NZ Snow Data - Chart Extraction & Alternatives | |
emoji: ποΈ | |
colorFrom: blue | |
colorTo: white | |
sdk: gradio | |
sdk_version: 4.0.0 | |
app_file: app.py | |
pinned: false | |
--- | |
# New Zealand Snow Data: Practical Solutions | |
**Real solutions for accessing NZ alpine snow depth data when APIs require complex authentication.** | |
## π― What This App Does | |
**Chart Data Extraction:** | |
- Computer vision analysis of NIWA snow depth charts | |
- Extracts approximate numerical values (Β±20-30cm accuracy) | |
- Provides trends and current estimates for 5 major stations | |
**Alternative Data Sources:** | |
- Tests other weather APIs with New Zealand coverage | |
- Identifies services that provide snow data for NZ coordinates | |
- Recommends practical alternatives to NIWA DataHub | |
**Practical Access Guide:** | |
- Multiple approaches from quick estimates to research-grade data | |
- Clear instructions for each data source type | |
- Realistic expectations about accuracy and access | |
## ποΈ Stations Covered | |
- Mueller Hut EWS (1818m) - Mount Cook National Park | |
- Mt Potts EWS (2128m) - Highest elevation station | |
- Mahanga EWS (1940m) - Tasman region | |
- Upper Rakaia EWS (1752m) - Canterbury | |
- Albert Burn EWS (1280m) - Mt Aspiring region | |
## π§ Use Cases | |
- Avalanche safety planning | |
- Alpine recreation planning | |
- Research proof-of-concept | |
- Climate monitoring | |
- Water resource assessment | |
Perfect when you need NZ snow data but can't navigate complex authentication systems! | |
""" |