ADD ALOT OF POLISHING AND UPDATES...
This commit is contained in:
@@ -23,12 +23,14 @@ class DetectionResult {
|
||||
final double confidence;
|
||||
final ObstacleDirection direction;
|
||||
final String estimatedDistance;
|
||||
final BoundingBox? box;
|
||||
|
||||
const DetectionResult({
|
||||
required this.label,
|
||||
required this.confidence,
|
||||
required this.direction,
|
||||
required this.estimatedDistance,
|
||||
this.box,
|
||||
});
|
||||
|
||||
String get directionName => direction.name.toUpperCase();
|
||||
@@ -39,7 +41,7 @@ class DetectionResult {
|
||||
ObstacleDirection.center => 'tengah',
|
||||
ObstacleDirection.right => 'kanan',
|
||||
};
|
||||
return 'Hati-hati, $label di $area. Jarak $estimatedDistance.';
|
||||
return 'Hati-hati, ${ObstacleAnalyzer.spokenLabel(label)} di $area. Jarak $estimatedDistance.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +70,7 @@ class ObstacleAnalyzer {
|
||||
ObstacleDirection.center => 'depan',
|
||||
ObstacleDirection.right => 'kanan',
|
||||
};
|
||||
return 'Hati-hati, ${result.label} di $directionLabel. '
|
||||
return 'Hati-hati, ${spokenLabel(result.label)} di $directionLabel. '
|
||||
'Jarak ${result.estimatedDistance}.';
|
||||
}
|
||||
|
||||
@@ -90,7 +92,12 @@ class ObstacleAnalyzer {
|
||||
final bi = order.indexOf(b.estimatedDistance);
|
||||
final aRank = ai == -1 ? order.length : ai;
|
||||
final bRank = bi == -1 ? order.length : bi;
|
||||
return aRank.compareTo(bRank);
|
||||
final distanceCompare = aRank.compareTo(bRank);
|
||||
if (distanceCompare != 0) return distanceCompare;
|
||||
final directionCompare =
|
||||
_directionRisk(a.direction).compareTo(_directionRisk(b.direction));
|
||||
if (directionCompare != 0) return directionCompare;
|
||||
return b.confidence.compareTo(a.confidence);
|
||||
});
|
||||
return sorted.first;
|
||||
}
|
||||
@@ -102,15 +109,43 @@ class ObstacleAnalyzer {
|
||||
return detections.where((d) => d.confidence >= threshold).toList();
|
||||
}
|
||||
|
||||
DetectionResult analyzeFallback({
|
||||
String label = 'person',
|
||||
double confidence = 0.86,
|
||||
}) {
|
||||
return DetectionResult(
|
||||
label: label,
|
||||
confidence: confidence,
|
||||
direction: ObstacleDirection.center,
|
||||
estimatedDistance: 'Close (1-2m)',
|
||||
);
|
||||
int _directionRisk(ObstacleDirection direction) {
|
||||
return switch (direction) {
|
||||
ObstacleDirection.center => 0,
|
||||
ObstacleDirection.left => 1,
|
||||
ObstacleDirection.right => 1,
|
||||
};
|
||||
}
|
||||
|
||||
static String spokenLabel(String label) {
|
||||
return switch (label.toLowerCase()) {
|
||||
'person' => 'orang',
|
||||
'bicycle' => 'sepeda',
|
||||
'car' => 'mobil',
|
||||
'motorcycle' => 'motor',
|
||||
'airplane' => 'pesawat',
|
||||
'bus' => 'bus',
|
||||
'train' => 'kereta',
|
||||
'truck' => 'truk',
|
||||
'boat' => 'perahu',
|
||||
'traffic light' => 'lampu lalu lintas',
|
||||
'fire hydrant' => 'hidran',
|
||||
'stop sign' => 'rambu berhenti',
|
||||
'parking meter' => 'meter parkir',
|
||||
'bench' => 'bangku',
|
||||
'chair' => 'kursi',
|
||||
'couch' => 'sofa',
|
||||
'potted plant' => 'pot tanaman',
|
||||
'dining table' => 'meja',
|
||||
'tv' => 'televisi',
|
||||
'backpack' => 'tas',
|
||||
'umbrella' => 'payung',
|
||||
'handbag' => 'tas tangan',
|
||||
'suitcase' => 'koper',
|
||||
'skateboard' => 'papan skate',
|
||||
'surfboard' => 'papan selancar',
|
||||
'sports ball' => 'bola',
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,31 @@ class YoloDetector {
|
||||
YoloTensorType? _outputType;
|
||||
bool _ready = false;
|
||||
String? _lastError;
|
||||
int _lastDecodedCount = 0;
|
||||
int _lastConfidenceCount = 0;
|
||||
int _lastObstacleCount = 0;
|
||||
int _lastKeptCount = 0;
|
||||
int _lastInferenceMs = 0;
|
||||
String? _lastBestLabel;
|
||||
double? _lastBestConfidence;
|
||||
|
||||
YoloDetector(this._analyzer);
|
||||
|
||||
bool get isReady => _ready && _runtime != null;
|
||||
String? get lastError => _lastError;
|
||||
String get diagnosticsSummary {
|
||||
if (!isReady) {
|
||||
return _lastError == null
|
||||
? 'YOLO runtime belum aktif'
|
||||
: 'YOLO runtime error: $_lastError';
|
||||
}
|
||||
final bestLabel = _lastBestLabel;
|
||||
final bestConfidence = _lastBestConfidence;
|
||||
final bestText = bestLabel == null || bestConfidence == null
|
||||
? 'best: none'
|
||||
: 'best: $bestLabel ${(bestConfidence * 100).toStringAsFixed(0)}%';
|
||||
return 'frames ok, decoded $_lastDecodedCount, >=threshold $_lastConfidenceCount, obstacle $_lastObstacleCount, kept $_lastKeptCount, $bestText, ${_lastInferenceMs}ms';
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
dispose();
|
||||
@@ -47,55 +67,64 @@ class YoloDetector {
|
||||
_ready = true;
|
||||
} catch (e) {
|
||||
_lastError = e.toString();
|
||||
debugPrint('YOLO fallback mode: $e');
|
||||
debugPrint('YOLO runtime initialization skipped: $e');
|
||||
_ready = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<DetectionResult?> detect(
|
||||
CameraImage image, {
|
||||
double confidenceThreshold = 0.45,
|
||||
double confidenceThreshold = 0.25,
|
||||
}) async {
|
||||
if (!isReady) return detectFallback();
|
||||
if (!isReady) return null;
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final input = _buildCameraInput(image);
|
||||
final output = Uint8List(_runtime!.outputByteLength);
|
||||
_runtime!.run(input, output);
|
||||
final detections = _decodeDetections(output);
|
||||
_recordDecoded(detections);
|
||||
final filtered =
|
||||
_analyzer.filterByConfidence(detections, confidenceThreshold);
|
||||
return _analyzer.prioritize(filtered);
|
||||
final kept = _nonMaxSuppression(filtered);
|
||||
_lastInferenceMs = stopwatch.elapsedMilliseconds;
|
||||
_lastConfidenceCount = filtered.length;
|
||||
_lastObstacleCount =
|
||||
filtered.where((d) => _isObstacleLabel(d.label)).length;
|
||||
_lastKeptCount = kept.length;
|
||||
return _analyzer.prioritize(kept);
|
||||
} catch (e) {
|
||||
_lastError = e.toString();
|
||||
debugPrint('YOLO inference fallback: $e');
|
||||
return detectFallback();
|
||||
debugPrint('YOLO inference skipped: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<DetectionResult?> detectSynthetic({
|
||||
double confidenceThreshold = 0.25,
|
||||
}) async {
|
||||
if (!isReady) return detectFallback();
|
||||
if (!isReady) return null;
|
||||
try {
|
||||
final input = _buildSyntheticInput();
|
||||
final output = Uint8List(_runtime!.outputByteLength);
|
||||
_runtime!.run(input, output);
|
||||
final detections = _decodeDetections(output);
|
||||
_recordDecoded(detections);
|
||||
final filtered =
|
||||
_analyzer.filterByConfidence(detections, confidenceThreshold);
|
||||
return _analyzer.prioritize(filtered) ?? detectFallback();
|
||||
final kept = _nonMaxSuppression(filtered);
|
||||
_lastConfidenceCount = filtered.length;
|
||||
_lastObstacleCount =
|
||||
filtered.where((d) => _isObstacleLabel(d.label)).length;
|
||||
_lastKeptCount = kept.length;
|
||||
return _analyzer.prioritize(kept);
|
||||
} catch (e) {
|
||||
_lastError = e.toString();
|
||||
debugPrint('YOLO synthetic fallback: $e');
|
||||
return detectFallback();
|
||||
debugPrint('YOLO synthetic skipped: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<DetectionResult?> detectFallback() async {
|
||||
final label = _labels.isNotEmpty ? _labels.first : 'person';
|
||||
return _analyzer.analyzeFallback(label: label);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_runtime?.close();
|
||||
_runtime = null;
|
||||
@@ -104,6 +133,26 @@ class YoloDetector {
|
||||
_inputType = null;
|
||||
_outputType = null;
|
||||
_ready = false;
|
||||
_lastDecodedCount = 0;
|
||||
_lastConfidenceCount = 0;
|
||||
_lastObstacleCount = 0;
|
||||
_lastKeptCount = 0;
|
||||
_lastInferenceMs = 0;
|
||||
_lastBestLabel = null;
|
||||
_lastBestConfidence = null;
|
||||
}
|
||||
|
||||
void _recordDecoded(List<DetectionResult> detections) {
|
||||
_lastDecodedCount = detections.length;
|
||||
_lastBestLabel = null;
|
||||
_lastBestConfidence = null;
|
||||
for (final detection in detections) {
|
||||
if (_lastBestConfidence == null ||
|
||||
detection.confidence > _lastBestConfidence!) {
|
||||
_lastBestLabel = detection.label;
|
||||
_lastBestConfidence = detection.confidence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List _buildCameraInput(CameraImage image) {
|
||||
@@ -292,7 +341,7 @@ class YoloDetector {
|
||||
}
|
||||
if (type == YoloTensorType.float16) {
|
||||
// Most exported YOLOv8 TFLite files use float32 output. Float16 is rare;
|
||||
// fallback keeps the app usable if an unsupported variant is selected.
|
||||
// Keep preprocessing explicit if an unsupported variant is selected.
|
||||
throw StateError('Float16 YOLO output is not supported yet');
|
||||
}
|
||||
throw StateError('Unsupported YOLO output type: $type');
|
||||
@@ -484,19 +533,164 @@ class YoloDetector {
|
||||
confidence: confidence,
|
||||
direction: _analyzer.analyzeDirection(box),
|
||||
estimatedDistance: _analyzer.estimateDistance(box),
|
||||
box: box,
|
||||
);
|
||||
}
|
||||
|
||||
List<DetectionResult> _nonMaxSuppression(
|
||||
List<DetectionResult> detections, {
|
||||
double iouThreshold = 0.45,
|
||||
int maxDetections = 8,
|
||||
}) {
|
||||
final candidates = detections
|
||||
.where((d) => d.box != null && _isObstacleLabel(d.label))
|
||||
.toList()
|
||||
..sort((a, b) => b.confidence.compareTo(a.confidence));
|
||||
final selected = <DetectionResult>[];
|
||||
|
||||
for (final detection in candidates) {
|
||||
final box = detection.box;
|
||||
if (box == null) continue;
|
||||
final overlaps = selected.any((kept) =>
|
||||
kept.label == detection.label &&
|
||||
kept.box != null &&
|
||||
_iou(box, kept.box!) > iouThreshold);
|
||||
if (overlaps) continue;
|
||||
selected.add(detection);
|
||||
if (selected.length >= maxDetections) break;
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
bool _isObstacleLabel(String label) {
|
||||
return _walkGuideObstacleLabels.contains(label.toLowerCase());
|
||||
}
|
||||
|
||||
double _iou(BoundingBox a, BoundingBox b) {
|
||||
final left = math.max(a.left, b.left);
|
||||
final top = math.max(a.top, b.top);
|
||||
final right = math.min(a.right, b.right);
|
||||
final bottom = math.min(a.bottom, b.bottom);
|
||||
final width = math.max(0.0, right - left);
|
||||
final height = math.max(0.0, bottom - top);
|
||||
final intersection = width * height;
|
||||
final union = a.width * a.height + b.width * b.height - intersection;
|
||||
if (union <= 0) return 0;
|
||||
return intersection / union;
|
||||
}
|
||||
}
|
||||
|
||||
const Set<String> _walkGuideObstacleLabels = {
|
||||
'person',
|
||||
'bicycle',
|
||||
'car',
|
||||
'motorcycle',
|
||||
'bus',
|
||||
'train',
|
||||
'truck',
|
||||
'traffic light',
|
||||
'fire hydrant',
|
||||
'stop sign',
|
||||
'parking meter',
|
||||
'bench',
|
||||
'backpack',
|
||||
'umbrella',
|
||||
'handbag',
|
||||
'suitcase',
|
||||
'chair',
|
||||
'couch',
|
||||
'potted plant',
|
||||
'dining table',
|
||||
'tv',
|
||||
'laptop',
|
||||
'cell phone',
|
||||
'bottle',
|
||||
'cup',
|
||||
'book',
|
||||
};
|
||||
|
||||
const Map<int, String> _cocoObstacleLabels = {
|
||||
0: 'person',
|
||||
1: 'bicycle',
|
||||
2: 'car',
|
||||
3: 'motorcycle',
|
||||
4: 'airplane',
|
||||
5: 'bus',
|
||||
6: 'train',
|
||||
7: 'truck',
|
||||
8: 'boat',
|
||||
9: 'traffic light',
|
||||
10: 'fire hydrant',
|
||||
11: 'stop sign',
|
||||
12: 'parking meter',
|
||||
13: 'bench',
|
||||
14: 'bird',
|
||||
15: 'cat',
|
||||
16: 'dog',
|
||||
17: 'horse',
|
||||
18: 'sheep',
|
||||
19: 'cow',
|
||||
20: 'elephant',
|
||||
21: 'bear',
|
||||
22: 'zebra',
|
||||
23: 'giraffe',
|
||||
24: 'backpack',
|
||||
25: 'umbrella',
|
||||
26: 'handbag',
|
||||
27: 'tie',
|
||||
28: 'suitcase',
|
||||
29: 'frisbee',
|
||||
30: 'skis',
|
||||
31: 'snowboard',
|
||||
32: 'sports ball',
|
||||
33: 'kite',
|
||||
34: 'baseball bat',
|
||||
35: 'baseball glove',
|
||||
36: 'skateboard',
|
||||
37: 'surfboard',
|
||||
38: 'tennis racket',
|
||||
39: 'bottle',
|
||||
40: 'wine glass',
|
||||
41: 'cup',
|
||||
42: 'fork',
|
||||
43: 'knife',
|
||||
44: 'spoon',
|
||||
45: 'bowl',
|
||||
46: 'banana',
|
||||
47: 'apple',
|
||||
48: 'sandwich',
|
||||
49: 'orange',
|
||||
50: 'broccoli',
|
||||
51: 'carrot',
|
||||
52: 'hot dog',
|
||||
53: 'pizza',
|
||||
54: 'donut',
|
||||
55: 'cake',
|
||||
56: 'chair',
|
||||
57: 'couch',
|
||||
58: 'potted plant',
|
||||
59: 'bed',
|
||||
60: 'dining table',
|
||||
61: 'toilet',
|
||||
62: 'tv',
|
||||
63: 'laptop',
|
||||
64: 'mouse',
|
||||
65: 'remote',
|
||||
66: 'keyboard',
|
||||
67: 'cell phone',
|
||||
68: 'microwave',
|
||||
69: 'oven',
|
||||
70: 'toaster',
|
||||
71: 'sink',
|
||||
72: 'refrigerator',
|
||||
73: 'book',
|
||||
74: 'clock',
|
||||
75: 'vase',
|
||||
76: 'scissors',
|
||||
77: 'teddy bear',
|
||||
78: 'hair drier',
|
||||
79: 'toothbrush',
|
||||
};
|
||||
|
||||
class _InputInfo {
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart'; // Wajib import ini
|
||||
|
||||
class ApiService {
|
||||
static String get baseUrl {
|
||||
if (kIsWeb) {
|
||||
// Jika di Chrome/Web, tembak localhost langsung
|
||||
return 'http://localhost:8080/api';
|
||||
} else {
|
||||
// Jika di Emulator Android, tembak IP khusus 10.0.2.2
|
||||
return 'http://10.0.2.2:8080/api';
|
||||
}
|
||||
}
|
||||
static const baseUrl = String.fromEnvironment(
|
||||
'WALKGUIDE_API_BASE_URL',
|
||||
defaultValue: 'http://202.46.28.160:8080/api/v1',
|
||||
);
|
||||
|
||||
final Dio _dio = Dio(BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
// Penting buat Web agar tidak kena error CORS di sisi Client
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
@@ -25,4 +18,4 @@ class ApiService {
|
||||
Future<Response> post(String path, Map<String, dynamic> data) async {
|
||||
return await _dio.post(path, data: data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
String friendlyErrorMessage(
|
||||
Object error, {
|
||||
required String fallback,
|
||||
String? connectionHint,
|
||||
}) {
|
||||
if (error is DioException) {
|
||||
return friendlyDioMessage(
|
||||
error,
|
||||
fallback: fallback,
|
||||
connectionHint: connectionHint,
|
||||
);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String friendlyDioMessage(
|
||||
DioException error, {
|
||||
required String fallback,
|
||||
String? connectionHint,
|
||||
}) {
|
||||
final backendMessage = _cleanBackendMessage(error.response?.data);
|
||||
if (backendMessage != null) return backendMessage;
|
||||
|
||||
final status = error.response?.statusCode;
|
||||
if (status == 401) return 'Sesi sudah habis atau data login salah.';
|
||||
if (status == 403) return 'Akun kamu belum punya akses ke fitur ini.';
|
||||
if (status == 404) return 'Data belum ditemukan.';
|
||||
if (status == 409) return 'Data sudah dipakai. Coba gunakan data lain.';
|
||||
if (status != null && status >= 500) {
|
||||
return 'Server sedang bermasalah. Coba lagi sebentar.';
|
||||
}
|
||||
|
||||
if (error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.sendTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout) {
|
||||
return 'Server terlalu lama merespons. Coba lagi.';
|
||||
}
|
||||
if (error.type == DioExceptionType.connectionError) {
|
||||
return connectionHint ?? 'Tidak bisa terhubung ke server.';
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String? _cleanBackendMessage(Object? data) {
|
||||
Object? raw;
|
||||
if (data is Map) {
|
||||
raw = data['message'] ?? data['error'] ?? data['errorCode'];
|
||||
} else if (data is String) {
|
||||
raw = data;
|
||||
}
|
||||
final message = raw?.toString().trim();
|
||||
if (message == null || message.isEmpty || _looksTechnical(message)) {
|
||||
return null;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
bool _looksTechnical(String message) {
|
||||
final lower = message.toLowerCase();
|
||||
const blocked = [
|
||||
'exception',
|
||||
'dioexception',
|
||||
'typeerror',
|
||||
'stacktrace',
|
||||
'instance of',
|
||||
'_jsonmap',
|
||||
'socketexception',
|
||||
'package:',
|
||||
'null check operator',
|
||||
'nosuchmethod',
|
||||
'formatexception',
|
||||
];
|
||||
return blocked.any(lower.contains);
|
||||
}
|
||||
|
||||
Future<T?> runFriendly<T>(
|
||||
Future<T?> Function() action, {
|
||||
required void Function(String message) onError,
|
||||
required String fallback,
|
||||
String? connectionHint,
|
||||
}) async {
|
||||
try {
|
||||
return await action();
|
||||
} catch (error) {
|
||||
onError(friendlyErrorMessage(
|
||||
error,
|
||||
fallback: fallback,
|
||||
connectionHint: connectionHint,
|
||||
));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> runFriendlyAction(
|
||||
Future<void> Function() action, {
|
||||
required void Function(String message) onError,
|
||||
required String fallback,
|
||||
String? connectionHint,
|
||||
}) async {
|
||||
try {
|
||||
await action();
|
||||
return true;
|
||||
} catch (error) {
|
||||
onError(friendlyErrorMessage(
|
||||
error,
|
||||
fallback: fallback,
|
||||
connectionHint: connectionHint,
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class WebSocketService {
|
||||
bool get isConnected => _connected;
|
||||
|
||||
/// Connect ke WebSocket server.
|
||||
/// Dipanggil setelah login berhasil (dari screens.dart _startPostLoginServices).
|
||||
/// Dipanggil setelah login berhasil untuk membuka channel realtime.
|
||||
Future<void> connect(String serverUrl) async {
|
||||
await disconnect();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user