flutter_vision 1.1.3 flutter_vision: ^1.1.3 copied to clipboard
A Flutter plugin for managing Yolov5, Yolov8 and Tesseract v5 accessing with TensorFlow Lite 2.x. Support object detection, segmentation and OCR on Android. iOS not updated, working in progress.
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'dart:async';
import 'package:flutter_vision/flutter_vision.dart';
import 'package:image_picker/image_picker.dart';
enum Options { none, imagev5, imagev8, imagev8seg, frame, tesseract, vision }
late List<CameraDescription> cameras;
main() async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
runApp(
const MaterialApp(
home: MyApp(),
),
);
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late FlutterVision vision;
Options option = Options.none;
@override
void initState() {
super.initState();
vision = FlutterVision();
}
@override
void dispose() async {
super.dispose();
await vision.closeTesseractModel();
await vision.closeYoloModel();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: task(option),
floatingActionButton: SpeedDial(
//margin bottom
icon: Icons.menu, //icon on Floating action button
activeIcon: Icons.close, //icon when menu is expanded on button
backgroundColor: Colors.black12, //background color of button
foregroundColor: Colors.white, //font color, icon color in button
activeBackgroundColor:
Colors.deepPurpleAccent, //background color when menu is expanded
activeForegroundColor: Colors.white,
visible: true,
closeManually: false,
curve: Curves.bounceIn,
overlayColor: Colors.black,
overlayOpacity: 0.5,
buttonSize: const Size(56.0, 56.0),
children: [
SpeedDialChild(
//speed dial child
child: const Icon(Icons.video_call),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
label: 'Yolo on Frame',
labelStyle: const TextStyle(fontSize: 18.0),
onTap: () {
setState(() {
option = Options.frame;
});
},
),
SpeedDialChild(
child: const Icon(Icons.camera),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
label: 'YoloV8seg on Image',
labelStyle: const TextStyle(fontSize: 18.0),
onTap: () {
setState(() {
option = Options.imagev8seg;
});
},
),
SpeedDialChild(
child: const Icon(Icons.camera),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
label: 'YoloV8 on Image',
labelStyle: const TextStyle(fontSize: 18.0),
onTap: () {
setState(() {
option = Options.imagev8;
});
},
),
SpeedDialChild(
child: const Icon(Icons.camera),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
label: 'YoloV5 on Image',
labelStyle: const TextStyle(fontSize: 18.0),
onTap: () {
setState(() {
option = Options.imagev5;
});
},
),
SpeedDialChild(
child: const Icon(Icons.text_snippet_outlined),
foregroundColor: Colors.white,
backgroundColor: Colors.green,
label: 'Tesseract',
labelStyle: const TextStyle(fontSize: 18.0),
onTap: () {
setState(() {
option = Options.tesseract;
});
},
),
// SpeedDialChild(
// child: const Icon(Icons.document_scanner),
// foregroundColor: Colors.white,
// backgroundColor: Colors.green,
// label: 'Vision',
// labelStyle: const TextStyle(fontSize: 18.0),
// onTap: () {
// setState(() {
// option = Options.vision;
// });
// },
// ),
],
),
);
}
Widget task(Options option) {
if (option == Options.frame) {
return YoloVideo(vision: vision);
}
if (option == Options.imagev5) {
return YoloImageV5(vision: vision);
}
if (option == Options.imagev8) {
return YoloImageV8(vision: vision);
}
if (option == Options.imagev8seg) {
return YoloImageV8Seg(vision: vision);
}
if (option == Options.tesseract) {
return TesseractImage(vision: vision);
}
return const Center(child: Text("Choose Task"));
}
}
class YoloVideo extends StatefulWidget {
final FlutterVision vision;
const YoloVideo({Key? key, required this.vision}) : super(key: key);
@override
State<YoloVideo> createState() => _YoloVideoState();
}
class _YoloVideoState extends State<YoloVideo> {
late CameraController controller;
late List<Map<String, dynamic>> yoloResults;
CameraImage? cameraImage;
bool isLoaded = false;
bool isDetecting = false;
@override
void initState() {
super.initState();
init();
}
init() async {
cameras = await availableCameras();
controller = CameraController(cameras[0], ResolutionPreset.medium);
controller.initialize().then((value) {
loadYoloModel().then((value) {
setState(() {
isLoaded = true;
isDetecting = false;
yoloResults = [];
});
});
});
}
@override
void dispose() async {
super.dispose();
controller.dispose();
}
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
if (!isLoaded) {
return const Scaffold(
body: Center(
child: Text("Model not loaded, waiting for it"),
),
);
}
return Stack(
fit: StackFit.expand,
children: [
AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: CameraPreview(
controller,
),
),
...displayBoxesAroundRecognizedObjects(size),
Positioned(
bottom: 75,
width: MediaQuery.of(context).size.width,
child: Container(
height: 80,
width: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 5, color: Colors.white, style: BorderStyle.solid),
),
child: isDetecting
? IconButton(
onPressed: () async {
stopDetection();
},
icon: const Icon(
Icons.stop,
color: Colors.red,
),
iconSize: 50,
)
: IconButton(
onPressed: () async {
await startDetection();
},
icon: const Icon(
Icons.play_arrow,
color: Colors.white,
),
iconSize: 50,
),
),
),
],
);
}
Future<void> loadYoloModel() async {
await widget.vision.loadYoloModel(
labels: 'assets/labels.txt',
modelPath: 'assets/yolov8n.tflite',
modelVersion: "yolov8",
numThreads: 2,
useGpu: true);
setState(() {
isLoaded = true;
});
}
Future<void> yoloOnFrame(CameraImage cameraImage) async {
final result = await widget.vision.yoloOnFrame(
bytesList: cameraImage.planes.map((plane) => plane.bytes).toList(),
imageHeight: cameraImage.height,
imageWidth: cameraImage.width,
iouThreshold: 0.4,
confThreshold: 0.4,
classThreshold: 0.5);
if (result.isNotEmpty) {
setState(() {
yoloResults = result;
});
}
}
Future<void> startDetection() async {
setState(() {
isDetecting = true;
});
if (controller.value.isStreamingImages) {
return;
}
await controller.startImageStream((image) async {
if (isDetecting) {
cameraImage = image;
yoloOnFrame(image);
}
});
}
Future<void> stopDetection() async {
setState(() {
isDetecting = false;
yoloResults.clear();
});
}
List<Widget> displayBoxesAroundRecognizedObjects(Size screen) {
if (yoloResults.isEmpty) return [];
double factorX = screen.width / (cameraImage?.height ?? 1);
double factorY = screen.height / (cameraImage?.width ?? 1);
Color colorPick = const Color.fromARGB(255, 50, 233, 30);
return yoloResults.map((result) {
return Positioned(
left: result["box"][0] * factorX,
top: result["box"][1] * factorY,
width: (result["box"][2] - result["box"][0]) * factorX,
height: (result["box"][3] - result["box"][1]) * factorY,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
border: Border.all(color: Colors.pink, width: 2.0),
),
child: Text(
"${result['tag']} ${(result['box'][4] * 100).toStringAsFixed(0)}%",
style: TextStyle(
background: Paint()..color = colorPick,
color: Colors.white,
fontSize: 18.0,
),
),
),
);
}).toList();
}
}
class YoloImageV5 extends StatefulWidget {
final FlutterVision vision;
const YoloImageV5({Key? key, required this.vision}) : super(key: key);
@override
State<YoloImageV5> createState() => _YoloImageV5State();
}
class _YoloImageV5State extends State<YoloImageV5> {
late List<Map<String, dynamic>> yoloResults;
File? imageFile;
int imageHeight = 1;
int imageWidth = 1;
bool isLoaded = false;
@override
void initState() {
super.initState();
loadYoloModel().then((value) {
setState(() {
yoloResults = [];
isLoaded = true;
});
});
}
@override
void dispose() async {
super.dispose();
}
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
if (!isLoaded) {
return const Scaffold(
body: Center(
child: Text("Model not loaded, waiting for it"),
),
);
}
return Stack(
fit: StackFit.expand,
children: [
imageFile != null ? Image.file(imageFile!) : const SizedBox(),
Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: pickImage,
child: const Text("Pick image"),
),
ElevatedButton(
onPressed: yoloOnImage,
child: const Text("Detect"),
)
],
),
),
...displayBoxesAroundRecognizedObjects(size),
],
);
}
Future<void> loadYoloModel() async {
await widget.vision.loadYoloModel(
labels: 'assets/labels.txt',
modelPath: 'assets/yolov5n.tflite',
modelVersion: "yolov5",
quantization: false,
numThreads: 2,
useGpu: true);
setState(() {
isLoaded = true;
});
}
Future<void> pickImage() async {
final ImagePicker picker = ImagePicker();
// Capture a photo
final XFile? photo = await picker.pickImage(source: ImageSource.gallery);
if (photo != null) {
setState(() {
imageFile = File(photo.path);
});
}
}
yoloOnImage() async {
yoloResults.clear();
Uint8List byte = await imageFile!.readAsBytes();
final image = await decodeImageFromList(byte);
imageHeight = image.height;
imageWidth = image.width;
final result = await widget.vision.yoloOnImage(
bytesList: byte,
imageHeight: image.height,
imageWidth: image.width,
iouThreshold: 0.8,
confThreshold: 0.4,
classThreshold: 0.5);
if (result.isNotEmpty) {
setState(() {
yoloResults = result;
});
}
}
List<Widget> displayBoxesAroundRecognizedObjects(Size screen) {
if (yoloResults.isEmpty) return [];
double factorX = screen.width / (imageWidth);
double imgRatio = imageWidth / imageHeight;
double newWidth = imageWidth * factorX;
double newHeight = newWidth / imgRatio;
double factorY = newHeight / (imageHeight);
double pady = (screen.height - newHeight) / 2;
Color colorPick = const Color.fromARGB(255, 50, 233, 30);
return yoloResults.map((result) {
return Positioned(
left: result["box"][0] * factorX,
top: result["box"][1] * factorY + pady,
width: (result["box"][2] - result["box"][0]) * factorX,
height: (result["box"][3] - result["box"][1]) * factorY,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
border: Border.all(color: Colors.pink, width: 2.0),
),
child: Text(
"${result['tag']} ${(result['box'][4] * 100).toStringAsFixed(0)}%",
style: TextStyle(
background: Paint()..color = colorPick,
color: Colors.white,
fontSize: 18.0,
),
),
),
);
}).toList();
}
}
class YoloImageV8 extends StatefulWidget {
final FlutterVision vision;
const YoloImageV8({Key? key, required this.vision}) : super(key: key);
@override
State<YoloImageV8> createState() => _YoloImageV8State();
}
class _YoloImageV8State extends State<YoloImageV8> {
late List<Map<String, dynamic>> yoloResults;
File? imageFile;
int imageHeight = 1;
int imageWidth = 1;
bool isLoaded = false;
@override
void initState() {
super.initState();
loadYoloModel().then((value) {
setState(() {
yoloResults = [];
isLoaded = true;
});
});
}
@override
void dispose() async {
super.dispose();
}
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
if (!isLoaded) {
return const Scaffold(
body: Center(
child: Text("Model not loaded, waiting for it"),
),
);
}
return Stack(
fit: StackFit.expand,
children: [
imageFile != null ? Image.file(imageFile!) : const SizedBox(),
Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: pickImage,
child: const Text("Pick an image"),
),
ElevatedButton(
onPressed: yoloOnImage,
child: const Text("Detect"),
)
],
),
),
...displayBoxesAroundRecognizedObjects(size),
],
);
}
Future<void> loadYoloModel() async {
await widget.vision.loadYoloModel(
labels: 'assets/labels.txt',
modelPath: 'assets/yolov8n.tflite',
modelVersion: "yolov8",
quantization: false,
numThreads: 2,
useGpu: true);
setState(() {
isLoaded = true;
});
}
Future<void> pickImage() async {
final ImagePicker picker = ImagePicker();
// Capture a photo
final XFile? photo = await picker.pickImage(source: ImageSource.gallery);
if (photo != null) {
setState(() {
imageFile = File(photo.path);
});
}
}
yoloOnImage() async {
yoloResults.clear();
Uint8List byte = await imageFile!.readAsBytes();
final image = await decodeImageFromList(byte);
imageHeight = image.height;
imageWidth = image.width;
final result = await widget.vision.yoloOnImage(
bytesList: byte,
imageHeight: image.height,
imageWidth: image.width,
iouThreshold: 0.8,
confThreshold: 0.4,
classThreshold: 0.5);
if (result.isNotEmpty) {
setState(() {
yoloResults = result;
});
}
}
List<Widget> displayBoxesAroundRecognizedObjects(Size screen) {
if (yoloResults.isEmpty) return [];
double factorX = screen.width / (imageWidth);
double imgRatio = imageWidth / imageHeight;
double newWidth = imageWidth * factorX;
double newHeight = newWidth / imgRatio;
double factorY = newHeight / (imageHeight);
double pady = (screen.height - newHeight) / 2;
Color colorPick = const Color.fromARGB(255, 50, 233, 30);
return yoloResults.map((result) {
return Positioned(
left: result["box"][0] * factorX,
top: result["box"][1] * factorY + pady,
width: (result["box"][2] - result["box"][0]) * factorX,
height: (result["box"][3] - result["box"][1]) * factorY,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
border: Border.all(color: Colors.pink, width: 2.0),
),
child: Text(
"${result['tag']} ${(result['box'][4] * 100).toStringAsFixed(0)}%",
style: TextStyle(
background: Paint()..color = colorPick,
color: Colors.white,
fontSize: 18.0,
),
),
),
);
}).toList();
}
}
class YoloImageV8Seg extends StatefulWidget {
final FlutterVision vision;
const YoloImageV8Seg({Key? key, required this.vision}) : super(key: key);
@override
State<YoloImageV8Seg> createState() => _YoloImageV8SegState();
}
class _YoloImageV8SegState extends State<YoloImageV8Seg> {
late List<Map<String, dynamic>> yoloResults;
File? imageFile;
int imageHeight = 1;
int imageWidth = 1;
bool isLoaded = false;
@override
void initState() {
super.initState();
loadYoloModel().then((value) {
setState(() {
yoloResults = [];
isLoaded = true;
});
});
}
@override
void dispose() async {
super.dispose();
}
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
if (!isLoaded) {
return const Scaffold(
body: Center(
child: Text("Model not loaded, waiting for it"),
),
);
}
return Stack(
fit: StackFit.expand,
children: [
imageFile != null ? Image.file(imageFile!) : const SizedBox(),
Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: pickImage,
child: const Text("Pick image"),
),
ElevatedButton(
onPressed: yoloOnImage,
child: const Text("Detect"),
)
],
),
),
...displayBoxesAroundRecognizedObjects(size),
],
);
}
Future<void> loadYoloModel() async {
await widget.vision.loadYoloModel(
labels: 'assets/labels.txt',
modelPath: 'assets/yolov8n-seg.tflite',
modelVersion: "yolov8seg",
quantization: false,
numThreads: 2,
useGpu: true);
setState(() {
isLoaded = true;
});
}
Future<void> pickImage() async {
final ImagePicker picker = ImagePicker();
// Capture a photo
final XFile? photo = await picker.pickImage(source: ImageSource.gallery);
if (photo != null) {
setState(() {
imageFile = File(photo.path);
});
}
}
yoloOnImage() async {
yoloResults.clear();
Uint8List byte = await imageFile!.readAsBytes();
final image = await decodeImageFromList(byte);
imageHeight = image.height;
imageWidth = image.width;
final result = await widget.vision.yoloOnImage(
bytesList: byte,
imageHeight: image.height,
imageWidth: image.width,
iouThreshold: 0.8,
confThreshold: 0.4,
classThreshold: 0.5);
if (result.isNotEmpty) {
setState(() {
yoloResults = result;
});
}
}
List<Widget> displayBoxesAroundRecognizedObjects(Size screen) {
if (yoloResults.isEmpty) return [];
double factorX = screen.width / (imageWidth);
double imgRatio = imageWidth / imageHeight;
double newWidth = imageWidth * factorX;
double newHeight = newWidth / imgRatio;
double factorY = newHeight / (imageHeight);
double pady = (screen.height - newHeight) / 2;
Color colorPick = const Color.fromARGB(255, 50, 233, 30);
return yoloResults.map((result) {
return Stack(children: [
Positioned(
left: result["box"][0] * factorX,
top: result["box"][1] * factorY + pady,
width: (result["box"][2] - result["box"][0]) * factorX,
height: (result["box"][3] - result["box"][1]) * factorY,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
border: Border.all(color: Colors.pink, width: 2.0),
),
child: Text(
"${result['tag']} ${(result['box'][4] * 100).toStringAsFixed(0)}%",
style: TextStyle(
background: Paint()..color = colorPick,
color: Colors.white,
fontSize: 18.0,
),
),
),
),
Positioned(
left: result["box"][0] * factorX,
top: result["box"][1] * factorY + pady,
width: (result["box"][2] - result["box"][0]) * factorX,
height: (result["box"][3] - result["box"][1]) * factorY,
child: CustomPaint(
painter: PolygonPainter(
points: (result["polygons"] as List<dynamic>).map((e) {
Map<String, double> xy = Map<String, double>.from(e);
xy['x'] = (xy['x'] as double) * factorX;
xy['y'] = (xy['y'] as double) * factorY;
return xy;
}).toList()),
)),
]);
}).toList();
}
}
class PolygonPainter extends CustomPainter {
final List<Map<String, double>> points;
PolygonPainter({required this.points});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color.fromARGB(129, 255, 2, 124)
..strokeWidth = 2
..style = PaintingStyle.fill;
final path = Path();
if (points.isNotEmpty) {
path.moveTo(points[0]['x']!, points[0]['y']!);
for (var i = 1; i < points.length; i++) {
path.lineTo(points[i]['x']!, points[i]['y']!);
}
path.close();
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
class TesseractImage extends StatefulWidget {
final FlutterVision vision;
const TesseractImage({Key? key, required this.vision}) : super(key: key);
@override
State<TesseractImage> createState() => _TesseractImageState();
}
class _TesseractImageState extends State<TesseractImage> {
late List<Map<String, dynamic>> tesseractResults = [];
File? imageFile;
bool isLoaded = false;
@override
void initState() {
super.initState();
loadTesseractModel().then((value) {
setState(() {
isLoaded = true;
tesseractResults = [];
});
});
}
@override
void dispose() async {
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!isLoaded) {
return const Scaffold(
body: Center(
child: Text("Model not loaded, waiting for it"),
),
);
}
return Center(
child: SingleChildScrollView(
child: Column(
children: [
imageFile != null ? Image.file(imageFile!) : const SizedBox(),
tesseractResults.isEmpty
? const SizedBox()
: Align(child: Text(tesseractResults[0]["text"])),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: pickImage,
child: const Text("Pick an image"),
),
ElevatedButton(
onPressed: tesseractOnImage,
child: const Text("Get Text"),
)
],
),
],
),
),
);
}
Future<void> loadTesseractModel() async {
await widget.vision.loadTesseractModel(
args: {
'psm': '11',
'oem': '1',
'preserve_interword_spaces': '1',
},
language: 'spa',
);
setState(() {
isLoaded = true;
});
}
Future<void> pickImage() async {
final ImagePicker picker = ImagePicker();
// Capture a photo
final XFile? photo = await picker.pickImage(source: ImageSource.gallery);
if (photo != null) {
setState(() {
imageFile = File(photo.path);
});
}
}
tesseractOnImage() async {
tesseractResults.clear();
Uint8List byte = await imageFile!.readAsBytes();
final result = await widget.vision.tesseractOnImage(bytesList: byte);
if (result.isNotEmpty) {
setState(() {
tesseractResults = result;
});
}
}
}