370 lines
14 KiB
HTML
370 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Upload Gambar{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8">
|
|
<div class="card shadow">
|
|
<div class="card-header bg-success text-white">
|
|
<h4 class="mb-0">Upload Gambar</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
|
|
<!-- Info Panel -->
|
|
<div class="alert alert-info mb-4">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
<strong>Petunjuk:</strong> Upload gambar sapi dengan format JPG, JPEG, PNG, atau BMP.
|
|
Pastikan gambar jelas dan bagian tubuh sapi terlihat dengan baik.
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
{% if error %}
|
|
<div class="alert alert-danger">
|
|
<i class="fas fa-exclamation-circle me-2"></i>
|
|
{{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Upload Form -->
|
|
<form method="POST" action="{{ url_for('predict') }}" enctype="multipart/form-data" id="uploadForm">
|
|
<div class="mb-4">
|
|
<label for="file" class="form-label fw-bold">Pilih Gambar</label>
|
|
|
|
<!-- Custom File Upload -->
|
|
<div class="upload-area border border-2 border-dashed rounded p-4 text-center"
|
|
id="uploadArea">
|
|
<i class="fas fa-cloud-upload-alt fa-4x text-success mb-3"></i>
|
|
<h5>Drag & Drop, Pilih dari Galeri, atau Ambil dari Kamera</h5>
|
|
<p class="text-muted mb-3">Format: JPG, JPEG, PNG, BMP (Maks. 16MB)</p>
|
|
<input type="file" class="form-control d-none" id="file" name="image"
|
|
accept="image/*" required>
|
|
<div class="d-flex flex-wrap justify-content-center gap-2">
|
|
<button type="button" class="btn btn-success" id="browseBtn">
|
|
<i class="fas fa-images me-2"></i>Galeri
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success" id="cameraBtn">
|
|
<i class="fas fa-camera me-2"></i>Kamera
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Info -->
|
|
<div id="fileInfo" class="mt-3 d-none">
|
|
<div class="alert alert-info">
|
|
<span id="fileName"></span>
|
|
<button type="button" class="btn-close float-end" id="removeFile"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Preview -->
|
|
<div class="mb-4 text-center" id="previewContainer" style="display: none;">
|
|
<h6 class="mb-3">Preview Gambar:</h6>
|
|
<div class="preview-wrapper">
|
|
<img id="preview" class="img-fluid rounded shadow"
|
|
style="max-height: 300px; border: 3px solid #28a745;" alt="Preview">
|
|
</div>
|
|
<p class="text-muted mt-2 small" id="imageDimensions"></p>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<button type="submit" class="btn btn-success btn-lg w-100" id="submitBtn" disabled>
|
|
Upload
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Loading Indicator -->
|
|
<div id="loading" class="text-center mt-4 d-none">
|
|
<div class="spinner-border text-success" style="width: 3rem; height: 3rem;" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 fw-bold">Sedang menganalisis gambar...</p>
|
|
<p class="text-muted small">Proses ini membutuhkan waktu beberapa detik</p>
|
|
</div>
|
|
|
|
<!-- Format yang Didukung -->
|
|
<div class="mt-4">
|
|
<h6 class="mb-2">Format yang Didukung:</h6>
|
|
<div class="d-flex gap-2">
|
|
<span class="badge bg-secondary">JPG</span>
|
|
<span class="badge bg-secondary">JPEG</span>
|
|
<span class="badge bg-secondary">PNG</span>
|
|
<span class="badge bg-secondary">BMP</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tips Card -->
|
|
<div class="card mt-4">
|
|
<div class="card-header bg-info text-white">
|
|
<h5 class="mb-0"><i class="fas fa-lightbulb me-2"></i>Tips Pengambilan Gambar</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6><i class="fas fa-check-circle text-success me-2"></i>Disarankan:</h6>
|
|
<ul class="list-unstyled">
|
|
<li><i class="fas fa-arrow-right text-success me-2"></i>Gambar dengan pencahayaan cukup</li>
|
|
<li><i class="fas fa-arrow-right text-success me-2"></i>Fokus pada bagian mulut dan kaki</li>
|
|
<li><i class="fas fa-arrow-right text-success me-2"></i>Ambil dari jarak dekat (0.5-1 meter)</li>
|
|
<li><i class="fas fa-arrow-right text-success me-2"></i>Gunakan latar belakang polos</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6><i class="fas fa-times-circle text-danger me-2"></i>Tidak Disarankan:</h6>
|
|
<ul class="list-unstyled">
|
|
<li><i class="fas fa-times text-danger me-2"></i>Gambar buram atau blur</li>
|
|
<li><i class="fas fa-times text-danger me-2"></i>Terlalu jauh dari objek</li>
|
|
<li><i class="fas fa-times text-danger me-2"></i>Pencahayaan kurang/tidak merata</li>
|
|
<li><i class="fas fa-times text-danger me-2"></i>Banyak objek dalam satu frame</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.upload-area {
|
|
border: 2px dashed #28a745 !important;
|
|
background-color: #f8f9fa;
|
|
transition: all 0.3s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.upload-area:hover {
|
|
background-color: #e9ecef;
|
|
border-color: #218838 !important;
|
|
}
|
|
|
|
.upload-area.dragover {
|
|
background-color: #d4edda;
|
|
border-color: #28a745 !important;
|
|
}
|
|
|
|
.preview-wrapper {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
#removeFile {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-outline-success {
|
|
border-color: #28a745;
|
|
color: #28a745;
|
|
}
|
|
|
|
.btn-outline-success:hover {
|
|
background-color: #28a745;
|
|
color: #fff;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const uploadArea = document.getElementById('uploadArea');
|
|
const fileInput = document.getElementById('file');
|
|
const browseBtn = document.getElementById('browseBtn');
|
|
const cameraBtn = document.getElementById('cameraBtn');
|
|
const fileInfo = document.getElementById('fileInfo');
|
|
const fileName = document.getElementById('fileName');
|
|
const removeFile = document.getElementById('removeFile');
|
|
const previewContainer = document.getElementById('previewContainer');
|
|
const preview = document.getElementById('preview');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const loading = document.getElementById('loading');
|
|
const imageDimensions = document.getElementById('imageDimensions');
|
|
const uploadForm = document.getElementById('uploadForm');
|
|
|
|
// Click on upload area
|
|
uploadArea.addEventListener('click', function() {
|
|
fileInput.click();
|
|
});
|
|
|
|
// Browse button click
|
|
browseBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
fileInput.removeAttribute('capture');
|
|
fileInput.setAttribute('accept', 'image/*');
|
|
fileInput.click();
|
|
});
|
|
|
|
// Camera button click
|
|
cameraBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
fileInput.setAttribute('accept', 'image/*');
|
|
fileInput.setAttribute('capture', 'environment');
|
|
fileInput.click();
|
|
});
|
|
|
|
// Drag and drop events
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
uploadArea.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
uploadArea.addEventListener(eventName, highlight, false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
uploadArea.addEventListener(eventName, unhighlight, false);
|
|
});
|
|
|
|
function highlight() {
|
|
uploadArea.classList.add('dragover');
|
|
}
|
|
|
|
function unhighlight() {
|
|
uploadArea.classList.remove('dragover');
|
|
}
|
|
|
|
uploadArea.addEventListener('drop', handleDrop, false);
|
|
|
|
function handleDrop(e) {
|
|
const dt = e.dataTransfer;
|
|
const files = dt.files;
|
|
fileInput.files = files;
|
|
handleFileSelect(files[0]);
|
|
}
|
|
|
|
// File input change
|
|
fileInput.addEventListener('change', function() {
|
|
if (this.files[0]) {
|
|
handleFileSelect(this.files[0]);
|
|
}
|
|
});
|
|
|
|
// Handle file selection
|
|
function handleFileSelect(file) {
|
|
// Clear previous validation messages immediately
|
|
const oldValidationMsg = document.getElementById('validationMsg');
|
|
if (oldValidationMsg) oldValidationMsg.remove();
|
|
|
|
// Check file size (16MB max)
|
|
if (file.size > 16 * 1024 * 1024) {
|
|
alert('File terlalu besar. Maksimal 16MB.');
|
|
fileInput.value = '';
|
|
return;
|
|
}
|
|
|
|
// Check file type
|
|
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp'];
|
|
if (!allowedTypes.includes(file.type)) {
|
|
alert('Tipe file tidak didukung. Gunakan JPG, JPEG, PNG, atau BMP.');
|
|
fileInput.value = '';
|
|
return;
|
|
}
|
|
|
|
// Show file info
|
|
fileName.textContent = `${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
|
|
fileInfo.classList.remove('d-none');
|
|
|
|
// Show preview & get dimensions
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
preview.src = e.target.result;
|
|
previewContainer.style.display = 'block';
|
|
|
|
// Get image dimensions
|
|
const img = new Image();
|
|
img.onload = function() {
|
|
imageDimensions.textContent = `${this.width} x ${this.height} pixels`;
|
|
};
|
|
img.src = e.target.result;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
// ===== REAL-TIME VALIDATION =====
|
|
// Disable submit button sampai validasi selesai
|
|
submitBtn.disabled = true;
|
|
|
|
// Show loading message
|
|
const validationMsg = document.createElement('div');
|
|
validationMsg.id = 'validationMsg';
|
|
validationMsg.innerHTML = '<div class="alert alert-info mt-3">Sedang memvalidasi gambar...</div>';
|
|
fileInfo.parentElement.insertBefore(validationMsg, fileInfo.nextSibling);
|
|
|
|
// Call validation API
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
fetch('/api/validate-image', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Remove old message
|
|
const oldMsg = document.getElementById('validationMsg');
|
|
if (oldMsg) oldMsg.remove();
|
|
|
|
if (data.is_cattle) {
|
|
// Gambar VALID
|
|
const successMsg = document.createElement('div');
|
|
successMsg.id = 'validationMsg';
|
|
successMsg.innerHTML = `<div class="alert alert-success mt-3">${data.message}</div>`;
|
|
fileInfo.parentElement.insertBefore(successMsg, fileInfo.nextSibling);
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Deteksi';
|
|
} else {
|
|
// Gambar INVALID
|
|
const errorMsg = document.createElement('div');
|
|
errorMsg.id = 'validationMsg';
|
|
errorMsg.innerHTML = `<div class="alert alert-danger mt-3">${data.message || 'Gambar ditolak'}</div>`;
|
|
fileInfo.parentElement.insertBefore(errorMsg, fileInfo.nextSibling);
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Upload';
|
|
|
|
// Clear file input and preview
|
|
fileInput.value = '';
|
|
fileInfo.classList.add('d-none');
|
|
previewContainer.style.display = 'none';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
const oldMsg = document.getElementById('validationMsg');
|
|
if (oldMsg) oldMsg.remove();
|
|
|
|
const errorMsg = document.createElement('div');
|
|
errorMsg.id = 'validationMsg';
|
|
errorMsg.innerHTML = `<div class="alert alert-warning mt-3">Error: ${error}</div>`;
|
|
fileInfo.parentElement.insertBefore(errorMsg, fileInfo.nextSibling);
|
|
submitBtn.disabled = true;
|
|
});
|
|
}
|
|
|
|
// Remove file
|
|
removeFile.addEventListener('click', function() {
|
|
fileInput.value = '';
|
|
fileInfo.classList.add('d-none');
|
|
previewContainer.style.display = 'none';
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Upload';
|
|
|
|
// Clear validation message
|
|
const validationMsg = document.getElementById('validationMsg');
|
|
if (validationMsg) validationMsg.remove();
|
|
});
|
|
|
|
// Form submit
|
|
uploadForm.addEventListener('submit', function(e) {
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Sedang diproses...';
|
|
loading.classList.remove('d-none');
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |