add preview dataset
This commit is contained in:
@@ -220,12 +220,39 @@ def health():
|
|||||||
def dataset_count():
|
def dataset_count():
|
||||||
"""Endpoint para ver cuántas capturas hay en el dataset"""
|
"""Endpoint para ver cuántas capturas hay en el dataset"""
|
||||||
try:
|
try:
|
||||||
files = os.listdir(DATASET_DIR)
|
files = [f for f in os.listdir(DATASET_DIR) if f.endswith('.jpg')]
|
||||||
plates = len([f for f in files if f.endswith('_plate.jpg')])
|
return {"plates_captured": len(files), "total_files": len(files)}
|
||||||
return {"plates_captured": plates, "total_files": len(files)}
|
|
||||||
except:
|
except:
|
||||||
return {"plates_captured": 0, "total_files": 0}
|
return {"plates_captured": 0, "total_files": 0}
|
||||||
|
|
||||||
|
@app.route("/dataset/list")
|
||||||
|
def dataset_list():
|
||||||
|
"""Lista todas las imágenes del dataset"""
|
||||||
|
try:
|
||||||
|
files = [f for f in os.listdir(DATASET_DIR) if f.endswith('.jpg')]
|
||||||
|
# Ordenar por fecha (más recientes primero)
|
||||||
|
files.sort(reverse=True)
|
||||||
|
|
||||||
|
images = []
|
||||||
|
for f in files[:50]: # Limitar a últimas 50
|
||||||
|
parts = f.replace('.jpg', '').split('_')
|
||||||
|
plate = parts[0] if parts else 'Unknown'
|
||||||
|
images.append({
|
||||||
|
'filename': f,
|
||||||
|
'plate': plate,
|
||||||
|
'url': f'/dataset/images/{f}'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"images": images, "total": len(files)}
|
||||||
|
except Exception as e:
|
||||||
|
return {"images": [], "total": 0, "error": str(e)}
|
||||||
|
|
||||||
|
@app.route("/dataset/images/<filename>")
|
||||||
|
def dataset_image(filename):
|
||||||
|
"""Sirve una imagen específica del dataset"""
|
||||||
|
from flask import send_from_directory
|
||||||
|
return send_from_directory(DATASET_DIR, filename)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
t = threading.Thread(target=camera_loop, daemon=True)
|
t = threading.Thread(target=camera_loop, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
import { Users, CheckCircle, XCircle, Shield, Trash2, Camera, AlertCircle, Database } from 'lucide-react';
|
import { Users, CheckCircle, XCircle, Shield, Trash2, Camera, AlertCircle, Database, X, Image } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import LanguageSelector from '../components/LanguageSelector';
|
import LanguageSelector from '../components/LanguageSelector';
|
||||||
|
|
||||||
@@ -28,6 +28,9 @@ function AdminDashboard({ token }) {
|
|||||||
|
|
||||||
// Dataset State
|
// Dataset State
|
||||||
const [datasetCount, setDatasetCount] = useState(0);
|
const [datasetCount, setDatasetCount] = useState(0);
|
||||||
|
const [showDatasetModal, setShowDatasetModal] = useState(false);
|
||||||
|
const [datasetImages, setDatasetImages] = useState([]);
|
||||||
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -107,6 +110,20 @@ function AdminDashboard({ token }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchDatasetImages = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/dataset/list');
|
||||||
|
setDatasetImages(res.data.images || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching dataset images');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDatasetModal = () => {
|
||||||
|
fetchDatasetImages();
|
||||||
|
setShowDatasetModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSearchRut = (e) => {
|
const handleSearchRut = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const normalizedRut = searchRut.replace(/\./g, '').toUpperCase();
|
const normalizedRut = searchRut.replace(/\./g, '').toUpperCase();
|
||||||
@@ -221,12 +238,15 @@ function AdminDashboard({ token }) {
|
|||||||
<Camera /> {t('monitor_area')}
|
<Camera /> {t('monitor_area')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Dataset Counter */}
|
{/* Dataset Counter - Clickable */}
|
||||||
<div className="flex items-center gap-2 bg-gradient-to-r from-emerald-900/50 to-teal-900/50 px-4 py-2 rounded-lg border border-emerald-700/50">
|
<button
|
||||||
|
onClick={openDatasetModal}
|
||||||
|
className="flex items-center gap-2 bg-gradient-to-r from-emerald-900/50 to-teal-900/50 px-4 py-2 rounded-lg border border-emerald-700/50 hover:from-emerald-800/50 hover:to-teal-800/50 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
<Database size={18} className="text-emerald-400" />
|
<Database size={18} className="text-emerald-400" />
|
||||||
<span className="text-emerald-300 font-mono font-bold">{datasetCount}</span>
|
<span className="text-emerald-300 font-mono font-bold">{datasetCount}</span>
|
||||||
<span className="text-emerald-400/70 text-sm">capturas</span>
|
<span className="text-emerald-400/70 text-sm">capturas</span>
|
||||||
</div>
|
</button>
|
||||||
<div className="flex bg-slate-800 rounded p-1">
|
<div className="flex bg-slate-800 rounded p-1">
|
||||||
<button onClick={() => setViewMode('live')} className={`px-3 py-1 rounded ${viewMode === 'live' ? 'bg-blue-600' : ''}`}>Live</button>
|
<button onClick={() => setViewMode('live')} className={`px-3 py-1 rounded ${viewMode === 'live' ? 'bg-blue-600' : ''}`}>Live</button>
|
||||||
<button onClick={() => setViewMode('history')} className={`px-3 py-1 rounded ${viewMode === 'history' ? 'bg-blue-600' : ''}`}>History</button>
|
<button onClick={() => setViewMode('history')} className={`px-3 py-1 rounded ${viewMode === 'history' ? 'bg-blue-600' : ''}`}>History</button>
|
||||||
@@ -482,10 +502,81 @@ function AdminDashboard({ token }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Dataset Gallery Modal */}
|
||||||
|
{showDatasetModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-slate-900 rounded-2xl border border-slate-700 w-full max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-slate-700">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Database className="text-emerald-400" size={24} />
|
||||||
|
<h2 className="text-xl font-bold text-white">Dataset de Capturas</h2>
|
||||||
|
<span className="bg-emerald-600 px-2 py-1 rounded text-sm font-mono">{datasetImages.length} imágenes</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowDatasetModal(false); setSelectedImage(null); }}
|
||||||
|
className="text-slate-400 hover:text-white p-2 hover:bg-slate-800 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-auto p-4">
|
||||||
|
{selectedImage ? (
|
||||||
|
/* Image Preview */
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedImage(null)}
|
||||||
|
className="self-start flex items-center gap-2 text-slate-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
← Volver a galería
|
||||||
|
</button>
|
||||||
|
<img
|
||||||
|
src={selectedImage.url}
|
||||||
|
alt={selectedImage.plate}
|
||||||
|
className="max-w-full max-h-[60vh] rounded-lg border border-slate-700"
|
||||||
|
/>
|
||||||
|
<div className="bg-slate-800 px-4 py-2 rounded-lg">
|
||||||
|
<span className="text-slate-400">Patente: </span>
|
||||||
|
<span className="font-mono font-bold text-emerald-400 text-lg">{selectedImage.plate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Image Grid */
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
|
{datasetImages.map((img, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setSelectedImage(img)}
|
||||||
|
className="cursor-pointer group relative aspect-video bg-slate-800 rounded-lg overflow-hidden border border-slate-700 hover:border-emerald-500 transition-all"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={img.url}
|
||||||
|
alt={img.plate}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-2">
|
||||||
|
<span className="font-mono text-sm text-emerald-300 font-bold">{img.plate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{datasetImages.length === 0 && (
|
||||||
|
<div className="col-span-full text-center py-12 text-slate-500">
|
||||||
|
<Image size={48} className="mx-auto mb-4 opacity-50" />
|
||||||
|
<p>No hay capturas en el dataset</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminDashboard;
|
export default AdminDashboard;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user