В этой статье мы создадим флаттер-приложение, которое будет маркировать изображение во время выполнения, а также возможность выбирать изображение из галереи и маркировать его. Для решения задачи мы будем использовать пакет Google ML-KIT в нашем приложении так же, как мы это делали во всех предыдущих статьях по ML. Прежде чем мы начнем нашу сегодняшнюю статью, давайте сначала разберемся, что такое маркировка изображений.

Что такое маркировка изображений?

Маркировка изображений — это тип маркировки данных, который фокусируется на идентификации и маркировке конкретных деталей изображения.

В компьютерном зрении маркировка данных включает добавление тегов к необработанным данным, таким как изображения и видео. Каждый тег представляет класс объекта, связанный с данными. Модели контролируемого машинного обучения используют метки при обучении идентификации определенного класса объектов в неклассифицированных данных. Это помогает этим моделям связывать значение с данными, что может помочь в обучении модели.

Почему маркировка изображений важна для ИИ и машинного обучения?

  • Разработка функциональных моделей искусственного интеллекта (ИИ) —инструменты и методы маркировки изображений помогают выделять или захватывать определенные объекты на изображении. Эти метки делают изображения читаемыми машинами, а выделенные изображения часто служат обучающими наборами данных для моделей ИИ и машинного обучения.
  • Улучшение компьютерного зрения: маркировка изображений и аннотации помогают повысить точность компьютерного зрения за счет распознавания объектов. Обучение искусственного интеллекта и машинное обучение с помощью меток помогают этим моделям выявлять закономерности до тех пор, пока они не смогут распознавать объекты самостоятельно.

Если вы хотите изучить упомянутую тему более подробно, пожалуйста, посетите это.



1- Создайте новый проект Flutter

Создайте новый проект флаттера в своей любимой среде IDE, моя — студия Android.

Примечание. Я буду использовать шаблон GetX для чистого кода и управления состоянием. Если вы хотите перевести проект на шаблон GetX, см. здесь, чтобы перевести проект с помощью GetCli. Вы также можете сделать свой простой проект.

2- Добавить зависимости

Установите приведенные ниже зависимости из pub. dev или скопируйте и вставьте его.

dependencies: 
  cupertino_icons: ^1.0.2
  get: 4.6.5
  flutter: 
    sdk: flutter
  image_picker: ^0.8.5+3       
  google_ml_kit: ^0.12.0
  camera: ^0.10.0+1
  google_mlkit_commons: ^0.2.0
  google_mlkit_image_labeling: ^0.4.0
  path_provider: ^2.0.11

3- Пользовательский интерфейс

Если вы используете простой флаттер без GetX, начните с main. dart, я использую шаблон GetX. Я зайду в свою папку views и напишу приведенный ниже код в свой home_view.

1- home_view

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/home_controller.dart';
import 'components/camera_view.dart';

class HomeView extends GetView<HomeController> {
  const HomeView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    Get.put(HomeController());
    return GetBuilder<HomeController>(
      builder: (context) {
        return   CameraView(
          title: 'Image Labeler by Bhatti',
          customPaint:controller.getCustomPaint,
          text: controller.getText,
          onImage: controller.processImage,
        );
      }
    );
  }
}

Это даст вам ошибку класса CameraView, потому что мы еще не создали его.

2-камера_просмотр

В папке представлений я создам подпапку с именем component для хранения других представлений пользовательского интерфейса или виджетов.

Примечание: camera_view очень важен, поскольку он содержит весь пользовательский интерфейс приложения, например

  • Изображение через камеру
  • Изображение через галерею
  • живая камера и т. д.
import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_mlkit_commons/google_mlkit_commons.dart';
import 'package:image_picker/image_picker.dart';

import '../../../../../main.dart';
enum ScreenMode { liveFeed, gallery }

