forked from vertify-earth/ForestFireGoa
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFireVulnerability.py
More file actions
1448 lines (1216 loc) · 59.9 KB
/
FireVulnerability.py
File metadata and controls
1448 lines (1216 loc) · 59.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
FireVulnerability.py: Python implementation of FireVulnerability.js using Earth Engine Python API.
This script maps fire vulnerability based on long-term trends, static factors,
and historical fire data using a Random Forest classifier.
Steps:
1. Load study area boundary.
2. Load and prepare historical fire points.
3. Load and process auxiliary datasets (NICFI, MODIS LST, DEM, Roads).
4. Calculate trends for NICFI and MODIS LST.
5. Load pre-computed trend layers from TrendFire.
6. Combine all input features into a single multi-band image.
7. Resample the combined feature image to a consistent grid (e.g., 30m EPSG:3857).
8. Prepare training data by sampling features at fire and non-fire locations.
9. Train a Random Forest classifier.
10. Evaluate the classifier.
11. Apply the classifier to the entire study area to create the vulnerability map.
12. Export the final vulnerability map.
"""
import ee
import os
import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import geopandas as gpd
from tqdm import tqdm
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
import seaborn as sns
# Define the Goa study area boundary
def get_study_boundary():
"""
Get the study area boundary for Goa, India.
Returns an ee.Geometry.
"""
# Method 1: Load from a shapefile if available locally
try:
boundary_path = os.path.join('data', 'pa_boundary.shp')
if os.path.exists(boundary_path):
boundary_gdf = gpd.read_file(boundary_path)
# Convert to GeoJSON
boundary_geojson = boundary_gdf.geometry.__geo_interface__
# Create an EE geometry
return ee.Geometry.Polygon(boundary_geojson['features'][0]['geometry']['coordinates'])
except Exception as e:
print(f"Could not load boundary from shapefile: {e}")
# Method 2: Use predefined feature from Earth Engine
try:
# Get the Goa district boundary from Earth Engine's administrative boundaries
goa = ee.FeatureCollection("FAO/GAUL/2015/level1").filter(ee.Filter.eq('ADM1_NAME', 'Goa'))
return goa.geometry()
except Exception as e:
print(f"Could not load boundary from Earth Engine: {e}")
# Method 3: Define manually as fallback
# These coordinates would need to be adjusted to match the study area
goa_coords = [
[73.6765, 15.7560],
[74.3161, 15.7560],
[74.3161, 14.8922],
[73.6765, 14.8922],
[73.6765, 15.7560]
]
return ee.Geometry.Polygon(goa_coords)
# Function to load fire events data
def load_fire_events(pa):
"""
Load fire events data from 2013-2023 and merge into a single feature collection.
Args:
pa: ee.Geometry representing the study area
Returns:
ee.FeatureCollection of fire events
"""
print("Loading fire events data...")
# Load fire events data (2013-2019)
try:
# If the data is available in Earth Engine, you can load it directly
fire_13_19 = ee.FeatureCollection("users/your_username/fire13_19")
except:
# Otherwise, try to find and upload it, or use a placeholder
print("Fire data 2013-2019 not available in Earth Engine. Using placeholder.")
fire_13_19 = ee.FeatureCollection([])
# Load fire events data (2020-2023)
try:
# If the data is available in Earth Engine, you can load it directly
fire_20_23 = ee.FeatureCollection("users/your_username/fire20_23")
except:
# Otherwise, try to find and upload it, or use a placeholder
print("Fire data 2020-2023 not available in Earth Engine. Using placeholder.")
fire_20_23 = ee.FeatureCollection([])
# Merge the two collections
fire_all = fire_13_19.merge(fire_20_23)
# Filter by the study area boundary
fire_all = fire_all.filterBounds(pa)
# Add a 'year' property based on the acquisition date
def add_year(feature):
acq_date = ee.String(feature.get('ACQ_DATE'))
year = ee.Number.parse(acq_date.slice(0, 4))
return feature.set('year', year)
fire_all = fire_all.map(add_year)
# Filter to include only 2013-2022 for training (2023 will be used for validation)
fire_training = fire_all.filter(ee.Filter.lt('year', 2023))
# Count the number of fire points
training_count = fire_training.size().getInfo()
print(f"Number of fire events for training (2013-2022): {training_count}")
# Get 2023 fire events for validation
fire_validation = fire_all.filter(ee.Filter.eq('year', 2023))
validation_count = fire_validation.size().getInfo()
print(f"Number of fire events for validation (2023): {validation_count}")
return fire_all, fire_training, fire_validation
# Function to load road data
def load_roads(pa):
"""
Load roads data and clip to the study area.
Args:
pa: ee.Geometry representing the study area
Returns:
ee.Image with roads data
"""
print("Loading roads data...")
try:
# Try to load the road data from Earth Engine assets
roads = ee.Image("users/your_username/road").clip(pa)
return roads
except:
print("Road data not available in Earth Engine. Using OSM roads as fallback.")
# Use OpenStreetMap data as a fallback
roads = ee.FeatureCollection('OSM/2020/roads').filterBounds(pa)
# Convert to an image
roads_image = roads.reduceToImage(
properties=['highway'],
reducer=ee.Reducer.first()
).clip(pa)
return roads_image
# Function to generate random non-fire points
def generate_non_fire_points(pa, fire_training, num_points=100, buffer_distance=500):
"""
Generate random points for non-fire locations within the study area.
Args:
pa: ee.Geometry representing the study area
fire_training: ee.FeatureCollection of fire points
num_points: Number of random points to generate
buffer_distance: Buffer distance (in meters) to avoid fire locations
Returns:
ee.FeatureCollection of non-fire points
"""
print(f"Generating {num_points} random non-fire points...")
# Create a buffer around fire points to avoid
fire_buffer = fire_training.geometry().buffer(buffer_distance)
# Create a mask to avoid sampling in fire buffer zones
mask = ee.Image(1).clip(pa).updateMask(
ee.Image(1).clip(pa).mask().subtract(
ee.Image(1).clip(fire_buffer).mask()
)
)
# Generate random points in the valid sampling area
non_fire_points = ee.FeatureCollection.randomPoints({
'region': pa,
'points': num_points,
'seed': 42,
'mask': mask
})
# Add a risk property (0 for non-fire)
non_fire_points = non_fire_points.map(lambda f: f.set('RiskNumeric', 0))
# Check if we have enough points
point_count = non_fire_points.size().getInfo()
print(f"Generated {point_count} non-fire points")
return non_fire_points
# Function to calculate Land Surface Temperature trends from MODIS data
def calculate_lst_trends(pa, start_date='2013-01-01', end_date='2022-12-31'):
"""
Calculate Land Surface Temperature (LST) trends from MODIS data.
Args:
pa: ee.Geometry representing the study area
start_date: Start date for trend calculation
end_date: End date for trend calculation
Returns:
ee.Image with LST trend bands
"""
print("Calculating Land Surface Temperature trends...")
# Function to mask clouds in MODIS LST imagery
def maskLstClouds(image):
qc = image.select('QC_Day')
# Bit 0-1: LST produced, good quality
mask = qc.bitwiseAnd(3).eq(0)
return image.updateMask(mask)
# Load the MODIS LST data
modis_lst = ee.ImageCollection('MODIS/061/MOD11A1') \
.filterDate(start_date, end_date) \
.filterBounds(pa)
# Apply cloud masking
modis_lst_masked = modis_lst.map(maskLstClouds)
# Convert LST to Celsius
modis_lst_celsius = modis_lst_masked.map(
lambda image: image.select(['LST_Day_1km', 'LST_Night_1km'])
.multiply(0.02)
.subtract(273.15)
.copyProperties(image, ['system:time_start'])
)
# Function to add time band for trend calculation
def addTime(image):
# Get the timestamp and convert to years since start
year = ee.Date(image.get('system:time_start')).difference(
ee.Date(start_date), 'year')
return image.addBands(ee.Image(year).rename('t'))
# Add time band to the collection
modis_lst_with_time = modis_lst_celsius.map(addTime)
# Calculate trends for day and night LST
day_trend = modis_lst_with_time.select(['t', 'LST_Day_1km']) \
.reduce(ee.Reducer.linearFit()) \
.select(['scale'], ['lst_day_trend'])
night_trend = modis_lst_with_time.select(['t', 'LST_Night_1km']) \
.reduce(ee.Reducer.linearFit()) \
.select(['scale'], ['lst_night_trend'])
# Calculate delta LST (day-night)
def calculateDeltaLST(image):
day = image.select('LST_Day_1km')
night = image.select('LST_Night_1km')
delta = day.subtract(night).rename('delta_lst')
return image.addBands(delta)
modis_lst_with_delta = modis_lst_celsius.map(calculateDeltaLST)
# Calculate delta LST trend
delta_trend = modis_lst_with_delta.map(addTime) \
.select(['t', 'delta_lst']) \
.reduce(ee.Reducer.linearFit()) \
.select(['scale'], ['delta_lst_trend'])
# Merge all LST trend layers
lst_trends = day_trend.addBands(night_trend).addBands(delta_trend)
return lst_trends.clip(pa)
# Function to load trend layers from TrendFire output
def load_trend_layers(pa):
"""
Load trend layers previously calculated using TrendFire.py.
Args:
pa: ee.Geometry representing the study area
Returns:
ee.Image with all trend bands
"""
print("Loading trend layers...")
try:
# Try to load the trend layers from Earth Engine assets
trends = ee.Image("users/your_username/goa_fire_trends").clip(pa)
print("Successfully loaded trend layers from Earth Engine asset.")
return trends
except:
print("Trend layers not found in Earth Engine assets. Calculating LST trends as fallback.")
# Calculate LST trends as a fallback
lst_trends = calculate_lst_trends(pa)
return lst_trends
# Function to categorize fire risk based on temperature difference
def categorize_fire_risk(fire_training, lst_data, pa):
"""
Categorize fire points by risk level based on temperature difference.
Args:
fire_training: ee.FeatureCollection of fire points
lst_data: ee.Image with LST data
pa: ee.Geometry representing the study area
Returns:
ee.FeatureCollection of categorized fire points
"""
print("Categorizing fire risk levels...")
# Sample the delta LST values at fire locations
fire_samples = lst_data.select('delta_lst_trend').sampleRegions({
'collection': fire_training,
'properties': ['ACQ_DATE', 'year'],
'scale': 1000, # MODIS LST resolution
'geometries': True
})
# Define threshold values for risk categories
# These thresholds would need to be calibrated based on local conditions
def categorize(feature):
delta_lst = feature.get('delta_lst_trend')
# Define risk categories based on delta LST
# 1: Low risk, 2: Moderate risk, 3: High risk
risk = ee.Algorithms.If(ee.Number(delta_lst).lt(0.1), 1,
ee.Algorithms.If(ee.Number(delta_lst).lt(0.2), 2, 3))
return feature.set('RiskNumeric', risk)
# Apply categorization to all fire points
categorized_fire = fire_samples.map(categorize)
# Count points in each risk category
low_risk = categorized_fire.filter(ee.Filter.eq('RiskNumeric', 1)).size().getInfo()
moderate_risk = categorized_fire.filter(ee.Filter.eq('RiskNumeric', 2)).size().getInfo()
high_risk = categorized_fire.filter(ee.Filter.eq('RiskNumeric', 3)).size().getInfo()
print(f"Fire risk categorization: {low_risk} Low, {moderate_risk} Moderate, {high_risk} High risk")
return categorized_fire
# Function to prepare training data
def prepare_training_data(fire_points_input, boundary, predictor_image, label_property, \
num_random_points=90, random_points_seed=42, \
moderate_risk_limit=70, use_risk_categories=True):
"""
Prepares training and validation data by combining categorized fire points
with generated non-fire points and sampling predictor values.
Args:
fire_points_input: ee.FeatureCollection of historical fire points.
Should contain 'Delta T' if use_risk_categories=True.
boundary: ee.Geometry defining the study area.
predictor_image: ee.Image containing all predictor bands.
label_property: String, the name of the property to use for classification label (e.g., 'RiskNumeric').
num_random_points: Integer, number of random non-fire points to generate.
random_points_seed: Integer, seed for random point generation.
moderate_risk_limit: Integer, threshold for Delta T to limit moderate risk points (if used).
use_risk_categories: Boolean, if True, categorize fires (0=NoFire, 1=Low, 2=Moderate, 3=High).
If False, label all fires as 1 (binary fire/no-fire).
Returns:
An ee.FeatureCollection containing sampled points with predictor values and the class label,
or None on error.
"""
print("\nPreparing training data...")
try:
# --- Categorize Fire Points ---
print("Categorizing fire points...")
no_fire_label = 0
fire_label = 1 # Used if use_risk_categories is False
if use_risk_categories:
# Use tqdm to show progress for client-side mapping/filtering if complex
# Note: The actual .map/.filter happens server-side, so tqdm only tracks
# the *creation* of these instructions, not their execution time.
print(" Defining fire risk categories (Low, Moderate, High)...")
low_risk = fire_points_input.filter(ee.Filter.rangeContains('Delta T', 0, 25))\
.map(lambda f: f.set(label_property, 1).set('RiskCategory', 'Low Risk'))
moderate_risk = fire_points_input.filter(ee.Filter.rangeContains('Delta T', 25, 35))\
.map(lambda f: f.set(label_property, 2).set('RiskCategory', 'Moderate Risk'))\
.limit(moderate_risk_limit) # Limit moderate risk points like JS
high_risk = fire_points_input.filter(ee.Filter.greaterThanOrEquals('Delta T', 35))\
.map(lambda f: f.set(label_property, 3).set('RiskCategory', 'High Risk'))
# Get counts (these trigger computation)
print(" Fetching category counts...")
low_count = low_risk.size().getInfo()
mod_count = moderate_risk.size().getInfo()
high_count = high_risk.size().getInfo()
print(f" Low Risk (<25): {low_count}")
print(f" Moderate Risk (25-35, limited to {moderate_risk_limit}): {mod_count}")
print(f" High Risk (>=35): {high_count}")
# Combine categorized fire points
categorized_fires = low_risk.merge(moderate_risk).merge(high_risk)
total_fires = categorized_fires.size().getInfo()
print(f" Total categorized fire points used: {total_fires}")
else:
# Binary classification: Label all input fires as 1
categorized_fires = fire_points_input.map(lambda f: f.set(label_property, fire_label))
total_fires = categorized_fires.size().getInfo()
print(f" Using binary classification. Total fire points: {total_fires}")
# --- Generate Non-Fire Points ---
print(f"Generating {num_random_points} random non-fire points...")
random_pts = ee.FeatureCollection.randomPoints(
region=boundary,
points=num_random_points,
seed=random_points_seed
)
non_fire_pts = random_pts.map(lambda feat: feat.set(label_property, no_fire_label))
non_fire_count = non_fire_pts.size().getInfo()
print(f" Generated {non_fire_count} non-fire points.")
# --- Combine Fire and Non-Fire Points ---
training_validation_points = categorized_fires.merge(non_fire_pts)
total_points = training_validation_points.size().getInfo()
print(f"Total points for sampling: {total_points}")
# --- Sample Predictor Values ---
print("\n>>> Sending request to GEE: Sample predictor values at points...") # Log Before
if predictor_image is None:
print("Error: Predictor image is missing.")
return None
if training_validation_points is None or total_points == 0:
print("Error: No training/validation points available for sampling.")
return None
# Ensure predictor image bands are float for sampling/training
predictor_image = predictor_image.toFloat()
# Define properties to keep (only the label)
properties_to_keep = [label_property]
# Sample the regions
sampled_data = predictor_image.sampleRegions(
collection=training_validation_points,
properties=properties_to_keep,
scale=predictor_image.projection().nominalScale(), # Use image native scale
geometries=False # Don't need geometries for training
)
# Force execution and check size BEFORE adding random column
print(">>> Waiting for GEE: Sampling predictor values...")
sampled_size_check = sampled_data.size().getInfo()
print(f"<<< GEE Response: Sampling returned {sampled_size_check} features.") # Log After
if sampled_size_check == 0:
print("Error: Sampling returned no features. Check predictor image, points, and permissions.")
return None
# Add a random column for splitting later
print("Adding random column for train/test split...")
sampled_data = sampled_data.randomColumn(columnName='random', seed=random_points_seed) # Explicit column name and seed
# Verify random column exists (optional check)
# print("Properties after adding random column:", sampled_data.first().propertyNames().getInfo())
print("Training data preparation complete.")
return sampled_data
except ee.EEException as e:
print(f"GEE error during training data preparation: {e}")
return None
except Exception as e:
print(f"Unexpected error during training data preparation: {e}")
return None
# Function to train a Random Forest classifier
def train_classifier(training_data, class_property, input_properties, num_trees=1000, variables_per_split=1, seed=42):
"""
Trains the ee.Classifier.smileRandomForest classifier.
Args:
training_data: ee.FeatureCollection used for training.
class_property: String, the name of the property containing the class label.
input_properties: List of strings, names of the predictor properties/bands.
num_trees: Integer, number of trees in the forest.
variables_per_split: Integer, number of variables to consider at each split.
Set to None or 0 to use sqrt(num_variables).
JS used 1.
seed: Integer, seed for reproducibility.
Returns:
A trained ee.Classifier object, or None on error.
"""
print(f"Training Random Forest classifier with {num_trees} trees...")
if training_data is None:
print("Error: Training data is missing.")
return None
if not input_properties:
print("Error: Input properties list is empty.")
return None
try:
classifier = ee.Classifier.smileRandomForest(
numberOfTrees=num_trees,
variablesPerSplit=variables_per_split, # Match JS, using 1
# minLeafPopulation=1, # Default is 1
# bagFraction=0.5, # Default is 0.5
seed=seed
).train(
features=training_data,
classProperty=class_property,
inputProperties=input_properties
)
print("Classifier trained successfully.")
# Optional: Explain the classifier (can be computationally intensive)
# try:
# explanation = classifier.explain()
# print("Classifier explanation:", explanation.getInfo())
# importance = explanation.get('importance')
# if importance:
# print("Variable Importance:")
# importance_info = importance.getInfo()
# # Ensure importance_info is a dictionary before iterating
# if isinstance(importance_info, dict):
# for prop, imp in importance_info.items():
# print(f" {prop}: {imp}")
# else:
# print(" Could not retrieve importance details in expected format.")
# except Exception as explain_e:
# print(f"Could not get classifier explanation: {explain_e}")
return classifier
except ee.EEException as e:
print(f"GEE error during classifier training: {e}")
return None
except Exception as e:
print(f"Unexpected error during classifier training: {e}")
return None
# Function to validate the Random Forest model
def validate_model(classifier, predictor_names, validation_data):
"""
Validate the Random Forest model using test data.
Args:
classifier: Trained ee.Classifier.RandomForest
predictor_names: List of predictor variable names
validation_data: ee.FeatureCollection with validation data
Returns:
Dictionary with validation metrics
"""
print("Validating Random Forest model...")
# Apply the classifier to the validation data
validated = validation_data.classify(classifier)
# Get the confusion matrix
confusion_matrix = validated.errorMatrix('RiskNumeric', 'classification')
# Calculate accuracy and Kappa statistics
accuracy = confusion_matrix.accuracy().getInfo()
kappa = confusion_matrix.kappa().getInfo()
print(f"Validation accuracy: {accuracy:.4f}")
print(f"Kappa statistic: {kappa:.4f}")
# Get confusion matrix as a list of lists
cm_data = confusion_matrix.getInfo()
# Return validation metrics
return {
'accuracy': accuracy,
'kappa': kappa,
'confusion_matrix': cm_data
}
# Function to create a fire vulnerability map
def create_vulnerability_map(classifier, predictor_variables, predictor_names, pa):
"""
Create a fire vulnerability map using the trained classifier.
Args:
classifier: Trained ee.Classifier.RandomForest
predictor_variables: ee.Image with predictor variables
predictor_names: List of predictor variable names
pa: ee.Geometry representing the study area
Returns:
ee.Image with fire vulnerability classification
"""
print("Creating fire vulnerability map...")
# Select only the predictor bands used for training
predictors = predictor_variables.select(predictor_names)
# Apply the classifier to create a vulnerability map
vulnerability_map = predictors.classify(classifier).clip(pa)
return vulnerability_map
# Function to export the vulnerability map to Google Drive
def export_vulnerability_map(vulnerability_map, pa, filename='fire_vulnerability_map', folder='ForestFireGoa'):
"""
Export the fire vulnerability map to Google Drive.
Args:
vulnerability_map: ee.Image with fire vulnerability classification
pa: ee.Geometry representing the study area
filename: Output filename
folder: Google Drive folder name
"""
print(f"Exporting fire vulnerability map to Google Drive folder '{folder}'...")
# Start the export task
task = ee.batch.Export.image.toDrive({
'image': vulnerability_map,
'description': filename,
'folder': folder,
'fileNamePrefix': filename,
'region': pa,
'scale': 30,
'maxPixels': 1e13
})
task.start()
print(f"Export task started with ID: {task.id}")
# Function to export the vulnerability map to an Earth Engine asset
def export_vulnerability_map_to_asset(vulnerability_map, pa, asset_name):
"""
Export the fire vulnerability map to an Earth Engine asset.
Args:
vulnerability_map: ee.Image with fire vulnerability classification
pa: ee.Geometry representing the study area
asset_name: Name of the Earth Engine asset
"""
print(f"Exporting fire vulnerability map to Earth Engine asset: {asset_name}")
# Start the export task
task = ee.batch.Export.image.toAsset({
'image': vulnerability_map,
'description': asset_name.split('/')[-1],
'assetId': asset_name,
'region': pa,
'scale': 30,
'maxPixels': 1e13
})
task.start()
print(f"Export task started with ID: {task.id}")
# Main function to run the fire vulnerability mapping workflow
def main():
"""Main script execution for fire vulnerability mapping."""
# --- Configuration ---
PROJECT_ID = 'ee-crop-health-telangana'
BOUNDARY_ASSET_ID = 'users/jonasnothnagel/pa_boundary'
FIRE_ASSET_BASE = 'users/jonasnothnagel/' # Base for fire points (e.g., fire13_19, fire20_23)
DEM_ASSET_ID = 'users/jonasnothnagel/dem'
ROAD_ASSET_ID = 'users/jonasnothnagel/roads'
TRENDFIRE_ASSET_BASE = 'users/jonasnothnagel/' # Base for JS TrendFire outputs (e.g., Trend2023_ndvi)
NICFI_COLLECTION = 'projects/planet-nicfi/assets/basemaps/asia'
MODIS_LST_COLLECTION = 'MODIS/061/MOD11A1'
# --- Option to use pre-computed resampled input --- #
USE_PRECOMPUTED_RESAMPLED = True # Set to True to load the asset below
PRECOMPUTED_RESAMPLED_ASSET = 'users/jonasnothnagel/inputResampled' # Asset ID of the uploaded 40-band tif
# ---------------------------------------------------- #
OUTPUT_ASSET_BASE = 'users/jonasnothnagel/'
VULNERABILITY_MAP_ASSET = OUTPUT_ASSET_BASE + 'FireVulnerability_py'
# Asset ID for the resampled image *generated* by this script (if USE_PRECOMPUTED_RESAMPLED=False)
GENERATED_RESAMPLED_FEATURES_ASSET = OUTPUT_ASSET_BASE + 'FireVulnerability_InputsGenerated_py'
TRAINING_POINTS_ASSET = OUTPUT_ASSET_BASE + 'FireVulnerability_TrainingPoints_py'
GRID_SCALE = 30
GRID_CRS = 'EPSG:3857'
NUM_RANDOM_POINTS = 90 # Match JS
RF_TREES = 1000 # Match JS
RF_VAR_SPLIT = 1 # Match JS
TRAIN_TEST_SPLIT_RATIO = 0.8
CLASS_LABEL = 'RiskNumeric'
# Use Delta T categories (True) or binary fire/no-fire (False)
# JS used only high risk (Delta T >= 35) vs non-fire (0). Setting this to True enables categorization,
# but prepare_training_data currently merges all categories or uses binary.
# To exactly match JS sampling, logic inside prepare_training_data needs adjustment.
USE_RISK_CATEGORIES = True
RANDOM_SEED = 42 # For reproducibility in random points and RF training
# --- Initialization ---
initialize_ee(PROJECT_ID)
boundary = get_boundary(BOUNDARY_ASSET_ID)
if not boundary:
return
# --- Processing Steps ---
print("\n--- Starting Fire Vulnerability Workflow ---")
input_resampled = None # This will hold the final predictor image for sampling/classification
fire_pts_processed = None # To hold loaded fire points
# --- Mode 1: Load Precomputed Resampled Input --- #
if USE_PRECOMPUTED_RESAMPLED:
print(f"---> Mode: Using Precomputed Input <---")
print(f"Attempting to load pre-computed resampled input: {PRECOMPUTED_RESAMPLED_ASSET}")
try:
input_resampled = ee.Image(PRECOMPUTED_RESAMPLED_ASSET).toFloat() # Ensure float
# Verify it has bands
if not input_resampled.bandNames().getInfo():
raise ee.EEException(f"Loaded asset {PRECOMPUTED_RESAMPLED_ASSET} has no bands.")
print("Successfully loaded pre-computed resampled input.")
print("Skipping intermediate data loading, combining, and resampling steps.")
# Load fire points separately as they are needed for training data prep
fire_pts_processed = load_fire_points(FIRE_ASSET_BASE, boundary)
if fire_pts_processed is None:
print("Error: Failed to load fire points even when using precomputed input. Exiting.")
return
except ee.EEException as e:
print(f"Error loading pre-computed asset '{PRECOMPUTED_RESAMPLED_ASSET}': {e}")
print("Exiting. Cannot proceed without input features.")
return # Exit if loading precomputed fails
except Exception as e:
print(f"Unexpected error loading pre-computed asset: {e}")
print("Exiting. Cannot proceed without input features.")
return # Exit if loading precomputed fails
# --- Mode 2: Generate Inputs from Components --- #
if input_resampled is None: # This block runs if USE_PRECOMPUTED_RESAMPLED is False or loading failed
print("---> Mode: Generating Input Features from Components <--- ")
# 1. Load Fire Points (needed regardless for training data)
fire_pts_processed = load_fire_points(FIRE_ASSET_BASE, boundary)
if fire_pts_processed is None:
print("Exiting: Failed to load fire points.")
return
# 2. Process NICFI (Optional)
nicfi_trends = process_nicfi(boundary)
if nicfi_trends is None:
print("Warning: Failed to process NICFI trends. Proceeding without them.")
# 3. Process MODIS LST (Optional)
modis_trends = process_modis_lst(boundary)
if modis_trends is None:
print("Warning: Failed to process MODIS LST trends. Proceeding without them.")
# 4. Load DEM Slope
dem_slope = load_dem_slope(DEM_ASSET_ID, boundary)
if dem_slope is None:
print("Warning: Failed to process DEM slope. Proceeding without it.")
# 5. Load Roads
roads_img = load_roads(ROAD_ASSET_ID, boundary) # Corrected arguments
if roads_img is None:
print("Warning: Failed to process Roads layer. Proceeding without it.")
# 6. Load TrendFire Outputs (Core Trends)
trendfire_img = load_trendfire_outputs(TRENDFIRE_ASSET_BASE, boundary)
if trendfire_img is None:
print("Exiting: Failed to load TrendFire outputs.")
return
# 7. Combine Available Features
print("Combining available features...")
combined_features = combine_features(trendfire_img, nicfi_trends, modis_trends, dem_slope, roads_img)
if combined_features is None or not combined_features.bandNames().getInfo():
print("Exiting: Failed to combine input features or resulting image has no bands.")
return
print(f"Combined features image created with bands: {combined_features.bandNames().getInfo()}")
# 8. Resample & Export Generated Intermediate Input
print(f"Resampling combined features to {GRID_SCALE}m scale, CRS {GRID_CRS}...")
input_resampled = resample_features(combined_features, GRID_SCALE, GRID_CRS, boundary)
if input_resampled is None or not input_resampled.bandNames().getInfo():
print("Exiting: Failed to resample features or resulting image has no bands.")
return
print("Resampling complete.")
# Export the generated resampled image for comparison/debugging
print(f"Exporting the generated resampled input features to asset: {GENERATED_RESAMPLED_FEATURES_ASSET}")
export_asset(input_resampled, GENERATED_RESAMPLED_FEATURES_ASSET, boundary, "Generated_Inputs_FV_py", GRID_SCALE, GRID_CRS)
# --- Classification Workflow (Uses the selected 'input_resampled') --- #
if input_resampled is None or fire_pts_processed is None:
print("Error: Cannot proceed to classification. Missing resampled input image or fire points.")
return
print(f"\nProceeding with classification using predictor image with bands: {input_resampled.bandNames().getInfo()}")
# 9. Prepare Training Data
training_data_fc = prepare_training_data(
fire_points_input=fire_pts_processed,
boundary=boundary,
predictor_image=input_resampled,
label_property=CLASS_LABEL,
num_random_points=NUM_RANDOM_POINTS,
random_points_seed=RANDOM_SEED,
use_risk_categories=USE_RISK_CATEGORIES
)
if training_data_fc is None:
print("Exiting: Failed to prepare training data.")
return
# Export the full sampled dataset before splitting (useful for external analysis)
export_table(training_data_fc, TRAINING_POINTS_ASSET, "Training_Validation_Points_FV_py")
# 10. Split Data
print(f"Splitting data using ratio: {TRAIN_TEST_SPLIT_RATIO:.1f} train / {1-TRAIN_TEST_SPLIT_RATIO:.1f} validation...")
# Filter based on the random column added in prepare_training_data
training_sample = training_data_fc.filter(ee.Filter.lt('random', TRAIN_TEST_SPLIT_RATIO))
validation_sample = training_data_fc.filter(ee.Filter.gte('random', TRAIN_TEST_SPLIT_RATIO))
train_count = training_sample.size().getInfo()
val_count = validation_sample.size().getInfo()
print(f" Training samples: {train_count}, Validation samples: {val_count}")
if train_count == 0 or val_count == 0:
print("Error: Training or validation sample is empty after splitting. Check data preparation.")
return
# Get predictor names from the resampled image (used as input properties for RF)
predictor_names = input_resampled.bandNames()
if not predictor_names.getInfo():
print("Error: Could not retrieve band names from the input predictor image.")
return
# 11. Train Classifier
print("\n>>> Sending request to GEE: Train Random Forest classifier...") # Log Before
classifier = train_classifier(
training_data=training_sample,
class_property=CLASS_LABEL,
input_properties=predictor_names, # Pass Python list
num_trees=RF_TREES,
variables_per_split=RF_VAR_SPLIT,
seed=RANDOM_SEED
)
# Note: train() itself can take time, but the result (trained classifier object)
# is usually returned quickly to the client unless explain() is called.
# The heavy lifting happens when classifier.classify() is called.
print("<<< GEE Response: Classifier training definition complete.") # Log After
if classifier is None:
print("Exiting: Classifier training failed.")
return
# 12. Evaluate Classifier
print("\n>>> Sending request to GEE: Evaluate classifier...") # Log Before
evaluation_results = evaluate_classifier(classifier, validation_sample, CLASS_LABEL)
print("<<< GEE Response: Evaluation complete.") # Log After
# evaluation_results is a dictionary containing accuracy, kappa, matrix etc.
if evaluation_results is None:
print("Warning: Classifier evaluation step failed or produced no results.")
# 13. Classify Image
print("\n>>> Sending request to GEE: Classify the full predictor image...") # Log Before
# Ensure the image being classified has float type bands (required by some classifiers)
vulnerability_map = input_resampled.toFloat().classify(classifier)
# Force execution by getting some info (optional, but confirms the step is done)
# print(">>> Waiting for GEE: Classification...")
# _ = vulnerability_map.bandNames().getInfo() # Or use another light .getInfo() call
print("<<< GEE Response: Classification definition complete.") # Log After
# 14. Export Final Map
print(f"\nExporting final Fire Vulnerability Map to asset:")
print(f" {VULNERABILITY_MAP_ASSET}")
export_asset(
image=vulnerability_map.toInt8(), # Classifiers output integer labels, cast to save space
asset_id=VULNERABILITY_MAP_ASSET,
region=boundary,
description="Fire_Vulnerability_Map_py",
scale=GRID_SCALE,
crs=GRID_CRS
)
print("\n--- Fire Vulnerability Workflow Completed --- ")
print("Monitor GEE tasks dashboard for export completion.")
# --- Fire Vulnerability Specific Functions ---
def load_fire_points(asset_base, boundary):
"""
Loads historical fire point assets, merges them, and filters out 2023 data.
Args:
asset_base: String, the base path for fire point assets (e.g., 'users/username/').
boundary: ee.Geometry object for optional filtering (though JS didn't explicitly filter here).
Returns:
An ee.FeatureCollection containing fire points from 2013-2022, or None on error.
"""
print("Loading and preparing historical fire points...")
fire_asset_13_19 = asset_base + 'fire13_19'
fire_asset_20_23 = asset_base + 'fire20_23'
try:
print(f" Loading {fire_asset_13_19}...")
fire13_19 = ee.FeatureCollection(fire_asset_13_19)
print(f" Loading {fire_asset_20_23}...")
fire20_23 = ee.FeatureCollection(fire_asset_20_23)
# Merge the collections
fire13_23 = fire13_19.merge(fire20_23)
print(f" Merged collections. Total points (2013-2023): {fire13_23.size().getInfo()}")
# Filter out points from 2023 based on the 'acq_date' property
# Assumes 'acq_date' format allows simple string filtering like 'YYYY-MM-DD'
fire13_22 = fire13_23.filter(ee.Filter.stringContains('acq_date', '-2023').Not())
# Alternative filter if acq_date is ee.Date object:
# fire13_22 = fire13_23.filter(ee.Filter.date('2013-01-01', '2022-12-31'))
print(f" Filtered out 2023 points. Total points (2013-2022): {fire13_22.size().getInfo()}")
# Optional: Filter by boundary if needed, although JS didn't seem to do it here
# fire13_22 = fire13_22.filterBounds(boundary)
# print(f" Filtered by boundary. Points within boundary: {fire13_22.size().getInfo()}")
return fire13_22
except ee.EEException as e:
print(f"Error loading or processing fire point assets: {e}")
print(f"Please ensure assets '{fire_asset_13_19}' and '{fire_asset_20_23}' exist and properties like 'acq_date' are correct.")
return None
except Exception as e:
print(f"An unexpected error occurred loading fire points: {e}")
return None
def process_nicfi(boundary, start_date='2016-03-01', end_date='2024-02-28', collection_id='projects/planet-nicfi/assets/basemaps/asia'):
"""
Loads Planet-NICFI data, calculates NDVI, and computes trends for R, G, B, N, NDVI bands.
Args:
boundary: ee.Geometry for filtering and clipping.
start_date: Start date for filtering NICFI data.
end_date: End date for filtering NICFI data.
collection_id: GEE asset ID for the NICFI collection.
Returns:
ee.Image containing slope and intercept bands for R, G, B, N, NDVI, or None on error.
"""
print(f"Processing NICFI trends ({start_date} to {end_date})...")
try:
# Load the collection
nicfi_col = ee.ImageCollection(collection_id) \
.map(lambda img: img.clip(boundary)) \
.filterDate(start_date, end_date)
# Function to calculate and add NDVI band
def addNDVI(image):
# Ensure bands are correctly named ('N', 'R', 'G', 'B')
# May need to rename depending on the specific NICFI collection version
# Example: .select(['B4', 'B3', 'B2', 'B1'], ['N', 'R', 'G', 'B']) if needed
ndvi = image.normalizedDifference(['N', 'R']).rename('NDVI')
return image.addBands(ndvi)
nicfi_col = nicfi_col.map(addNDVI)
# Add time band (years since 2016-01-01, as per JS)
ref_date = ee.Date('2016-01-01')
def addTime(image):
img_date = image.date()
years = img_date.difference(ref_date, 'year')
return image.addBands(ee.Image(years).rename('time').float())
nicfi_col_time = nicfi_col.map(addTime)
# Indices for trend calculation
indices = ['R', 'G', 'B', 'N', 'NDVI']
combined_trends = ee.Image().toFloat() # Start empty
# Calculate trends for each index
for index in indices:
print(f" Calculating NICFI trend for: {index}")
stacked = nicfi_col_time.select(['time', index])
regression = stacked.reduce(ee.Reducer.linearFit()) # Outputs 'scale' and 'offset'
slope = regression.select('scale').rename(f'{index}_Slope')
intercept = regression.select('offset').rename(f'{index}_Intercept')
combined_trends = combined_trends.addBands(slope).addBands(intercept)
print("NICFI trends calculated successfully.")
return combined_trends
except ee.EEException as e:
print(f"GEE error during NICFI processing: {e}")
print(f"Please ensure you have access to the collection: {collection_id}")
return None
except Exception as e:
print(f"Unexpected error during NICFI processing: {e}")
return None