predict method

Future<Map<String, dynamic>> predict(
  1. Uint8List imageBytes, {
  2. double? confidenceThreshold,
  3. double? iouThreshold,
})

Runs inference on a single image.

Takes raw image bytes as input and returns a map containing the inference results. The returned map contains:

  • 'boxes': List of detected objects with bounding box coordinates
  • 'detections': List of detections in YOLOResult-compatible format
  • Task-specific data (keypoints for pose, mask for segmentation, etc.)

The model must be loaded with loadModel before calling this method.

Example:

// Basic detection usage
final results = await yolo.predict(imageBytes);
final boxes = results['boxes'] as List<dynamic>;
for (var box in boxes) {
  print('Class: ${box['class']}, Confidence: ${box['confidence']}');
}

// Pose estimation with YOLOResult
final results = await yolo.predict(imageBytes);
final detections = results['detections'] as List<dynamic>;
for (var detection in detections) {
  final result = YOLOResult.fromMap(detection);
  if (result.keypoints != null) {
    print('Found ${result.keypoints!.length} keypoints');
    for (int i = 0; i < result.keypoints!.length; i++) {
      final kp = result.keypoints![i];
      final conf = result.keypointConfidences![i];
      print('Keypoint $i: (${kp.x}, ${kp.y}) confidence: $conf');
    }
  }
}

@param imageBytes The raw image data as a Uint8List @param confidenceThreshold Optional confidence threshold (0.0-1.0). Defaults to 0.25 if not specified. @param iouThreshold Optional IoU threshold for NMS (0.0-1.0). Defaults to 0.4 if not specified. @return A map containing:

  • 'boxes': List of bounding boxes
  • 'detections': List of YOLOResult-compatible detection maps
  • 'keypoints': (pose only) Raw keypoints data from platform @throws ModelNotLoadedException if the model has not been loaded @throws InferenceException if there's an error during inference @throws PlatformException if there's an issue with the platform-specific code

Implementation

