Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.1.47

- **Bug Fix**: Fix OBB (Oriented Bounding Box) coordinate retrieval in detection results
- **Flutter**: Fixed missing `polygon` field in OBB detection results, enabling `YOLOResult.obbPoints` to be properly extracted

## 0.1.46

- **Critical Bug Fix**: Fix SIGSEGV crash when YOLOView is disposed while TensorFlow Lite inference is running
Expand Down Expand Up @@ -66,6 +71,7 @@
## 0.1.38

- **Bug Fix**: iOS performance metrics not updating in `YOLOView`

- Moved EventChannel subscription from `initState` to `_onPlatformViewCreated` to ensure native channel readiness on iOS
- Aligned streaming config key with iOS by renaming `throttleInterval` to `throttleIntervalMs` when sending params
- iOS now sources performance metrics from the latest inference result: `processingTimeMs = result.speed * 1000`, `fps = result.fps`
Expand Down
1 change: 1 addition & 0 deletions android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler
mapOf(
"points" to poly.map { mapOf("x" to it.x, "y" to it.y) },
"class" to obb.cls,
"classIndex" to obb.index,
"confidence" to obb.confidence
)
}
Expand Down
1 change: 1 addition & 0 deletions ios/Classes/YOLOInstanceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ class YOLOInstanceManager {
obbArray.append([
"points": points,
"class": obbResult.cls,
"classIndex": obbResult.index,
"confidence": obbResult.confidence,
])
}
Expand Down
28 changes: 23 additions & 5 deletions lib/core/yolo_inference.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,19 @@ class YOLOInference {

for (final obb in obbList) {
if (obb is Map) {
final obbMap = MapConverter.convertToTypedMap(obb);
final points = obb['points'] as List<dynamic>? ?? [];

// Extract polygon points for obbPoints field
final polygonPoints = points.whereType<Map>().map((point) {
final pointMap = MapConverter.convertToTypedMap(point);
return {
'x': MapConverter.safeGetDouble(pointMap, 'x'),
'y': MapConverter.safeGetDouble(pointMap, 'y'),
};
}).toList();

// Calculate bounding box from polygon points
double minX = double.infinity, minY = double.infinity;
double maxX = double.negativeInfinity, maxY = double.negativeInfinity;

Expand All @@ -227,13 +238,18 @@ class YOLOInference {
}
}

// Handle edge case: empty points list
if (points.isEmpty) {
minX = 0.0;
minY = 0.0;
maxX = 0.0;
maxY = 0.0;
}

final detection = <String, dynamic>{
'classIndex': 0,
'classIndex': MapConverter.safeGetInt(obbMap, 'classIndex'),
'className': obb['class'] ?? '',
'confidence': MapConverter.safeGetDouble(
MapConverter.convertToTypedMap(obb),
'confidence',
),
'confidence': MapConverter.safeGetDouble(obbMap, 'confidence'),
'boundingBox': {
'left': minX,
'top': minY,
Expand All @@ -246,6 +262,8 @@ class YOLOInference {
'right': maxX,
'bottom': maxY,
},
// Add polygon field so YOLOResult.fromMap can extract obbPoints
'polygon': polygonPoints,
};

detections.add(detection);
Expand Down
30 changes: 30 additions & 0 deletions lib/models/yolo_result.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class YOLOResult {
/// and ranges from 0.0 to 1.0.
final List<double>? keypointConfidences;

/// The oriented bounding box points for rotated object detection.
final List<Map<String, num>>? obbPoints;

YOLOResult({
required this.classIndex,
required this.className,
Expand All @@ -83,6 +86,7 @@ class YOLOResult {
this.mask,
this.keypoints,
this.keypointConfidences,
this.obbPoints,
});

/// Creates a [YOLOResult] from a map representation.
Expand Down Expand Up @@ -130,6 +134,29 @@ class YOLOResult {
keypointConfidences = keypointResult.confidences;
}

// Support both 'polygon' and 'obbPoints' keys for backward compatibility
// 'polygon' is the primary key used by the inference pipeline
final polygonRaw = map['polygon'] ?? map['obbPoints'];
final polygon = polygonRaw is List ? polygonRaw : null;
final List<Map<String, num>>? obbPoints = polygon
?.whereType<Map>()
.map((item) {
final pointMap = <String, num>{};
item.forEach((k, v) {
if (v is num) {
pointMap[k.toString()] = v;
} else if (v != null) {
final numValue = num.tryParse(v.toString());
if (numValue != null) {
pointMap[k.toString()] = numValue;
}
}
});
return pointMap;
})
.where((point) => point.isNotEmpty)
.toList();

return YOLOResult(
classIndex: classIndex,
className: className,
Expand All @@ -139,6 +166,7 @@ class YOLOResult {
mask: mask,
keypoints: keypoints,
keypointConfidences: keypointConfidences,
obbPoints: obbPoints,
);
}

Expand All @@ -159,6 +187,8 @@ class YOLOResult {
'right': normalizedBox.right,
'bottom': normalizedBox.bottom,
},

if (obbPoints != null) 'polygon': obbPoints,
};

if (mask != null) {
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

name: ultralytics_yolo
description: Flutter plugin for Ultralytics YOLO computer vision models.
version: 0.1.46
version: 0.1.47
homepage: https://github.com/ultralytics/yolo-flutter-app
repository: https://github.com/ultralytics/yolo-flutter-app
issue_tracker: https://github.com/ultralytics/yolo-flutter-app/issues
Expand Down
69 changes: 69 additions & 0 deletions test/yolo_result_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,75 @@ void main() {
expect(result.keypointConfidences, isNull);
});

test('fromMap safely handles polygon with mixed types and nulls', () {
final map = {
'classIndex': 1,
'className': 'airplane',
'confidence': 0.92,
'boundingBox': {
'left': 100.0,
'top': 150.0,
'right': 300.0,
'bottom': 250.0,
},
'normalizedBox': {
'left': 0.1,
'top': 0.15,
'right': 0.3,
'bottom': 0.25,
},

'polygon': [
{'x': 100.0, 'y': 150.0},
{'x': 300, 'y': 150},
{'x': '300.0', 'y': '250.0'},
{'x': null, 'y': 250.0},
{'x': 100.0, 'y': 'invalid'},
],
};

final result = YOLOResult.fromMap(map);

expect(result.obbPoints, isNotNull);
expect(result.obbPoints!.length, greaterThanOrEqualTo(2));
expect(result.obbPoints![0]['x'], 100.0);
expect(result.obbPoints![0]['y'], 150.0);
expect(result.obbPoints![1]['x'], 300);
expect(result.obbPoints![1]['y'], 150);
});

test('toMap and fromMap round-trip correctly with polygon key', () {
final original = YOLOResult(
classIndex: 2,
className: 'airplane',
confidence: 0.92,
boundingBox: const Rect.fromLTRB(100, 150, 300, 250),
normalizedBox: const Rect.fromLTRB(0.1, 0.15, 0.3, 0.25),
obbPoints: [
{'x': 100.0, 'y': 150.0},
{'x': 300.0, 'y': 150.0},
{'x': 300.0, 'y': 250.0},
{'x': 100.0, 'y': 250.0},
],
);

final map = original.toMap();
final roundTripped = YOLOResult.fromMap(map);

expect(roundTripped.classIndex, original.classIndex);
expect(roundTripped.className, original.className);
expect(roundTripped.confidence, original.confidence);
expect(roundTripped.obbPoints, isNotNull);
expect(roundTripped.obbPoints!.length, original.obbPoints!.length);
expect(map.containsKey('polygon'), isTrue);
expect(map['polygon'], isA<List>());

for (var i = 0; i < original.obbPoints!.length; i++) {
expect(roundTripped.obbPoints![i]['x'], original.obbPoints![i]['x']);
expect(roundTripped.obbPoints![i]['y'], original.obbPoints![i]['y']);
}
});

test('constructor creates instance with all parameters', () {
final keypoints = [Point(0.5, 0.3), Point(0.6, 0.4)];
final keypointConfidences = [0.8, 0.9];
Expand Down
Loading