rainyl / opencv_dart

OpenCV bindings for Dart language and Flutter. Support Asynchronous Now!
https://pub.dev/packages/opencv_dart
Apache License 2.0
108 stars 13 forks source link

Image stitching advice #206

Open GoxeeVladan opened 1 month ago

GoxeeVladan commented 1 month ago

Read README carefully first Star :star: this project if you want to ask a question, no star, no answer

Question

Hi, I need advice about image stitching, i'm trying to create a screen in flutter which looks alike camera panoramic mode, how to achieve that so image stitching is done progressively? I've managed to stitch the images with hconcat but it's not creating panoramic image but just stitched images as they are ( which is logical actually) but when i use Stitcher.stich method i don't get any output. I'm sending my working code, if you can give an advice how and where to put Stitcher stitch in there or if there is any other solution for my problem?

` import 'dart:io'; import 'package:camera/camera.dart'; import 'package:gallery_saver/gallery_saver.dart'; import 'package:get/get.dart'; import 'package:intel_booth_app/photos/car_images_list_view_model.dart'; import 'package:opencv_dart/core.dart'; import 'package:opencv_dart/features2d.dart'; import 'package:opencv_dart/calib3d.dart'; import 'package:opencv_dart/imgcodecs.dart'; import 'package:opencv_dart/imgproc.dart' as Imgproc; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart';

class Camera360Controller extends GetxController { var progressPercentage = 0.0.obs; var cameras = [].obs; var imagePaths = [].obs; var stitchedImagePath = ''.obs; var cameraController = Rx<CameraController?>(null); var isCapturing = false.obs; final carImagesListViewModel = Get.find();

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

Future initializeCamera() async { cameras.value = await availableCameras(); if (cameras.isNotEmpty) { cameraController(CameraController(cameras.first, ResolutionPreset.high)); await cameraController.value?.initialize(); cameraController.refresh(); update(); print('Camera initialized successfully.'); } else { print('No cameras found.'); } }

Future startCapturing() async { isCapturing.value = true; progressPercentage.value = 0.0; imagePaths.clear(); stitchedImagePath.value = ''; await captureAndStitchImages(); }

void updateProgress(double value) { progressPercentage.value = value; update(); }

Future captureAndStitchImages() async { final directory = await getApplicationDocumentsDirectory(); for (int i = 0; i < 5; i++) { if (!isCapturing.value) break;

  final path = join(directory.path, '${DateTime.now().millisecondsSinceEpoch}.jpg');
  try {
    final XFile? picture = await cameraController.value?.takePicture();
    if (picture != null) {
      await picture.saveTo(path);
      if (await File(path).exists()) {
        imagePaths.add(path);
        updateProgress((i + 1) / 5.0);
        print('Captured image: $path');
        await stitchCurrentImages();
        await Future.delayed(const Duration(seconds: 3));
        update();
      } else {
        print('Error saving image: File does not exist after saving');
      }
    } else {
      print("Error capturing image: Picture is null");
    }
  } catch (e) {
    print("Error capturing image: $e");
  }
}

if (isCapturing.value) {
  try {
    await finalizeStitchingAndUpload();
    Get.back();
  } catch (e) {
    print('Error finalizing capture: $e');
  } finally {
    isCapturing.value = false;
    update();
  }
} else {
  print("No images were captured.");
}

}

Future stitchCurrentImages() async { if (imagePaths.isEmpty) return;

try {
  final directory = await getApplicationDocumentsDirectory();
  final tempStitchedImagePath = join(directory.path, 'stitched_temp.jpg');

  // Using OpenCV to stitch images horizontally
  List<Mat> images = [];
  for (String path in imagePaths) {
    Mat img = imread(path);
    images.add(img);
  }

  if (images.isEmpty) return;

  Mat stitchedImage = images[0].clone(); // Start with the first image

  for (int i = 1; i < images.length; i++) {
    Mat result = Mat.create();

    hconcat(stitchedImage, images[i], dst: result);
    stitchedImage = result;
  }

  imwrite(tempStitchedImagePath, stitchedImage);

  if (await File(tempStitchedImagePath).exists()) {
    stitchedImagePath.value = tempStitchedImagePath;
    // await finalizeStitchingAndUpload();
    update();
    print('Stitched image path: $tempStitchedImagePath');
  } else {
    print('Error: Stitched image file does not exist');
  }
} catch (e) {
  print('Error during incremental stitching: $e');
}

}

Future finalizeStitchingAndUpload() async { if (stitchedImagePath.value.isEmpty) return;

try {
  final stitchedImageFile = File(stitchedImagePath.value);
  if (await stitchedImageFile.exists()) {
    // Upload your stitched image

    await GallerySaver.saveImage(stitchedImageFile.path);
    carImagesListViewModel.uploadImage(stitchedImageFile, true);
    update();
    print('Image uploaded: ${stitchedImageFile.path}');
  } else {
    print("Stitched image file does not exist: ${stitchedImagePath.value}");
  }
} catch (e) {
  print('Error uploading stitched image: $e');
}

}

@override void onClose() { cameraController.value?.dispose(); super.onClose(); } }`

