Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Satellite imagery analysis and remote sensing for earth science research
.claude/skills/brycewang-stanford-satellite-remote-sensing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 63% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 185% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 60% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 65% | 0% |
A skill for processing and analyzing satellite imagery for earth science research. Covers data acquisition from major satellite platforms, preprocessing workflows, spectral index computation, land cover classification, and change detection using Python geospatial tools.
| Mission | Operator | Resolution | Revisit | Key Bands | Access | |---------|----------|-----------|---------|-----------|--------| | Landsat 8/9 | USGS/NASA | 30m (MS), 15m (pan) | 16 days | 11 bands, OLI+TIRS | Free (USGS EarthExplorer) | | Sentinel-2 | ESA | 10m-60m | 5 days | 13 bands, MSI | Free (Copernicus Open Access Hub) | | MODIS | NASA | 250m-1km | 1-2 days | 36 bands | Free (NASA LAADS DAAC) | | Sentinel-1 | ESA | 5-20m | 6 days | C-band SAR | Free (Copernicus) | | GOES-16/17 | NOAA | 0.5-2km | 5-15 min | 16 bands, ABI | Free (NOAA CLASS) |
pythonimport planetary_computer import pystac_client import rioxarray # Search Sentinel-2 imagery via Microsoft Planetary Computer catalog = pystac_client.Client.open( "https://planetarycomputer.microsoft.com/api/stac/v1", modifier=planetary_computer.sign_inplace, ) # Search for cloud-free imagery over a region search = catalog.search( collections=["sentinel-2-l2a"], bbox=[11.0, 46.0, 12.0, 47.0], # Tyrol, Austria datetime="2025-06-01/2025-08-31", query={"eo:cloud_cover": {"lt": 10}}, ) items = search.item_collection() print(f"Found {len(items)} scenes with <10% cloud cover") # Load a specific band as xarray DataArray item = items[0] red = rioxarray.open_rasterio(item.assets["B04"].href) nir = rioxarray.open_rasterio(item.assets["B08"].href)
Raw satellite data (Level-1) must be atmospherically corrected to obtain surface reflectance (Level-2):
python# Cloud masking for Sentinel-2 using the SCL band import numpy as np def mask_clouds_sentinel2(scl_band: np.ndarray) -> np.ndarray: """ Create cloud mask from Sentinel-2 Scene Classification Layer. SCL values: 0=no_data, 1=saturated, 2=dark_area, 3=cloud_shadow, 4=vegetation, 5=bare_soil, 6=water, 7=unclassified, 8=cloud_medium, 9=cloud_high, 10=cirrus, 11=snow """ cloud_classes = {0, 1, 3, 8, 9, 10} mask = np.isin(scl_band, list(cloud_classes)) return mask # True where clouds/invalid
pythonimport rasterio from rasterio.merge import merge from rasterio.warp import calculate_default_transform, reproject, Resampling def reproject_raster(src_path: str, dst_path: str, dst_crs: str = "EPSG:4326"): """Reproject a raster to a target coordinate reference system.""" with rasterio.open(src_path) as src: transform, width, height = calculate_default_transform( src.crs, dst_crs, src.width, src.height, *src.bounds ) kwargs = src.meta.copy() kwargs.update({ "crs": dst_crs, "transform": transform, "width": width, "height": height, }) with rasterio.open(dst_path, "w", **kwargs) as dst: for i in range(1, src.count + 1): reproject( source=rasterio.band(src, i), destination=rasterio.band(dst, i), src_transform=src.transform, src_crs=src.crs, dst_transform=transform, dst_crs=dst_crs, resampling=Resampling.bilinear, )
pythondef compute_indices(red: np.ndarray, nir: np.ndarray, green: np.ndarray, swir: np.ndarray) -> dict: """ Compute common spectral indices from surface reflectance bands. All inputs should be float arrays with values in [0, 1]. """ eps = 1e-10 # avoid division by zero ndvi = (nir - red) / (nir + red + eps) ndwi = (green - nir) / (green + nir + eps) nbr = (nir - swir) / (nir + swir + eps) evi = 2.5 * (nir - red) / (nir + 6 * red - 7.5 * 0.0001 + 1 + eps) savi = 1.5 * (nir - red) / (nir + red + 0.5 + eps) return { "NDVI": ndvi, # vegetation vigor [-1, 1] "NDWI": ndwi, # water bodies [-1, 1] "NBR": nbr, # burn severity [-1, 1] "EVI": evi, # enhanced vegetation "SAVI": savi, # soil-adjusted vegetation }
| Index | Range | Low Values | High Values | |-------|-------|-----------|-------------| | NDVI | -1 to 1 | Water, bare soil, clouds | Dense green vegetation | | NDWI | -1 to 1 | Dry land | Open water bodies | | NBR | -1 to 1 | Recently burned areas | Healthy vegetation | | EVI | -1 to 1 | Non-vegetated | Dense canopy (less saturated than NDVI) |
pythonfrom sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score # Stack bands into feature array: (n_pixels, n_bands) # training_labels: land cover classes from ground truth polygons bands = np.stack([blue, green, red, nir, swir1, swir2, ndvi, ndwi], axis=-1) n_rows, n_cols, n_bands = bands.shape X = bands.reshape(-1, n_bands) # Train Random Forest classifier rf = RandomForestClassifier(n_estimators=200, max_depth=20, n_jobs=-1) scores = cross_val_score(rf, X_train, y_train, cv=5, scoring="f1_macro") print(f"5-fold F1: {scores.mean():.3f} +/- {scores.std():.3f}") rf.fit(X_train, y_train) classification = rf.predict(X).reshape(n_rows, n_cols)
Multi-temporal analysis for detecting land cover changes (deforestation, urbanization, flood extent):
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 13,154 | 21,552 | +64% | 1 | 1 | 0% | 2,605 | 4,258 | +63% | 0 | 0 | — |
case-02 | fail→fail | 22,838 | 18,481 | -19% | 1 | 1 | 0% | 4,308 | 5,954 | +38% | 0 | 0 | — |
case-03 | fail→pass | 14,190 | 18,136 | +28% | 1 | 1 | 0% | 2,504 | 4,849 | +94% | 0 | 0 | — |
case-04 | pass→pass | 17,706 | 17,123 | -3% | 1 | 1 | 0% | 3,082 | 4,918 | +60% | 0 | 0 | — |
case-05 | pass→pass | 15,824 | 15,144 | -4% | 1 | 1 | 0% | 3,114 | 5,130 | +65% | 0 | 0 | — |
case-06 | pass→pass | 8,535 | 8,410 | -1% | 1 | 1 | 0% | 1,859 | 3,671 | +97% | 0 | 0 | — |
case-07 | fail→fail | 17,613 | 16,212 | -8% | 1 | 1 | 0% | 2,844 | 4,884 | +72% | 0 | 0 | — |
case-08 | pass→pass | 12,394 | 7,257 | -41% | 1 | 1 | 0% | 2,278 | 3,499 | +54% | 0 | 0 | — |
case-09 | pass→pass | 11,277 | 8,563 | -24% | 1 | 1 | 0% | 1,908 | 3,488 | +83% | 0 | 0 | — |
case-10 | pass→pass | 13,806 | 12,842 | -7% | 1 | 1 | 0% | 2,251 | 4,409 | +96% | 0 | 0 | — |
case-11 | pass→pass | 12,795 | 11,060 | -14% | 1 | 1 | 0% | 2,085 | 3,914 | +88% | 0 | 0 | — |
case-12 | pass→pass | 12,159 | 8,393 | -31% | 1 | 1 | 0% | 1,942 | 3,254 | +68% | 0 | 0 | — |
case-13 | pass→pass | 15,609 | 15,246 | -2% | 1 | 1 | 0% | 2,259 | 4,440 | +97% | 0 | 0 | — |
case-14 | pass→pass | 13,393 | 8,232 | -39% | 1 | 1 | 0% | 2,003 | 3,403 | +70% | 0 | 0 | — |
case-15 | pass→pass | 10,004 | 11,474 | +15% | 1 | 1 | 0% | 1,808 | 4,155 | +130% | 0 | 0 | — |
case-16 | pass→pass | 10,033 | 4,888 | -51% | 1 | 1 | 0% | 1,665 | 2,814 | +69% | 0 | 0 | — |
case-17 | fail→pass | 5,802 | 4,543 | -22% | 1 | 1 | 0% | 997 | 2,845 | +185% | 0 | 0 | — |
case-18 | pass→pass | 21,255 | 23,486 | +10% | 1 | 1 | 0% | 3,093 | 5,533 | +79% | 0 | 0 | — |
case-19 | pass→pass | 12,598 | 7,519 | -40% | 1 | 1 | 0% | 1,963 | 3,359 | +71% | 0 | 0 | — |
case-20 | pass→pass | 14,485 | 15,474 | +7% | 1 | 1 | 0% | 2,194 | 4,479 | +104% | 0 | 0 | — |
case-21 | pass→pass | 8,739 | 5,138 | -41% | 1 | 1 | 0% | 1,641 | 2,961 | +80% | 0 | 0 | — |
case-22 | pass→pass | 16,825 | 14,496 | -14% | 1 | 1 | 0% | 2,943 | 4,636 | +58% | 0 | 0 | — |
case-23 | pass→pass | 21,916 | 21,898 | -0% | 1 | 1 | 0% | 3,649 | 5,161 | +41% | 0 | 0 | — |
case-24 | pass→pass | 27,080 | 26,202 | -3% | 1 | 1 | 0% | 4,430 | 6,500 | +47% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +13 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.