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/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
+ }