Future<Map<String, dynamic>> predict(
  Uint8List imageBytes, {
  double? confidenceThreshold,
  double? iouThreshold,
}) async {
  if (imageBytes.isEmpty) {
    throw InvalidInputException('Image data is empty');
  }

  // Validate threshold values if provided
  if (confidenceThreshold != null &&
      (confidenceThreshold < 0.0 || confidenceThreshold > 1.0)) {
    throw InvalidInputException(
      'Confidence threshold must be between 0.0 and 1.0',
    );
  }
  if (iouThreshold != null && (iouThreshold < 0.0 || iouThreshold > 1.0)) {
    throw InvalidInputException('IoU threshold must be between 0.0 and 1.0');
  }

  try {
    final Map<String, dynamic> arguments = {'image': imageBytes};

    // Add optional thresholds if provided
    if (confidenceThreshold != null) {
      arguments['confidenceThreshold'] = confidenceThreshold;
    }
    if (iouThreshold != null) {
      arguments['iouThreshold'] = iouThreshold;
    }

    // Only include instanceId for multi-instance mode
    if (_instanceId != 'default') {
      arguments['instanceId'] = _instanceId;
    }

    final result = await _channel.invokeMethod(
      'predictSingleImage',
      arguments,
    );

    if (result is Map) {
      // Convert Map<Object?, Object?> to Map<String, dynamic>
      final Map<String, dynamic> resultMap = Map<String, dynamic>.fromEntries(
        result.entries.map((e) => MapEntry(e.key.toString(), e.value)),
      );

      // Convert boxes list if it exists
      final List<Map<String, dynamic>> boxes = [];
      if (resultMap.containsKey('boxes') && resultMap['boxes'] is List) {
        boxes.addAll(
          (resultMap['boxes'] as List).map((item) {
            if (item is Map) {
              return Map<String, dynamic>.fromEntries(
                item.entries.map((e) => MapEntry(e.key.toString(), e.value)),
              );
            }
            return <String, dynamic>{};
          }),
        );

        resultMap['boxes'] = boxes;
      }

      // Create detections array with YOLOResult-compatible structure
      final List<Map<String, dynamic>> detections = [];

      // Handle different task types
      switch (task) {
        case YOLOTask.pose:
          // For pose estimation, merge boxes with keypoints
          if (resultMap.containsKey('keypoints')) {
            final keypointsList =
                resultMap['keypoints'] as List<dynamic>? ?? [];

            for (
              int i = 0;
              i < boxes.length && i < keypointsList.length;
              i++
            ) {
              final box = boxes[i];
              final detection = _createDetectionMap(box);

              // Convert keypoints to flat array format expected by YOLOResult
              if (keypointsList[i] is Map) {
                final personKeypoints =
                    keypointsList[i] as Map<dynamic, dynamic>;
                final coordinates =
                    personKeypoints['coordinates'] as List<dynamic>? ?? [];

                final flatKeypoints = <double>[];
                for (final coord in coordinates) {
                  if (coord is Map) {
                    flatKeypoints.add(
                      (coord['x'] as num?)?.toDouble() ?? 0.0,
                    );
                    flatKeypoints.add(
                      (coord['y'] as num?)?.toDouble() ?? 0.0,
                    );
                    flatKeypoints.add(
                      (coord['confidence'] as num?)?.toDouble() ?? 0.0,
                    );
                  }
                }

                if (flatKeypoints.isNotEmpty) {
                  detection['keypoints'] = flatKeypoints;
                }
              }

              detections.add(detection);
            }
          }
          break;

        case YOLOTask.segment:
          // For segmentation, include mask data with boxes
          final masks = resultMap['masks'] as List<dynamic>? ?? [];

          for (int i = 0; i < boxes.length; i++) {
            final box = boxes[i];
            final detection = _createDetectionMap(box);

            // Add mask data if available for this detection
            if (i < masks.length && masks[i] != null) {
              // masks[i] is already in the correct List<List<double>> format
              final maskData = masks[i] as List<dynamic>;
              final mask = maskData.map((row) {
                return (row as List<dynamic>).map((val) {
                  return (val as num).toDouble();
                }).toList();
              }).toList();

              detection['mask'] = mask;
            }

            detections.add(detection);
          }
          break;

        case YOLOTask.classify:
          // For classification, create a single detection with classification data
          if (resultMap.containsKey('classification')) {
            final classification =
                resultMap['classification'] as Map<dynamic, dynamic>;

            // Classification doesn't have boxes, create a full-image detection
            final detection = <String, dynamic>{
              'classIndex': 0, // Would need class mapping
              'className': classification['topClass'] ?? '',
              'confidence':
                  (classification['topConfidence'] as num?)?.toDouble() ??
                  0.0,
              'boundingBox': {
                'left': 0.0,
                'top': 0.0,
                'right': 1.0, // Full image
                'bottom': 1.0,
              },
              'normalizedBox': {
                'left': 0.0,
                'top': 0.0,
                'right': 1.0,
                'bottom': 1.0,
              },
            };

            detections.add(detection);
          }
          break;

        case YOLOTask.obb:
          // For OBB, convert oriented bounding boxes
          if (resultMap.containsKey('obb')) {
            final obbList = resultMap['obb'] as List<dynamic>? ?? [];

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

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

                for (final point in points) {
                  if (point is Map) {
                    final x = (point['x'] as num?)?.toDouble() ?? 0.0;
                    final y = (point['y'] as num?)?.toDouble() ?? 0.0;
                    minX = minX > x ? x : minX;
                    minY = minY > y ? y : minY;
                    maxX = maxX < x ? x : maxX;
                    maxY = maxY < y ? y : maxY;
                  }
                }

                final detection = <String, dynamic>{
                  'classIndex': 0, // Would need class mapping
                  'className': obb['class'] ?? '',
                  'confidence':
                      (obb['confidence'] as num?)?.toDouble() ?? 0.0,
                  'boundingBox': {
                    'left': minX,
                    'top': minY,
                    'right': maxX,
                    'bottom': maxY,
                  },
                  'normalizedBox': {
                    'left': minX,
                    'top': minY,
                    'right': maxX,
                    'bottom': maxY,
                  },
                };

                detections.add(detection);
              }
            }
          }
          break;

        case YOLOTask.detect:
          // For detection, just convert boxes
          for (final box in boxes) {
            detections.add(_createDetectionMap(box));
          }
          break;
      }

      // Add detections to result map
      resultMap['detections'] = detections;

      return resultMap;
    }

    throw InferenceException('Invalid result format returned from inference');
  } on PlatformException catch (e) {
    if (e.code == 'MODEL_NOT_LOADED') {
      throw ModelNotLoadedException(
        'Model has not been loaded. Call loadModel() first.',
      );
    } else if (e.code == 'INVALID_IMAGE') {
      throw InvalidInputException(
        'Invalid image format or corrupted image data',
      );
    } else if (e.code == 'INFERENCE_ERROR') {
      throw InferenceException('Error during inference: ${e.message}');
    } else {
      throw InferenceException(
        'Platform error during inference: ${e.message}',
      );
    }
  } catch (e) {
    throw InferenceException('Unknown error during inference: $e');
  }
}