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 {
|
||||
|
||||
Reference in New Issue
Block a user