hand_landmarker 2.1.1
hand_landmarker: ^2.1.1 copied to clipboard
A Flutter plugin for real-time hand landmark detection on Android using Google's MediaPipe Hand Landmarker task and a JNI bridge.
example/lib/main.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// Import the plugin's main class.
import 'package:hand_landmarker/hand_landmarker.dart';
late List<CameraDescription> _cameras;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
_cameras = await availableCameras();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Hand Landmarker Example',
home: HandTrackerView(),
);
}
}
class HandTrackerView extends StatefulWidget {
const HandTrackerView({super.key});
@override
State<HandTrackerView> createState() => _HandTrackerViewState();
}
class _HandTrackerViewState extends State<HandTrackerView> {
CameraController? _controller;
// The plugin instance that will handle all the heavy lifting.
HandLandmarkerPlugin? _plugin;
// The results from the plugin will be stored in this list.
List<Hand> _landmarks = [];
// A flag to show a loading indicator while the camera and plugin are initializing.
bool _isInitialized = false;
// A guard to prevent processing multiple frames at once.
bool _isDetecting = false;
@override
void initState() {
super.initState();
_initialize();
}
Future<void> _initialize() async {
final camera = _cameras.firstWhere(
(cam) => cam.lensDirection == CameraLensDirection.front,
orElse: () => _cameras.first,
);
_controller = CameraController(
camera,
ResolutionPreset.medium,
enableAudio: false,
);
// Create an instance of our plugin with custom options.
_plugin = HandLandmarkerPlugin.create(
numHands: 2,
minHandDetectionConfidence: 0.7,
delegate: HandLandmarkerDelegate.GPU,
);
await _controller!.initialize();
await _controller!.startImageStream(_processCameraImage);
if (mounted) {
setState(() {
_isInitialized = true;
});
}
}
@override
void dispose() {
// Stop the image stream and dispose of the controller.
_controller?.stopImageStream();
_controller?.dispose();
// Dispose of the plugin to release native resources.
_plugin?.dispose();
super.dispose();
}
Future<void> _processCameraImage(CameraImage image) async {
if (_isDetecting || !_isInitialized || _plugin == null) return;
_isDetecting = true;
try {
// The detect method is now synchronous (not async).
final hands = _plugin!.detect(
image,
_controller!.description.sensorOrientation,
);
if (mounted) {
setState(() {
_landmarks = hands;
});
}
} catch (e) {
debugPrint('Error detecting landmarks: $e');
} finally {
// Allow the next frame to be processed.
_isDetecting = false;
}
}
@override
Widget build(BuildContext context) {
// Show a loading indicator while initializing.
if (!_isInitialized) {
return const Center(child: CircularProgressIndicator());
}
final controller = _controller!;
final previewSize = controller.value.previewSize!;
final previewAspectRatio = previewSize.height / previewSize.width;
return Scaffold(
appBar: AppBar(title: const Text('Live Hand Tracking')),
body: Center(
child: AspectRatio(
aspectRatio: previewAspectRatio,
child: Stack(
children: [
CameraPreview(controller),
CustomPaint(
// Tell the painter to fill the available space
size: Size.infinite,
painter: LandmarkPainter(
hands: _landmarks,
// Pass the camera's resolution explicitly
previewSize: previewSize,
lensDirection: controller.description.lensDirection,
sensorOrientation: controller.description.sensorOrientation,
),
),
],
),
),
),
);
}
}
/// A custom painter that renders the hand landmarks and connections.
class LandmarkPainter extends CustomPainter {
LandmarkPainter({
required this.hands,
required this.previewSize,
required this.lensDirection,
required this.sensorOrientation,
});
final List<Hand> hands;
final Size previewSize;
final CameraLensDirection lensDirection;
final int sensorOrientation;
@override
void paint(Canvas canvas, Size size) {
final scale = size.width / previewSize.height;
final paint = Paint()
..color = Colors.red
..strokeWidth = 8 / scale
..strokeCap = StrokeCap.round;
final linePaint = Paint()
..color = Colors.lightBlueAccent
..strokeWidth = 4 / scale;
canvas.save();
final center = Offset(size.width / 2, size.height / 2);
canvas.translate(center.dx, center.dy);
canvas.rotate(sensorOrientation * math.pi / 180);
if (lensDirection == CameraLensDirection.front) {
canvas.scale(-1, 1);
canvas.rotate(math.pi);
}
canvas.scale(scale);
// Assign logicalWidth to the sensor's width and logicalHeight to the sensor's height.
final logicalWidth = previewSize.width;
final logicalHeight = previewSize.height;
for (final hand in hands) {
for (final landmark in hand.landmarks) {
// Now dx is scaled by width, and dy is scaled by height.
final dx = (landmark.x - 0.5) * logicalWidth;
final dy = (landmark.y - 0.5) * logicalHeight;
canvas.drawCircle(Offset(dx, dy), 8 / scale, paint);
}
for (final connection in HandLandmarkConnections.connections) {
final start = hand.landmarks[connection[0]];
final end = hand.landmarks[connection[1]];
final startDx = (start.x - 0.5) * logicalWidth;
final startDy = (start.y - 0.5) * logicalHeight;
final endDx = (end.x - 0.5) * logicalWidth;
final endDy = (end.y - 0.5) * logicalHeight;
canvas.drawLine(
Offset(startDx, startDy),
Offset(endDx, endDy),
linePaint,
);
}
}
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
/// Helper class.
class HandLandmarkConnections {
static const List<List<int>> connections = [
[0, 1], [1, 2], [2, 3], [3, 4], // Thumb
[0, 5], [5, 6], [6, 7], [7, 8], // Index finger
[5, 9], [9, 10], [10, 11], [11, 12], // Middle finger
[9, 13], [13, 14], [14, 15], [15, 16], // Ring finger
[13, 17], [0, 17], [17, 18], [18, 19], [19, 20], // Pinky
];
}