deepgee 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepgee/__init__.py +36 -0
- deepgee/auth.py +154 -0
- deepgee/data.py +413 -0
- deepgee/models.py +491 -0
- deepgee/utils.py +401 -0
- deepgee-0.1.0.dist-info/METADATA +325 -0
- deepgee-0.1.0.dist-info/RECORD +10 -0
- deepgee-0.1.0.dist-info/WHEEL +5 -0
- deepgee-0.1.0.dist-info/licenses/LICENSE +21 -0
- deepgee-0.1.0.dist-info/top_level.txt +1 -0
deepgee/models.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deep learning models for Earth observation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from typing import Optional, List, Tuple, Dict, Union
|
|
8
|
+
from sklearn.model_selection import train_test_split
|
|
9
|
+
from sklearn.preprocessing import StandardScaler
|
|
10
|
+
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, cohen_kappa_score
|
|
11
|
+
import joblib
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import tensorflow as tf
|
|
15
|
+
from tensorflow import keras
|
|
16
|
+
TF_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
TF_AVAILABLE = False
|
|
19
|
+
print("Warning: TensorFlow not available. Install with: pip install tensorflow")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LandCoverClassifier:
|
|
23
|
+
"""
|
|
24
|
+
Deep learning classifier for land cover classification.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
n_classes: int,
|
|
30
|
+
architecture: str = 'dense',
|
|
31
|
+
input_shape: Optional[Tuple[int]] = None
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize land cover classifier.
|
|
35
|
+
|
|
36
|
+
Parameters:
|
|
37
|
+
-----------
|
|
38
|
+
n_classes : int
|
|
39
|
+
Number of land cover classes
|
|
40
|
+
architecture : str
|
|
41
|
+
Model architecture: 'dense', 'cnn1d', 'simple'
|
|
42
|
+
input_shape : tuple, optional
|
|
43
|
+
Input shape (n_features,) for dense, (n_features, 1) for cnn1d
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
---------
|
|
47
|
+
>>> classifier = LandCoverClassifier(n_classes=9, architecture='dense')
|
|
48
|
+
>>> classifier.build_model(input_shape=(14,))
|
|
49
|
+
"""
|
|
50
|
+
if not TF_AVAILABLE:
|
|
51
|
+
raise ImportError("TensorFlow is required. Install with: pip install tensorflow")
|
|
52
|
+
|
|
53
|
+
self.n_classes = n_classes
|
|
54
|
+
self.architecture = architecture
|
|
55
|
+
self.input_shape = input_shape
|
|
56
|
+
self.model = None
|
|
57
|
+
self.scaler = StandardScaler()
|
|
58
|
+
self.history = None
|
|
59
|
+
self.class_names = None
|
|
60
|
+
|
|
61
|
+
def build_model(self, input_shape: Optional[Tuple[int]] = None) -> keras.Model:
|
|
62
|
+
"""
|
|
63
|
+
Build the neural network model.
|
|
64
|
+
|
|
65
|
+
Parameters:
|
|
66
|
+
-----------
|
|
67
|
+
input_shape : tuple, optional
|
|
68
|
+
Input shape
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
--------
|
|
72
|
+
keras.Model : Built model
|
|
73
|
+
"""
|
|
74
|
+
if input_shape is not None:
|
|
75
|
+
self.input_shape = input_shape
|
|
76
|
+
|
|
77
|
+
if self.input_shape is None:
|
|
78
|
+
raise ValueError("input_shape must be provided")
|
|
79
|
+
|
|
80
|
+
if self.architecture == 'dense':
|
|
81
|
+
self.model = self._build_dense_model()
|
|
82
|
+
elif self.architecture == 'cnn1d':
|
|
83
|
+
self.model = self._build_cnn1d_model()
|
|
84
|
+
elif self.architecture == 'simple':
|
|
85
|
+
self.model = self._build_simple_model()
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"Unknown architecture: {self.architecture}")
|
|
88
|
+
|
|
89
|
+
return self.model
|
|
90
|
+
|
|
91
|
+
def _build_dense_model(self) -> keras.Model:
|
|
92
|
+
"""Build dense neural network."""
|
|
93
|
+
model = keras.Sequential([
|
|
94
|
+
keras.layers.Dense(128, activation='relu', input_shape=self.input_shape),
|
|
95
|
+
keras.layers.BatchNormalization(),
|
|
96
|
+
keras.layers.Dropout(0.3),
|
|
97
|
+
|
|
98
|
+
keras.layers.Dense(64, activation='relu'),
|
|
99
|
+
keras.layers.BatchNormalization(),
|
|
100
|
+
keras.layers.Dropout(0.3),
|
|
101
|
+
|
|
102
|
+
keras.layers.Dense(32, activation='relu'),
|
|
103
|
+
keras.layers.BatchNormalization(),
|
|
104
|
+
keras.layers.Dropout(0.2),
|
|
105
|
+
|
|
106
|
+
keras.layers.Dense(self.n_classes, activation='softmax')
|
|
107
|
+
], name='DenseClassifier')
|
|
108
|
+
|
|
109
|
+
model.compile(
|
|
110
|
+
optimizer=keras.optimizers.Adam(learning_rate=0.001),
|
|
111
|
+
loss='sparse_categorical_crossentropy',
|
|
112
|
+
metrics=['accuracy']
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return model
|
|
116
|
+
|
|
117
|
+
def _build_cnn1d_model(self) -> keras.Model:
|
|
118
|
+
"""Build 1D CNN model."""
|
|
119
|
+
model = keras.Sequential([
|
|
120
|
+
keras.layers.Conv1D(64, 3, activation='relu', input_shape=self.input_shape),
|
|
121
|
+
keras.layers.BatchNormalization(),
|
|
122
|
+
keras.layers.MaxPooling1D(2),
|
|
123
|
+
keras.layers.Dropout(0.3),
|
|
124
|
+
|
|
125
|
+
keras.layers.Conv1D(32, 3, activation='relu'),
|
|
126
|
+
keras.layers.BatchNormalization(),
|
|
127
|
+
keras.layers.GlobalMaxPooling1D(),
|
|
128
|
+
keras.layers.Dropout(0.3),
|
|
129
|
+
|
|
130
|
+
keras.layers.Dense(64, activation='relu'),
|
|
131
|
+
keras.layers.Dropout(0.2),
|
|
132
|
+
keras.layers.Dense(self.n_classes, activation='softmax')
|
|
133
|
+
], name='CNN1DClassifier')
|
|
134
|
+
|
|
135
|
+
model.compile(
|
|
136
|
+
optimizer='adam',
|
|
137
|
+
loss='sparse_categorical_crossentropy',
|
|
138
|
+
metrics=['accuracy']
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return model
|
|
142
|
+
|
|
143
|
+
def _build_simple_model(self) -> keras.Model:
|
|
144
|
+
"""Build simple neural network."""
|
|
145
|
+
model = keras.Sequential([
|
|
146
|
+
keras.layers.Dense(64, activation='relu', input_shape=self.input_shape),
|
|
147
|
+
keras.layers.Dropout(0.3),
|
|
148
|
+
keras.layers.Dense(32, activation='relu'),
|
|
149
|
+
keras.layers.Dropout(0.2),
|
|
150
|
+
keras.layers.Dense(self.n_classes, activation='softmax')
|
|
151
|
+
], name='SimpleClassifier')
|
|
152
|
+
|
|
153
|
+
model.compile(
|
|
154
|
+
optimizer='adam',
|
|
155
|
+
loss='sparse_categorical_crossentropy',
|
|
156
|
+
metrics=['accuracy']
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return model
|
|
160
|
+
|
|
161
|
+
def prepare_data(
|
|
162
|
+
self,
|
|
163
|
+
X: np.ndarray,
|
|
164
|
+
y: np.ndarray,
|
|
165
|
+
test_size: float = 0.2,
|
|
166
|
+
random_state: int = 42,
|
|
167
|
+
normalize: bool = True
|
|
168
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
169
|
+
"""
|
|
170
|
+
Prepare data for training.
|
|
171
|
+
|
|
172
|
+
Parameters:
|
|
173
|
+
-----------
|
|
174
|
+
X : np.ndarray
|
|
175
|
+
Features
|
|
176
|
+
y : np.ndarray
|
|
177
|
+
Labels
|
|
178
|
+
test_size : float
|
|
179
|
+
Test set proportion
|
|
180
|
+
random_state : int
|
|
181
|
+
Random seed
|
|
182
|
+
normalize : bool
|
|
183
|
+
Normalize features
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
--------
|
|
187
|
+
tuple : X_train, X_test, y_train, y_test
|
|
188
|
+
"""
|
|
189
|
+
# Split data
|
|
190
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
191
|
+
X, y, test_size=test_size, random_state=random_state, stratify=y
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Normalize
|
|
195
|
+
if normalize:
|
|
196
|
+
X_train = self.scaler.fit_transform(X_train)
|
|
197
|
+
X_test = self.scaler.transform(X_test)
|
|
198
|
+
|
|
199
|
+
# Reshape for CNN if needed
|
|
200
|
+
if self.architecture == 'cnn1d':
|
|
201
|
+
X_train = X_train.reshape(-1, X_train.shape[1], 1)
|
|
202
|
+
X_test = X_test.reshape(-1, X_test.shape[1], 1)
|
|
203
|
+
|
|
204
|
+
return X_train, X_test, y_train, y_test
|
|
205
|
+
|
|
206
|
+
def train(
|
|
207
|
+
self,
|
|
208
|
+
X_train: np.ndarray,
|
|
209
|
+
y_train: np.ndarray,
|
|
210
|
+
validation_split: float = 0.2,
|
|
211
|
+
epochs: int = 100,
|
|
212
|
+
batch_size: int = 64,
|
|
213
|
+
callbacks: Optional[List] = None,
|
|
214
|
+
verbose: int = 1
|
|
215
|
+
) -> keras.callbacks.History:
|
|
216
|
+
"""
|
|
217
|
+
Train the model.
|
|
218
|
+
|
|
219
|
+
Parameters:
|
|
220
|
+
-----------
|
|
221
|
+
X_train : np.ndarray
|
|
222
|
+
Training features
|
|
223
|
+
y_train : np.ndarray
|
|
224
|
+
Training labels
|
|
225
|
+
validation_split : float
|
|
226
|
+
Validation split proportion
|
|
227
|
+
epochs : int
|
|
228
|
+
Number of epochs
|
|
229
|
+
batch_size : int
|
|
230
|
+
Batch size
|
|
231
|
+
callbacks : list, optional
|
|
232
|
+
Keras callbacks
|
|
233
|
+
verbose : int
|
|
234
|
+
Verbosity level
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
--------
|
|
238
|
+
History : Training history
|
|
239
|
+
|
|
240
|
+
Examples:
|
|
241
|
+
---------
|
|
242
|
+
>>> classifier.train(X_train, y_train, epochs=50)
|
|
243
|
+
"""
|
|
244
|
+
if self.model is None:
|
|
245
|
+
raise ValueError("Model not built. Call build_model() first.")
|
|
246
|
+
|
|
247
|
+
if callbacks is None:
|
|
248
|
+
callbacks = [
|
|
249
|
+
keras.callbacks.EarlyStopping(
|
|
250
|
+
monitor='val_loss',
|
|
251
|
+
patience=10,
|
|
252
|
+
restore_best_weights=True,
|
|
253
|
+
verbose=1
|
|
254
|
+
),
|
|
255
|
+
keras.callbacks.ReduceLROnPlateau(
|
|
256
|
+
monitor='val_loss',
|
|
257
|
+
factor=0.5,
|
|
258
|
+
patience=5,
|
|
259
|
+
min_lr=1e-7,
|
|
260
|
+
verbose=1
|
|
261
|
+
)
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
self.history = self.model.fit(
|
|
265
|
+
X_train, y_train,
|
|
266
|
+
validation_split=validation_split,
|
|
267
|
+
epochs=epochs,
|
|
268
|
+
batch_size=batch_size,
|
|
269
|
+
callbacks=callbacks,
|
|
270
|
+
verbose=verbose
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return self.history
|
|
274
|
+
|
|
275
|
+
def evaluate(
|
|
276
|
+
self,
|
|
277
|
+
X_test: np.ndarray,
|
|
278
|
+
y_test: np.ndarray,
|
|
279
|
+
class_names: Optional[List[str]] = None
|
|
280
|
+
) -> Dict:
|
|
281
|
+
"""
|
|
282
|
+
Evaluate the model.
|
|
283
|
+
|
|
284
|
+
Parameters:
|
|
285
|
+
-----------
|
|
286
|
+
X_test : np.ndarray
|
|
287
|
+
Test features
|
|
288
|
+
y_test : np.ndarray
|
|
289
|
+
Test labels
|
|
290
|
+
class_names : list, optional
|
|
291
|
+
Class names for report
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
--------
|
|
295
|
+
dict : Evaluation metrics
|
|
296
|
+
"""
|
|
297
|
+
if self.model is None:
|
|
298
|
+
raise ValueError("Model not trained")
|
|
299
|
+
|
|
300
|
+
# Predictions
|
|
301
|
+
y_pred_proba = self.model.predict(X_test, verbose=0)
|
|
302
|
+
y_pred = y_pred_proba.argmax(axis=1)
|
|
303
|
+
|
|
304
|
+
# Metrics
|
|
305
|
+
accuracy = accuracy_score(y_test, y_pred)
|
|
306
|
+
kappa = cohen_kappa_score(y_test, y_pred)
|
|
307
|
+
cm = confusion_matrix(y_test, y_pred)
|
|
308
|
+
|
|
309
|
+
results = {
|
|
310
|
+
'accuracy': accuracy,
|
|
311
|
+
'kappa': kappa,
|
|
312
|
+
'confusion_matrix': cm,
|
|
313
|
+
'predictions': y_pred,
|
|
314
|
+
'probabilities': y_pred_proba
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if class_names is not None:
|
|
318
|
+
self.class_names = class_names
|
|
319
|
+
results['classification_report'] = classification_report(
|
|
320
|
+
y_test, y_pred, target_names=class_names
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return results
|
|
324
|
+
|
|
325
|
+
def predict(
|
|
326
|
+
self,
|
|
327
|
+
X: np.ndarray,
|
|
328
|
+
normalize: bool = True,
|
|
329
|
+
batch_size: int = 10000
|
|
330
|
+
) -> np.ndarray:
|
|
331
|
+
"""
|
|
332
|
+
Make predictions on new data.
|
|
333
|
+
|
|
334
|
+
Parameters:
|
|
335
|
+
-----------
|
|
336
|
+
X : np.ndarray
|
|
337
|
+
Features to predict
|
|
338
|
+
normalize : bool
|
|
339
|
+
Normalize features using fitted scaler
|
|
340
|
+
batch_size : int
|
|
341
|
+
Batch size for prediction
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
--------
|
|
345
|
+
np.ndarray : Predicted class labels
|
|
346
|
+
"""
|
|
347
|
+
if self.model is None:
|
|
348
|
+
raise ValueError("Model not trained")
|
|
349
|
+
|
|
350
|
+
# Normalize
|
|
351
|
+
if normalize:
|
|
352
|
+
X = self.scaler.transform(X)
|
|
353
|
+
|
|
354
|
+
# Reshape for CNN if needed
|
|
355
|
+
if self.architecture == 'cnn1d':
|
|
356
|
+
X = X.reshape(-1, X.shape[1], 1)
|
|
357
|
+
|
|
358
|
+
# Predict in batches
|
|
359
|
+
predictions = []
|
|
360
|
+
for i in range(0, len(X), batch_size):
|
|
361
|
+
batch = X[i:i+batch_size]
|
|
362
|
+
pred = self.model.predict(batch, verbose=0)
|
|
363
|
+
predictions.append(pred)
|
|
364
|
+
|
|
365
|
+
predictions = np.concatenate(predictions, axis=0)
|
|
366
|
+
return predictions.argmax(axis=1)
|
|
367
|
+
|
|
368
|
+
def save(self, model_path: str, scaler_path: str) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Save model and scaler.
|
|
371
|
+
|
|
372
|
+
Parameters:
|
|
373
|
+
-----------
|
|
374
|
+
model_path : str
|
|
375
|
+
Path to save model (.h5 or .keras)
|
|
376
|
+
scaler_path : str
|
|
377
|
+
Path to save scaler (.pkl)
|
|
378
|
+
"""
|
|
379
|
+
if self.model is None:
|
|
380
|
+
raise ValueError("No model to save")
|
|
381
|
+
|
|
382
|
+
self.model.save(model_path)
|
|
383
|
+
joblib.dump(self.scaler, scaler_path)
|
|
384
|
+
print(f"✓ Model saved to: {model_path}")
|
|
385
|
+
print(f"✓ Scaler saved to: {scaler_path}")
|
|
386
|
+
|
|
387
|
+
def load(self, model_path: str, scaler_path: str) -> None:
|
|
388
|
+
"""
|
|
389
|
+
Load model and scaler.
|
|
390
|
+
|
|
391
|
+
Parameters:
|
|
392
|
+
-----------
|
|
393
|
+
model_path : str
|
|
394
|
+
Path to model file
|
|
395
|
+
scaler_path : str
|
|
396
|
+
Path to scaler file
|
|
397
|
+
"""
|
|
398
|
+
self.model = keras.models.load_model(model_path)
|
|
399
|
+
self.scaler = joblib.load(scaler_path)
|
|
400
|
+
print(f"✓ Model loaded from: {model_path}")
|
|
401
|
+
print(f"✓ Scaler loaded from: {scaler_path}")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class ChangeDetector:
|
|
405
|
+
"""
|
|
406
|
+
Change detection using deep learning.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
def __init__(self, method: str = 'difference'):
|
|
410
|
+
"""
|
|
411
|
+
Initialize change detector.
|
|
412
|
+
|
|
413
|
+
Parameters:
|
|
414
|
+
-----------
|
|
415
|
+
method : str
|
|
416
|
+
Detection method: 'difference', 'ratio', 'pca', 'neural'
|
|
417
|
+
"""
|
|
418
|
+
self.method = method
|
|
419
|
+
self.model = None
|
|
420
|
+
|
|
421
|
+
def detect_changes(
|
|
422
|
+
self,
|
|
423
|
+
image1: np.ndarray,
|
|
424
|
+
image2: np.ndarray,
|
|
425
|
+
threshold: Optional[float] = None
|
|
426
|
+
) -> np.ndarray:
|
|
427
|
+
"""
|
|
428
|
+
Detect changes between two images.
|
|
429
|
+
|
|
430
|
+
Parameters:
|
|
431
|
+
-----------
|
|
432
|
+
image1 : np.ndarray
|
|
433
|
+
First image (time 1)
|
|
434
|
+
image2 : np.ndarray
|
|
435
|
+
Second image (time 2)
|
|
436
|
+
threshold : float, optional
|
|
437
|
+
Change threshold
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
--------
|
|
441
|
+
np.ndarray : Binary change map
|
|
442
|
+
"""
|
|
443
|
+
if self.method == 'difference':
|
|
444
|
+
change = np.abs(image2 - image1)
|
|
445
|
+
elif self.method == 'ratio':
|
|
446
|
+
change = image2 / (image1 + 1e-10)
|
|
447
|
+
else:
|
|
448
|
+
raise NotImplementedError(f"Method {self.method} not implemented")
|
|
449
|
+
|
|
450
|
+
if threshold is not None:
|
|
451
|
+
change_binary = (change > threshold).astype(np.uint8)
|
|
452
|
+
return change_binary
|
|
453
|
+
else:
|
|
454
|
+
return change
|
|
455
|
+
|
|
456
|
+
def calculate_change_statistics(
|
|
457
|
+
self,
|
|
458
|
+
change_map: np.ndarray,
|
|
459
|
+
pixel_area: float = 900.0
|
|
460
|
+
) -> Dict:
|
|
461
|
+
"""
|
|
462
|
+
Calculate change statistics.
|
|
463
|
+
|
|
464
|
+
Parameters:
|
|
465
|
+
-----------
|
|
466
|
+
change_map : np.ndarray
|
|
467
|
+
Binary change map
|
|
468
|
+
pixel_area : float
|
|
469
|
+
Area per pixel in square meters
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
--------
|
|
473
|
+
dict : Change statistics
|
|
474
|
+
"""
|
|
475
|
+
total_pixels = change_map.size
|
|
476
|
+
changed_pixels = np.sum(change_map)
|
|
477
|
+
unchanged_pixels = total_pixels - changed_pixels
|
|
478
|
+
|
|
479
|
+
changed_area_m2 = changed_pixels * pixel_area
|
|
480
|
+
changed_area_km2 = changed_area_m2 / 1e6
|
|
481
|
+
|
|
482
|
+
change_percentage = (changed_pixels / total_pixels) * 100
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
'total_pixels': int(total_pixels),
|
|
486
|
+
'changed_pixels': int(changed_pixels),
|
|
487
|
+
'unchanged_pixels': int(unchanged_pixels),
|
|
488
|
+
'changed_area_m2': float(changed_area_m2),
|
|
489
|
+
'changed_area_km2': float(changed_area_km2),
|
|
490
|
+
'change_percentage': float(change_percentage)
|
|
491
|
+
}
|