dog_detection 0.0.1
dog_detection: ^0.0.1 copied to clipboard
Dog face and landmark detection using on-device TFLite models.
example/lib/main.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:file_selector/file_selector.dart';
import 'package:dog_detection/dog_detection.dart';
void main() {
runApp(const DogDetectionApp());
}
class DogDetectionApp extends StatelessWidget {
const DogDetectionApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dog Detection Demo',
theme: ThemeData(
colorSchemeSeed: Colors.brown,
useMaterial3: true,
),
home: const DogDetectionHome(),
);
}
}
class DogDetectionHome extends StatelessWidget {
const DogDetectionHome({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dog Detection Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.pets, size: 100, color: Colors.brown[300]),
const SizedBox(height: 48),
Text(
'Dog Detection',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
Text(
'Detect dogs with body pose and 46 facial landmarks',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 48),
SizedBox(
width: 400,
child: Card(
elevation: 4,
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const StillImageScreen(),
),
);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(24),
child: Row(
children: [
const Icon(Icons.image, size: 64, color: Colors.brown),
const SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Still Image',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Detect dogs in photos from gallery, camera, or samples',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.grey[600]),
),
],
),
),
const Icon(Icons.arrow_forward_ios),
],
),
),
),
),
),
],
),
),
);
}
}
class StillImageScreen extends StatefulWidget {
const StillImageScreen({super.key});
@override
State<StillImageScreen> createState() => _StillImageScreenState();
}
class _StillImageScreenState extends State<StillImageScreen> {
DogDetectorIsolate? _detector;
final ImagePicker _picker = ImagePicker();
bool _useEnsemble = false;
DogDetectionMode _detectionMode = DogDetectionMode.full;
bool _isInitialized = false;
bool _isProcessing = false;
bool _isDownloading = false;
String _downloadStatus = '';
Uint8List? _imageBytes;
int _imageWidth = 0;
int _imageHeight = 0;
List<Dog> _results = [];
String? _errorMessage;
static const List<String> _samplePaths = [
'packages/dog_detection/assets/samples/sample_dog_1.png',
'packages/dog_detection/assets/samples/sample_dog_2.png',
'packages/dog_detection/assets/samples/sample_dog_3.png',
'packages/dog_detection/assets/samples/sample_dog_4.png',
'packages/dog_detection/assets/samples/sample_dog_5.png',
];
int _currentSampleIndex = 0;
@override
void initState() {
super.initState();
_initializeDetector();
}
Future<void> _initializeDetector() async {
setState(() {
_isProcessing = true;
_isInitialized = false;
_errorMessage = null;
});
try {
await _detector?.dispose();
if (_useEnsemble) {
final cached = await DogDetector.isEnsembleCached();
if (!cached) {
setState(() {
_isDownloading = true;
_downloadStatus = 'Downloading ensemble models...';
});
}
}
_detector = await DogDetectorIsolate.spawn(
mode: _detectionMode,
landmarkModel:
_useEnsemble ? DogLandmarkModel.ensemble : DogLandmarkModel.full,
performanceConfig: PerformanceConfig.disabled,
onDownloadProgress: (model, received, total) {
if (!mounted) return;
final mb = (received / 1024 / 1024).toStringAsFixed(1);
final totalMb =
total > 0 ? (total / 1024 / 1024).toStringAsFixed(1) : '?';
final name = model.contains('256') ? '256px' : '320px';
setState(() {
_downloadStatus = 'Downloading $name model: $mb / $totalMb MB';
});
},
);
if (!mounted) return;
setState(() {
_isInitialized = true;
_isProcessing = false;
_isDownloading = false;
_downloadStatus = '';
});
} catch (e) {
if (!mounted) return;
setState(() {
_isProcessing = false;
_isDownloading = false;
_downloadStatus = '';
_errorMessage = 'Failed to initialize: $e';
});
}
}
Future<void> _toggleEnsemble(bool value) async {
if (value == _useEnsemble) return;
setState(() {
_useEnsemble = value;
_results = [];
});
await _initializeDetector();
if (_imageBytes != null && _isInitialized) {
await _runDetection(_imageBytes!);
}
}
Future<void> _changeDetectionMode(DogDetectionMode mode) async {
if (mode == _detectionMode) return;
setState(() {
_detectionMode = mode;
_results = [];
});
await _initializeDetector();
if (_imageBytes != null && _isInitialized) {
await _runDetection(_imageBytes!);
}
}
Future<void> _pickImage(ImageSource source) async {
try {
final XFile? pickedFile = await _picker.pickImage(source: source);
if (pickedFile == null) return;
final Uint8List bytes = await pickedFile.readAsBytes();
await _runDetection(bytes);
} catch (e) {
setState(() {
_isProcessing = false;
_errorMessage = 'Error: $e';
});
}
}
Future<void> _pickFileFromSystem() async {
try {
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'],
);
final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]);
if (file == null) return;
final Uint8List bytes = await File(file.path).readAsBytes();
await _runDetection(bytes);
} catch (e) {
setState(() {
_isProcessing = false;
_errorMessage = 'Error: $e';
});
}
}
bool get _isDesktop =>
!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
Future<void> _loadSample() async {
try {
final String path = _samplePaths[_currentSampleIndex];
_currentSampleIndex = (_currentSampleIndex + 1) % _samplePaths.length;
final ByteData data = await rootBundle.load(path);
final Uint8List bytes = data.buffer.asUint8List();
await _runDetection(bytes);
} catch (e) {
setState(() {
_isProcessing = false;
_errorMessage = 'Error loading sample: $e';
});
}
}
Future<void> _runDetection(Uint8List bytes) async {
setState(() {
_isProcessing = true;
_errorMessage = null;
_results = [];
});
try {
final List<Dog> results = await _detector!.detectDogs(bytes);
int imgW = 0;
int imgH = 0;
if (results.isNotEmpty) {
imgW = results.first.imageWidth;
imgH = results.first.imageHeight;
} else {
final decoded = await decodeImageFromList(bytes);
imgW = decoded.width;
imgH = decoded.height;
}
setState(() {
_imageBytes = bytes;
_imageWidth = imgW;
_imageHeight = imgH;
_results = results;
_isProcessing = false;
if (results.isEmpty) {
_errorMessage = 'No dogs detected in image';
}
});
} catch (e) {
setState(() {
_isProcessing = false;
_errorMessage = 'Error: $e';
});
}
}
@override
void dispose() {
_detector?.dispose();
_detector = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dog Detection'),
actions: [
if (_isInitialized && _results.isNotEmpty)
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: _showDetectionInfo,
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: _showSettings,
),
],
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isDownloading) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 48),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 24),
Text(
_downloadStatus,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 8),
Text(
'This is a one-time download. Models will be cached for future use.',
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.grey[600]),
),
],
),
),
);
}
if (!_isInitialized && _isProcessing) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Initializing dog detector...'),
],
),
);
}
if (_errorMessage != null && _imageBytes == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _initializeDetector,
child: const Text('Retry'),
),
],
),
);
}
if (_imageBytes == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.pets, size: 100, color: Colors.grey[400]),
const SizedBox(height: 24),
Text(
'Select an image to detect dogs',
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
if (_useEnsemble)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Chip(
avatar: const Icon(Icons.auto_awesome, size: 16),
label: const Text('Ensemble mode'),
backgroundColor: Colors.amber[50],
),
),
const SizedBox(height: 24),
_buildActionButtons(),
],
),
);
}
return SingleChildScrollView(
child: Column(
children: [
DogVisualizerWidget(
imageBytes: _imageBytes!,
imageWidth: _imageWidth,
imageHeight: _imageHeight,
results: _results,
),
if (_isProcessing)
const Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text('Detecting dogs...'),
],
),
),
if (_errorMessage != null && !_isProcessing)
Padding(
padding: const EdgeInsets.all(16),
child: Card(
color: Colors.orange[50],
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Icon(Icons.info_outline, color: Colors.orange),
const SizedBox(width: 8),
Expanded(child: Text(_errorMessage!)),
],
),
),
),
),
if (_results.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Detected: ${_results.length} dog${_results.length > 1 ? 's' : ''}',
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
),
if (_useEnsemble)
Chip(
avatar: const Icon(Icons.auto_awesome, size: 14),
label: const Text('Ensemble'),
visualDensity: VisualDensity.compact,
backgroundColor: Colors.amber[50],
),
],
),
const SizedBox(height: 8),
for (final dog in _results) ...[
Text(
'Score: ${(dog.score * 100).toStringAsFixed(1)}%',
style: Theme.of(context).textTheme.bodyMedium,
),
if (dog.species != null)
Text(
'Species: ${dog.species}',
style: Theme.of(context).textTheme.bodyMedium,
),
if (dog.pose != null)
Text(
'Pose landmarks: ${dog.pose!.landmarks.length}',
style: Theme.of(context).textTheme.bodyMedium,
),
if (dog.face != null && dog.face!.hasLandmarks)
Text(
'Face landmarks: ${dog.face!.landmarks.length}',
style: Theme.of(context).textTheme.bodyMedium,
),
if (_results.length > 1 && dog != _results.last)
const Divider(),
],
],
),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: _buildActionButtons(),
),
],
),
);
}
Widget _buildActionButtons() {
return Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
ElevatedButton.icon(
onPressed: _isInitialized && !_isProcessing
? () => _isDesktop
? _pickFileFromSystem()
: _pickImage(ImageSource.gallery)
: null,
icon: const Icon(Icons.photo_library),
label: Text(_isDesktop ? 'Open File' : 'Gallery'),
),
if (!_isDesktop)
ElevatedButton.icon(
onPressed: _isInitialized && !_isProcessing
? () => _pickImage(ImageSource.camera)
: null,
icon: const Icon(Icons.camera_alt),
label: const Text('Camera'),
),
ElevatedButton.icon(
onPressed: _isInitialized && !_isProcessing ? _loadSample : null,
icon: const Icon(Icons.auto_awesome),
label: const Text('Load Sample'),
),
],
);
}
void _showSettings() {
showModalBottomSheet(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setSheetState) => Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Settings',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 24),
Text(
'Detection Mode',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
RadioGroup<DogDetectionMode>(
groupValue: _detectionMode,
onChanged: _isDownloading
? (_) {}
: (value) {
if (value == null) return;
setSheetState(() {});
Navigator.pop(context);
_changeDetectionMode(value);
},
child: Column(
children: [
for (final mode in DogDetectionMode.values)
RadioListTile<DogDetectionMode>(
title: Text(_modeLabel(mode)),
subtitle: Text(_modeDescription(mode)),
value: mode,
),
],
),
),
const Divider(),
SwitchListTile(
title: const Text('Ensemble mode'),
subtitle: Text(
_useEnsemble
? '3-model ensemble (256+320+384px) for ~8% better accuracy'
: 'Single 384px model (default)',
),
secondary: const Icon(Icons.auto_awesome),
value: _useEnsemble,
onChanged: (_detectionMode == DogDetectionMode.poseOnly ||
_isDownloading)
? null
: (value) {
setSheetState(() {});
Navigator.pop(context);
_toggleEnsemble(value);
},
),
Padding(
padding: const EdgeInsets.only(left: 72, top: 4),
child: Text(
_useEnsemble
? 'Extra models are cached locally after first download.'
: 'Enable to download two extra models (~110 MB total) for improved landmark accuracy.',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.grey[600]),
),
),
const SizedBox(height: 24),
],
),
),
),
);
}
String _modeLabel(DogDetectionMode mode) {
switch (mode) {
case DogDetectionMode.full:
return 'Full';
case DogDetectionMode.poseOnly:
return 'Pose Only';
case DogDetectionMode.faceOnly:
return 'Face Only';
}
}
String _modeDescription(DogDetectionMode mode) {
switch (mode) {
case DogDetectionMode.full:
return 'Body detection + species + pose + face landmarks';
case DogDetectionMode.poseOnly:
return 'Body detection + species + body pose only';
case DogDetectionMode.faceOnly:
return 'Face localizer + face landmarks only (legacy)';
}
}
void _showDetectionInfo() {
if (_results.isEmpty) return;
showModalBottomSheet(
context: context,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.4,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) => ListView(
controller: scrollController,
padding: const EdgeInsets.all(16),
children: [
Text(
'Detection Details',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
for (final dog in _results) ...[
Text(
'Dog (score: ${(dog.score * 100).toStringAsFixed(1)}%)',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
if (dog.species != null)
Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4),
child: Text('Species: ${dog.species}'),
),
if (dog.pose != null && dog.pose!.hasLandmarks) ...[
const SizedBox(height: 8),
Text(
'Body Pose (${dog.pose!.landmarks.length} keypoints)',
style: Theme.of(context).textTheme.titleSmall,
),
...dog.pose!.landmarks.map((lm) => Card(
margin: const EdgeInsets.only(bottom: 4),
child: ListTile(
dense: true,
leading: CircleAvatar(
radius: 14,
backgroundColor: Colors.red,
child: Text(
lm.type.index.toString(),
style: const TextStyle(
fontSize: 9, color: Colors.white),
),
),
title: Text(
lm.type.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
'Position: (${lm.x.toStringAsFixed(1)}, ${lm.y.toStringAsFixed(1)}) conf: ${(lm.confidence * 100).toStringAsFixed(0)}%',
),
),
)),
],
if (dog.face != null && dog.face!.hasLandmarks) ...[
const SizedBox(height: 8),
Text(
'Face Landmarks (${dog.face!.landmarks.length} keypoints)',
style: Theme.of(context).textTheme.titleSmall,
),
...dog.face!.landmarks.map((lm) => Card(
margin: const EdgeInsets.only(bottom: 4),
child: ListTile(
dense: true,
leading: CircleAvatar(
radius: 14,
backgroundColor: _landmarkColor(lm.type),
child: Text(
lm.type.index.toString(),
style: const TextStyle(
fontSize: 9, color: Colors.white),
),
),
title: Text(
lm.type.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
'Position: (${lm.x.toStringAsFixed(1)}, ${lm.y.toStringAsFixed(1)})',
),
),
)),
],
if (dog != _results.last) const Divider(height: 24),
],
],
),
),
);
}
Color _landmarkColor(DogLandmarkType type) {
final String name = type.name;
if (name.startsWith('leftEar') || name.startsWith('rightEar')) {
return Colors.blue;
} else if (name.startsWith('leftEye') || name.startsWith('rightEye')) {
return Colors.green;
} else if (name.startsWith('noseBridge') || name.startsWith('noseRing')) {
return Colors.orange;
} else {
return Colors.orange;
}
}
}
class DogVisualizerWidget extends StatelessWidget {
final Uint8List imageBytes;
final int imageWidth;
final int imageHeight;
final List<Dog> results;
const DogVisualizerWidget({
super.key,
required this.imageBytes,
required this.imageWidth,
required this.imageHeight,
required this.results,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return Stack(
children: [
Image.memory(imageBytes, fit: BoxFit.contain),
Positioned.fill(
child: CustomPaint(
painter: DogOverlayPainter(
results: results,
imageWidth: imageWidth,
imageHeight: imageHeight,
),
),
),
],
);
});
}
}
class DogOverlayPainter extends CustomPainter {
final List<Dog> results;
final int imageWidth;
final int imageHeight;
DogOverlayPainter({
required this.results,
required this.imageWidth,
required this.imageHeight,
});
@override
void paint(Canvas canvas, Size size) {
if (results.isEmpty || imageWidth == 0 || imageHeight == 0) return;
final double imageAspect = imageWidth / imageHeight;
final double canvasAspect = size.width / size.height;
double scaleX, scaleY;
double offsetX = 0, offsetY = 0;
if (canvasAspect > imageAspect) {
scaleY = size.height / imageHeight;
scaleX = scaleY;
offsetX = (size.width - imageWidth * scaleX) / 2;
} else {
scaleX = size.width / imageWidth;
scaleY = scaleX;
offsetY = (size.height - imageHeight * scaleY) / 2;
}
for (final dog in results) {
_drawBodyBoundingBox(canvas, dog, scaleX, scaleY, offsetX, offsetY);
_drawSpeciesLabel(canvas, dog, scaleX, scaleY, offsetX, offsetY);
if (dog.pose != null && dog.pose!.hasLandmarks) {
_drawBodySkeleton(canvas, dog, scaleX, scaleY, offsetX, offsetY);
_drawBodyKeypoints(canvas, dog, scaleX, scaleY, offsetX, offsetY);
}
if (dog.face != null) {
_drawFaceBoundingBox(
canvas, dog.face!, scaleX, scaleY, offsetX, offsetY);
if (dog.face!.hasLandmarks) {
_drawFaceConnections(
canvas, dog.face!, scaleX, scaleY, offsetX, offsetY);
_drawFaceLandmarks(
canvas, dog.face!, scaleX, scaleY, offsetX, offsetY);
}
}
}
}
void _drawBodyBoundingBox(Canvas canvas, Dog dog, double scaleX,
double scaleY, double offsetX, double offsetY) {
final Paint strokePaint = Paint()
..color = Colors.orange.withValues(alpha: 0.9)
..style = PaintingStyle.stroke
..strokeWidth = 3;
final Paint fillPaint = Paint()
..color = Colors.orange.withValues(alpha: 0.08)
..style = PaintingStyle.fill;
final double x1 = dog.boundingBox.left * scaleX + offsetX;
final double y1 = dog.boundingBox.top * scaleY + offsetY;
final double x2 = dog.boundingBox.right * scaleX + offsetX;
final double y2 = dog.boundingBox.bottom * scaleY + offsetY;
final Rect rect = Rect.fromLTRB(x1, y1, x2, y2);
canvas.drawRect(rect, fillPaint);
canvas.drawRect(rect, strokePaint);
}
void _drawSpeciesLabel(Canvas canvas, Dog dog, double scaleX, double scaleY,
double offsetX, double offsetY) {
if (dog.species == null) return;
final double x1 = dog.boundingBox.left * scaleX + offsetX;
final double y1 = dog.boundingBox.top * scaleY + offsetY;
final String breedInfo = dog.breed != null && dog.speciesConfidence != null
? ' (${dog.breed}, ${(dog.speciesConfidence! * 100).toStringAsFixed(0)}%)'
: '';
final String label = '${dog.species}$breedInfo';
final TextPainter textPainter = TextPainter(
text: TextSpan(
text: label,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
final double padding = 4;
final double labelY = y1 - textPainter.height - padding * 2;
final Rect bgRect = Rect.fromLTWH(
x1,
labelY,
textPainter.width + padding * 2,
textPainter.height + padding * 2,
);
canvas.drawRect(
bgRect,
Paint()..color = Colors.green.withValues(alpha: 0.85),
);
textPainter.paint(canvas, Offset(x1 + padding, labelY + padding));
}
void _drawBodySkeleton(Canvas canvas, Dog dog, double scaleX, double scaleY,
double offsetX, double offsetY) {
final Paint posePaint = Paint()
..color = Colors.orange.withValues(alpha: 0.8)
..strokeWidth = 2.5
..strokeCap = StrokeCap.round;
for (final bone in animalPoseConnections) {
final start = dog.pose!.getLandmark(bone[0]);
final end = dog.pose!.getLandmark(bone[1]);
if (start != null && end != null) {
canvas.drawLine(
Offset(start.x * scaleX + offsetX, start.y * scaleY + offsetY),
Offset(end.x * scaleX + offsetX, end.y * scaleY + offsetY),
posePaint,
);
}
}
}
void _drawBodyKeypoints(Canvas canvas, Dog dog, double scaleX, double scaleY,
double offsetX, double offsetY) {
for (final lm in dog.pose!.landmarks) {
final Offset center =
Offset(lm.x * scaleX + offsetX, lm.y * scaleY + offsetY);
canvas.drawCircle(center, 5, Paint()..color = Colors.red);
canvas.drawCircle(center, 2, Paint()..color = Colors.white);
}
}
void _drawFaceBoundingBox(Canvas canvas, DogFace face, double scaleX,
double scaleY, double offsetX, double offsetY) {
final Paint strokePaint = Paint()
..color = Colors.cyan.withValues(alpha: 0.9)
..style = PaintingStyle.stroke
..strokeWidth = 2;
final double x1 = face.boundingBox.left * scaleX + offsetX;
final double y1 = face.boundingBox.top * scaleY + offsetY;
final double x2 = face.boundingBox.right * scaleX + offsetX;
final double y2 = face.boundingBox.bottom * scaleY + offsetY;
canvas.drawRect(Rect.fromLTRB(x1, y1, x2, y2), strokePaint);
}
void _drawFaceConnections(Canvas canvas, DogFace face, double scaleX,
double scaleY, double offsetX, double offsetY) {
final Paint paint = Paint()
..color = Colors.white.withValues(alpha: 0.7)
..strokeWidth = 2
..strokeCap = StrokeCap.round;
for (final connection in dogLandmarkConnections) {
final DogLandmark? start = face.getLandmark(connection[0]);
final DogLandmark? end = face.getLandmark(connection[1]);
if (start != null && end != null) {
canvas.drawLine(
Offset(start.x * scaleX + offsetX, start.y * scaleY + offsetY),
Offset(end.x * scaleX + offsetX, end.y * scaleY + offsetY),
paint,
);
}
}
}
void _drawFaceLandmarks(Canvas canvas, DogFace face, double scaleX,
double scaleY, double offsetX, double offsetY) {
for (final lm in face.landmarks) {
final Offset center =
Offset(lm.x * scaleX + offsetX, lm.y * scaleY + offsetY);
final Color color = _landmarkColor(lm.type);
final Paint glowPaint = Paint()..color = color.withValues(alpha: 0.3);
final Paint dotPaint = Paint()..color = color;
final Paint centerPaint = Paint()..color = Colors.white;
canvas.drawCircle(center, 7, glowPaint);
canvas.drawCircle(center, 4, dotPaint);
canvas.drawCircle(center, 1.5, centerPaint);
}
}
Color _landmarkColor(DogLandmarkType type) {
final String name = type.name;
if (name.startsWith('leftEar') || name.startsWith('rightEar')) {
return Colors.blue;
} else if (name.startsWith('leftEye') || name.startsWith('rightEye')) {
return Colors.green;
} else if (name.startsWith('noseBridge') || name.startsWith('noseRing')) {
return Colors.orange;
} else {
return Colors.yellow;
}
}
@override
bool shouldRepaint(DogOverlayPainter oldDelegate) => true;
}