class CameraView extends StatefulWidget {
  CameraView(
      {Key? key,
        required this.title,
        required this.customPaint,
        this.text,
        required this.onImage,
        this.onScreenModeChanged,
        this.initialDirection = CameraLensDirection.back})
      : super(key: key);

  final String title;
  final CustomPaint? customPaint;
  final String? text;
  final Function(InputImage inputImage) onImage;
  final Function(ScreenMode mode)? onScreenModeChanged;
  final CameraLensDirection initialDirection;

  @override
  _CameraViewState createState() => _CameraViewState();
}

class _CameraViewState extends State<CameraView> {
  ScreenMode _mode = ScreenMode.liveFeed;
  CameraController? _controller;
  File? _image;
  String? _path;
  ImagePicker? _imagePicker;
  num _cameraIndex = 0;
  double zoomLevel = 0.0, minZoomLevel = 0.0, maxZoomLevel = 0.0;
  final bool _allowPicker = true;
  bool _changingCameraLens = false;


  @override
  void initState()   {
    super.initState();

    _imagePicker = ImagePicker();


    if (cameras.any(
          (element) =>
      element.lensDirection == widget.initialDirection &&
          element.sensorOrientation == 90,
    )) {
      _cameraIndex = cameras.indexOf(
        cameras.firstWhere((element) =>
        element.lensDirection == widget.initialDirection &&
            element.sensorOrientation == 90),
      );
    } else {
      _cameraIndex = cameras.indexOf(
        cameras.firstWhere(
              (element) => element.lensDirection == widget.initialDirection,
        ),
      );
    }

    _startLiveFeed();
  }

