Primera app funcional
This commit is contained in:
175
lib/screens/citizen/ai_camera_screen.dart
Normal file
175
lib/screens/citizen/ai_camera_screen.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:tflite_flutter/tflite_flutter.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
List<CameraDescription> _cameras = [];
|
||||
|
||||
class AiCameraScreen extends StatefulWidget {
|
||||
const AiCameraScreen({super.key});
|
||||
@override State<AiCameraScreen> createState() => _AiCameraScreenState();
|
||||
}
|
||||
|
||||
class _AiCameraScreenState extends State<AiCameraScreen> {
|
||||
CameraController? _cam;
|
||||
Interpreter? _interpreter;
|
||||
bool _processing = false;
|
||||
String _result = 'Apunta a un residuo y escanea';
|
||||
String _confidence = '';
|
||||
bool _modelLoaded = false;
|
||||
|
||||
// 0=Orgánico, 1=Inorgánico (según waste_classification_model)
|
||||
final _labels = ['Residuo Orgánico ♻️', 'Residuo Inorgánico 🗑️'];
|
||||
final _labelColors = [AppColors.verdeExito, AppColors.naranjaAlerta];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
_cameras = await availableCameras();
|
||||
} catch (_) {}
|
||||
await _initCamera();
|
||||
await _loadModel();
|
||||
}
|
||||
|
||||
Future<void> _initCamera() async {
|
||||
if (_cameras.isEmpty) return;
|
||||
_cam = CameraController(_cameras[0], ResolutionPreset.medium, enableAudio: false);
|
||||
try {
|
||||
await _cam!.initialize();
|
||||
if (mounted) setState(() {});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _loadModel() async {
|
||||
try {
|
||||
_interpreter = await Interpreter.fromAsset('assets/models/waste_model.tflite');
|
||||
setState(() => _modelLoaded = true);
|
||||
} catch (e) {
|
||||
setState(() => _result = '⚠️ Modelo no encontrado.\nAgrega waste_model.tflite a assets/models/');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _classify() async {
|
||||
if (_cam == null || !_cam!.value.isInitialized || _processing || !_modelLoaded) return;
|
||||
setState(() { _processing = true; _result = 'Analizando...'; _confidence = ''; });
|
||||
try {
|
||||
final pic = await _cam!.takePicture();
|
||||
final raw = await File(pic.path).readAsBytes();
|
||||
img.Image? decoded = img.decodeImage(raw);
|
||||
if (decoded == null) throw Exception('No se pudo decodificar');
|
||||
final resized = img.copyResize(decoded, width: 150, height: 150);
|
||||
|
||||
var input = List.generate(1, (_) =>
|
||||
List.generate(150, (_) => List.generate(150, (_) => List.generate(3, (_) => 0.0))));
|
||||
|
||||
for (int y = 0; y < 150; y++) {
|
||||
for (int x = 0; x < 150; x++) {
|
||||
final px = resized.getPixel(x, y);
|
||||
input[0][y][x][0] = px.r / 255.0;
|
||||
input[0][y][x][1] = px.g / 255.0;
|
||||
input[0][y][x][2] = px.b / 255.0;
|
||||
}
|
||||
}
|
||||
var output = List.filled(2, 0.0).reshape([1, 2]);
|
||||
_interpreter!.run(input, output);
|
||||
|
||||
final pred = List<double>.from(output[0]);
|
||||
final maxIdx = pred[0] > pred[1] ? 0 : 1;
|
||||
final conf = pred[maxIdx] * 100;
|
||||
|
||||
await File(pic.path).delete();
|
||||
setState(() {
|
||||
_result = _labels[maxIdx];
|
||||
_confidence = 'Confianza: ${conf.toStringAsFixed(1)}%';
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _result = 'Error en análisis');
|
||||
} finally {
|
||||
setState(() => _processing = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cam?.dispose();
|
||||
_interpreter?.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final resultColor = _result.contains('Orgánico') ? AppColors.verdeExito
|
||||
: _result.contains('Inorgánico') ? AppColors.naranjaAlerta
|
||||
: AppColors.guindaPrimary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: const Text('Clasificador IA de Residuos'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
),
|
||||
body: Column(children: [
|
||||
// Visor cámara
|
||||
Expanded(flex: 4,
|
||||
child: Container(margin: const EdgeInsets.all(14),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.guindaPrimary, width: 3)),
|
||||
child: _cam != null && _cam!.value.isInitialized
|
||||
? CameraPreview(_cam!)
|
||||
: const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.camera_alt, color: Colors.white54, size: 48),
|
||||
SizedBox(height: 8),
|
||||
Text('Iniciando cámara...', style: TextStyle(color: Colors.white54)),
|
||||
])),
|
||||
),
|
||||
),
|
||||
// Panel resultado
|
||||
Expanded(flex: 2,
|
||||
child: Container(width: double.infinity,
|
||||
decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.06),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(28))),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text(_result, textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: resultColor)),
|
||||
if (_confidence.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(_confidence, style: const TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w500)),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
if (!_modelLoaded)
|
||||
Container(padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade300)),
|
||||
child: const Text('ℹ️ Para usar la IA, coloca waste_model.tflite en assets/models/',
|
||||
textAlign: TextAlign.center, style: TextStyle(fontSize: 11))),
|
||||
if (_modelLoaded)
|
||||
SizedBox(width: double.infinity, height: 50,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _processing ? null : _classify,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14))),
|
||||
icon: _processing
|
||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: const Icon(Icons.center_focus_strong),
|
||||
label: Text(_processing ? 'Procesando...' : 'Escanear Residuo',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user