change to NAS
This commit is contained in:
@@ -0,0 +1,284 @@
|
|||||||
|
# Model Upload Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This system now supports uploading custom models for both **Cloud Removal** and **Land Classification** tasks with full metadata tracking.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
remote-sensing/
|
||||||
|
├── cloud_removal_model/ # Cloud removal models (U-Net, GAN, etc.)
|
||||||
|
│ ├── *.pth # PyTorch model files
|
||||||
|
│ └── *.json # Metadata sidecar files
|
||||||
|
├── land_classification_model/ # Land use classification models
|
||||||
|
│ ├── *.pth, *.pkl, *.joblib # Model files (various formats)
|
||||||
|
│ ├── *.h5, *.keras # TensorFlow/Keras models
|
||||||
|
│ └── *.json # Metadata sidecar files
|
||||||
|
└── model_train/ # Legacy training outputs (other models)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloud Removal Model Upload
|
||||||
|
|
||||||
|
### Supported Format
|
||||||
|
- **File Extension**: `.pth` (PyTorch)
|
||||||
|
- **Use Case**: Remove clouds from Sentinel-2 imagery
|
||||||
|
|
||||||
|
### Metadata Fields
|
||||||
|
- **Epoch** (int): Training epoch number
|
||||||
|
- **Validation Loss** (float): Best validation loss achieved
|
||||||
|
- **Training Loss** (float): Final training loss
|
||||||
|
- **Input Channels** (int): Number of input channels (e.g., 6 for S2+S1)
|
||||||
|
- **Output Channels** (int): Number of output channels (e.g., 4 for RGBN)
|
||||||
|
- **Use Sentinel-1** (bool): Whether model uses SAR data
|
||||||
|
- **Description** (string): Optional notes about the model
|
||||||
|
|
||||||
|
### API Endpoint
|
||||||
|
```http
|
||||||
|
POST /api/cloud-removal/upload
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
{
|
||||||
|
"file": <binary>,
|
||||||
|
"epoch": 50,
|
||||||
|
"val_loss": 0.0134,
|
||||||
|
"train_loss": 0.0142,
|
||||||
|
"in_channels": 6,
|
||||||
|
"out_channels": 4,
|
||||||
|
"use_s1": true,
|
||||||
|
"description": "Trained on winter dataset"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Metadata File
|
||||||
|
`cloud_removal_unet_winter.pth.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"filename": "cloud_removal_unet_winter.pth",
|
||||||
|
"epoch": 50,
|
||||||
|
"train_loss": 0.0142,
|
||||||
|
"val_loss": 0.0134,
|
||||||
|
"in_channels": 6,
|
||||||
|
"out_channels": 4,
|
||||||
|
"use_s1": true,
|
||||||
|
"description": "Trained on winter dataset, 50 epochs",
|
||||||
|
"uploaded_at": "2026-01-26T15:30:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Land Classification Model Upload
|
||||||
|
|
||||||
|
### Supported Formats
|
||||||
|
- **PyTorch**: `.pth`
|
||||||
|
- **Scikit-learn**: `.pkl`, `.joblib`
|
||||||
|
- **TensorFlow/Keras**: `.h5`, `.keras`
|
||||||
|
|
||||||
|
### Metadata Fields
|
||||||
|
- **Model Type**: `mobilenet`, `cnn`, `swin`, `xgboost`, `random_forest`, `other`
|
||||||
|
- **Epoch** (int): Training epochs
|
||||||
|
- **Train Accuracy** (float %): Training accuracy percentage
|
||||||
|
- **Val Accuracy** (float %): Validation accuracy percentage
|
||||||
|
- **Train Loss** (float): Final training loss
|
||||||
|
- **Val Loss** (float): Final validation loss
|
||||||
|
- **Number of Classes** (int): Number of land use classes (e.g., 10)
|
||||||
|
- **Input Size** (int): Input image dimension (e.g., 64x64)
|
||||||
|
- **Description** (string): Optional notes
|
||||||
|
|
||||||
|
### API Endpoint
|
||||||
|
```http
|
||||||
|
POST /api/land-classification/upload
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
{
|
||||||
|
"file": <binary>,
|
||||||
|
"model_type": "mobilenet",
|
||||||
|
"epoch": 100,
|
||||||
|
"train_accuracy": 95.5,
|
||||||
|
"val_accuracy": 93.2,
|
||||||
|
"train_loss": 0.12,
|
||||||
|
"val_loss": 0.18,
|
||||||
|
"num_classes": 10,
|
||||||
|
"input_size": 64,
|
||||||
|
"description": "MobileNetV2 trained on Mekong Delta"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Metadata File
|
||||||
|
`mobilenet_mekong_v2.pth.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"filename": "mobilenet_mekong_v2.pth",
|
||||||
|
"model_type": "mobilenet",
|
||||||
|
"epoch": 100,
|
||||||
|
"train_accuracy": 95.5,
|
||||||
|
"val_accuracy": 93.2,
|
||||||
|
"train_loss": 0.12,
|
||||||
|
"val_loss": 0.18,
|
||||||
|
"num_classes": 10,
|
||||||
|
"input_size": 64,
|
||||||
|
"description": "MobileNetV2 trained on Mekong Delta dataset",
|
||||||
|
"uploaded_at": "2026-01-26T15:45:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage in Web Interface
|
||||||
|
|
||||||
|
### Cloud Removal Models
|
||||||
|
1. Navigate to **Prediction Interface**
|
||||||
|
2. Select **Cloud Removal Method** → "Deep Learning (U-Net)"
|
||||||
|
3. Click **📤 Upload Cloud Removal Model (.pth)**
|
||||||
|
4. Fill in metadata form
|
||||||
|
5. Click **✅ Upload with Metadata**
|
||||||
|
6. Model appears in dropdown with epoch/loss info
|
||||||
|
|
||||||
|
### Land Classification Models
|
||||||
|
1. Navigate to **Prediction Interface**
|
||||||
|
2. In **Model Selection** section
|
||||||
|
3. Click **📤 Upload Land Classification Model**
|
||||||
|
4. Fill in metadata form (model type, accuracy, etc.)
|
||||||
|
5. Click **✅ Upload with Metadata**
|
||||||
|
6. Model appears in main model dropdown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### List Models
|
||||||
|
|
||||||
|
**Cloud Removal:**
|
||||||
|
```http
|
||||||
|
GET /api/cloud-removal/models
|
||||||
|
```
|
||||||
|
|
||||||
|
**Land Classification:**
|
||||||
|
```http
|
||||||
|
GET /api/land-classification/models
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"filename": "model.pth",
|
||||||
|
"epoch": 50,
|
||||||
|
"val_loss": 0.0134,
|
||||||
|
"size_mb": 356.2,
|
||||||
|
"has_metadata": true,
|
||||||
|
"created": 1706284800
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Model
|
||||||
|
|
||||||
|
**Cloud Removal:**
|
||||||
|
```http
|
||||||
|
DELETE /api/cloud-removal/models/{filename}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Land Classification:**
|
||||||
|
```http
|
||||||
|
DELETE /api/land-classification/models/{filename}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Naming Convention**: Use descriptive names
|
||||||
|
- ✅ `cloud_removal_unet_winter_50ep.pth`
|
||||||
|
- ✅ `mobilenet_v2_mekong_acc93.pth`
|
||||||
|
- ❌ `model1.pth`
|
||||||
|
|
||||||
|
2. **Metadata Accuracy**: Always fill in actual training metrics
|
||||||
|
- Helps compare model performance
|
||||||
|
- Enables informed model selection
|
||||||
|
|
||||||
|
3. **Version Control**: Include version/date in description
|
||||||
|
- "v2.0 - Improved augmentation"
|
||||||
|
- "2026-01-15 - Fixed class imbalance"
|
||||||
|
|
||||||
|
4. **File Size**: Monitor model sizes
|
||||||
|
- Cloud removal models: 50-500 MB typical
|
||||||
|
- Land classification: 5-200 MB typical
|
||||||
|
- Large models may require more GPU memory
|
||||||
|
|
||||||
|
5. **Testing**: Always test uploaded model on small region first
|
||||||
|
- Verify predictions are reasonable
|
||||||
|
- Check for errors/crashes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Upload Fails with "Already Exists"
|
||||||
|
- Model filename is duplicate
|
||||||
|
- Delete old model first or rename new one
|
||||||
|
|
||||||
|
### Model Shows Default Values (0, 0, 0)
|
||||||
|
- Server needs restart to load `Form(...)` imports
|
||||||
|
- Refresh page and try again
|
||||||
|
|
||||||
|
### Model Not Appearing in Dropdown
|
||||||
|
- Click **🔄 Refresh** button
|
||||||
|
- Check file extension is valid
|
||||||
|
- Verify model saved to correct folder
|
||||||
|
|
||||||
|
### Metadata Not Displaying
|
||||||
|
- Check `.json` file exists alongside model
|
||||||
|
- Verify JSON format is valid
|
||||||
|
- Look for server errors in terminal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration from Old System
|
||||||
|
|
||||||
|
If you have models in `model_train/`:
|
||||||
|
|
||||||
|
1. **Cloud Removal Models**: Move to `cloud_removal_model/`
|
||||||
|
```bash
|
||||||
|
mv model_train/cloud_removal_*.pth cloud_removal_model/
|
||||||
|
mv model_train/*_unet*.pth cloud_removal_model/
|
||||||
|
mv model_train/*GAN*.pth cloud_removal_model/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Land Classification Models**: Move to `land_classification_model/`
|
||||||
|
```bash
|
||||||
|
mv model_train/mobilenet*.pth land_classification_model/
|
||||||
|
mv model_train/cnn*.pth land_classification_model/
|
||||||
|
mv model_train/swin*.pth land_classification_model/
|
||||||
|
mv model_train/*.pkl land_classification_model/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create metadata files** by re-uploading through web interface
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
✅ **File Extension Validation**: Only allowed formats accepted
|
||||||
|
✅ **Path Traversal Prevention**: No `../` or `/` in filenames
|
||||||
|
✅ **Duplicate Detection**: Prevents overwriting existing models
|
||||||
|
✅ **Size Limits**: Prevents extremely large uploads
|
||||||
|
✅ **JSON Sanitization**: Metadata stored safely
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Batch model upload
|
||||||
|
- [ ] Model versioning system
|
||||||
|
- [ ] Automated benchmarking
|
||||||
|
- [ ] Model comparison tool
|
||||||
|
- [ ] Export/import model configs
|
||||||
|
- [ ] Cloud storage integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: January 26, 2026
|
||||||
+205
@@ -764,6 +764,211 @@ async def upload_cloud_removal_model(
|
|||||||
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/land-classification/upload")
|
||||||
|
async def upload_land_classification_model(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
model_type: str = Form("mobilenet"), # mobilenet, cnn, swin, xgboost, etc.
|
||||||
|
epoch: int = Form(0),
|
||||||
|
train_accuracy: float = Form(0.0),
|
||||||
|
val_accuracy: float = Form(0.0),
|
||||||
|
train_loss: float = Form(0.0),
|
||||||
|
val_loss: float = Form(0.0),
|
||||||
|
num_classes: int = Form(10),
|
||||||
|
input_size: int = Form(64),
|
||||||
|
description: str = Form("")
|
||||||
|
):
|
||||||
|
"""Upload land classification model with metadata"""
|
||||||
|
|
||||||
|
# Debug logging
|
||||||
|
print(f"[Land Upload] Received parameters:")
|
||||||
|
print(f" File: {file.filename}")
|
||||||
|
print(f" Model Type: {model_type}")
|
||||||
|
print(f" Epoch: {epoch}")
|
||||||
|
print(f" Train Accuracy: {train_accuracy}")
|
||||||
|
print(f" Val Accuracy: {val_accuracy}")
|
||||||
|
print(f" Train Loss: {train_loss}")
|
||||||
|
print(f" Val Loss: {val_loss}")
|
||||||
|
print(f" Num Classes: {num_classes}")
|
||||||
|
print(f" Input Size: {input_size}")
|
||||||
|
print(f" Description: {description}")
|
||||||
|
|
||||||
|
# Validate file extension
|
||||||
|
valid_extensions = ['.pth', '.pkl', '.joblib', '.h5', '.keras']
|
||||||
|
if not any(file.filename.endswith(ext) for ext in valid_extensions):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Only {', '.join(valid_extensions)} files are allowed"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Security check
|
||||||
|
if ".." in file.filename or "/" in file.filename or "\\" in file.filename:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
|
|
||||||
|
try:
|
||||||
|
model_dir = Path("land_classification_model")
|
||||||
|
model_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Save uploaded file
|
||||||
|
file_path = model_dir / file.filename
|
||||||
|
|
||||||
|
# Check if file already exists
|
||||||
|
if file_path.exists():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Model {file.filename} already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write file
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
content = await file.read()
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
file_size = file_path.stat().st_size
|
||||||
|
|
||||||
|
# Save metadata as JSON sidecar file
|
||||||
|
metadata_file = file_path.with_suffix(file_path.suffix + '.json')
|
||||||
|
|
||||||
|
metadata_dict = {
|
||||||
|
"filename": file.filename,
|
||||||
|
"model_type": model_type,
|
||||||
|
"epoch": epoch,
|
||||||
|
"train_accuracy": train_accuracy,
|
||||||
|
"val_accuracy": val_accuracy,
|
||||||
|
"train_loss": train_loss,
|
||||||
|
"val_loss": val_loss,
|
||||||
|
"num_classes": num_classes,
|
||||||
|
"input_size": input_size,
|
||||||
|
"description": description,
|
||||||
|
"uploaded_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(metadata_file, 'w') as f:
|
||||||
|
json.dump(metadata_dict, f, indent=2)
|
||||||
|
|
||||||
|
print(f"[Land Upload] Saved model: {file_path}")
|
||||||
|
print(f"[Land Upload] Saved metadata: {metadata_file}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": f"Successfully uploaded {file.filename}",
|
||||||
|
"filename": file.filename,
|
||||||
|
"size_mb": round(file_size / 1024 / 1024, 2),
|
||||||
|
"path": str(file_path),
|
||||||
|
"metadata": metadata_dict
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Land Upload] Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/land-classification/models")
|
||||||
|
async def list_land_classification_models():
|
||||||
|
"""List all uploaded land classification models"""
|
||||||
|
model_dir = Path("land_classification_model")
|
||||||
|
if not model_dir.exists():
|
||||||
|
return {"models": [], "count": 0}
|
||||||
|
|
||||||
|
models = []
|
||||||
|
# Search for model files
|
||||||
|
for model_file in model_dir.rglob("*"):
|
||||||
|
# Skip JSON metadata files
|
||||||
|
if model_file.suffix == '.json':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only include model files
|
||||||
|
valid_extensions = ['.pth', '.pkl', '.joblib', '.h5', '.keras']
|
||||||
|
if not any(model_file.name.endswith(ext) for ext in valid_extensions):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to load metadata from JSON sidecar file
|
||||||
|
metadata_file = Path(str(model_file) + '.json')
|
||||||
|
if metadata_file.exists():
|
||||||
|
with open(metadata_file, 'r') as f:
|
||||||
|
metadata = json.load(f)
|
||||||
|
|
||||||
|
models.append({
|
||||||
|
"filename": model_file.name,
|
||||||
|
"path": str(model_file),
|
||||||
|
"relative_path": str(model_file.relative_to(model_dir)),
|
||||||
|
"model_type": metadata.get('model_type', 'unknown'),
|
||||||
|
"epoch": metadata.get('epoch', 0),
|
||||||
|
"train_accuracy": metadata.get('train_accuracy', 0),
|
||||||
|
"val_accuracy": metadata.get('val_accuracy', 0),
|
||||||
|
"train_loss": metadata.get('train_loss', 0),
|
||||||
|
"val_loss": metadata.get('val_loss', 0),
|
||||||
|
"num_classes": metadata.get('num_classes', 10),
|
||||||
|
"input_size": metadata.get('input_size', 64),
|
||||||
|
"description": metadata.get('description', ''),
|
||||||
|
"created": model_file.stat().st_mtime,
|
||||||
|
"size_mb": model_file.stat().st_size / (1024 * 1024),
|
||||||
|
"has_metadata": True
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# No metadata file, use defaults
|
||||||
|
models.append({
|
||||||
|
"filename": model_file.name,
|
||||||
|
"path": str(model_file),
|
||||||
|
"relative_path": str(model_file.relative_to(model_dir)),
|
||||||
|
"model_type": "unknown",
|
||||||
|
"epoch": 0,
|
||||||
|
"train_accuracy": 0,
|
||||||
|
"val_accuracy": 0,
|
||||||
|
"train_loss": 0,
|
||||||
|
"val_loss": 0,
|
||||||
|
"num_classes": 10,
|
||||||
|
"input_size": 64,
|
||||||
|
"description": "",
|
||||||
|
"created": model_file.stat().st_mtime,
|
||||||
|
"size_mb": model_file.stat().st_size / (1024 * 1024),
|
||||||
|
"has_metadata": False
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Land Models] Error loading {model_file}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sort by creation time (newest first)
|
||||||
|
models.sort(key=lambda x: x['created'], reverse=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"models": models,
|
||||||
|
"count": len(models)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/land-classification/models/{filename}")
|
||||||
|
async def delete_land_classification_model(filename: str):
|
||||||
|
"""Delete a land classification model"""
|
||||||
|
model_dir = Path("land_classification_model")
|
||||||
|
model_path = model_dir / filename
|
||||||
|
|
||||||
|
# Security check
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
|
|
||||||
|
if not model_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail=f"Model not found: {filename}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Delete model file
|
||||||
|
model_path.unlink()
|
||||||
|
|
||||||
|
# Delete metadata file if exists
|
||||||
|
metadata_file = Path(str(model_path) + '.json')
|
||||||
|
if metadata_file.exists():
|
||||||
|
metadata_file.unlink()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Deleted land classification model: {filename}"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to delete: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/cloud-removal/models/{filename}")
|
@app.delete("/api/cloud-removal/models/{filename}")
|
||||||
async def delete_cloud_removal_model(filename: str):
|
async def delete_cloud_removal_model(filename: str):
|
||||||
"""Xóa cloud removal model"""
|
"""Xóa cloud removal model"""
|
||||||
|
|||||||
+278
-87
@@ -12,8 +12,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -21,45 +22,85 @@
|
|||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: white;
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border-radius: 15px;
|
backdrop-filter: blur(20px);
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 25px 80px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 30px;
|
padding: 40px 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
right: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||||
|
animation: headerGlow 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes headerGlow {
|
||||||
|
0%, 100% { transform: translate(0, 0); }
|
||||||
|
50% { transform: translate(-20%, -20%); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h1 {
|
.header h1 {
|
||||||
font-size: 2.5em;
|
font-size: 2.8em;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-shadow: 0 2px 20px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header p {
|
.header p {
|
||||||
font-size: 1.1em;
|
font-size: 1.15em;
|
||||||
opacity: 0.9;
|
opacity: 0.95;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
background: #f8f9fa;
|
background: rgba(255,255,255,0.8);
|
||||||
padding: 15px 30px;
|
backdrop-filter: blur(10px);
|
||||||
border-bottom: 2px solid #e9ecef;
|
padding: 18px 30px;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.03);
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav a {
|
.nav a {
|
||||||
color: #667eea;
|
padding: 12px 24px;
|
||||||
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-right: 20px;
|
border-radius: 12px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: color 0.3s;
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav a:nth-child(1) { background: linear-gradient(135deg, #667eea, #764ba2); }
|
||||||
|
.nav a:nth-child(2) { background: linear-gradient(135deg, #f093fb, #f5576c); }
|
||||||
|
.nav a:nth-child(3) { background: linear-gradient(135deg, #4facfe, #00f2fe); }
|
||||||
|
.nav a:nth-child(4) { background: linear-gradient(135deg, #43e97b, #38f9d7); }
|
||||||
|
|
||||||
.nav a:hover {
|
.nav a:hover {
|
||||||
color: #764ba2;
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -68,44 +109,72 @@
|
|||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
padding: 28px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.04);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:hover {
|
||||||
|
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.12);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 1.5em;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: #333;
|
-webkit-background-clip: text;
|
||||||
margin-bottom: 15px;
|
-webkit-text-fill-color: transparent;
|
||||||
padding-bottom: 10px;
|
background-clip: text;
|
||||||
border-bottom: 3px solid #667eea;
|
font-size: 1.6em;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #f8f9fa;
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border-left: 4px solid #667eea;
|
border: 1px solid rgba(0,0,0,0.05);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: #333;
|
color: #374151;
|
||||||
|
font-size: 0.95em;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="number"],
|
input[type="number"],
|
||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px 16px;
|
||||||
border: 2px solid #e9ecef;
|
border: 2px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
transition: border-color 0.3s;
|
transition: all 0.3s ease;
|
||||||
|
background: white;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:hover,
|
||||||
|
input[type="number"]:hover,
|
||||||
|
select:hover {
|
||||||
|
border-color: #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
@@ -113,54 +182,98 @@
|
|||||||
select:focus {
|
select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-group {
|
.checkbox-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 12px 30px;
|
padding: 14px 32px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.6s, height 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover::before {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #6c757d;
|
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(107, 114, 128, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 25px rgba(107, 114, 128, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: #dc3545;
|
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 25px rgba(220, 53, 69, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-success {
|
.btn-success {
|
||||||
background: #28a745;
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-list {
|
.model-list {
|
||||||
@@ -170,87 +283,122 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.model-card {
|
.model-card {
|
||||||
background: white;
|
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
|
||||||
border: 2px solid #e9ecef;
|
border: 1px solid rgba(0,0,0,0.08);
|
||||||
border-radius: 10px;
|
border-radius: 14px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card:hover {
|
.model-card:hover {
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
|
||||||
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card h3 {
|
.model-card h3 {
|
||||||
color: #667eea;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
margin-bottom: 10px;
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-info {
|
.model-info {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #6c757d;
|
color: #6b7280;
|
||||||
margin: 5px 0;
|
margin: 6px 0;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 5px 15px;
|
padding: 6px 16px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-size: 0.9em;
|
font-size: 0.85em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-success {
|
.status-success {
|
||||||
background: #d4edda;
|
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||||
color: #155724;
|
color: #155724;
|
||||||
|
box-shadow: 0 2px 8px rgba(21, 87, 36, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-training {
|
.status-training {
|
||||||
background: #fff3cd;
|
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||||
color: #856404;
|
color: #856404;
|
||||||
|
box-shadow: 0 2px 8px rgba(133, 100, 4, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-error {
|
.status-error {
|
||||||
background: #f8d7da;
|
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
|
||||||
color: #721c24;
|
color: #721c24;
|
||||||
|
box-shadow: 0 2px 8px rgba(114, 28, 36, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30px;
|
height: 32px;
|
||||||
background: #e9ecef;
|
background: linear-gradient(to right, #e5e7eb, #f3f4f6);
|
||||||
border-radius: 15px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #667eea 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
font-size: 0.9em;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
background: #e7f3ff;
|
background: linear-gradient(135deg, #e3f2fd 0%, #f0f7ff 100%);
|
||||||
border-left: 4px solid #2196F3;
|
border-left: 5px solid #2196F3;
|
||||||
padding: 15px;
|
padding: 20px;
|
||||||
border-radius: 5px;
|
border-radius: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box:hover {
|
||||||
|
box-shadow: 0 6px 25px rgba(33, 150, 243, 0.15);
|
||||||
|
transform: translateX(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-box {
|
.warning-box {
|
||||||
background: #fff3cd;
|
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||||
border-left: 4px solid #ffc107;
|
border-left: 5px solid #ffc107;
|
||||||
padding: 15px;
|
padding: 20px;
|
||||||
border-radius: 5px;
|
border-radius: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 193, 7, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box:hover {
|
||||||
|
box-shadow: 0 6px 25px rgba(255, 193, 7, 0.15);
|
||||||
|
transform: translateX(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-2 {
|
.grid-2 {
|
||||||
@@ -273,16 +421,18 @@
|
|||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
box-shadow: inset 0 2px 10px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs .log-entry {
|
.logs .log-entry {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs .log-info {
|
.logs .log-info {
|
||||||
@@ -332,9 +482,18 @@
|
|||||||
<form id="trainingForm">
|
<form id="trainingForm">
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>📂 Data Directory</label>
|
<label>🏗️ Model Architecture</label>
|
||||||
<input type="text" id="dataDir" value="winter_dataset" required>
|
<select id="modelArchitecture" required>
|
||||||
<small style="color: #6c757d;">Thư mục chứa dữ liệu SEN12MS-CR</small>
|
<option value="unet">U-Net (Classic CNN)</option>
|
||||||
|
<option value="crgan">CR-GAN (Cloud Removal GAN)</option>
|
||||||
|
<option value="spagan">SpA-GAN (Spatial Attention GAN)</option>
|
||||||
|
<option value="glfcr">GLF-CR (Global-Local Fusion)</option>
|
||||||
|
<option value="sen12mscr">SEN12MS-CR (Multi-modal)</option>
|
||||||
|
<option value="rsdehazenet">RSDehazeNet (Remote Sensing)</option>
|
||||||
|
<option value="cloudnet">Cloud-Net (Encoder-Decoder)</option>
|
||||||
|
<option value="dsen2cr">DSen2-CR (Deep Sentinel-2)</option>
|
||||||
|
</select>
|
||||||
|
<small style="color: #6c757d;">Chọn kiến trúc deep learning cho cloud removal</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -343,6 +502,12 @@
|
|||||||
<small style="color: #6c757d;">Tên model để lưu</small>
|
<small style="color: #6c757d;">Tên model để lưu</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>📂 Data Directory</label>
|
||||||
|
<input type="text" id="dataDir" value="winter_dataset" required>
|
||||||
|
<small style="color: #6c757d;">Thư mục chứa dữ liệu SEN12MS-CR</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>📦 Batch Size</label>
|
<label>📦 Batch Size</label>
|
||||||
<input type="number" id="batchSize" value="8" min="1" max="32" required>
|
<input type="number" id="batchSize" value="8" min="1" max="32" required>
|
||||||
@@ -412,30 +577,54 @@
|
|||||||
|
|
||||||
<!-- Methods Info -->
|
<!-- Methods Info -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2 class="section-title">📖 Cloud Removal Methods</h2>
|
<h2 class="section-title">📖 Cloud Removal Deep Learning Architectures</h2>
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>🔹 Classic (Default)</h3>
|
<h3>🔹 U-Net</h3>
|
||||||
<p>3-step approach: temporal → median → spatial interpolation</p>
|
<p>Classic encoder-decoder with skip connections. Fast training, good baseline performance.</p>
|
||||||
<div class="status-badge status-success">Fast</div>
|
<div class="status-badge status-success">Recommended for beginners</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>🔹 Hybrid</h3>
|
<h3>🔹 CR-GAN</h3>
|
||||||
<p>Classical + ML KNN - balanced speed & quality</p>
|
<p>Cloud Removal GAN - adversarial training cho kết quả chân thực hơn.</p>
|
||||||
<div class="status-badge status-success">Recommended</div>
|
<div class="status-badge status-training">Advanced</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>🔹 ML KNN</h3>
|
<h3>🔹 SpA-GAN</h3>
|
||||||
<p>K-Nearest Neighbors inpainting - good quality</p>
|
<p>Spatial Attention GAN - attention mechanism tập trung vào vùng có mây.</p>
|
||||||
<div class="status-badge status-training">Medium Speed</div>
|
<div class="status-badge status-success">Best quality</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>🔹 Deep Learning</h3>
|
<h3>🔹 GLF-CR</h3>
|
||||||
<p>U-Net CNN - best quality for large gaps</p>
|
<p>Global-Local Fusion - kết hợp features global và local cho chi tiết tốt hơn.</p>
|
||||||
<div class="status-badge status-error">Requires Model</div>
|
<div class="status-badge status-training">High accuracy</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🔹 SEN12MS-CR</h3>
|
||||||
|
<p>Multi-modal fusion - kết hợp Sentinel-1 radar và Sentinel-2 optical.</p>
|
||||||
|
<div class="status-badge status-success">Multi-sensor</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🔹 RSDehazeNet</h3>
|
||||||
|
<p>Remote Sensing Dehaze Network - chuyên cho ảnh viễn thám.</p>
|
||||||
|
<div class="status-badge status-training">RS specialized</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🔹 Cloud-Net</h3>
|
||||||
|
<p>Encoder-Decoder architecture với residual connections.</p>
|
||||||
|
<div class="status-badge status-success">Balanced</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🔹 DSen2-CR</h3>
|
||||||
|
<p>Deep Sentinel-2 Cloud Removal - tận dụng temporal information.</p>
|
||||||
|
<div class="status-badge status-training">Temporal fusion</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -456,6 +645,7 @@
|
|||||||
const config = {
|
const config = {
|
||||||
data_dir: document.getElementById('dataDir').value,
|
data_dir: document.getElementById('dataDir').value,
|
||||||
model_name: document.getElementById('modelName').value,
|
model_name: document.getElementById('modelName').value,
|
||||||
|
architecture: document.getElementById('modelArchitecture').value,
|
||||||
use_s1: document.getElementById('useS1').checked,
|
use_s1: document.getElementById('useS1').checked,
|
||||||
batch_size: parseInt(document.getElementById('batchSize').value),
|
batch_size: parseInt(document.getElementById('batchSize').value),
|
||||||
num_epochs: parseInt(document.getElementById('numEpochs').value),
|
num_epochs: parseInt(document.getElementById('numEpochs').value),
|
||||||
@@ -505,6 +695,7 @@
|
|||||||
modelsList.innerHTML = data.models.map(model => `
|
modelsList.innerHTML = data.models.map(model => `
|
||||||
<div class="model-card">
|
<div class="model-card">
|
||||||
<h3>📦 ${model.filename}</h3>
|
<h3>📦 ${model.filename}</h3>
|
||||||
|
<div class="model-info">🏗️ Architecture: ${model.architecture || 'U-Net'}</div>
|
||||||
<div class="model-info">📊 Epoch: ${model.epoch}</div>
|
<div class="model-info">📊 Epoch: ${model.epoch}</div>
|
||||||
<div class="model-info">📉 Train Loss: ${model.train_loss.toFixed(6)}</div>
|
<div class="model-info">📉 Train Loss: ${model.train_loss.toFixed(6)}</div>
|
||||||
<div class="model-info">📉 Val Loss: ${model.val_loss.toFixed(6)}</div>
|
<div class="model-info">📉 Val Loss: ${model.val_loss.toFixed(6)}</div>
|
||||||
|
|||||||
+527
-149
@@ -17,8 +17,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
@@ -26,27 +27,53 @@
|
|||||||
.container {
|
.container {
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: white;
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border-radius: 20px;
|
backdrop-filter: blur(20px);
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 25px 80px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 30px;
|
padding: 40px 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
right: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||||
|
animation: headerGlow 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes headerGlow {
|
||||||
|
0%, 100% { transform: translate(0, 0); }
|
||||||
|
50% { transform: translate(-20%, -20%); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h1 {
|
.header h1 {
|
||||||
font-size: 2.5em;
|
font-size: 2.8em;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-shadow: 0 2px 20px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header p {
|
.header p {
|
||||||
opacity: 0.9;
|
opacity: 0.95;
|
||||||
font-size: 1.1em;
|
font-size: 1.15em;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -58,8 +85,9 @@
|
|||||||
|
|
||||||
#predictMap {
|
#predictMap {
|
||||||
height: 500px;
|
height: 500px;
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
|
||||||
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-container {
|
.map-container {
|
||||||
@@ -67,59 +95,95 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.map-instructions {
|
.map-instructions {
|
||||||
background: #e3f2fd;
|
background: linear-gradient(135deg, #e3f2fd 0%, #f0f7ff 100%);
|
||||||
padding: 15px;
|
padding: 20px;
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 20px;
|
||||||
border-left: 4px solid #2196f3;
|
border-left: 5px solid #2196f3;
|
||||||
|
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-instructions:hover {
|
||||||
|
box-shadow: 0 6px 25px rgba(33, 150, 243, 0.15);
|
||||||
|
transform: translateX(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-instructions h3 {
|
.map-instructions h3 {
|
||||||
color: #1976d2;
|
color: #1976d2;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-instructions p {
|
.map-instructions p {
|
||||||
color: #555;
|
color: #555;
|
||||||
margin: 5px 0;
|
margin: 6px 0;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
padding: 20px;
|
padding: 28px;
|
||||||
background: #f8f9fa;
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.04);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:hover {
|
||||||
|
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.12);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h2 {
|
.section h2 {
|
||||||
color: #667eea;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
margin-bottom: 15px;
|
-webkit-background-clip: text;
|
||||||
font-size: 1.5em;
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 8px;
|
||||||
color: #333;
|
color: #374151;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 0.95em;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input, .form-group select {
|
.form-group input, .form-group select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 12px 16px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid #e5e7eb;
|
||||||
border-radius: 5px;
|
border-radius: 12px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
transition: border-color 0.3s;
|
transition: all 0.3s ease;
|
||||||
|
background: white;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:hover, .form-group select:hover {
|
||||||
|
border-color: #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus, .form-group select:focus {
|
.form-group input:focus, .form-group select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
@@ -129,43 +193,78 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 12px 30px;
|
padding: 14px 32px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 12px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.6s, height 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover::before {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-success {
|
.btn-success {
|
||||||
background: #28a745;
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-success:hover {
|
.btn-success:hover {
|
||||||
background: #218838;
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #6c757d;
|
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(107, 114, 128, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 25px rgba(107, 114, 128, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-box {
|
.status-box {
|
||||||
@@ -192,42 +291,68 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
height: 30px;
|
height: 32px;
|
||||||
background: #e0e0e0;
|
background: linear-gradient(to right, #e5e7eb, #f3f4f6);
|
||||||
border-radius: 15px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 10px 0;
|
margin: 12px 0;
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #667eea 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
font-size: 0.9em;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-card {
|
.metric-card {
|
||||||
background: white;
|
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
|
||||||
padding: 15px;
|
padding: 24px;
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border: 1px solid rgba(0,0,0,0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-card h4 {
|
.metric-card h4 {
|
||||||
color: #667eea;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
margin-bottom: 10px;
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-card .value {
|
.metric-card .value {
|
||||||
font-size: 2em;
|
font-size: 2.2em;
|
||||||
font-weight: bold;
|
font-weight: 800;
|
||||||
color: #333;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
@@ -260,18 +385,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prediction-item {
|
.prediction-item {
|
||||||
background: white;
|
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
|
||||||
padding: 15px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 14px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
border-left: 4px solid #667eea;
|
border-left: 5px solid #667eea;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prediction-item:hover {
|
.prediction-item:hover {
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
|
||||||
|
transform: translateX(5px);
|
||||||
|
border-left-width: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -292,22 +421,22 @@
|
|||||||
<p>Phân loại đất cho khu vực mới sử dụng model đã train</p>
|
<p>Phân loại đất cho khu vực mới sử dụng model đã train</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background: white; padding: 15px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; border-bottom: 2px solid #e0e0e0;">
|
<div style="background: rgba(255,255,255,0.8); backdrop-filter: blur(10px); padding: 18px; display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; border-bottom: 1px solid rgba(0,0,0,0.08); box-shadow: 0 2px 10px rgba(0,0,0,0.03);">
|
||||||
<a href="/" style="padding: 10px 20px; background: #667eea; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🏠 Trang Chủ</a>
|
<a href="/" style="padding: 12px 24px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border-radius: 12px; text-decoration: none; font-weight: 600; transition: all 0.3s; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(102, 126, 234, 0.3)'" onmouseout="this.style.transform=''; this.style.boxShadow='0 4px 12px rgba(102, 126, 234, 0.2)'">🏠 Trang Chủ</a>
|
||||||
<a href="/training" style="padding: 10px 20px; background: #f093fb; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🎓 Training</a>
|
<a href="/training" style="padding: 12px 24px; background: linear-gradient(135deg, #f093fb, #f5576c); color: white; border-radius: 12px; text-decoration: none; font-weight: 600; transition: all 0.3s; box-shadow: 0 4px 12px rgba(240, 147, 251, 0.2);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(240, 147, 251, 0.3)'" onmouseout="this.style.transform=''; this.style.boxShadow='0 4px 12px rgba(240, 147, 251, 0.2)'">🎓 Training</a>
|
||||||
<a href="/cloud-training" style="padding: 10px 20px; background: #00bcd4; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌥️ Cloud Removal</a>
|
<a href="/cloud-training" style="padding: 12px 24px; background: linear-gradient(135deg, #00bcd4, #0097a7); color: white; border-radius: 12px; text-decoration: none; font-weight: 600; transition: all 0.3s; box-shadow: 0 4px 12px rgba(0, 188, 212, 0.2);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(0, 188, 212, 0.3)'" onmouseout="this.style.transform=''; this.style.boxShadow='0 4px 12px rgba(0, 188, 212, 0.2)'">🌥️ Cloud Removal</a>
|
||||||
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction (Active)</a>
|
<a href="/prediction" style="padding: 12px 24px; background: linear-gradient(135deg, #4facfe, #00f2fe); color: white; border-radius: 12px; text-decoration: none; font-weight: 600; transition: all 0.3s; box-shadow: 0 6px 20px rgba(79, 172, 254, 0.4); transform: translateY(-2px);">🗺️ Prediction (Active)</a>
|
||||||
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing</a>
|
<a href="/batch" style="padding: 12px 24px; background: linear-gradient(135deg, #764ba2, #667eea); color: white; border-radius: 12px; text-decoration: none; font-weight: 600; transition: all 0.3s; box-shadow: 0 4px 12px rgba(118, 75, 162, 0.2);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(118, 75, 162, 0.3)'" onmouseout="this.style.transform=''; this.style.boxShadow='0 4px 12px rgba(118, 75, 162, 0.2)'">🚀 Batch Processing</a>
|
||||||
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
|
<a href="/ndvi" style="padding: 12px 24px; background: linear-gradient(135deg, #2ecc71, #27ae60); color: white; border-radius: 12px; text-decoration: none; font-weight: 600; transition: all 0.3s; box-shadow: 0 4px 12px rgba(46, 204, 113, 0.2);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(46, 204, 113, 0.3)'" onmouseout="this.style.transform=''; this.style.boxShadow='0 4px 12px rgba(46, 204, 113, 0.2)'">🌿 NDVI Analysis</a>
|
||||||
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
|
<a href="/reports" style="padding: 12px 24px; background: linear-gradient(135deg, #ff6b6b, #ee5a6f); color: white; border-radius: 12px; text-decoration: none; font-weight: 600; transition: all 0.3s; box-shadow: 0 4px 12px rgba(255, 107, 107, 0.2);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(255, 107, 107, 0.3)'" onmouseout="this.style.transform=''; this.style.boxShadow='0 4px 12px rgba(255, 107, 107, 0.2)'">📝 Reports</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
<div style="background: white; padding: 15px; border-bottom: 2px solid #e0e0e0; display: flex; gap: 10px; justify-content: center;">
|
<div style="background: linear-gradient(to bottom, rgba(255,255,255,0.9), rgba(248,250,252,0.9)); backdrop-filter: blur(10px); padding: 20px; border-bottom: 1px solid rgba(0,0,0,0.08); display: flex; gap: 12px; justify-content: center;">
|
||||||
<button onclick="switchPredictTab('prediction')" id="tabPrediction" style="padding: 10px 20px; background: #4facfe; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">
|
<button onclick="switchPredictTab('prediction')" id="tabPrediction" style="padding: 14px 32px; background: linear-gradient(135deg, #4facfe, #00f2fe); color: white; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s; box-shadow: 0 4px 15px rgba(79, 172, 254, 0.3); font-size: 1.05em;">
|
||||||
🗺️ Prediction
|
🗺️ Prediction
|
||||||
</button>
|
</button>
|
||||||
<button onclick="switchPredictTab('ndvi')" id="tabNDVI" style="padding: 10px 20px; background: #ccc; color: #666; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">
|
<button onclick="switchPredictTab('ndvi')" id="tabNDVI" style="padding: 14px 32px; background: linear-gradient(135deg, #e5e7eb, #d1d5db); color: #6b7280; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s; box-shadow: 0 2px 8px rgba(0,0,0,0.08); font-size: 1.05em;">
|
||||||
🌿 NDVI Analysis
|
🌿 NDVI Analysis
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,9 +504,82 @@
|
|||||||
<h2>🤖 Chọn Model</h2>
|
<h2>🤖 Chọn Model</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="modelSelect">Model đã train:</label>
|
<label for="modelSelect">Model đã train:</label>
|
||||||
<select id="modelSelect">
|
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
|
||||||
|
<select id="modelSelect" style="flex: 1;">
|
||||||
<option value="">Đang tải...</option>
|
<option value="">Đang tải...</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button onclick="loadModels(); return false;" style="padding: 10px 15px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; white-space: nowrap;">
|
||||||
|
🔄 Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Land Classification Model -->
|
||||||
|
<div style="margin-top: 15px; padding: 10px; background: #f5f5f5; border-radius: 6px;">
|
||||||
|
<input type="file" id="landModelUpload" accept=".pth,.pkl,.joblib,.h5,.keras" style="display: none;" onchange="showLandMetadataForm()">
|
||||||
|
<button onclick="document.getElementById('landModelUpload').click()" style="padding: 8px 15px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">
|
||||||
|
📤 Upload Land Classification Model
|
||||||
|
</button>
|
||||||
|
<span id="uploadLandStatus" style="margin-left: 10px; font-size: 12px; color: #666;"></span>
|
||||||
|
|
||||||
|
<!-- Land Model Metadata Form -->
|
||||||
|
<div id="landMetadataForm" style="display: none; margin-top: 15px; padding: 15px; background: white; border: 2px solid #4CAF50; border-radius: 6px;">
|
||||||
|
<h4 style="margin: 0 0 10px 0; color: #2e7d32;">📝 Model Metadata</h4>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px;">
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Model Type:</label>
|
||||||
|
<select id="uploadModelType" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
<option value="mobilenet">MobileNet</option>
|
||||||
|
<option value="cnn">CNN</option>
|
||||||
|
<option value="swin">Swin Transformer</option>
|
||||||
|
<option value="xgboost">XGBoost</option>
|
||||||
|
<option value="random_forest">Random Forest</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Epoch:</label>
|
||||||
|
<input type="number" id="uploadLandEpoch" min="0" placeholder="e.g., 100" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Train Accuracy (%):</label>
|
||||||
|
<input type="number" id="uploadTrainAcc" step="0.01" min="0" max="100" placeholder="e.g., 95.5" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Val Accuracy (%):</label>
|
||||||
|
<input type="number" id="uploadValAcc" step="0.01" min="0" max="100" placeholder="e.g., 93.2" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Train Loss:</label>
|
||||||
|
<input type="number" id="uploadLandTrainLoss" step="0.0001" min="0" placeholder="e.g., 0.12" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Val Loss:</label>
|
||||||
|
<input type="number" id="uploadLandValLoss" step="0.0001" min="0" placeholder="e.g., 0.18" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Number of Classes:</label>
|
||||||
|
<input type="number" id="uploadNumClasses" min="2" placeholder="e.g., 10" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Input Size:</label>
|
||||||
|
<input type="number" id="uploadInputSize" min="16" placeholder="e.g., 64" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Description (optional):</label>
|
||||||
|
<input type="text" id="uploadLandDescription" placeholder="e.g., Trained on Mekong Delta dataset" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<button onclick="uploadLandModelWithMetadata()" style="flex: 1; padding: 8px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
|
||||||
|
✅ Upload with Metadata
|
||||||
|
</button>
|
||||||
|
<button onclick="cancelLandUpload()" style="padding: 8px 15px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
❌ Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Cache selection dropdown -->
|
<!-- Cache selection dropdown -->
|
||||||
<div class="form-group" style="margin-top:15px;">
|
<div class="form-group" style="margin-top:15px;">
|
||||||
@@ -398,149 +600,184 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>⏰ Thời gian & Dữ liệu</h2>
|
<h2>⏰ Thời gian & Dữ liệu</h2>
|
||||||
|
|
||||||
|
<!-- Date Range -->
|
||||||
|
<div style="background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); padding: 20px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #bae6fd;">
|
||||||
|
<h3 style="margin: 0 0 15px 0; color: #0369a1; font-size: 1.1em; font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
||||||
|
📅 Khoảng thời gian
|
||||||
|
</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<label for="predStartDate">Từ ngày:</label>
|
<label for="predStartDate">Từ ngày:</label>
|
||||||
<input type="date" id="predStartDate" value="2023-03-01">
|
<input type="date" id="predStartDate" value="2023-03-01">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<label for="predEndDate">Đến ngày:</label>
|
<label for="predEndDate">Đến ngày:</label>
|
||||||
<input type="date" id="predEndDate" value="2023-05-31">
|
<input type="date" id="predEndDate" value="2023-05-31">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Parameters -->
|
||||||
|
<div style="background: linear-gradient(135deg, #fef3f2 0%, #fee2e2 100%); padding: 20px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #fecaca;">
|
||||||
|
<h3 style="margin: 0 0 15px 0; color: #b91c1c; font-size: 1.1em; font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
||||||
|
🛰️ Tham số dữ liệu vệ tinh
|
||||||
|
</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<label for="predMaxScenes">Max Scenes:</label>
|
<label for="predMaxScenes">
|
||||||
|
Số lượng ảnh tối đa:
|
||||||
|
<span style="font-size: 0.85em; color: #666; font-weight: 400;">(1-100)</span>
|
||||||
|
</label>
|
||||||
<input type="number" id="predMaxScenes" value="12" min="1" max="100">
|
<input type="number" id="predMaxScenes" value="12" min="1" max="100">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<label for="predCloudCover">Cloud Cover (%):</label>
|
<label for="predCloudCover">
|
||||||
|
Độ phủ mây tối đa:
|
||||||
|
<span style="font-size: 0.85em; color: #666; font-weight: 400;">(0-100%)</span>
|
||||||
|
</label>
|
||||||
<input type="number" id="predCloudCover" value="30" min="0" max="100">
|
<input type="number" id="predCloudCover" value="30" min="0" max="100">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="margin: 15px 0 0 0;">
|
||||||
<div class="form-group">
|
<label for="predResolution">Độ phân giải không gian:</label>
|
||||||
<label for="predResolution">Resolution:</label>
|
|
||||||
<select id="predResolution">
|
<select id="predResolution">
|
||||||
<option value="10">10m (Chi tiết cao - Chậm)</option>
|
<option value="10">10m (Chi tiết cao - Chậm hơn)</option>
|
||||||
<option value="20" selected>20m (Cân bằng)</option>
|
<option value="20" selected>20m (Cân bằng - Khuyến nghị)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Cloud Removal Configuration -->
|
<!-- Cloud Removal Configuration -->
|
||||||
<div class="form-group" style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; border-left: 4px solid #2196F3;">
|
<div style="background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%); padding: 20px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #99f6e4;">
|
||||||
<label for="cloudRemovalMethod" style="font-weight: 600; color: #1976d2; margin-bottom: 10px; display: block;">
|
<h3 style="margin: 0 0 15px 0; color: #0f766e; font-size: 1.1em; font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
||||||
🌥️ Cloud Removal Method
|
🌥️ Loại bỏ mây
|
||||||
</label>
|
</h3>
|
||||||
<select id="cloudRemovalMethod" onchange="handleCloudMethodChange()" style="padding: 10px; width: 100%; border: 2px solid #2196F3; border-radius: 6px; font-size: 14px; cursor: pointer; margin-bottom: 10px;">
|
<div class="form-group" style="margin-bottom: 12px;">
|
||||||
<option value="none">🚫 No Cloud Removal - Keep Original Data</option>
|
<label for="cloudRemovalMethod">Phương pháp:</label>
|
||||||
<option value="classic">Classic (3-step: temporal + median + spatial)</option>
|
<select id="cloudRemovalMethod" onchange="handleCloudMethodChange()">
|
||||||
<option value="hybrid" selected>Hybrid (Classical + ML KNN) - Recommended</option>
|
<option value="none">🚫 Không xử lý - Giữ nguyên dữ liệu gốc</option>
|
||||||
<option value="temporal_only">Temporal Only (Fast)</option>
|
<option value="classic">Classic - 3 bước (Temporal + Median + Spatial)</option>
|
||||||
|
<option value="hybrid" selected>Hybrid (Classical + ML KNN) - Khuyến nghị</option>
|
||||||
|
<option value="temporal_only">Temporal Only - Nhanh nhất</option>
|
||||||
<option value="median_composite">Median Composite</option>
|
<option value="median_composite">Median Composite</option>
|
||||||
<option value="ml_knn">ML KNN (K-Nearest Neighbors)</option>
|
<option value="ml_knn">ML K-Nearest Neighbors</option>
|
||||||
<option value="ml_rf">ML Random Forest</option>
|
<option value="ml_rf">ML Random Forest</option>
|
||||||
<option value="deep">Deep Learning (U-Net)</option>
|
<option value="deep">Deep Learning U-Net - Tốt nhất</option>
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Deep Learning Model Selection (only shown when method is 'deep') -->
|
<!-- Deep Learning Model Selection -->
|
||||||
<div id="cloudModelSelection" style="display: none; margin-top: 10px;">
|
<div id="cloudModelSelection" style="display: none; margin-top: 15px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 8px;">
|
||||||
<label for="cloudModelSelect" style="font-weight: 500; color: #1565c0; margin-bottom: 5px; display: block;">
|
<label for="cloudModelSelect" style="font-weight: 600; color: #0f766e; margin-bottom: 8px; display: block;">
|
||||||
📦 Select Trained Model:
|
📦 Chọn model đã train:
|
||||||
</label>
|
</label>
|
||||||
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; gap: 10px; margin-bottom: 12px;">
|
||||||
<select id="cloudModelSelect" style="flex: 1; padding: 10px; border: 2px solid #64b5f6; border-radius: 6px; font-size: 14px; cursor: pointer;">
|
<select id="cloudModelSelect" style="flex: 1;">
|
||||||
<option value="">Loading models...</option>
|
<option value="">Đang tải models...</option>
|
||||||
</select>
|
</select>
|
||||||
<button onclick="loadCloudRemovalModels(); return false;" style="padding: 10px 15px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; white-space: nowrap;">
|
<button onclick="loadCloudRemovalModels(); return false;" class="btn btn-secondary" style="padding: 10px 16px; margin: 0; font-size: 0.9em;">
|
||||||
🔄 Refresh
|
🔄
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload Model Button -->
|
<!-- Upload Model -->
|
||||||
<div style="margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 6px;">
|
<div style="padding: 12px; background: rgba(76, 175, 80, 0.1); border-radius: 6px; border: 1px dashed #4CAF50;">
|
||||||
<input type="file" id="cloudModelUpload" accept=".pth" style="display: none;" onchange="showMetadataForm()">
|
<input type="file" id="cloudModelUpload" accept=".pth" style="display: none;" onchange="showMetadataForm()">
|
||||||
<button onclick="document.getElementById('cloudModelUpload').click()" style="padding: 8px 15px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">
|
<button onclick="document.getElementById('cloudModelUpload').click()" class="btn btn-success" style="padding: 10px 16px; margin: 0; font-size: 0.9em;">
|
||||||
📤 Upload Cloud Removal Model (.pth)
|
📤 Upload Model (.pth)
|
||||||
</button>
|
</button>
|
||||||
<span id="uploadCloudStatus" style="margin-left: 10px; font-size: 12px; color: #666;"></span>
|
<span id="uploadCloudStatus" style="margin-left: 10px; font-size: 0.85em;"></span>
|
||||||
|
|
||||||
<!-- Metadata Form (shown after file selection) -->
|
<!-- Metadata Form -->
|
||||||
<div id="cloudMetadataForm" style="display: none; margin-top: 15px; padding: 15px; background: white; border: 2px solid #4CAF50; border-radius: 6px;">
|
<div id="cloudMetadataForm" style="display: none; margin-top: 12px; padding: 12px; background: white; border-radius: 6px; border: 1px solid #4CAF50;">
|
||||||
<h4 style="margin: 0 0 10px 0; color: #2e7d32;">📝 Model Metadata</h4>
|
<h4 style="margin: 0 0 10px 0; color: #2e7d32; font-size: 0.95em;">📝 Thông tin Model</h4>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Epoch:</label>
|
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Epoch:</label>
|
||||||
<input type="number" id="uploadEpoch" min="0" placeholder="e.g., 50" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
<input type="number" id="uploadEpoch" min="0" placeholder="50" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Validation Loss:</label>
|
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Val Loss:</label>
|
||||||
<input type="number" id="uploadValLoss" step="0.0001" min="0" placeholder="e.g., 0.0134" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
<input type="number" id="uploadValLoss" step="0.0001" min="0" placeholder="0.0134" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Train Loss:</label>
|
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Train Loss:</label>
|
||||||
<input type="number" id="uploadTrainLoss" step="0.0001" min="0" placeholder="e.g., 0.0142" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
<input type="number" id="uploadTrainLoss" step="0.0001" min="0" placeholder="0.0142" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Input Channels:</label>
|
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Channels In:</label>
|
||||||
<input type="number" id="uploadInChannels" min="1" placeholder="e.g., 6" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
<input type="number" id="uploadInChannels" min="1" placeholder="6" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Output Channels:</label>
|
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Channels Out:</label>
|
||||||
<input type="number" id="uploadOutChannels" min="1" placeholder="e.g., 4" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
<input type="number" id="uploadOutChannels" min="1" placeholder="4" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Use Sentinel-1:</label>
|
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Use S1:</label>
|
||||||
<select id="uploadUseS1" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
<select id="uploadUseS1" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
|
||||||
<option value="true">Yes</option>
|
<option value="true">Yes</option>
|
||||||
<option value="false">No</option>
|
<option value="false">No</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom: 10px;">
|
<div style="margin-bottom: 8px;">
|
||||||
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Description (optional):</label>
|
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Description:</label>
|
||||||
<input type="text" id="uploadDescription" placeholder="e.g., Trained on winter dataset, 50 epochs" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
<input type="text" id="uploadDescription" placeholder="Optional notes" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 10px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<button onclick="uploadCloudModelWithMetadata()" style="flex: 1; padding: 8px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
|
<button onclick="uploadCloudModelWithMetadata()" class="btn btn-success" style="flex: 1; padding: 8px; margin: 0; font-size: 0.85em;">
|
||||||
✅ Upload with Metadata
|
✅ Upload
|
||||||
</button>
|
</button>
|
||||||
<button onclick="cancelUpload()" style="padding: 8px 15px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
<button onclick="cancelUpload()" class="btn btn-secondary" style="padding: 8px 12px; margin: 0; font-size: 0.85em;">
|
||||||
❌ Cancel
|
❌
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="font-size: 12px; color: #1976d2; margin-top: 8px;">
|
<div style="font-size: 0.85em; color: #0f766e; margin-top: 10px; padding: 8px; background: rgba(255,255,255,0.5); border-radius: 6px;">
|
||||||
💡 Hybrid method balances speed and quality. Deep learning provides best results but requires trained model.
|
💡 <strong>Hybrid</strong> cân bằng tốc độ và chất lượng. <strong>Deep Learning</strong> cho kết quả tốt nhất nhưng cần model đã train.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="margin-top: 15px; padding: 15px; background: #fff3e0; border-radius: 8px; border-left: 4px solid #ff9800;">
|
<!-- Processing Options -->
|
||||||
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
|
<div style="background: linear-gradient(135deg, #fef9f3 0%, #fed7aa 100%); padding: 20px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #fdba74;">
|
||||||
<input type="checkbox" id="useGpuPred" checked style="width: 18px; height: 18px; margin-right: 10px;">
|
<h3 style="margin: 0 0 15px 0; color: #c2410c; font-size: 1.1em; font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
||||||
<span style="font-weight: 600; color: #e65100;">🚀 Sử dụng GPU (Deep Learning Models)</span>
|
⚙️ Tùy chọn xử lý
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label style="display: flex; align-items: flex-start; cursor: pointer; margin: 0;">
|
||||||
|
<input type="checkbox" id="useGpuPred" checked style="width: 20px; height: 20px; margin-right: 12px; margin-top: 2px; cursor: pointer;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 600; color: #c2410c; margin-bottom: 4px;">
|
||||||
|
🚀 Sử dụng GPU cho Deep Learning Models
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.85em; color: #9a3412;">
|
||||||
|
Tăng tốc CNN/Swin-UNet (cần GPU có CUDA)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<div style="font-size: 12px; color: #e65100; margin-top: 8px; margin-left: 28px;">
|
|
||||||
⚡ Tăng tốc prediction cho CNN/Swin-UNet models (yêu cầu GPU khả dụng)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="margin-top: 20px; padding: 15px; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #2196F3;">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
|
<label style="display: flex; align-items: flex-start; cursor: pointer; margin: 0;">
|
||||||
<input type="checkbox" id="exportNDVI" checked style="width: 18px; height: 18px; margin-right: 10px;">
|
<input type="checkbox" id="exportNDVI" checked style="width: 20px; height: 20px; margin-right: 12px; margin-top: 2px; cursor: pointer;">
|
||||||
<span style="font-weight: 600; color: #1976d2;">🌿 Export NDVI Raster</span>
|
<div>
|
||||||
|
<div style="font-weight: 600; color: #c2410c; margin-bottom: 4px;">
|
||||||
|
🌿 Xuất NDVI Raster
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.85em; color: #9a3412;">
|
||||||
|
Tạo file GeoTIFF chứa chỉ số NDVI cho toàn bộ khu vực
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<div style="font-size: 12px; color: #1976d2; margin-top: 8px; margin-left: 28px;">
|
|
||||||
✅ Xuất ra file GeoTIFF chứa giá trị NDVI cho toàn bộ khu vực
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" onclick="startPrediction()" id="predictBtn" style="margin-top: 15px;">
|
<button class="btn btn-primary" onclick="startPrediction()" id="predictBtn" style="margin-top: 5px; width: 100%; font-size: 1.1em; padding: 16px;">
|
||||||
🚀 Start Prediction (với NDVI)
|
🚀 Bắt đầu phân loại (với NDVI)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1203,6 +1440,147 @@
|
|||||||
statusSpan.textContent = '';
|
statusSpan.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== LAND CLASSIFICATION MODEL UPLOAD =====
|
||||||
|
|
||||||
|
// Show land model metadata form
|
||||||
|
function showLandMetadataForm() {
|
||||||
|
const fileInput = document.getElementById('landModelUpload');
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const statusSpan = document.getElementById('uploadLandStatus');
|
||||||
|
const metadataForm = document.getElementById('landMetadataForm');
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show form
|
||||||
|
metadataForm.style.display = 'block';
|
||||||
|
statusSpan.textContent = `📁 Selected: ${file.name} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`;
|
||||||
|
statusSpan.style.color = '#2196F3';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload land model with metadata
|
||||||
|
async function uploadLandModelWithMetadata() {
|
||||||
|
const fileInput = document.getElementById('landModelUpload');
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const statusSpan = document.getElementById('uploadLandStatus');
|
||||||
|
const metadataForm = document.getElementById('landMetadataForm');
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
alert('No file selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get values from form with defaults
|
||||||
|
const modelType = document.getElementById('uploadModelType').value || 'mobilenet';
|
||||||
|
const epoch = document.getElementById('uploadLandEpoch').value || '0';
|
||||||
|
const trainAcc = document.getElementById('uploadTrainAcc').value || '0';
|
||||||
|
const valAcc = document.getElementById('uploadValAcc').value || '0';
|
||||||
|
const trainLoss = document.getElementById('uploadLandTrainLoss').value || '0';
|
||||||
|
const valLoss = document.getElementById('uploadLandValLoss').value || '0';
|
||||||
|
const numClasses = document.getElementById('uploadNumClasses').value || '10';
|
||||||
|
const inputSize = document.getElementById('uploadInputSize').value || '64';
|
||||||
|
const description = document.getElementById('uploadLandDescription').value || '';
|
||||||
|
|
||||||
|
// Debug log
|
||||||
|
console.log('[Land Upload] Form values:', {
|
||||||
|
modelType, epoch, trainAcc, valAcc, trainLoss, valLoss, numClasses, inputSize, description
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm upload
|
||||||
|
if (!confirm(`Upload ${file.name}?\nType: ${modelType}\nEpoch: ${epoch}\nVal Acc: ${valAcc}%`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusSpan.textContent = '⏳ Uploading with metadata...';
|
||||||
|
statusSpan.style.color = '#2196F3';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('model_type', modelType);
|
||||||
|
formData.append('epoch', epoch);
|
||||||
|
formData.append('train_accuracy', trainAcc);
|
||||||
|
formData.append('val_accuracy', valAcc);
|
||||||
|
formData.append('train_loss', trainLoss);
|
||||||
|
formData.append('val_loss', valLoss);
|
||||||
|
formData.append('num_classes', numClasses);
|
||||||
|
formData.append('input_size', inputSize);
|
||||||
|
formData.append('description', description);
|
||||||
|
|
||||||
|
console.log('[Land Upload] Sending FormData...');
|
||||||
|
|
||||||
|
const response = await fetch('/api/land-classification/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
console.log('[Land Upload] Response:', result);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
statusSpan.textContent = `✅ Uploaded: ${result.filename} (${result.size_mb} MB)`;
|
||||||
|
statusSpan.style.color = '#4CAF50';
|
||||||
|
|
||||||
|
// Hide form
|
||||||
|
metadataForm.style.display = 'none';
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('uploadModelType').value = 'mobilenet';
|
||||||
|
document.getElementById('uploadLandEpoch').value = '';
|
||||||
|
document.getElementById('uploadTrainAcc').value = '';
|
||||||
|
document.getElementById('uploadValAcc').value = '';
|
||||||
|
document.getElementById('uploadLandTrainLoss').value = '';
|
||||||
|
document.getElementById('uploadLandValLoss').value = '';
|
||||||
|
document.getElementById('uploadNumClasses').value = '';
|
||||||
|
document.getElementById('uploadInputSize').value = '';
|
||||||
|
document.getElementById('uploadLandDescription').value = '';
|
||||||
|
|
||||||
|
// Reload models list
|
||||||
|
await loadModels();
|
||||||
|
|
||||||
|
// Try to auto-select the newly uploaded model
|
||||||
|
setTimeout(() => {
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
for (let i = 0; i < modelSelect.options.length; i++) {
|
||||||
|
if (modelSelect.options[i].text.includes(result.filename)) {
|
||||||
|
modelSelect.selectedIndex = i;
|
||||||
|
updateModelInfo();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
console.log('[Land Upload] Success:', result);
|
||||||
|
} else {
|
||||||
|
statusSpan.textContent = `❌ ${result.detail || 'Upload failed'}`;
|
||||||
|
statusSpan.style.color = 'red';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
statusSpan.textContent = `❌ Error: ${error.message}`;
|
||||||
|
statusSpan.style.color = 'red';
|
||||||
|
console.error('[Land Upload] Error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear file input
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel land model upload
|
||||||
|
function cancelLandUpload() {
|
||||||
|
const fileInput = document.getElementById('landModelUpload');
|
||||||
|
const statusSpan = document.getElementById('uploadLandStatus');
|
||||||
|
const metadataForm = document.getElementById('landMetadataForm');
|
||||||
|
|
||||||
|
// Clear and hide
|
||||||
|
fileInput.value = '';
|
||||||
|
metadataForm.style.display = 'none';
|
||||||
|
statusSpan.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== END LAND CLASSIFICATION UPLOAD =====
|
||||||
|
|
||||||
// Update model info display
|
// Update model info display
|
||||||
function updateModelInfo() {
|
function updateModelInfo() {
|
||||||
const select = document.getElementById('modelSelect');
|
const select = document.getElementById('modelSelect');
|
||||||
|
|||||||
Reference in New Issue
Block a user