  @override
  void dispose() {
    _stopLiveFeed();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        actions: [
          if (_allowPicker)
            Padding(
              padding: EdgeInsets.only(right: 20.0),
              child: GestureDetector(
                onTap: _switchScreenMode,
                child: Icon(
                  _mode == ScreenMode.liveFeed
                      ? Icons.photo_library_outlined
                      : (Platform.isIOS
                      ? Icons.camera_alt_outlined
                      : Icons.camera),
                ),
              ),
            ),
        ],
      ),
      body: _body(),
      floatingActionButton: _floatingActionButton(),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }

  Widget? _floatingActionButton()  {

    if (_mode == ScreenMode.gallery) return null;
    if (cameras.length == 1) return null;
    return SizedBox(
        height: 70.0,
        width: 70.0,
        child: FloatingActionButton(
          child: Icon(
            Platform.isIOS
                ? Icons.flip_camera_ios_outlined
                : Icons.flip_camera_android_outlined,
            size: 40,
          ),
          onPressed: _switchLiveCamera,
        ));
  }

  Widget _body() {
    Widget body;
    if (_mode == ScreenMode.liveFeed) {
      body = _liveFeedBody();
    } else {
      body = _galleryBody();
    }
    return body;
  }

  Widget _liveFeedBody() {
    if (_controller?.value.isInitialized == false) {
      return Container();
    }

    final size = MediaQuery.of(context).size;
    // calculate scale depending on screen and camera ratios
    // this is actually size.aspectRatio / (1 / camera.aspectRatio)
    // because camera preview size is received as landscape
    // but we're calculating for portrait orientation
    var scale = size.aspectRatio * _controller!.value.aspectRatio;

    // to prevent scaling down, invert the value
    if (scale < 1) scale = 1 / scale;

    return Container(
      color: Colors.black,
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          Transform.scale(
            scale: scale,
            child: Center(
              child: _changingCameraLens
                  ? Center(
                child: const Text('Changing camera lens'),
              )
                  : CameraPreview(_controller!),
            ),
          ),
          if (widget.customPaint != null) widget.customPaint!,
          Positioned(
            bottom: 100,
            left: 50,
            right: 50,
            child: Slider(
              value: zoomLevel,
              min: minZoomLevel,
              max: maxZoomLevel,
              onChanged: (newSliderValue) {
                setState(() {
                  zoomLevel = newSliderValue;
                  _controller!.setZoomLevel(zoomLevel);
                });
              },
              divisions: (maxZoomLevel - 1).toInt() < 1
                  ? null
                  : (maxZoomLevel - 1).toInt(),
            ),
          )
        ],
      ),
    );
  }

  Widget _galleryBody() {
    return ListView(shrinkWrap: true, children: [
      _image != null
          ? SizedBox(
        height: 400,
        width: 400,
        child: Stack(
          fit: StackFit.expand,
          children: <Widget>[
            Image.file(_image!),
            if (widget.customPaint != null) widget.customPaint!,
          ],
        ),
      )
          : Icon(
        Icons.image,
        size: 200,
      ),
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 16),
        child: ElevatedButton(
          child: Text('From Gallery'),
          onPressed: () => _getImage(ImageSource.gallery),
        ),
      ),
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 16),
        child: ElevatedButton(
          child: Text('Take a picture'),
          onPressed: () => _getImage(ImageSource.camera),
        ),
      ),
      if (_image != null)
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
              '${_path == null ? '' : 'Image path: $_path'}\n\n${widget.text ?? ''}'),
        ),
    ]);
  }

  Future _getImage(ImageSource source) async {
    setState(() {
      _image = null;
      _path = null;
    });
    final pickedFile = await _imagePicker?.pickImage(source: source);
    if (pickedFile != null) {
      _processPickedFile(pickedFile);
    }
    setState(() {});
  }

  void _switchScreenMode() {
    _image = null;
    if (_mode == ScreenMode.liveFeed) {
      _mode = ScreenMode.gallery;
      _stopLiveFeed();
    } else {
      _mode = ScreenMode.liveFeed;
      _startLiveFeed();
    }
    if (widget.onScreenModeChanged != null) {
      widget.onScreenModeChanged!(_mode);
    }
    setState(() {});
  }

  Future _startLiveFeed() async {
    var cameras= await availableCameras();
    final camera = cameras[_cameraIndex.toInt()];
    _controller = CameraController(
      camera,
      ResolutionPreset.high,
      enableAudio: false,
    );
    _controller?.initialize().then((_) {
      if (!mounted) {
        return;
      }
      _controller?.getMinZoomLevel().then((value) {
        zoomLevel = value;
        minZoomLevel = value;
      });
      _controller?.getMaxZoomLevel().then((value) {
        maxZoomLevel = value;
      });
      _controller?.startImageStream(_processCameraImage);
      setState(() {});
    });
  }

  Future _stopLiveFeed() async {
    await _controller?.stopImageStream();
    await _controller?.dispose();
    _controller = null;
  }

  Future _switchLiveCamera() async {

    setState(() => _changingCameraLens = true);
    _cameraIndex = (_cameraIndex + 1) % cameras.length;

    await _stopLiveFeed();
    await _startLiveFeed();
    setState(() => _changingCameraLens = false);
  }

  Future _processPickedFile(XFile? pickedFile) async {
    final path = pickedFile?.path;
    if (path == null) {
      return;
    }
    setState(() {
      _image = File(path);
    });
    _path = path;
    final inputImage = InputImage.fromFilePath(path);
    widget.onImage(inputImage);
  }

  Future _processCameraImage(CameraImage image) async {
    final WriteBuffer allBytes = WriteBuffer();
    for (final Plane plane in image.planes) {
      allBytes.putUint8List(plane.bytes);
    }
    final bytes = allBytes.done().buffer.asUint8List();

    final Size imageSize =
    Size(image.width.toDouble(), image.height.toDouble());

    final camera = cameras[_cameraIndex.toInt()];
    final imageRotation =
    InputImageRotationValue.fromRawValue(camera.sensorOrientation);
    if (imageRotation == null) return;

    final inputImageFormat =
    InputImageFormatValue.fromRawValue(image.format.raw);
    if (inputImageFormat == null) return;

    final planeData = image.planes.map(
          (Plane plane) {
        return InputImagePlaneMetadata(
          bytesPerRow: plane.bytesPerRow,
          height: plane.height,
          width: plane.width,
        );
      },
    ).toList();

    final inputImageData = InputImageData(
      size: imageSize,
      imageRotation: imageRotation,
      inputImageFormat: inputImageFormat,
      planeData: planeData,
    );

    final inputImage =
    InputImage.fromBytes(bytes: bytes, inputImageData: inputImageData);

    widget.onImage(inputImage);
  }
}

