-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtailingsClassifier
1759 lines (1379 loc) · 70.6 KB
/
tailingsClassifier
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
/*********************************************
* *
* Tailings Classification Model *
* *
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *
* *
* Author: Dan Jewell *
* Saints Mary's University, 2022 *
* *
*********************************************/
/* This model is intended to sample Sentinel-2 surface reflectance imagery and classify it to indicate waste from
historic gold mines. Model is trained on data from the NS Mine Tailings Database.
/*********************************************
* *
* Initialize Variables and Load Assets *
* *
*********************************************/
//////////////////////////////////////////////
// User Parameters //
//////////////////////////////////////////////
//These parameters will determine behaviour of model
//How many times sampler/classifier will loop
var iterations = 5;
//Change scale if changing bands. Should only be 10 or 20.
var scale = 20;
//Total sample points for non-tailings. Generally, more = better precision, fewer = better recall
//Tailings samples are collected as proportion of tailings pixels
var totalSamplePoints = 7500;
//Determine whether indices are used in classifier. Indices will still be used for filtering training data
var includeIndices = true;
//Determine whether spectral bands are used
var includeBands = true;
// Use best bands based on testing.
// Overrides includeIndices and includeBands
var useBestBands = true;
//Use random districts, or the target districts below
//Randomizes for EACH iteration - monte carlo cross-validation (see Lyons et al, 2018)
var randomDistricts = false;
//List of districts that will be used if not random
var targetDistrictNames = ee.List(["Montague"]);
//Use custom-drawn polygon
var userTargetArea = false;
//Adjust the precision/recall weight for F Score.
//1 = equal weight. 2 = recall is twice as important as precision
//Must be in range [0,2]
var b = 1;
// Thresholds for masking vegetation and water in pixels
var vegetationThreshold = 0.6;
var waterThreshold = 0.0;
// Parameter Dictionary for Records
var parameterDictionary = ee.Dictionary({
"iterations": iterations,
"scale": scale,
"nSamplePoints": totalSamplePoints,
"randomDistricts": randomDistricts,
"usingBestBands": useBestBands,
"vegetationThreshold": vegetationThreshold,
"waterThreshold": waterThreshold,
"fScoreBValue": b
});
print(parameterDictionary, "Model Parameters");
//////////////////////////////////////////////
// Misc. Variables //
//////////////////////////////////////////////
//Image to be used for training (Halifax area)
// var trainingImage = trainingImageAsset;
var trainingImage = s2sr
.filterBounds(ROI)
.filterDate("2019-07-01", "2019-09-01")
.sort("CLOUDY_PIXEL_PERCENTAGE")
.first();
//Mainland NS polygon to cut out large amounts of ocean
var mainlandNS = ee.Feature(mainlandNS);
//Clip training image to mainland
trainingImage = trainingImage.clip(mainlandNS);
//Radius of buffered circle around districts in meters
var districtBufferDistance = 2500;
//A list of seeds for various server-side uses
// var seedList = ee.List.sequence({start: 0, count: 1000}).shuffle({seed: ee.Number(Math.random() * 1000).floor()});
var seedList = ee.List.sequence({start: 0, count: 1000}).shuffle({seed: 5283});
//////////////////////////////////////////////
// Band Sets //
//////////////////////////////////////////////
//All bands with 10m resolution plus SCL (land classification band)
var bands10 = ["B2", "B3", "B4", "B8"];
//All bands with 20m resolution plus SCL
var bands20 = ["B2", "B3", "B4", "B5", "B6", "B7", "B8", "B8A", "B11", "B12"];
// Based on testing, the best-performing set of variables
var bestBands = ["B3", "B6", "B7", "B8A", "B12", "ndvi", "mndwi"];
//////////////////////////////////////////////
// Cloud Mask //
//////////////////////////////////////////////
// Make sure that clouds are indicated when switching training images
//Clouds look similar spectrally to concrete/urban, which can look like tailings.
//Automatic cloud removal is difficult and risks removing tailing as well
//Display cloudiness of whole image
//Unless clouds can be separated from tailings more reliably, simply displaying cloudiness is best
var cloudChance = ee.Number(trainingImage.get("CLOUDY_PIXEL_PERCENTAGE"));
print("The % of cloudy pixels in this image:", cloudChance);
//////////////////////////////////////////////
// Projection and Band Selection //
//////////////////////////////////////////////
//A message on iterations
// print("Number of iterations:", iterations);
//A Message on FScore b value
// print("B Value of F-Score:", b);
//Set automatically based on parameters above:
//Projection for 10m bands
var projection10 = trainingImage.select("B4").projection();
//Projection for 20m bands
var projection20 = trainingImage.select("B8A").projection();
//Bands and projection are set to scale
if(scale == 10){
var bands = bands10;
var projection = projection10.atScale(scale);
} else if(scale == 20){
var bands = bands20;
var projection = projection20.atScale(scale);
}
//Add SCL band to bands list for sampling later
bands = ee.List(bands).add("SCL");
//Set training image with relevant bands
trainingImage = trainingImage.select(bands);
/*********************************************
* *
* Initial Data Processesing *
* *
*********************************************/
//////////////////////////////////////////////
// Tailings Areas //
//////////////////////////////////////////////
/* Original data from the NS Mine Tailings Database (from E. W. Hennick and J. C. Poole, 2020).
* This data is filtered to show only tailings areas from gold mines in mainland NS. */
// Cast imported asset to a feature collection
var tailings = ee.FeatureCollection(tailingsNS);
// Clip to only mainland NS
tailings = tailings.map(function(f){
return f.intersection(mainlandNS);
});
// Filter to only tailings from gold mines
tailings = tailings.filter(ee.Filter.and(
ee.Filter.eq("Commodity", "Gold"),
ee.Filter.eq("GCODE", "TAILINGS_AREA"),
ee.Filter.neq("Crusher1", "0"))); //Crushers with no associated tailings are labeled "Crusher1 = 0"
// print(tailings, "Tailings Feature Collection");
Map.addLayer(tailings.draw({color: "Grey", strokeWidth: 1}), null, "Tailings Database Polygons");
// Convert tailings features to a raster image
function CreateTailingsImage(tailings, image){
// Create a background image with value of 0 and reduce its resolution to match lowest of input bands
var background = ee.Image(0)
.reproject(projection)
.clip(image.geometry()); //Created background image is boundless. Clip to input (training image) extent
// Give each tailings feature a property, "tailings", with value = 1
var tailingsWithProperty = tailings.map(function(f){return f.set("tailings", 1);});
// Convert tailings features to an image (raster)
var tailingsImage = tailingsWithProperty.reduceToImage(["tailings"], ee.Reducer.max()) //Reducer max takes highest value (0 or 1)
.reproject(projection)
.reduceResolution(ee.Reducer.mean());
// Where tailings image (excluding No Data) is not 0, replace background pixels with value of 1
return background.where(tailingsImage.eq(1), tailingsImage)
.reproject(projection)
.rename("tailings")
.eq(1); //Quickly changes range to [0,1]
}
var tailingsImage = CreateTailingsImage(tailings, trainingImage);
// print(tailingsImage, "Tailings Image")
// Map.addLayer(tailingsImage)
//Rename "DistrictNa" property of districtPoints
districtPoints = districtPoints.map(function(f){
var name = f.get("DistrictNa");
return ee.Feature(f.geometry(), {"ShortName": name}).copyProperties({source: f, exclude: ["DistrictNa", "Watershed"]});
});
//////////////////////////////////////////////
// Training/Validation Image //
//////////////////////////////////////////////
/* Tailings image is filtered to remove pixels that indicate vegetation or open water.
* This is to simulate the surfaces that are visible to the Sentinel-2 sensors and
* may be classified as tailings.
* NDVI and NDWI are also used as inputs to the classifier (when includeIndices is true)
*/
//Thresholds for masking
//Set above in model parameters
// var vegetationThreshold = 0.5;
// var waterThreshold = 0.0; //Currently using s2's SCL layer for water
//Create normalized difference images for masking
var ndvi = trainingImage.normalizedDifference(["B8", "B4"]).rename("ndvi"); //Normalized Difference Vegetation Index
var ndwi = trainingImage.normalizedDifference(["B3", "B8"]).rename("ndwi"); //Normalized Difference Water Index
var mndwi = trainingImage.expression("mndwi = (b('B3') - b('B11')) / (b('B3') + b('B11'))"); // Modified Normalized Difference Water Index
// Histogram to show distribution of vegetation and water index values
var ndviSample = ndvi.sample({numPixels: 10000, dropNulls: true});
var ndviHisto= ui.Chart.feature.histogram(ndviSample, "ndvi");
// print(ndviHisto);
var mndwiSample = mndwi.sample({numPixels: 10000, dropNulls: true});
var mndwiHisto= ui.Chart.feature.histogram(mndwiSample, "mndwi");
// print(mndwiHisto);
//Water layer of S2's SCL band
var water = trainingImage.select("SCL").eq(6);
//Create mask images. Any pixel with value under threshold will return with value of 1
var vegetationMask = ndvi.lte(vegetationThreshold).reproject(projection).rename("vegetationMask");
var waterMask = mndwi.lte(waterThreshold).reproject(projection).rename("waterMask");
// var waterMask = water.eq(0).reproject(projection);
function ApplyMasks(imageToMask, vegetationMask, waterMask){
return imageToMask.multiply(vegetationMask).multiply(waterMask);
}
var maskedTailings = ApplyMasks(tailingsImage, vegetationMask, waterMask).reproject({crs: projection, scale: scale});
//Masks for visualization:
Map.addLayer(vegetationMask, {min: 0, max: 1, palette: ["green", "brown"]}, "Vegetation Mask", false);
Map.addLayer(waterMask.eq(0).selfMask(), {min: 0, max: 1, palette: ["blue", "brown"]}, "Water Mask", false);
//Images used for vegetation mask:
Map.addLayer(ndvi, {palette: ["brown", "green"], min: -1, max: 1}, "NDVI", false);
//Image used for water mask:
Map.addLayer(mndwi, {palette: ["brown", "blue"]}, "MNDWI", false);
// Map.addLayer(water.selfMask(), {palette: ["white", "blue"]}, "Water Mask from SCL")
/*********************************************
* *
* Multi-Use Tools *
* *
*********************************************/
//General-use functions that are used multiple times
//Counts pixels in an image and divides by a given quotient
function GetSampleCount(image, region, factor){
image = ee.Image(image);
//Convert to vector
var imgSamples = image.sample({
region: region,
scale: scale,
projection: projection,
factor: factor,
dropNulls: true});
return imgSamples.size().floor();
}
//Classify points
//This should be used for the sample points that are selected after filtering training image for viable training areas
//Returns one classified image with two bands:
//1) "classified" with two classes (0 or 1 - tailings or not-tailings),
//2) "tailings" with two classes. This is the masked tailings database layer for reference and accuracy assessment
function ClassifyFromSamples(points, variables){
points = ee.Dictionary(points);
variables = ee.List(variables);
var testingVector = ee.FeatureCollection(points.get("districtVector"));
var classificationArea = trainingImage.clipToCollection(testingVector);
//Get a new seed each time
//points contains a "seed" property
var classifierSeed = ee.Number(points.get("seed"));
//Unpack feature collections from dictionary
var tailingsPoints = ee.FeatureCollection(points.get("tailingsPoints"));
var nonTailingsPoints = ee.FeatureCollection(points.get("nonTailingsPoints"));
//Combine all points to train classifier
var trainingPoints = nonTailingsPoints.merge(tailingsPoints);
//Random Forest
var classifier = ee.Classifier.smileRandomForest({
numberOfTrees: 250,
seed: classifierSeed
})
.train({
features: trainingPoints,
classProperty: "tailings",
inputProperties: variables
});
var classifiedImage = classificationArea.classify(classifier)
.addBands(maskedTailings) //For confusion matrix later
.reproject({crs: projection, scale: scale})
.eq(1) //sets bitdepth to 1
.set("seed", classifierSeed); //Seed is attached for later use.
//Return a classified image.
return classifiedImage;
}
// To show what variables were used in classification
function ClassifyFromSamples_SchemaTest(points, variables){
points = ee.Dictionary(points);
variables = ee.List(variables);
var testingVector = ee.FeatureCollection(points.get("districtVector"));
var classificationArea = trainingImage.clipToCollection(testingVector);
//Get a new seed each time
//points contains a "seed" property
var classifierSeed = ee.Number(points.get("seed"));
//Unpack feature collections from dictionary
var tailingsPoints = ee.FeatureCollection(points.get("tailingsPoints"));
var nonTailingsPoints = ee.FeatureCollection(points.get("nonTailingsPoints"));
//Combine all points to train classifier
var trainingPoints = nonTailingsPoints.merge(tailingsPoints);
//Random Forest
var classifier = ee.Classifier.smileRandomForest({
numberOfTrees: 250,
seed: classifierSeed
})
.train({
features: trainingPoints,
classProperty: "tailings",
inputProperties: variables
});
var classifierSchema = classifier.schema();
return classifierSchema;
}
//Get Variable Importance
//Similar to ClassifyFromSamples, but returns a dictionary with information on classifier results
function GetVariableImportance(points, variableList){
points = ee.Dictionary(points);
variableList = ee.List(variableList); //A list of strings containing the names of variables
//Get a new seed each time
//points contains a "seed" property
var classifierSeed = ee.Number(points.get("seed"));
//Unpack feature collections from dictionary
var tailingsPoints = ee.FeatureCollection(points.get("tailingsPoints"));
var nonTailingsPoints = ee.FeatureCollection(points.get("nonTailingsPoints"));
//Combine all points to train classifier
var trainingPoints = nonTailingsPoints.merge(tailingsPoints);
//Random Forest
var classifier = ee.Classifier.smileRandomForest({
numberOfTrees: 250,
seed: classifierSeed //Get a new seed from the list at top for each iteration of classifier
})
.train({
features: trainingPoints,
classProperty: "tailings",
inputProperties: variableList
});
//.explain() returns a dictionary with variable importance, out of bag error, and information on the classifier parameters
var classifierExplained = classifier.explain();
return classifierExplained;
}
//Pass a list of results from GetVariableImportance here to obtain mean VI of individual variables
function AggregateImportance(listOfVarImportance){
listOfVarImportance = ee.List(listOfVarImportance); //A list of dictionaries
// listOfVariables = ee.Dictionary(listOfVariables); //This should be a list of strings
//Map through dictionary where each key is a variable and value is the corresponding variable importance
//Using keys from the first dictionary in the list - they each have the same keys
//Replace value with a list of features, each containing VI property for that variable
//e.g.: B2: [0.7, 0.6, 0.7, 0.5, 0.5, ...] this will be aggregated to obtain mean VI
var keyValue = ee.Dictionary(ee.Dictionary(listOfVarImportance.get(0)).get("importance"));
var VIFromList = keyValue.map(function(key, value){
var variable = key; //The variable being assessed, from list of bands
//Map through list of variable importance dictionaries
var listOfVI = listOfVarImportance.map(function(currentImportance){
currentImportance = ee.Dictionary(currentImportance);
var allVI = ee.Dictionary(currentImportance.get("importance")); //Dictionary of every variable's importance for this iteration
var thisImportance = allVI.get(variable); //Get the importance of each variable independantly
var viFeature = ee.Feature(null, {"VariableImportance": thisImportance});
return viFeature;
});
var viFC = ee.FeatureCollection(listOfVI);
var stats = viFC.aggregate_stats("VariableImportance");
return stats;
});
return VIFromList;
}
// Mean spectra per district
// Input Feature Collection of districts and bands, return FC of means
// Image must include tailings
function MeanDistrictTailingsSpectra(districtsFC, image, bands){
districtsFC = ee.FeatureCollection(districtsFC);
image = ee.Image(image);
// Remove SCL. Could remove tailings as well, but it works for QC
bands = ee.List(bands).filter(ee.Filter.inList("item", ["SCL"]).not());
// Mask by tailings pixels
var tailingsImage = image.updateMask(image.select("tailings").eq(1));
// Sample from each district
var spectralMeans = districtsFC.map(function(district){
district = ee.Feature(district);
var districtName = district.get("ShortName");
var districtSample = tailingsImage.sample({
region: district.geometry(),
scale: scale,
projection: projection,
factor: 0.1,
dropNulls: true});
// Get mean for each band as features
var bandMeans = bands.map(function(band){
band = ee.String(band);
var bandMean = districtSample.aggregate_mean(band);
return bandMean;
});
// Combine bands and band means into a dictionary
var featureProperties = ee.Dictionary.fromLists(bands, bandMeans)
.set("District", districtName); //Add district name
// Return feature collection from list of features
return ee.Feature(null, featureProperties);
});
return spectralMeans;
}
/*********************************************
* *
* Indices *
* *
*********************************************/
{
var indicesImages = ee.List([]);
//Add indices generated for masking to indices list
var indices = ee.List(["ndvi", "mndwi"]);
indicesImages = ee.List(indicesImages).add(ndvi).add(mndwi);
//////////////////////////////////////////////
// Ferric Iron //
//////////////////////////////////////////////
//Defined in Mielke et. al. (2014) as "SWIR2 / NIR + RED / GREEN"
if(scale == 20){
var fii = trainingImage.expression(
"fii = b('B12') / b('B8') + b('B4') / b('B3')");
indicesImages = indicesImages.add(fii);
indices = ee.List(indices).add("fii");
// Map.addLayer(fii, {palette: ["white", "brown"]}, "Ferric Iron Index");
}
//////////////////////////////////////////////
// IFD //
//////////////////////////////////////////////
//Iron feature depth is designed for HS images
if(scale == 20){
var rInt = trainingImage.expression(
"rInt = b('B4') + (b('B12') - b('B4')) * (835 - 665) / (1640 - 665)");
//We don't want to keep rInt once IFD is calculated
var ifdTrainingIntermediate = trainingImage.addBands(rInt);
var ifd = ifdTrainingIntermediate.expression(
"ifd = b('rInt') - b('B8A')");
//Only values above 0 are meaningful
// var ifdMax = ee.Dictionary(ifd.reduceRegion({reducer: ee.Reducer.max(), bestEffort: true})).get("ifd");
// var ifdMaxImage = ee.Image.constant(ifdMax);
// ifd = ifd.updateMask(ifd.gte(0)).divide(ifdMaxImage).multiply(ee.Image(100*100));
// var ifdSample = ifd.sample({numPixels: 10000, dropNulls: true});
// var ifdHisto = ui.Chart.feature.histogram(ifdSample, "ifd");
indicesImages = indicesImages.add(ifd);
indices = ee.List(indices).add("ifd");
// Map.addLayer(ifd, {min: 0, max: 100, palette: ["white", "blue"]}, "IFD");
}
//////////////////////////////////////////////
// Coordinates //
//////////////////////////////////////////////
// var coordinateImage = ee.Image.pixelLonLat();
// indicesImages = indicesImages.add(coordinateImage);
// indices = indices.add("longitude").add("latitude");
//////////////////////////////////////////////
// Combine all Used Indices //
//////////////////////////////////////////////
//Add each index image as a band to training image
if(includeIndices && !useBestBands){
// print(indices);
bands = ee.List(bands).cat(indices);
} else if(useBestBands){
indices = indices.filter(ee.Filter.inList("item", bestBands));
}
trainingImage = ee.Image(indicesImages.iterate(function(img, lastImg){
img = ee.Image(img);
lastImg = ee.Image(lastImg);
return lastImg.addBands(img);
}, trainingImage));
}
//////////////////////////////////////////////
// Distribution of Band Values //
//////////////////////////////////////////////
// Values of masked pixels
// var maskedSample = trainingImage.addBands(maskedTailings.rename("maskedTailings")).stratifiedSample({
// numPoints: 0,
// classBand: "maskedTailings",
// classValues: [0,1],
// classPoints: [0, 5000],
// dropNulls: true,
// scale: scale
// });
/*********************************************
* *
* Set Training/Testing Districts *
* *
*********************************************/
///////////////////////////////////////////////
// Filter Districts Without Tailings //
///////////////////////////////////////////////
//Create district variable from table asset
var districts = ee.FeatureCollection(districtPoints);
//Remove Moose River, which is significantly different from database record
districts = districts.filter(ee.Filter.neq("ShortName", "Moose River"));
//Filter to only gold districts that contain a certain area of tailings
function filterDistrictsByTailings(region, districts, tailingsMask, scale){
//Filter to tailings within target region
var regionTailings = tailings.filterBounds(region);
//Buffer all district points by distance set in Misc. Variable section above
var districtBuffer = ee.FeatureCollection(districts.map(function(f){
var bufferedDistrict = f.buffer(districtBufferDistance);
//Sample every masked tailings pixel, then get count of sample features
var tailingsCount = ee.FeatureCollection(maskedTailings.selfMask().sample({
region: bufferedDistrict.geometry(),
scale: scale,
projection: projection,
factor: 1,
dropNulls: true,
geometries: true
})).size();
return bufferedDistrict.set("TailingsPixelCount", tailingsCount);
})
);
//Keep districts with at least 5 masked tailings pixels
var districtsWithTailings = districtBuffer.filter(ee.Filter.gte("TailingsPixelCount", 5));
return ee.FeatureCollection(districtsWithTailings);
}
var districtsFiltered = filterDistrictsByTailings(trainingImage.geometry(), districts, maskedTailings, scale);
//Build dictionary of districts for storing stats
if(randomDistricts){
var districtNames = districtsFiltered.toList(districtsFiltered.size())
.map(function(f){return ee.Feature(f).get("ShortName")});
//Create dictionary of dictionaries to later be filled with stats. Empty for now.
var districtStats = ee.Dictionary.fromLists(districtNames, ee.List.repeat(ee.Dictionary({}), districtNames.size()));
} else{
//Create dictionary of dictionaries to later be filled with stats. Empty for now.
var districtStats = ee.Dictionary.fromLists(targetDistrictNames, ee.List.repeat(ee.Dictionary({}), targetDistrictNames.size()));
}
// print(districtsFiltered.filterBounds(trainingImage.geometry()));
///////////////////////////////////////////////
// Split Districts Into Training/Testing //
///////////////////////////////////////////////
//Sorts districts into training and testing, then creates a buffered area around them
function subsetDistricts(districtsCleared, scale, seed){
//For quickly changing the train/test ratio
var split = ee.Number(0.8);
//Add random column to district features and buffer
//Creates a training set containing 80% of districts and testing set with 20%
var districtsRand = districtsCleared.randomColumn("random", seed)
.sort("random");
var districtsCount = ee.Number(districtsRand.size()); //Size of districts collection
var districtList = districtsRand.toList(districtsCount); //Create list to call by index
//Set an index to each feature, ranked by random value
var districtsOrdered = ee.List.sequence({start: 0, count: districtsCount}).map(function(n){
var feature = ee.Feature(districtList.get(n));
return feature.set("index", n);
});
districtsOrdered = ee.FeatureCollection(districtsOrdered);
//Split districts based on number of input districts, rather than actual random value, to maintain consistent # of sample areas
var trainingSplit = ee.Number(districtsCount.multiply(split)).floor();
//Create district buffer, split into training and validation
return ee.Dictionary({
training: ee.FeatureCollection(districtsOrdered
.filter(ee.Filter.lt("index", trainingSplit))),
testing: ee.FeatureCollection(districtsOrdered
.filter(ee.Filter.gte("index", trainingSplit)))
});
}
//Test Subsets
// print(districtSubset.get("training"), "Training Districts");
// print(districtSubset.get("testing"), "Testing Districts");
// Map.addLayer(ee.FeatureCollection(districtSubset.get("training")), {color: "blue"}, "Training Districts");
// Map.addLayer(ee.FeatureCollection(districtSubset.get("testing")), {color: "red"}, "Testing Districts");
/*********************************************
* *
* Collect Sample Points *
* *
*********************************************/
//Add masked tailings and indices
//Tailings band is used for stratified sampling. It is removed before classifier is trained.
trainingImage = trainingImage.addBands(maskedTailings, ["tailings"]);
bands = ee.List(bands).add("tailings");
//If not using spectral bands, they are removed here (set up top)
if(!includeBands && scale == 20){
bands = bands.removeAll(bands20).add("SCL"); //SCL has to be re-added
} else if(!includeBands && scale == 10){
bands = bands.removeAll(bands10).add("SCL");
}
//If using set districts, the following is run once.
//Otherwise, it's run in the subset loop further below
if(!randomDistricts){
var targetDistricts = districtsFiltered.filter(ee.Filter.inList("ShortName", targetDistrictNames));
var nonTargetDistricts = districtsFiltered.filter(ee.Filter.inList("ShortName", targetDistrictNames).not());
var districtSubset = ee.Dictionary({training: nonTargetDistricts, testing: targetDistricts});
var testingVector = targetDistricts;
}
//Show districts being tested
// Map.addLayer(testingVector.style({color: "grey", fillColor: "00000000"}), null, "Testing Areas");
//Sample Points Quantities
var totalSamplePoints = ee.Number(totalSamplePoints); //Set up top
//Set sample points in each land cover class according to their areal proportion
var classVals = ee.List.sequence(2, 9);
/*Classes of interest in S2's land cover band:
// 2 Dark Area Pixels
// 3 Cloud Shadows
// 4 Vegetation
// 5 Bare Soils
// 6 Water
// 7 Clouds Low Probability / Unclassified
// 8 Clouds Medium Probability
// 9 Clouds High Probability
*/
var pixelCount = classVals.map(function(n){ //Loop through each class value and count number of pixels
//Return a list of the number of pixels in each class
return trainingImage.select("SCL").eq(ee.Number(n))
.selfMask()
.reduceRegion({reducer: ee.Reducer.count(), scale: scale, maxPixels: 1e12})
.get("SCL");
});
var totalPixels = ee.Number(pixelCount.reduce(ee.Reducer.sum())); //Sum of non-null pixels in training image
//Produce list of counts for each SCL value, based on proportion of pixels in image per class
var nonTailingsClassPoints = pixelCount.map(function(n){
n = ee.Number(n); //n = total pixels per SCL label
return ee.Number(n
.divide(totalPixels) //Get ratio of pixels in each class to total image pixels
.multiply(totalSamplePoints) //Make proportional samples match the total number of samples
.floor() //Convert to int, rounding down
.add(25)); //Add base number to ensure under-represented classes are still sampled. If there are fewer than n pixels, all are sampled.
});
// var classPoints = [100, 25, 500, 50, 100, 50, 25, 25];
// //[287,250,3495,298,1897,268,251,250]
// print(classPoints, "Class Points");
//Collect training points. Training image and training area (districts) are set above and don't change
function CollectTrainingPoints(bands, seed, districtSubsetDictionary){
//Set to true for illustrating sample point collection, debugging, or exporting
var drawGeometries = true;
//Unpack district subset dictionary
districtSubsetDictionary = ee.Dictionary(districtSubsetDictionary);
var trainingVector = ee.FeatureCollection(districtSubsetDictionary.get("training"));
var testingVector = ee.FeatureCollection(districtSubsetDictionary.get("testing"));
var allDistricts = ee.FeatureCollection(trainingVector.merge(testingVector));
//Sample non-tailings outside of districts
var nonTailingsTrainingRegion = trainingImage.geometry().difference(allDistricts);
//Collect Points
//Non-tailings points are stratified by S2's land classification band
var nonTailingsSample = trainingImage.stratifiedSample({
numPoints: 0, //Overridden by classPoints
region: nonTailingsTrainingRegion,
scale: scale,
seed: seed,
classBand: "SCL",
classValues: classVals, //Which SCL labels to use. Set above
classPoints: nonTailingsClassPoints, //How many points per class
dropNulls: true,
geometries: drawGeometries
});
//Sample tailings from everywhere except test areas
var tailingsTrainingRegion = trainingImage.geometry().difference(testingVector);
//Adjust sample count by total available pixels
//Take 2/3 of tailings pixels
var tailingsPoints = GetSampleCount(maskedTailings.selfMask(), tailingsTrainingRegion, 0.33);
var tailingsClassPoints = ee.List([0, tailingsPoints]);
var tailingsSample = trainingImage.stratifiedSample({
numPoints: 0, //Overridden by classPoints
region: tailingsTrainingRegion,
scale: scale,
seed: seed,
classBand: "tailings",
classValues: [0,1],
classPoints: tailingsClassPoints,
dropNulls: true,
geometries: drawGeometries
});
return ee.Dictionary({nonTailingsPoints: nonTailingsSample,
tailingsPoints: tailingsSample,
seed: seed,
districtVector: testingVector});
}
///////////////////////////////////////////////
// Create List of Sample Points //
///////////////////////////////////////////////
var sampleBands = ee.List(bands).remove("SCL");
//We want to classify points many times and collect accuracy as we go. Create as many points as "iterations" the model is to be run
//Use district subset from list set above
var samplePointsSetFixed = ee.List.sequence({start: 0, count: iterations}).map(function(n){
n = ee.Number(n);
//Get seed from earlier seedlist using n
var sampleSeed = seedList.get(n);
//Add sample selection to list
return CollectTrainingPoints(sampleBands, sampleSeed, districtSubset);
});
//Use new, random districts each time samples are collected
var samplePointsSetRandom = ee.List.sequence({start: 0, count: iterations}).map(function(n){
n = ee.Number(n);
//Get seed from earlier seedlist using n
var sampleSeed = seedList.get(n);
//Get new district subset each iteration
var districtSubset = subsetDistricts(districtsFiltered, scale, sampleSeed);
//Add samples to list
return CollectTrainingPoints(sampleBands, sampleSeed, districtSubset);
});
//Based on boolean variable set at top, one of these will be called
if(!randomDistricts){
var samplePointsSet = samplePointsSetFixed;
} else{
var samplePointsSet = samplePointsSetRandom;
}
//Some tests to show that each list entry is unique (turn on geometries in sample function)
// Map.addLayer(ee.FeatureCollection(ee.Dictionary(ee.List(samplePointsSet).get(0)).get("nonTailingsPoints")), {color: "red"}, "Non tailings points", false);
// Map.addLayer(ee.FeatureCollection(ee.Dictionary(ee.List(samplePointsSet).get(0)).get("tailingsPoints")), {color: "green"}, "Training points", false);
// Map.addLayer(ee.FeatureCollection(ee.Dictionary(ee.List(samplePointsSet).get(9)).get("nonTailingsPoints")), {color: "blue"}, "Non training points");
// Map.addLayer(ee.FeatureCollection(ee.Dictionary(ee.List(samplePointsSet).get(9)).get("tailingsPoints")), {color: "yellow"}, "Training points");
/*********************************************
* *
* Classification *
* *
*********************************************/
//Section includes accuracy assessment tools, variable importance ranking tools,
//and final classification
//This is set up top. Classify full image or just test areas.
// if(fullMap){
// var classificationArea = trainingImage;
// }else{
// var classificationArea = testingArea;
// }
/////////////////////////////////////////////
// Accuracy Assessment Function //
/////////////////////////////////////////////
//Each image has a classification and a tailings band
//Sample all tailings in reference layer and see if they were classified - recall
//Sample all classified tailings and see if they're marked as tailings in reference band - precision
//Calculate FScore of classified image. Set b at top
function GetAccuracy(img, b, testVector){
b = ee.Number(b); //Adjust the precision/recall weight for F Score. 1 = equal weight. 2 = recall is twice as important as precision
img = ee.Image(img);
//Sample count will adjust based total pixels
//Sample portion of tailings
var tailingsCount = GetSampleCount(img.select("tailings").selfMask(), testingVector, 0.5);
//Sample portion of positive classified pixels
var classifiedCount = GetSampleCount(img.select("classification").selfMask(), testingVector, 0.5);
var tailingsSampleCount = ee.List([tailingsCount.multiply(2), tailingsCount]);
var classifiedSampleCount = ee.List([tailingsCount.multiply(2), tailingsCount]);
//Sample from both reference ("tailings") and classified pixels, then combine. Ensures sampling of FP and FN
var samplePointsTailings = img.stratifiedSample({
numPoints: 0, //Overridden by classPoints
classBand: "tailings",
region: testingVector,
scale: scale,
projection: projection,
seed: seedList.shuffle().get(0),
classValues: [0,1],
classPoints: tailingsSampleCount,
dropNulls: true,
geometries: false
});
var samplePointsClassification = img.stratifiedSample({
numPoints: 0, //Overridden by classPoints
classBand: "classification",
region: testingVector,
scale: scale,
projection: projection,
seed: seedList.shuffle().get(0),
classValues: [0,1],
classPoints: classifiedSampleCount,
dropNulls: true,
geometries: false
})
.filter(ee.Filter.bounds(samplePointsTailings, 1).not()); //Pixels that were sampled twice are removed by filter before merging.
var samplePoints = samplePointsTailings.merge(samplePointsClassification); //Combine sample sets
//Generate error matrix as an array
var errorMatrix = samplePoints.errorMatrix("tailings", "classification").array();
//Unpack error matrix to calculate variables for f1-score
//True positives
var TP = errorMatrix.get([1,1]);
//False negatives
var FN = errorMatrix.get([1,0]);
//False positives
var FP = errorMatrix.get([0,1]);
//Recall is correct classifications out of total positives in reference data (i.e., masked tailings pixels)
var recall = ee.Number.expression("TP / (TP + FN)", {"TP": TP, "FN": FN});
//Precision is correct classifications out of total positive classifications
var precision = ee.Number.expression("TP / (TP + FP)", {"TP" : TP, "FP": FP});
//F-Score
var fScore = ee.Number.expression("(1 + b**2) * ((recall * precision) / ((b**2 * precision) + recall))",
{"b": b, "recall": recall, "precision": precision});
return ee.Dictionary({fScore: fScore, recall: recall, precision: precision});
}
///////////////////////////////////////////////
// Variable Importance //
///////////////////////////////////////////////
//Variables to be tested, one at a time
var variableTestList = bands.removeAll(["SCL", "tailings", "B8"]); //Remove bands that aren't used for training/testing
if(!includeIndices){
variableTestList = variableTestList.removeAll(indices);
}
if(!includeBands){
variableTestList = variableTestList.removeAll(bands);
}
// print(variableTestList, "Variables Used in Classifier");
//Importance List
//Generate variable importance for each sample point set
var variableImportanceList = samplePointsSet.map(function(points){
var importance = GetVariableImportance(points, variableTestList);
return importance;
});
//Loop through all variables being used and aggregate stats for each
var importanceStats = AggregateImportance(variableImportanceList);
// print("Importance Stats", importanceStats);
//Create list of lists by combining variables with aggregated stats
var importanceKeys = importanceStats.keys();
var importanceValues = importanceStats.values().map(function(dict){return ee.Dictionary(dict).get("mean")});
var importanceList = importanceKeys.zip(importanceValues);
//Convert to FC by mapping through list of lists
var importanceFC = ee.FeatureCollection(importanceList.map(function(list){
list = ee.List(list);
var variable = list.get(0);
var mean = list.get(1);