change to NAS

This commit is contained in:
Victor Phan
2026-02-15 19:47:58 +07:00
parent 3fe1944b98
commit 3d145f9d54
5 changed files with 1315 additions and 257 deletions
+284
View File
@@ -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
View File
@@ -764,6 +764,211 @@ async def upload_cloud_removal_model(
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}")
async def delete_cloud_removal_model(filename: str):
"""Xóa cloud removal model"""
+278 -87
View File
@@ -12,8 +12,9 @@
}
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-attachment: fixed;
min-height: 100vh;
padding: 20px;
}
@@ -21,45 +22,85 @@
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
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;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
padding: 40px 30px;
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 {
font-size: 2.5em;
margin-bottom: 10px;
font-size: 2.8em;
margin-bottom: 12px;
font-weight: 700;
position: relative;
z-index: 1;
text-shadow: 0 2px 20px rgba(0,0,0,0.2);
}
.header p {
font-size: 1.1em;
opacity: 0.9;
font-size: 1.15em;
opacity: 0.95;
position: relative;
z-index: 1;
font-weight: 400;
}
.nav {
background: #f8f9fa;
padding: 15px 30px;
border-bottom: 2px solid #e9ecef;
background: rgba(255,255,255,0.8);
backdrop-filter: blur(10px);
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 {
color: #667eea;
padding: 12px 24px;
color: white;
text-decoration: none;
margin-right: 20px;
font-weight: 500;
transition: color 0.3s;
border-radius: 12px;
font-weight: 600;
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 {
color: #764ba2;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
.content {
@@ -68,44 +109,72 @@
.section {
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 {
font-size: 1.5em;
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 3px solid #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-size: 1.6em;
font-weight: 700;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.card {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 12px;
padding: 24px;
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 {
margin-bottom: 20px;
position: relative;
}
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
color: #374151;
font-size: 0.95em;
letter-spacing: 0.01em;
}
input[type="text"],
input[type="number"],
select {
width: 100%;
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 12px;
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,
@@ -113,54 +182,98 @@
select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
transform: translateY(-1px);
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
align-items: flex-start;
gap: 12px;
cursor: pointer;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
margin-top: 2px;
}
.btn {
padding: 12px 30px;
padding: 14px 32px;
border: none;
border-radius: 8px;
border-radius: 12px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
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 {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
}
.btn-secondary {
background: #6c757d;
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
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 {
background: #dc3545;
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
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 {
background: #28a745;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
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 {
@@ -170,87 +283,122 @@
}
.model-card {
background: white;
border: 2px solid #e9ecef;
border-radius: 10px;
padding: 20px;
transition: all 0.3s;
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
border: 1px solid rgba(0,0,0,0.08);
border-radius: 14px;
padding: 24px;
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 {
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 {
color: #667eea;
margin-bottom: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 12px;
font-size: 1.2em;
}
.model-info {
font-size: 0.9em;
color: #6c757d;
margin: 5px 0;
color: #6b7280;
margin: 6px 0;
line-height: 1.5;
}
.status-badge {
display: inline-block;
padding: 5px 15px;
padding: 6px 16px;
border-radius: 20px;
font-size: 0.9em;
font-size: 0.85em;
font-weight: 600;
margin-top: 10px;
margin-top: 12px;
}
.status-success {
background: #d4edda;
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
color: #155724;
box-shadow: 0 2px 8px rgba(21, 87, 36, 0.2);
}
.status-training {
background: #fff3cd;
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
color: #856404;
box-shadow: 0 2px 8px rgba(133, 100, 4, 0.2);
}
.status-error {
background: #f8d7da;
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
color: #721c24;
box-shadow: 0 2px 8px rgba(114, 28, 36, 0.2);
}
.progress-bar {
width: 100%;
height: 30px;
background: #e9ecef;
border-radius: 15px;
height: 32px;
background: linear-gradient(to right, #e5e7eb, #f3f4f6);
border-radius: 16px;
overflow: hidden;
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 {
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;
display: flex;
align-items: center;
justify-content: center;
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 {
background: #e7f3ff;
border-left: 4px solid #2196F3;
padding: 15px;
border-radius: 5px;
background: linear-gradient(135deg, #e3f2fd 0%, #f0f7ff 100%);
border-left: 5px solid #2196F3;
padding: 20px;
border-radius: 12px;
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 {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
border-radius: 5px;
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border-left: 5px solid #ffc107;
padding: 20px;
border-radius: 12px;
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 {
@@ -273,16 +421,18 @@
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 8px;
border-radius: 12px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
max-height: 400px;
overflow-y: auto;
margin-top: 20px;
box-shadow: inset 0 2px 10px rgba(0,0,0,0.3);
}
.logs .log-entry {
margin: 5px 0;
padding: 4px 0;
}
.logs .log-info {
@@ -332,9 +482,18 @@
<form id="trainingForm">
<div class="grid-2">
<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>
<label>🏗️ Model Architecture</label>
<select id="modelArchitecture" required>
<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 class="form-group">
@@ -343,6 +502,12 @@
<small style="color: #6c757d;">Tên model để lưu</small>
</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">
<label>📦 Batch Size</label>
<input type="number" id="batchSize" value="8" min="1" max="32" required>
@@ -412,30 +577,54 @@
<!-- Methods Info -->
<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="card">
<h3>🔹 Classic (Default)</h3>
<p>3-step approach: temporal → median → spatial interpolation</p>
<div class="status-badge status-success">Fast</div>
<h3>🔹 U-Net</h3>
<p>Classic encoder-decoder with skip connections. Fast training, good baseline performance.</p>
<div class="status-badge status-success">Recommended for beginners</div>
</div>
<div class="card">
<h3>🔹 Hybrid</h3>
<p>Classical + ML KNN - balanced speed & quality</p>
<div class="status-badge status-success">Recommended</div>
<h3>🔹 CR-GAN</h3>
<p>Cloud Removal GAN - adversarial training cho kết quả chân thực hơn.</p>
<div class="status-badge status-training">Advanced</div>
</div>
<div class="card">
<h3>🔹 ML KNN</h3>
<p>K-Nearest Neighbors inpainting - good quality</p>
<div class="status-badge status-training">Medium Speed</div>
<h3>🔹 SpA-GAN</h3>
<p>Spatial Attention GAN - attention mechanism tập trung vào vùng có mây.</p>
<div class="status-badge status-success">Best quality</div>
</div>
<div class="card">
<h3>🔹 Deep Learning</h3>
<p>U-Net CNN - best quality for large gaps</p>
<div class="status-badge status-error">Requires Model</div>
<h3>🔹 GLF-CR</h3>
<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-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>
@@ -456,6 +645,7 @@
const config = {
data_dir: document.getElementById('dataDir').value,
model_name: document.getElementById('modelName').value,
architecture: document.getElementById('modelArchitecture').value,
use_s1: document.getElementById('useS1').checked,
batch_size: parseInt(document.getElementById('batchSize').value),
num_epochs: parseInt(document.getElementById('numEpochs').value),
@@ -505,6 +695,7 @@
modelsList.innerHTML = data.models.map(model => `
<div class="model-card">
<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">📉 Train Loss: ${model.train_loss.toFixed(6)}</div>
<div class="model-info">📉 Val Loss: ${model.val_loss.toFixed(6)}</div>
View File
+548 -170
View File
@@ -17,8 +17,9 @@
}
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-attachment: fixed;
padding: 20px;
min-height: 100vh;
}
@@ -26,27 +27,53 @@
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
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;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
padding: 40px 30px;
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 {
font-size: 2.5em;
margin-bottom: 10px;
font-size: 2.8em;
margin-bottom: 12px;
font-weight: 700;
position: relative;
z-index: 1;
text-shadow: 0 2px 20px rgba(0,0,0,0.2);
}
.header p {
opacity: 0.9;
font-size: 1.1em;
opacity: 0.95;
font-size: 1.15em;
position: relative;
z-index: 1;
font-weight: 400;
}
.content {
@@ -58,8 +85,9 @@
#predictMap {
height: 500px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
border: 1px solid rgba(0,0,0,0.1);
}
.map-container {
@@ -67,59 +95,95 @@
}
.map-instructions {
background: #e3f2fd;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
border-left: 4px solid #2196f3;
background: linear-gradient(135deg, #e3f2fd 0%, #f0f7ff 100%);
padding: 20px;
border-radius: 16px;
margin-bottom: 20px;
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 {
color: #1976d2;
margin-bottom: 8px;
margin-bottom: 10px;
font-weight: 700;
font-size: 1.1em;
}
.map-instructions p {
color: #555;
margin: 5px 0;
margin: 6px 0;
line-height: 1.6;
}
.section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
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 h2 {
color: #667eea;
margin-bottom: 15px;
font-size: 1.5em;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-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 {
margin-bottom: 15px;
margin-bottom: 20px;
position: relative;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #333;
margin-bottom: 8px;
color: #374151;
font-weight: 600;
font-size: 0.95em;
letter-spacing: 0.01em;
}
.form-group input, .form-group select {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 5px;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 12px;
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 {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
transform: translateY(-1px);
}
.form-row {
@@ -129,43 +193,78 @@
}
.btn {
padding: 12px 30px;
padding: 14px 32px;
border: none;
border-radius: 5px;
border-radius: 12px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
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 {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
}
.btn-primary:active {
transform: translateY(-1px);
}
.btn-success {
background: #28a745;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
}
.btn-success:hover {
background: #218838;
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5);
}
.btn-secondary {
background: #6c757d;
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
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 {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.status-box {
@@ -192,42 +291,68 @@
}
.progress {
height: 30px;
background: #e0e0e0;
border-radius: 15px;
height: 32px;
background: linear-gradient(to right, #e5e7eb, #f3f4f6);
border-radius: 16px;
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 {
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%;
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
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 {
background: white;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
padding: 24px;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
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 {
color: #667eea;
margin-bottom: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 12px;
font-weight: 700;
font-size: 1em;
}
.metric-card .value {
font-size: 2em;
font-weight: bold;
color: #333;
font-size: 2.2em;
font-weight: 800;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.alert {
@@ -260,18 +385,22 @@
}
.prediction-item {
background: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
border-left: 4px solid #667eea;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
padding: 20px;
border-radius: 14px;
margin-bottom: 12px;
border-left: 5px solid #667eea;
display: flex;
justify-content: space-between;
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 {
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) {
@@ -292,22 +421,22 @@
<p>Phân loại đất cho khu vực mới sử dụng model đã train</p>
</div>
<div style="background: white; padding: 15px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; border-bottom: 2px solid #e0e0e0;">
<a href="/" style="padding: 10px 20px; background: #667eea; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🏠 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="/cloud-training" style="padding: 10px 20px; background: #00bcd4; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌥️ 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="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 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="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
<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: 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: 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: 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: 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: 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: 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: 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>
<!-- Tab Navigation -->
<div style="background: white; padding: 15px; border-bottom: 2px solid #e0e0e0; display: flex; gap: 10px; 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;">
<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: 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
</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
</button>
</div>
@@ -375,9 +504,82 @@
<h2>🤖 Chọn Model</h2>
<div class="form-group">
<label for="modelSelect">Model đã train:</label>
<select id="modelSelect">
<option value="">Đang tải...</option>
</select>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
<select id="modelSelect" style="flex: 1;">
<option value="">Đang tải...</option>
</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>
<!-- Cache selection dropdown -->
<div class="form-group" style="margin-top:15px;">
@@ -398,149 +600,184 @@
<div class="section">
<h2>⏰ Thời gian & Dữ liệu</h2>
<div class="form-row">
<div class="form-group">
<label for="predStartDate">Từ ngày:</label>
<input type="date" id="predStartDate" value="2023-03-01">
</div>
<div class="form-group">
<label for="predEndDate">Đến ngày:</label>
<input type="date" id="predEndDate" value="2023-05-31">
<!-- 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-group" style="margin-bottom: 0;">
<label for="predStartDate">Từ ngày:</label>
<input type="date" id="predStartDate" value="2023-03-01">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label for="predEndDate">Đến ngày:</label>
<input type="date" id="predEndDate" value="2023-05-31">
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="predMaxScenes">Max Scenes:</label>
<input type="number" id="predMaxScenes" value="12" min="1" max="100">
<!-- 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-group" style="margin-bottom: 0;">
<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">
</div>
<div class="form-group" style="margin-bottom: 0;">
<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">
</div>
</div>
<div class="form-group">
<label for="predCloudCover">Cloud Cover (%):</label>
<input type="number" id="predCloudCover" value="30" min="0" max="100">
<div class="form-group" style="margin: 15px 0 0 0;">
<label for="predResolution">Độ phân giải không gian:</label>
<select id="predResolution">
<option value="10">10m (Chi tiết cao - Chậm hơn)</option>
<option value="20" selected>20m (Cân bằng - Khuyến nghị)</option>
</select>
</div>
</div>
<div class="form-group">
<label for="predResolution">Resolution:</label>
<select id="predResolution">
<option value="10">10m (Chi tiết cao - Chậm)</option>
<option value="20" selected>20m (Cân bằng)</option>
</select>
</div>
<!-- Cloud Removal Configuration -->
<div class="form-group" style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; border-left: 4px solid #2196F3;">
<label for="cloudRemovalMethod" style="font-weight: 600; color: #1976d2; margin-bottom: 10px; display: block;">
🌥️ Cloud Removal Method
</label>
<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;">
<option value="none">🚫 No Cloud Removal - Keep Original Data</option>
<option value="classic">Classic (3-step: temporal + median + spatial)</option>
<option value="hybrid" selected>Hybrid (Classical + ML KNN) - Recommended</option>
<option value="temporal_only">Temporal Only (Fast)</option>
<option value="median_composite">Median Composite</option>
<option value="ml_knn">ML KNN (K-Nearest Neighbors)</option>
<option value="ml_rf">ML Random Forest</option>
<option value="deep">Deep Learning (U-Net)</option>
</select>
<div style="background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%); padding: 20px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #99f6e4;">
<h3 style="margin: 0 0 15px 0; color: #0f766e; font-size: 1.1em; font-weight: 600; display: flex; align-items: center; gap: 8px;">
🌥️ Loại bỏ mây
</h3>
<div class="form-group" style="margin-bottom: 12px;">
<label for="cloudRemovalMethod">Phương pháp:</label>
<select id="cloudRemovalMethod" onchange="handleCloudMethodChange()">
<option value="none">🚫 Không xử lý - Giữ nguyên dữ liệu gốc</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="ml_knn">ML K-Nearest Neighbors</option>
<option value="ml_rf">ML Random Forest</option>
<option value="deep">Deep Learning U-Net - Tốt nhất</option>
</select>
</div>
<!-- Deep Learning Model Selection (only shown when method is 'deep') -->
<div id="cloudModelSelection" style="display: none; margin-top: 10px;">
<label for="cloudModelSelect" style="font-weight: 500; color: #1565c0; margin-bottom: 5px; display: block;">
📦 Select Trained Model:
<!-- Deep Learning Model Selection -->
<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: 600; color: #0f766e; margin-bottom: 8px; display: block;">
📦 Chọn model đã train:
</label>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
<select id="cloudModelSelect" style="flex: 1; padding: 10px; border: 2px solid #64b5f6; border-radius: 6px; font-size: 14px; cursor: pointer;">
<option value="">Loading models...</option>
<div style="display: flex; gap: 10px; margin-bottom: 12px;">
<select id="cloudModelSelect" style="flex: 1;">
<option value="">Đang tải models...</option>
</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;">
🔄 Refresh
<button onclick="loadCloudRemovalModels(); return false;" class="btn btn-secondary" style="padding: 10px 16px; margin: 0; font-size: 0.9em;">
🔄
</button>
</div>
<!-- Upload Model Button -->
<div style="margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 6px;">
<!-- Upload Model -->
<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()">
<button onclick="document.getElementById('cloudModelUpload').click()" style="padding: 8px 15px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">
📤 Upload Cloud Removal Model (.pth)
<button onclick="document.getElementById('cloudModelUpload').click()" class="btn btn-success" style="padding: 10px 16px; margin: 0; font-size: 0.9em;">
📤 Upload Model (.pth)
</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) -->
<div id="cloudMetadataForm" 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;">
<!-- Metadata Form -->
<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; font-size: 0.95em;">📝 Thông tin Model</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
<div>
<label style="font-size: 12px; 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;">
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Epoch:</label>
<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>
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Validation 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;">
<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="0.0134" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
</div>
<div>
<label style="font-size: 12px; 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;">
<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="0.0142" style="width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em;">
</div>
<div>
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Input Channels:</label>
<input type="number" id="uploadInChannels" min="1" placeholder="e.g., 6" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Channels In:</label>
<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>
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Output Channels:</label>
<input type="number" id="uploadOutChannels" min="1" placeholder="e.g., 4" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Channels Out:</label>
<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>
<label style="font-size: 12px; color: #666; display: block; margin-bottom: 3px;">Use Sentinel-1:</label>
<select id="uploadUseS1" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Use S1:</label>
<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="false">No</option>
</select>
</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="uploadDescription" placeholder="e.g., Trained on winter dataset, 50 epochs" style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
<div style="margin-bottom: 8px;">
<label style="font-size: 0.8em; color: #666; display: block; margin-bottom: 3px;">Description:</label>
<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 style="display: flex; gap: 10px;">
<button onclick="uploadCloudModelWithMetadata()" style="flex: 1; padding: 8px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">
✅ Upload with Metadata
<div style="display: flex; gap: 8px;">
<button onclick="uploadCloudModelWithMetadata()" class="btn btn-success" style="flex: 1; padding: 8px; margin: 0; font-size: 0.85em;">
✅ Upload
</button>
<button onclick="cancelUpload()" style="padding: 8px 15px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">
Cancel
<button onclick="cancelUpload()" class="btn btn-secondary" style="padding: 8px 12px; margin: 0; font-size: 0.85em;">
</button>
</div>
</div>
</div>
</div>
<div style="font-size: 12px; color: #1976d2; margin-top: 8px;">
💡 Hybrid method balances speed and quality. Deep learning provides best results but requires trained model.
<div style="font-size: 0.85em; color: #0f766e; margin-top: 10px; padding: 8px; background: rgba(255,255,255,0.5); border-radius: 6px;">
💡 <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 class="form-group" style="margin-top: 15px; padding: 15px; background: #fff3e0; border-radius: 8px; border-left: 4px solid #ff9800;">
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
<input type="checkbox" id="useGpuPred" checked style="width: 18px; height: 18px; margin-right: 10px;">
<span style="font-weight: 600; color: #e65100;">🚀 Sử dụng GPU (Deep Learning Models)</span>
</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)
<!-- Processing Options -->
<div style="background: linear-gradient(135deg, #fef9f3 0%, #fed7aa 100%); padding: 20px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #fdba74;">
<h3 style="margin: 0 0 15px 0; color: #c2410c; font-size: 1.1em; font-weight: 600; display: flex; align-items: center; gap: 8px;">
⚙️ 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>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label style="display: flex; align-items: flex-start; cursor: pointer; margin: 0;">
<input type="checkbox" id="exportNDVI" 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;">
🌿 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>
</div>
</div>
<div class="form-group" style="margin-top: 20px; padding: 15px; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #2196F3;">
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
<input type="checkbox" id="exportNDVI" checked style="width: 18px; height: 18px; margin-right: 10px;">
<span style="font-weight: 600; color: #1976d2;">🌿 Export NDVI Raster</span>
</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>
<button class="btn btn-primary" onclick="startPrediction()" id="predictBtn" style="margin-top: 15px;">
🚀 Start Prediction (với NDVI)
<button class="btn btn-primary" onclick="startPrediction()" id="predictBtn" style="margin-top: 5px; width: 100%; font-size: 1.1em; padding: 16px;">
🚀 Bắt đầu phân loại (với NDVI)
</button>
</div>
@@ -1203,6 +1440,147 @@
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
function updateModelInfo() {
const select = document.getElementById('modelSelect');