176 lines
6.8 KiB
Dart
176 lines
6.8 KiB
Dart
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 toca el botón';
|
||
String _confidence = '';
|
||
bool _modelLoaded = false;
|
||
|
||
// 0=Orgánico, 1=Inorgánico (según waste_classification_model)
|
||
final _labels = ['Residuo Organico', 'Residuo Inorganico'];
|
||
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)),
|
||
)),
|
||
]),
|
||
),
|
||
),
|
||
]),
|
||
);
|
||
}
|
||
}
|