Если вы знакомы с основами дротика и флаттера, вы поймете, что в приведенном выше коде мы: -

  • Инициализация CameraController из установленного пакета.
  • Доступ к функции живой камеры
  • Выбор изображения из галереи
  • Выбор изображения с камеры
  • Создание плавающей кнопки действия (FAB)
  • Обработка изображений
  • Построение всего тела домашней страницы.

Каждая функция выполняется в четком и отдельном методе.

Пользовательский интерфейс до сих пор будет выглядеть так.

Нажав значок на панели приложений, вы перейдете в режим живой камеры. Поскольку мы не закончили работу с алгоритмом Backend ML, он ничего не будет помечать на изображении.

4- Бэкенд

Теперь обработаем входные изображения с помощью Google ML-KIT и промаркируем все объекты. Прежде чем делать маркировку изображений Google, нам нужен специальный рисовальщик, который будет маркировать изображение и сообщать, что система видит или видит.

1 – Маркер этикеток

Создайте файл dart в папке компонентов с именем labeling_painter.

import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:google_mlkit_image_labeling/google_mlkit_image_labeling.dart';

class LabelDetectorPainter extends CustomPainter {
  LabelDetectorPainter(this.labels);

  final List<ImageLabel> labels;

  @override
  void paint(Canvas canvas, Size size) {
    final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
      ui.ParagraphStyle(
          textAlign: TextAlign.left,
          fontSize: 23,
          textDirection: TextDirection.ltr),
    );

    builder.pushStyle(ui.TextStyle(color: Colors.green));
    for (final ImageLabel label in labels) {
      builder.addText('Label: ${label.label}, '
          'Confidence: ${label.confidence.toStringAsFixed(2)}\n');
    }
    builder.pop();

    canvas.drawParagraph(
      builder.build()
        ..layout(ui.ParagraphConstraints(
          width: size.width,
        )),
      const Offset(0, 0),
    );
  }

  @override
  bool shouldRepaint(LabelDetectorPainter oldDelegate) {
    return oldDelegate.labels != labels;
  }
}

2- Домашний контроллер

Перейдите в папку Controllers и вставьте код в home_controller. дротик

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'dart:ui'as ui;
import 'package:google_ml_kit/google_ml_kit.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io' as io;
import '../views/components/labeling_painter.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

class HomeController extends GetxController {
  late ImageLabeler _imageLabeler;
  bool _canProcess = false;
  bool _isBusy = false;
  CustomPaint? _customPaint;
  String? _text;
  get getText {
    return _text;
  }
  get getCustomPaint {
    return _customPaint;
  }

  //////////////////////////////////////////////
  //TODO: Implement HomeController
  var selectedImagePath=''.obs;
  var extractedBarcode=''.obs;
  RxBool isLoading = false.obs;
  XFile ?iimageFile;
  List<DetectedObject> ?objectss;
  ui.Image ?iimage;
  void _initializeLabeler() async {
    // uncomment next line if you want to use the default model
    _imageLabeler = ImageLabeler(options: ImageLabelerOptions());

    // uncomment next lines if you want to use a local model
    // make sure to add tflite model to assets/ml
    //final path = 'assets/ml/lite-model_aiy_vision_classifier_birds_V1_3.tflite';
    //final path = 'assets/ml/object_labeler.tflite';
    //final modelPath = await _getModel(path);
    //final options = LocalLabelerOptions(modelPath: modelPath);
    //_imageLabeler = ImageLabeler(options: options);

    // uncomment next lines if you want to use a remote model
    // make sure to add model to firebase
    // final modelName = 'bird-classifier';
    // final response =
    //     await FirebaseImageLabelerModelManager().downloadModel(modelName);
    // print('Downloaded: $response');
    // final options =
    //     FirebaseLabelerOption(confidenceThreshold: 0.5, modelName: modelName);
    // _imageLabeler = ImageLabeler(options: options);

    _canProcess = true;
  }