rainyl commented 1 month ago

@GoxeeVladan can't run your code, provide a github repo in the next time, for the usage of cv.Stitcher, check https://github.com/rainyl/awesome-opencv_dart/tree/main/examples/stitching

GoxeeVladan commented 1 month ago

thanks, I can't share the repo since it's a company one... i can give you full code for the screen i'm using the plugin `import 'dart:typed_data'; import 'package:opencv_dart/opencv_dart.dart' as cv;

class ImageStitching { final stitcher = cv.Stitcher.create(mode: cv.StitcherMode.PANORAMA); Uint8List? _stitchedImage; Future<Uint8List?> stitch(List imageDatas) async { if (imageDatas.isEmpty) { throw ArgumentError("No images provided for stitching"); }

List<cv.Mat> images = [];
for (final imageData in imageDatas) {
  final image = cv.imdecode(imageData, cv.IMREAD_COLOR);
  if (image.isEmpty) {
    throw Exception("Failed to decode image");
  }
  images.add(image);
}

cv.Mat? stitchedImage;
for (int i = 0; i < images.length; i++) {
  final image = images[i];
  if (i == 0) {
    stitchedImage = image.clone();
  } else {
    final (status, dst) = await stitcher.stitchAsync(cv.VecMat.fromList([stitchedImage!, image]));
    if (status != cv.StitcherStatus.OK) {
      throw Exception("Stitcher failed with status $status");
    }
    stitchedImage = dst;
  }
}

if (stitchedImage == null) {
  throw Exception("Failed to stitch images");
}

final (success, bytes) = await cv.imencodeAsync(".jpg", stitchedImage);
if (!success) {
  throw Exception("Failed to encode image");
}
_stitchedImage = bytes;
return _stitchedImage;

} }`

I've create this helper class acording to your example code, i'm taking images from camera and trying to make panorama progressively, here is the code for the view controller in use now: `import 'dart:io'; import 'dart:typed_data'; import 'package:camera/camera.dart'; import 'package:gallery_saver/gallery_saver.dart'; import 'package:get/get.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:intel_booth_app/camera360/image_stitch.dart'; import 'package:intel_booth_app/photos/car_images_list_view_model.dart';

