deploy model Yolov5 to camera, tts, and bounding box
This commit is contained in:
parent
04383690d1
commit
61264de620
|
|
@ -1,10 +1,12 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../services/tts_service.dart';
|
||||
import '../services/yolo_detection_service.dart';
|
||||
|
||||
class CameraScreen extends StatefulWidget {
|
||||
const CameraScreen({super.key});
|
||||
|
||||
@override
|
||||
_CameraScreenState createState() => _CameraScreenState();
|
||||
State<CameraScreen> createState() => _CameraScreenState();
|
||||
}
|
||||
|
||||
class _CameraScreenState extends State<CameraScreen> {
|
||||
CameraController? controller;
|
||||
List<CameraDescription>? cameras;
|
||||
|
||||
final YoloDetectionService _detectionService = YoloDetectionService();
|
||||
final TTSService _tts = TTSService();
|
||||
|
||||
bool isRecording = false;
|
||||
bool isDetecting = false;
|
||||
String statusMessage = "Tekan tombol rekam untuk mulai deteksi";
|
||||
List<YoloDetection> detectedObjects = const [];
|
||||
Size? detectionFrameSize;
|
||||
Timer? _detectionTimer;
|
||||
String _lastSpokenMessage = "";
|
||||
DateTime? _lastSpokenAt;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -19,49 +39,198 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
}
|
||||
|
||||
Future<void> initCamera() async {
|
||||
cameras = await availableCameras();
|
||||
controller = CameraController(
|
||||
cameras![0],
|
||||
ResolutionPreset.high,
|
||||
);
|
||||
try {
|
||||
cameras = await availableCameras();
|
||||
|
||||
await controller!.initialize();
|
||||
setState(() {});
|
||||
if (cameras == null || cameras!.isEmpty) {
|
||||
setState(() {
|
||||
statusMessage = "Kamera tidak ditemukan";
|
||||
});
|
||||
await _tts.speak(statusMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
controller = CameraController(
|
||||
cameras![0],
|
||||
ResolutionPreset.high,
|
||||
enableAudio: false,
|
||||
);
|
||||
|
||||
await controller!.initialize();
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
statusMessage = "Gagal membuka kamera: $error";
|
||||
});
|
||||
await _tts.speak("Gagal membuka kamera");
|
||||
}
|
||||
}
|
||||
|
||||
// 🔴 START RECORD
|
||||
Future<void> startRecording() async {
|
||||
if (!controller!.value.isRecordingVideo) {
|
||||
await controller!.startVideoRecording();
|
||||
final CameraController? cameraController = controller;
|
||||
if (cameraController == null ||
|
||||
!cameraController.value.isInitialized ||
|
||||
isRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isRecording = true;
|
||||
statusMessage = "Deteksi dimulai";
|
||||
detectedObjects = const [];
|
||||
detectionFrameSize = null;
|
||||
});
|
||||
await _tts.speak("Deteksi dimulai");
|
||||
|
||||
await _detectCurrentFrame();
|
||||
_detectionTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
if (mounted && isRecording && !isDetecting) {
|
||||
_detectCurrentFrame();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> stopRecording() async {
|
||||
_detectionTimer?.cancel();
|
||||
_detectionTimer = null;
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
isRecording = false;
|
||||
isDetecting = false;
|
||||
statusMessage = "Deteksi dihentikan";
|
||||
detectedObjects = const [];
|
||||
detectionFrameSize = null;
|
||||
});
|
||||
await _tts.speak("Deteksi dihentikan");
|
||||
}
|
||||
|
||||
Future<void> _detectCurrentFrame() async {
|
||||
final CameraController? cameraController = controller;
|
||||
if (cameraController == null ||
|
||||
!cameraController.value.isInitialized ||
|
||||
cameraController.value.isTakingPicture ||
|
||||
isDetecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isDetecting = true;
|
||||
statusMessage = "Mengambil frame kamera...";
|
||||
});
|
||||
|
||||
File? capturedFile;
|
||||
|
||||
try {
|
||||
final XFile frame = await cameraController.takePicture();
|
||||
capturedFile = File(frame.path);
|
||||
final Size capturedImageSize = await _readImageSize(capturedFile);
|
||||
|
||||
if (!mounted || !isRecording) return;
|
||||
setState(() {
|
||||
isRecording = true;
|
||||
statusMessage = "Mengirim frame ke YOLOv5...";
|
||||
});
|
||||
|
||||
final YoloDetectionResponse response =
|
||||
await _detectionService.detectFrame(capturedFile);
|
||||
|
||||
if (!mounted || !isRecording) return;
|
||||
setState(() {
|
||||
detectedObjects = response.objects;
|
||||
detectionFrameSize = capturedImageSize;
|
||||
statusMessage = response.message;
|
||||
});
|
||||
|
||||
await _announceDetection(response);
|
||||
} catch (error) {
|
||||
if (!mounted || !isRecording) return;
|
||||
const String message = "Gagal memproses frame kamera";
|
||||
setState(() {
|
||||
statusMessage = "$message: $error";
|
||||
});
|
||||
await _speakOnce(message);
|
||||
} finally {
|
||||
if (capturedFile != null && await capturedFile.exists()) {
|
||||
try {
|
||||
await capturedFile.delete();
|
||||
} catch (_) {
|
||||
// File sementara bisa saja sudah dibersihkan oleh sistem.
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isDetecting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ⏹ STOP RECORD
|
||||
Future<void> stopRecording() async {
|
||||
if (controller!.value.isRecordingVideo) {
|
||||
await controller!.stopVideoRecording();
|
||||
setState(() {
|
||||
isRecording = false;
|
||||
});
|
||||
Future<Size> _readImageSize(File imageFile) async {
|
||||
final Uint8List bytes = await imageFile.readAsBytes();
|
||||
final ui.Codec codec = await ui.instantiateImageCodec(bytes);
|
||||
final ui.FrameInfo frameInfo = await codec.getNextFrame();
|
||||
return Size(
|
||||
frameInfo.image.width.toDouble(),
|
||||
frameInfo.image.height.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _announceDetection(YoloDetectionResponse response) async {
|
||||
if (!response.isSuccess) {
|
||||
await _speakOnce(response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.objects.isEmpty) {
|
||||
await _speakOnce("Tidak ada objek terdeteksi");
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, int> objectCounts = {};
|
||||
for (final YoloDetection object in response.objects) {
|
||||
objectCounts[object.name] = (objectCounts[object.name] ?? 0) + 1;
|
||||
}
|
||||
|
||||
final String objectSummary = objectCounts.entries
|
||||
.map(
|
||||
(entry) => entry.value > 1 ? "${entry.value} ${entry.key}" : entry.key,
|
||||
)
|
||||
.join(", ");
|
||||
|
||||
await _speakOnce("Terdeteksi $objectSummary");
|
||||
}
|
||||
|
||||
Future<void> _speakOnce(String message) async {
|
||||
final DateTime now = DateTime.now();
|
||||
final bool canRepeat = _lastSpokenAt == null ||
|
||||
now.difference(_lastSpokenAt!) > const Duration(seconds: 5);
|
||||
|
||||
if (message == _lastSpokenMessage && !canRepeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSpokenMessage = message;
|
||||
_lastSpokenAt = now;
|
||||
await _tts.speak(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_detectionTimer?.cancel();
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget buildCameraPreview() {
|
||||
final previewSize = controller?.value.previewSize;
|
||||
final Size? previewSize = controller?.value.previewSize;
|
||||
if (previewSize == null) {
|
||||
return CameraPreview(controller!);
|
||||
}
|
||||
|
||||
// Kamera biasanya memberikan rasio landscape, ubah ke portrait 9:16
|
||||
final double previewAspectRatio = previewSize.height / previewSize.width;
|
||||
|
||||
return Center(
|
||||
|
|
@ -88,19 +257,46 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
if (controller == null || !controller!.value.isInitialized) {
|
||||
return Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
body: Center(
|
||||
child: statusMessage == "Tekan tombol rekam untuk mulai deteksi"
|
||||
? const CircularProgressIndicator()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
statusMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// 📷 PREVIEW KAMERA (FULL SCREEN)
|
||||
Positioned.fill(
|
||||
child: buildCameraPreview(),
|
||||
),
|
||||
|
||||
// 🔙 TOMBOL KEMBALI (KANAN BAWAH)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: _BoundingBoxOverlay(
|
||||
imageSize: detectionFrameSize,
|
||||
objects: detectedObjects,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 48,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: _DetectionStatusPanel(
|
||||
isRecording: isRecording,
|
||||
isDetecting: isDetecting,
|
||||
statusMessage: statusMessage,
|
||||
objects: detectedObjects,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 40,
|
||||
right: 30,
|
||||
|
|
@ -112,12 +308,10 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Icon(Icons.arrow_back),
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 🔘 TOMBOL RECORD (TENGAH BAWAH)
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
left: MediaQuery.of(context).size.width / 2 - 35,
|
||||
|
|
@ -153,4 +347,243 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BoundingBoxOverlay extends StatelessWidget {
|
||||
const _BoundingBoxOverlay({
|
||||
required this.imageSize,
|
||||
required this.objects,
|
||||
});
|
||||
|
||||
final Size? imageSize;
|
||||
final List<YoloDetection> objects;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imageSize == null || objects.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return CustomPaint(
|
||||
painter: _BoundingBoxPainter(
|
||||
imageSize: imageSize!,
|
||||
objects: objects,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BoundingBoxPainter extends CustomPainter {
|
||||
_BoundingBoxPainter({
|
||||
required this.imageSize,
|
||||
required this.objects,
|
||||
});
|
||||
|
||||
final Size imageSize;
|
||||
final List<YoloDetection> objects;
|
||||
|
||||
static const double _previewAspectRatio = 9 / 16;
|
||||
static const List<Color> _boxColors = [
|
||||
Color(0xFF00E676),
|
||||
Color(0xFFFFD600),
|
||||
Color(0xFF40C4FF),
|
||||
Color(0xFFFF4081),
|
||||
Color(0xFFFF9100),
|
||||
];
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (imageSize.width <= 0 || imageSize.height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Rect previewRect = _centeredAspectRect(size, _previewAspectRatio);
|
||||
final double scale = _coverScale(imageSize, previewRect.size);
|
||||
final Size scaledImageSize = Size(
|
||||
imageSize.width * scale,
|
||||
imageSize.height * scale,
|
||||
);
|
||||
final Offset imageOffset = Offset(
|
||||
previewRect.left + (previewRect.width - scaledImageSize.width) / 2,
|
||||
previewRect.top + (previewRect.height - scaledImageSize.height) / 2,
|
||||
);
|
||||
|
||||
canvas.save();
|
||||
canvas.clipRect(previewRect);
|
||||
|
||||
for (int index = 0; index < objects.length; index++) {
|
||||
final YoloDetection object = objects[index];
|
||||
final Color color = _boxColors[index % _boxColors.length];
|
||||
final Rect box = Rect.fromLTRB(
|
||||
imageOffset.dx + object.xmin * scale,
|
||||
imageOffset.dy + object.ymin * scale,
|
||||
imageOffset.dx + object.xmax * scale,
|
||||
imageOffset.dy + object.ymax * scale,
|
||||
);
|
||||
|
||||
if (box.width <= 0 || box.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_drawBox(canvas, previewRect, box, color, object);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
void _drawBox(
|
||||
Canvas canvas,
|
||||
Rect previewRect,
|
||||
Rect box,
|
||||
Color color,
|
||||
YoloDetection object,
|
||||
) {
|
||||
final Paint shadowPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 5
|
||||
..color = Colors.black.withOpacity(0.55);
|
||||
final Paint boxPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3
|
||||
..color = color;
|
||||
|
||||
canvas.drawRect(box, shadowPaint);
|
||||
canvas.drawRect(box, boxPaint);
|
||||
|
||||
final String label = "${object.name} ${(object.confidence * 100).round()}%";
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(maxWidth: previewRect.width - 16);
|
||||
|
||||
final double labelWidth = textPainter.width + 12;
|
||||
final double labelHeight = textPainter.height + 8;
|
||||
double labelLeft = box.left;
|
||||
double labelTop = box.top - labelHeight;
|
||||
|
||||
if (labelTop < previewRect.top) {
|
||||
labelTop = box.top;
|
||||
}
|
||||
if (labelLeft + labelWidth > previewRect.right) {
|
||||
labelLeft = previewRect.right - labelWidth;
|
||||
}
|
||||
labelLeft =
|
||||
labelLeft.clamp(previewRect.left, previewRect.right - labelWidth).toDouble();
|
||||
|
||||
final RRect labelBackground = RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(labelLeft, labelTop, labelWidth, labelHeight),
|
||||
const Radius.circular(6),
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
labelBackground,
|
||||
Paint()..color = color.withOpacity(0.92),
|
||||
);
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(labelLeft + 6, labelTop + 4),
|
||||
);
|
||||
}
|
||||
|
||||
Rect _centeredAspectRect(Size containerSize, double aspectRatio) {
|
||||
final double containerRatio = containerSize.width / containerSize.height;
|
||||
|
||||
if (containerRatio > aspectRatio) {
|
||||
final double width = containerSize.height * aspectRatio;
|
||||
final double left = (containerSize.width - width) / 2;
|
||||
return Rect.fromLTWH(left, 0, width, containerSize.height);
|
||||
}
|
||||
|
||||
final double height = containerSize.width / aspectRatio;
|
||||
final double top = (containerSize.height - height) / 2;
|
||||
return Rect.fromLTWH(0, top, containerSize.width, height);
|
||||
}
|
||||
|
||||
double _coverScale(Size source, Size destination) {
|
||||
final double scaleX = destination.width / source.width;
|
||||
final double scaleY = destination.height / source.height;
|
||||
return scaleX > scaleY ? scaleX : scaleY;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _BoundingBoxPainter oldDelegate) {
|
||||
return oldDelegate.imageSize != imageSize || oldDelegate.objects != objects;
|
||||
}
|
||||
}
|
||||
|
||||
class _DetectionStatusPanel extends StatelessWidget {
|
||||
const _DetectionStatusPanel({
|
||||
required this.isRecording,
|
||||
required this.isDetecting,
|
||||
required this.statusMessage,
|
||||
required this.objects,
|
||||
});
|
||||
|
||||
final bool isRecording;
|
||||
final bool isDetecting;
|
||||
final String statusMessage;
|
||||
final List<YoloDetection> objects;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<YoloDetection> visibleObjects = objects.take(4).toList();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.72),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isRecording ? Icons.radio_button_checked : Icons.videocam,
|
||||
color: isRecording ? Colors.redAccent : Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isDetecting ? "YOLOv5 sedang berjalan..." : statusMessage,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (visibleObjects.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
for (final YoloDetection object in visibleObjects)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
"${object.name} - ${(object.confidence * 100).round()}%",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class ApiConfig {
|
|||
static const bool useAndroidEmulator = false;
|
||||
|
||||
// Ganti IP ini dengan IP laptop/PC Anda saat menggunakan HP fisik (ADB).
|
||||
static const String deviceHost = "192.168.0.31:8000";
|
||||
static const String deviceHost = "192.168.18.14:8000";
|
||||
|
||||
static String get baseUrl {
|
||||
if (Platform.isAndroid && useAndroidEmulator) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'api_config.dart';
|
||||
|
||||
class YoloDetection {
|
||||
const YoloDetection({
|
||||
required this.name,
|
||||
required this.confidence,
|
||||
required this.xmin,
|
||||
required this.ymin,
|
||||
required this.xmax,
|
||||
required this.ymax,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final double confidence;
|
||||
final int xmin;
|
||||
final int ymin;
|
||||
final int xmax;
|
||||
final int ymax;
|
||||
|
||||
factory YoloDetection.fromJson(Map<String, dynamic> json) {
|
||||
return YoloDetection(
|
||||
name: json["name"]?.toString() ?? "unknown",
|
||||
confidence: (json["confidence"] as num?)?.toDouble() ?? 0,
|
||||
xmin: (json["xmin"] as num?)?.toInt() ?? 0,
|
||||
ymin: (json["ymin"] as num?)?.toInt() ?? 0,
|
||||
xmax: (json["xmax"] as num?)?.toInt() ?? 0,
|
||||
ymax: (json["ymax"] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class YoloDetectionResponse {
|
||||
const YoloDetectionResponse({
|
||||
required this.status,
|
||||
required this.message,
|
||||
required this.objects,
|
||||
});
|
||||
|
||||
final String status;
|
||||
final String message;
|
||||
final List<YoloDetection> objects;
|
||||
|
||||
bool get isSuccess => status == "success";
|
||||
|
||||
factory YoloDetectionResponse.fromJson(Map<String, dynamic> json) {
|
||||
final Object? rawObjects = json["objects"];
|
||||
final List<YoloDetection> objects = rawObjects is List
|
||||
? rawObjects
|
||||
.whereType<Map>()
|
||||
.map((object) => YoloDetection.fromJson(
|
||||
Map<String, dynamic>.from(object),
|
||||
))
|
||||
.toList()
|
||||
: const [];
|
||||
|
||||
return YoloDetectionResponse(
|
||||
status: json["status"]?.toString() ?? "error",
|
||||
message: json["message"]?.toString() ?? "Respons server tidak valid",
|
||||
objects: objects,
|
||||
);
|
||||
}
|
||||
|
||||
factory YoloDetectionResponse.error(String message) {
|
||||
return YoloDetectionResponse(
|
||||
status: "error",
|
||||
message: message,
|
||||
objects: const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class YoloDetectionService {
|
||||
static const Duration _timeout = Duration(seconds: 30);
|
||||
|
||||
Future<YoloDetectionResponse> detectFrame(File imageFile) async {
|
||||
final Uri url = Uri.parse("${ApiConfig.baseUrl}/detect-frame");
|
||||
|
||||
try {
|
||||
final http.MultipartRequest request = http.MultipartRequest("POST", url);
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath("file", imageFile.path),
|
||||
);
|
||||
|
||||
final http.StreamedResponse streamedResponse =
|
||||
await request.send().timeout(_timeout);
|
||||
final http.Response response =
|
||||
await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
return YoloDetectionResponse.error(
|
||||
"Server mengembalikan status ${response.statusCode}",
|
||||
);
|
||||
}
|
||||
|
||||
final Object? decoded = jsonDecode(response.body);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
return YoloDetectionResponse.error("Format respons server tidak valid");
|
||||
}
|
||||
|
||||
return YoloDetectionResponse.fromJson(decoded);
|
||||
} on SocketException {
|
||||
return YoloDetectionResponse.error(
|
||||
"Gagal terhubung ke server. Pastikan API YOLOv5 sudah berjalan.",
|
||||
);
|
||||
} on TimeoutException {
|
||||
return YoloDetectionResponse.error("Koneksi ke YOLOv5 terlalu lama");
|
||||
} on FormatException {
|
||||
return YoloDetectionResponse.error("Respons server bukan JSON valid");
|
||||
} on http.ClientException {
|
||||
return YoloDetectionResponse.error("Koneksi ke server terputus");
|
||||
} catch (error) {
|
||||
return YoloDetectionResponse.error("Gagal deteksi objek: $error");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue