Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler
val poly = obb.box.toPolygon()
mapOf(
"points" to poly.map { mapOf("x" to it.x, "y" to it.y) },
"angle" to obb.box.angle,
"class" to obb.cls,
"confidence" to obb.confidence
)
Expand Down Expand Up @@ -541,4 +542,4 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack"
)
}
}
}
12 changes: 7 additions & 5 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,16 @@ class OBBExample {
final results = await yolo.predict(imageBytes);

// Process oriented bounding boxes
final boxes = results['boxes'] as List<dynamic>;
final boxes = results['obb'] as List<dynamic>? ?? [];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ HIGH: yolo.predict() is normalized by YOLOInference.predict, which returns OBB results under results['detections'], not the raw platform key results['obb']. As written, this example still won't match the Flutter API shape users receive, and the subsequent field accessors (class vs className) will be out of sync as well.

for (final box in boxes) {
print('Object: ${box['class']}');
print('Confidence: ${box['confidence']}');
print('Rotation: ${box['angle']} degrees');
final obb = box as Map<String, dynamic>;

print('Object: ${obb['class']}');
print('Confidence: ${obb['confidence']}');
print('Rotation: ${obb['angle']} radians');

// Access rotated box coordinates
final points = box['points'] as List<dynamic>? ?? [];
final points = obb['points'] as List<dynamic>? ?? [];
print('Box corners: $points');
}
}
Expand Down
1 change: 1 addition & 0 deletions ios/Classes/YOLOInstanceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ class YOLOInstanceManager {

obbArray.append([
"points": points,
"angle": Double(angle),
"class": obbResult.cls,
"confidence": obbResult.confidence,
])
Expand Down
13 changes: 9 additions & 4 deletions lib/core/yolo_inference.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Ultralytics πŸš€ AGPL-3.0 License - https://ultralytics.com/license

import 'dart:math' as math;

import 'package:flutter/services.dart';
import 'package:ultralytics_yolo/models/yolo_task.dart';
import 'package:ultralytics_yolo/models/yolo_exceptions.dart';
Expand Down Expand Up @@ -210,6 +212,7 @@ class YOLOInference {

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

double minX = double.infinity, minY = double.infinity;
Expand All @@ -230,10 +233,12 @@ class YOLOInference {
final detection = <String, dynamic>{
'classIndex': 0,
'className': obb['class'] ?? '',
'confidence': MapConverter.safeGetDouble(
MapConverter.convertToTypedMap(obb),
'confidence',
),
'confidence': MapConverter.safeGetDouble(obbMap, 'confidence'),
'angle': obbMap.containsKey('angle')
? MapConverter.safeGetDouble(obbMap, 'angle')
: MapConverter.safeGetDouble(obbMap, 'angleDegrees') *
math.pi /
180.0,
'boundingBox': {
'left': minX,
'top': minY,
Expand Down
109 changes: 109 additions & 0 deletions test/yolo_inference_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,114 @@ void main() {
arguments: {'image': imageBytes},
);
});

test('predict OBB includes angle in detections when present', () async {
final obbChannel = YOLOTestHelpers.setupMockChannel(
customResponses: {
'predictSingleImage': (_) => {
'obb': [
{
'points': [
{'x': 0.1, 'y': 0.1},
{'x': 0.2, 'y': 0.1},
{'x': 0.2, 'y': 0.2},
{'x': 0.1, 'y': 0.2},
],
'class': 'ship',
'confidence': 0.88,
'angle': 0.5235987756,
},
],
},
},
);

final inference = YOLOInference(
channel: obbChannel,
instanceId: 'test_instance',
task: YOLOTask.obb,
);

final result = await inference.predict(Uint8List.fromList([1, 2, 3]));
final detections = result['detections'] as List<dynamic>;
final first = detections.first as Map<String, dynamic>;

expect(first['className'], 'ship');
expect(first['confidence'], 0.88);
expect(first['angle'], closeTo(0.5235987756, 1e-9));
});

test(
'predict OBB falls back to angleDegrees and converts to radians',
() async {
final obbChannel = YOLOTestHelpers.setupMockChannel(
customResponses: {
'predictSingleImage': (_) => {
'obb': [
{
'points': [
{'x': 0.1, 'y': 0.1},
{'x': 0.2, 'y': 0.1},
{'x': 0.2, 'y': 0.2},
{'x': 0.1, 'y': 0.2},
],
'class': 'ship',
'confidence': 0.88,
'angleDegrees': 30.0,
},
],
},
},
);

final inference = YOLOInference(
channel: obbChannel,
instanceId: 'test_instance',
task: YOLOTask.obb,
);

final result = await inference.predict(Uint8List.fromList([1, 2, 3]));
final detections = result['detections'] as List<dynamic>;
final first = detections.first as Map<String, dynamic>;

expect(first['angle'], closeTo(0.5235987756, 1e-9));
},
);

test(
'predict OBB sets angle to default when angle fields are absent',
() async {
final obbChannel = YOLOTestHelpers.setupMockChannel(
customResponses: {
'predictSingleImage': (_) => {
'obb': [
{
'points': [
{'x': 0.1, 'y': 0.1},
{'x': 0.2, 'y': 0.1},
{'x': 0.2, 'y': 0.2},
{'x': 0.1, 'y': 0.2},
],
'class': 'ship',
'confidence': 0.88,
},
],
},
},
);

final inference = YOLOInference(
channel: obbChannel,
instanceId: 'test_instance',
task: YOLOTask.obb,
);

final result = await inference.predict(Uint8List.fromList([1, 2, 3]));
final detections = result['detections'] as List<dynamic>;
final first = detections.first as Map<String, dynamic>;

expect(first['angle'], 0.0);
},
);
});
}
Loading