class Camera360Controller extends GetxController { var progressPercentage = 0.0.obs; var cameras = [].obs; var imagePaths = [].obs; var stitchedImagePath = ''.obs; var cameraController = Rx<CameraController?>(null); var isCapturing = false.obs; final carImagesListViewModel = Get.find(); final ImageStitching imageStitching = ImageStitching();

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

Future initializeCamera() async { cameras.value = await availableCameras(); if (cameras.isNotEmpty) { cameraController(CameraController(cameras.first, ResolutionPreset.high)); await cameraController.value?.initialize(); cameraController.refresh(); update(); print('Camera initialized successfully.'); } else { print('No cameras found.'); } }

Future startCapturing() async { isCapturing.value = true; progressPercentage.value = 0.0; imagePaths.clear(); stitchedImagePath.value = ''; await captureAndStitchImages(); }

void updateProgress(double value) { progressPercentage.value = value; update(); }

Future captureAndStitchImages() async { final directory = await getApplicationDocumentsDirectory(); for (int i = 0; i < 5; i++) { if (!isCapturing.value) break;

  final path = join(directory.path, '${DateTime.now().millisecondsSinceEpoch}.jpg');
  try {
    final XFile? picture = await cameraController.value?.takePicture();
    if (picture != null) {
      await picture.saveTo(path);
      if (await File(path).exists()) {
        imagePaths.add(path);
        updateProgress((i + 1) / 5.0);
        print('Captured image: $path');
        await Future.delayed(const Duration(seconds: 3));
        update();
      } else {
        print('Error saving image: File does not exist after saving');
      }
    } else {
      print("Error capturing image: Picture is null");
    }
  } catch (e) {
    print("Error capturing image: $e");
  }
}

if (isCapturing.value) {
  try {
    await stitchAndSaveImages(directory, imagePaths);
    Get.back();
  } catch (e) {
    print('Error finalizing capture: $e');
  } finally {
    isCapturing.value = false;
    update();
  }
} else {
  print("No images were captured.");
}

}

Future stitchAndSaveImages(Directory directory, List imagePaths) async { try { final stitchedImageData = await stitchImages(imagePaths);

  if (stitchedImageData != null) {
    final tempStitchedImagePath = join(directory.path, 'stitched_result.jpg');

    final stitchedFile = File(tempStitchedImagePath);
    await stitchedFile.writeAsBytes(stitchedImageData);
    stitchedImagePath.value = tempStitchedImagePath;
    update();
    print('Stitched image path: $tempStitchedImagePath');

    // Save the stitched image to the gallery and upload it
    await finalizeStitchingAndUpload(tempStitchedImagePath);
  } else {
    print('Error: Stitched image returned null');
  }
} catch (e) {
  print('Error during stitching and saving: $e');
}

}

Future<Uint8List?> stitchImages(List imagePaths) async { if (imagePaths.length < 2) return null;

try {
  final List<Uint8List> imageDatas = [];
  for (String path in imagePaths) {
    imageDatas.add(await File(path).readAsBytes());
  }

  // Check if the images are valid before processing
  if (imageDatas.any((data) => data.isEmpty)) {
    print("Error: One of the images is empty.");
    return null;
  }

  // Uint8List? tempStitchedData = imageDatas[0];
  // for (int i = 1; i < imageDatas.length; i++) {
  //   tempStitchedData =
  //   await imageStitching.stitch(tempStitchedData!, imageDatas[i]);
  // }

  return await imageStitching.stitch(imageDatas);
} catch (e) {
  print('Error during stitching: $e');
  return null;
}

}

Future finalizeStitchingAndUpload(String stitchedImagePath) async { try { final stitchedImageFile = File(stitchedImagePath); if (await stitchedImageFile.exists()) { await GallerySaver.saveImage(stitchedImageFile.path); carImagesListViewModel.uploadImage(stitchedImageFile, true); update(); print('Image uploaded: ${stitchedImageFile.path}'); } else { print("Stitched image file does not exist: $stitchedImagePath"); } } catch (e) { print('Error uploading stitched image: $e'); } }

@override void onClose() { cameraController.value?.dispose(); super.onClose(); } } and the UI part:import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'camera_360_view_model.dart'; import 'dart:io';

