Image stitching advice
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 = <CameraDescription>[].obs; var imagePaths = <String>[].obs; var stitchedImagePath = ''.obs; var cameraController = Rx<CameraController?>(null); var isCapturing = false.obs; final carImagesListViewModel = Get.find<CarImagesListViewModel>();
@override void onInit() { super.onInit(); initializeCamera(); }
Future
Future
void updateProgress(double value) { progressPercentage.value = value; update(); }
Future
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
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
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(); } }`
@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
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<Uint8List> 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 = <CameraDescription>[].obs; var imagePaths = <String>[].obs; var stitchedImagePath = ''.obs; var cameraController = Rx<CameraController?>(null); var isCapturing = false.obs; final carImagesListViewModel = Get.find<CarImagesListViewModel>(); final ImageStitching imageStitching = ImageStitching();
@override void onInit() { super.onInit(); initializeCamera(); }
Future
Future
void updateProgress(double value) { progressPercentage.value = value; update(); }
Future
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
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<String> 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
@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<Camera360Controller> { const CameraPage({super.key});
@override Widget build(BuildContext context) { final Camera360Controller controller = Get.find<Camera360Controller>();
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?
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 = <CameraDescription>[].obs; var imagePaths = <String>[].obs; var stitchedImagePath = ''.obs; var cameraController = Rx<CameraController?>(null); var isCapturing = false.obs; final carImagesListViewModel = Get.find<CarImagesListViewModel>();
@override void onInit() { super.onInit(); initializeCamera(); }
Future
Future
void updateProgress(double value) { progressPercentage.value = value; update(); }
Future
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
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<String> 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
@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