  Future<void> processImage(InputImage inputImage) async {
    if (!_canProcess) return;
    if (_isBusy) return;
    _isBusy = true;

      _text = '';

    final labels = await _imageLabeler.processImage(inputImage);
    if (inputImage.inputImageData?.size != null &&
        inputImage.inputImageData?.imageRotation != null) {
      final painter = LabelDetectorPainter(labels);
      _customPaint = CustomPaint(painter: painter);
    } else {
      String text = 'Labels found: ${labels.length}\n\n';
      for (final label in labels) {
        text += 'Label: ${label.label}, '
            'Confidence: ${label.confidence.toStringAsFixed(2)}\n\n';
      }
      _text = text;
      _customPaint = null;
    }
    _isBusy = false;

      update();

  }

  Future<String> _getModel(String assetPath) async {
    if (io.Platform.isAndroid) {
      return 'flutter_assets/$assetPath';
    }
    final path = '${(await getApplicationSupportDirectory()).path}/$assetPath';
    await io.Directory(dirname(path)).create(recursive: true);
    final file = io.File(path);
    if (!await file.exists()) {
      final byteData = await rootBundle.load(assetPath);
      await file.writeAsBytes(byteData.buffer
          .asUint8List(byteData.offsetInBytes, byteData.lengthInBytes));
    }
    return file.path;
  }
  /////////////////////////////////////////////////////////////

  Future <void>getImageAndDetectObjects() async {
    final  imageFile = (await ImagePicker().pickImage(source: ImageSource.gallery)) ;
//var imageFilee=imageFile.toFile();
    isLoading.value = true;
selectedImagePath.value=imageFile!.path;
    final image = InputImage.fromFilePath(imageFile.path) ;
    var objectDetector=GoogleMlKit.vision.objectDetector(options: ObjectDetectorOptions(mode: DetectionMode.single, classifyObjects: true, multipleObjects: true));
    List<DetectedObject> objects = await objectDetector.processImage(image);
    iimageFile = imageFile;
    objectss = objects;
    _loadImage(imageFile);

    update();

  }

  _loadImage(XFile file) async {
    final data = await file.readAsBytes();
    await decodeImageFromList(data).then(
            (value) =>

        iimage = value);
    isLoading.value = false;

    update();
  }


  @override
  void onInit() {
    super.onInit();
    _initializeLabeler();
  }

  @override
  void onReady() {
    super.onReady();
  }

  @override
  void onClose() {
    _canProcess = false;
    _imageLabeler.close();
    super.onClose();
  }
}

Приведенный выше код хорошо прокомментирован, объясняя, что происходит. Он обнаруживает объекты по изображению и маркировке с достоверностью.

Выкройка будет выглядеть так

ВЫХОД

1-

2-

3- Прямая трансляция

Поздравляем! Вы готовы приступить к очень сложной и актуальной теме машинного обучения с помощью флаттера.

Похлопайте статье и подпишитесь на меня, чтобы не пропустить другие статьи, связанные с флаттером.

Исходный код

С GetX



Без GetX



Покажите мне некоторую поддержку

Я профессиональный разработчик мобильных приложений Flutter. Пожалуйста, свяжитесь со мной, чтобы помочь вам с вашими задачами и проектами.

Больше статей от меня