class CameraPage extends GetView { const CameraPage({super.key});

@override Widget build(BuildContext context) { final Camera360Controller controller = Get.find();

return Scaffold(
  extendBodyBehindAppBar: true,
  backgroundColor: Colors.black,
  appBar: AppBar(
    backgroundColor: Colors.transparent,
    elevation: 0,
    leading: IconButton(
      icon: const Icon(Icons.arrow_back, color: Colors.white),
      onPressed: () => Get.back(),
    ),
  ),
  body: Obx(() {
    if (controller.cameraController.value == null || !controller.cameraController.value!.value.isInitialized) {
      return const Center(child: CircularProgressIndicator());
    }

    return Stack(
      children: [
        CameraPreview(controller.cameraController.value!),
        Positioned(
          bottom: 20,
          left: 20,
          right: 20,
          child: Column(
            children: [
              Obx(() {
                if (controller.stitchedImagePath.value.isNotEmpty) {
                  return Container(
                    height: 100,
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.white),
                      borderRadius: BorderRadius.circular(10),
                    ),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(10),
                      child: Image.file(
                        File(controller.stitchedImagePath.value),
                        fit: BoxFit.cover,
                      ),
                    ),
                  );
                } else {
                  return Container();
                }
              }),
              const SizedBox(height: 10),
              Obx(() => LinearProgressIndicator(
                    value: controller.progressPercentage.value,
                    backgroundColor: Colors.white,
                    color: Colors.greenAccent,
                  )),
              const SizedBox(height: 20),
              Obx(() => IconButton(
                    onPressed: controller.isCapturing.value ? null : controller.startCapturing,
                    icon: const Icon(Icons.camera, color: Colors.white, size: 40),
                  )),
            ],
          ),
        ),
        Positioned(
          top: MediaQuery.of(context).size.height / 2 - 1,
          left: 0,
          right: 0,
          child: Column(
            children: [
              const Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.arrow_forward, color: Colors.white),
                  SizedBox(width: 8),
                  Text(
                    "Move along the arrow",
                    style: TextStyle(color: Colors.white),
                  ),
                ],
              ),
              Container(
                height: 2,
                color: Colors.yellow,
              ),
            ],
          ),
        ),
      ],
    );
  }),
);

} } when running it i get :E/cv::error()(23901): OpenCV(4.10.0) Error: Requested object was not found (could not open directory: /data/app/~~3F4TS8Z-Ua-7KBwU-igHyg==/com.goxeedealer.intel_booth_app-8BWIDMIFHGTnSBYhJozSfQ==/base.apk!/lib/arm64-v8a) in glob_rec, file /home/runner/work/opencv.full/opencv.full/build/opencv/modules/core/src/glob.cpp, line 279 E/cv::error()(23901): OpenCV(4.10.0) Error: Requested object was not found (could not open directory: /data/app/~~3F4TS8Z-Ua-7KBwU-igHyg==/com.goxeedealer.intel_booth_app-8BWIDMIFHGTnSBYhJozSfQ==/base.apk!/lib/arm64-v8a) in glob_rec, file /home/runner/work/opencv.full/opencv.full/build/opencv/modules/core/src/glob.cpp, line 279 E/cv::error()(23901): OpenCV(4.10.0) Error: Requested object was not found (could not open directory: /data/app/~~3F4TS8Z-Ua-7KBwU-igHyg==/com.goxeedealer.intel_booth_app-8BWIDMIFHGTnSBYhJozSfQ==/base.apk!/lib/arm64-v8a) in glob_rec, file /home/runner/work/opencv.full/opencv.full/build/opencv/modules/core/src/glob.cpp, line 279 I/flutter (23901): Error during stitching: Exception: Stitcher failed with status StitcherStatus.ERR_NEED_MORE_IMGS`

any idea how to make it work?

GoxeeVladan commented 1 month ago

import 'dart:io'; import 'dart:typed_data'; import 'package:camera/camera.dart'; import 'package:gallery_saver/gallery_saver.dart'; import 'package:get/get.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:opencv_dart/opencv_dart.dart' as cv; import 'package:intel_booth_app/photos/car_images_list_view_model.dart';

class Camera360Controller extends GetxController { var progressPercentage = 0.0.obs; var cameras = [].obs; var imagePaths = [].obs; var stitchedImagePath = ''.obs; var cameraController = Rx<CameraController?>(null); var isCapturing = false.obs; final carImagesListViewModel = Get.find();

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

Future initializeCamera() async { cameras.value = await availableCameras(); if (cameras.isNotEmpty) { cameraController(CameraController(cameras.first, ResolutionPreset.high)); await cameraController.value?.initialize(); cameraController.refresh(); update(); print('Camera initialized successfully.'); } else { print('No cameras found.'); } }

Future startCapturing() async { isCapturing.value = true; progressPercentage.value = 0.0; imagePaths.clear(); stitchedImagePath.value = ''; await captureAndStitchImages(); }

void updateProgress(double value) { progressPercentage.value = value; update(); }

Future captureAndStitchImages() async { final directory = await getApplicationDocumentsDirectory(); for (int i = 0; i < 5; i++) { if (!isCapturing.value) break;

  final path = join(directory.path, '${DateTime.now().millisecondsSinceEpoch}.jpg');
  try {
    final XFile? picture = await cameraController.value?.takePicture();
    if (picture != null) {
      await picture.saveTo(path);
      if (await File(path).exists()) {
        imagePaths.add(path);
        updateProgress((i + 1) / 5.0);
        print('Captured image: $path');
        await Future.delayed(const Duration(seconds: 2));
        update();
      } else {
        print('Error saving image: File does not exist after saving');
      }
    } else {
      print("Error capturing image: Picture is null");
    }
  } catch (e) {
    print("Error capturing image: $e");
  }
}

if (isCapturing.value) {
  try {
    await stitchAndSaveImages(directory, imagePaths);
    Get.back();
  } catch (e) {
    print('Error finalizing capture: $e');
  } finally {
    isCapturing.value = false;
    update();
  }
} else {
  print("No images were captured.");
}

}

Future stitchAndSaveImages(Directory directory, List imagePaths) async { try { final stitchedImageData = await stitchImages(imagePaths);

  if (stitchedImageData != null) {
    final tempStitchedImagePath = join(directory.path, 'stitched_result.png');

    final stitchedFile = File(tempStitchedImagePath);
    await stitchedFile.writeAsBytes(stitchedImageData);
    stitchedImagePath.value = tempStitchedImagePath;
    update();
    print('Stitched image path: $tempStitchedImagePath');

    // Save the stitched image to the gallery and upload it
    await finalizeStitchingAndUpload(tempStitchedImagePath);
  } else {
    print('Error: Stitched image returned null');
  }
} catch (e) {
  print('Error during stitching and saving: $e');
}

}

Future<Uint8List?> stitchImages(List imagePaths) async { if (imagePaths.isEmpty) { throw ArgumentError("No images provided for stitching"); }

try {
  final List<Uint8List> imageDatas = [];
  for (String path in imagePaths) {
    imageDatas.add(await File(path).readAsBytes());
  }

  if (imageDatas.any((data) => data.isEmpty)) {
    print("Error: One of the images is empty.");
    return null;
  }

  List<cv.Mat> images = [];
  for (final imageData in imageDatas) {
    final image = cv.imdecode(imageData, cv.IMREAD_COLOR);
    if (image.isEmpty) {
      print("Failed to decode image. Skipping this image.");
      continue;
    }
    images.add(image);
  }

  if (images.length < 2) {
    throw Exception("Not enough valid images for stitching.");
  }

  print("Number of valid images to stitch: ${images.length}");

  cv.Mat? stitchedImage;
  final stitcher = cv.Stitcher.create(mode: cv.StitcherMode.SCANS);  // Use SCANS mode

  for (int i = 0; i < images.length; i++) {
    print("Processing image $i with size ${images[i].size}");
    var image = images[i];
    if (i == 0) {
      stitchedImage = image.clone();
    } else {
      final (status, dst) = await stitcher.stitchAsync(cv.VecMat.fromList([stitchedImage!, image]));
      if (status == cv.StitcherStatus.ERR_NEED_MORE_IMGS) {
        print("Stitcher needs more images to continue. Status: $status on iteration $i");
        throw Exception("Stitcher failed: Not enough overlap or too few images.");
      } else if (status != cv.StitcherStatus.OK) {
        print("Stitcher failed with status $status on iteration $i");
        throw Exception("Stitcher failed with status $status");
      }
      stitchedImage = dst;
    }
  }

  if (stitchedImage == null) {
    throw Exception("Failed to stitch images");
  }

  final (success, bytes) = await cv.imencodeAsync(".png", stitchedImage);
  if (!success) {
    throw Exception("Failed to encode stitched image.");
  }
  return bytes;
} catch (e) {
  print('Error during stitching: $e');
  return null;
}

}

Future finalizeStitchingAndUpload(String stitchedImagePath) async { try { final stitchedImageFile = File(stitchedImagePath); if (await stitchedImageFile.exists()) { await GallerySaver.saveImage(stitchedImageFile.path); carImagesListViewModel.uploadImage(stitchedImageFile, true); update(); print('Image uploaded: ${stitchedImageFile.path}'); } else { print("Stitched image file does not exist: $stitchedImagePath"); } } catch (e) { print('Error uploading stitched image: $e'); } }

@override void onClose() { cameraController.value?.dispose(); super.onClose(); } } here's updated code after few attempts to solve the issue and now i get this:

I/flutter (11236): Error during stitching: Invalid argument(s): Failed to load dynamic library 'libopencv_dart.so': dlopen failed: library "libopencv_dart.so" not found I/flutter (11236): Error: Stitched image returned null