0

I'm writing a mobile app to take multiple pictures of an item as it rotates on a platform. This will give the user pictures in a 360 degree view of the item. So far, I can take the multiple pictures just fine using the Camera.Maui plugin, and I can even auto-save the pics to the phone's "Camera" folder just fine.

I want to give the user an option to preview the pics before they get saved. So, I have a page/activity where I display a "thumbnail" of all the pictures. If the user taps on a pic, it goes to another page/activity where I want to display the full-sized pic and allow the user to zoom in/out and pan around, then move to the next/previous pic to do the same. I haven't gotten to the zoom or pan features, as I'm stuck on trying to display the image on this page.

The problem happens in 2 places, and it's the same problem. Those places are when I try saving the image (which works as long as I don't try to display it first) and when I try to display the image on the 2nd preview/zoom page. When I try to use the ImageSource I get from the camera plugin a 2nd time, it says that the stream is closed, which I realize is to be expected.

I'm going to concentrate on displaying the picture a 2nd time, as I'm sure saving the image will reuse whatever solution is found.

So, walking through the previewing process: the MainPage has a CameraView element that displays what the camera sees. I use a thread to call the "GrabSnapshot" method multiple times, which takes a snapshot from the camera feed and puts it into the "pics" List as the raw ImageSource I got from the camera. (I use the thread to keep the main thread from getting bogged down, as well for other reasons unimportant to this question.)

When all the pictures are taken, the Preview button is made available so the user can go to a new page/activity that builds a grid and creates a new ImageButton for each picture taken. That button can be clicked/tapped so the whole List and the index of the selected picture are sent to another page. This new page tries to put the selected picture into an Image element and where it says the stream has already been closed.

The question: How do I display the pictures without closing the original stream?

Do I have to create a copy of each ImageSource and set the copy as the source of the ImageButton or Image elements? I've briefly tried to research how to do this and can't find a way to do this without first converting it into a byte[] then creating a new stream. This seems like a very indirect and memory expensive way to do that. I'm definitely worried about memory management with having at least 2 (if not 3) copies of every picture in memory.

Note: I'm not sure how many pictures people would actually want of an item, but I'd think they might want ~20 of small items and maybe 30, 40, 50, or more of larger items. I'm not even sure how much memory each picture takes up. The current saved image is a PNG at around 350k to 450k and is 720x1462 pixels. Unfortunately, it's more like a screenshot than an actual picture, so I'm going to try to find a different way to get hi-res pictures. I know that specific phone with that camera can take a JPG at 4160x3120 resolution, with a file size of around 3,800kb, which is what I want from the camera. (I don't know if this is what I'm getting from the camera plugin or if it's just how I'm saving it, but I'm saving it without resizing it or changing the DPI.)

I'm writing the app using C# .Net 7 and in a Maui project. So far I've only tested it in Android using real devices running APIs 28 and 31. As I mentioned earlier, I'm using the Camera.Maui plugin, which was the first camera plugin/feature I could get to work.

Maybe what I need is a different approach/plugin/feature to take the pictures in the first place.

There's a bunch of files/classes in my project to handle all this, so I've tried to pare down the code and combine them as much as possible.

My code:
MainPage.xaml.cs

using Camera.MAUI;

    private readonly CameraView camera;
    private readonly List<ImageSource> pics = new();

    public MainPage()
    {
        InitializeComponent();
        camera = cameraView; // cameraView is the XAML x:Name of the CameraView element
    }

    public void GrabSnapshot()
    {
        pics.Add(camera.GetSnapShot());
    }

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:cv="clr-namespace:Camera.MAUI;assembly=Camera.MAUI"
             x:Class="XXX.MainPage"
             Title="">
    <Grid VerticalOptions="Fill" HorizontalOptions="Fill">
        <cv:CameraView x:Name="cameraView"
                        VerticalOptions="Fill"
                        HorizontalOptions="Fill"
                        CamerasLoaded="CameraView_CamerasLoaded" />
        <HorizontalStackLayout Spacing="30" HorizontalOptions="Center" VerticalOptions="End" Margin="0,0,20,0" x:Name="ButtonPane">
            <ImageButton Source="switch_camera_icon.jpg" HeightRequest="50" WidthRequest="60" IsOpaque="True" Clicked="SwapButton_Clicked" x:Name="SwapButton" />
            <Button Text="Start" Clicked="StartButton_Clicked" x:Name="StartButton"/>
            <Button Text="Options" Clicked="OptionsButton_Clicked" x:Name="OptionsButton" />
            <Button Text="Preview" Clicked="PreviewButton_Clicked" x:Name="PreviewButton" IsVisible="False" IsEnabled="False" />
        </HorizontalStackLayout>
    </Grid>
</ContentPage>

PreviewPage.xaml.cs

    private void DisplayPics()
    {
        // yes, this could be done in a for loop, I just haven't cleaned this up yet
        int counter = 0;

        /*
        Grid row and column definitions for the "GridLayout"
        */

        foreach (ImageSource pic in pics)
        {
            int row = (int)Math.Floor(counter / 2m);
            int column = counter % 2;
            ImageButton button = new() { Source = pic };

            GridLayout.Add(button, column, row);
            counter++;
        }
    }
/*
<Grid x:Name="GridLayout" VerticalOptions="Fill" HorizontalOptions="Fill" />
*/

ImagePage.xaml

    private readonly List<ImageSource> pics = new();
    private int index;

    public ImagePage(int indexParam, List<ImageSource> picsList)
    {
        InitializeComponent();

        pics = picsList;
        index = indexParam;
        ImagePanel.Source = pics[index];
/*
<Image x:Name="ImagePanel" VerticalOptions="Fill" HorizontalOptions="Fill" />
*/
    }
computercarguy
  • 2,173
  • 1
  • 13
  • 27
  • the fact that `GetSnapShot` returns an `ImageSource` and not something more flexible like a `byte[]` or file path seems like a poor design. I'd see if there is another method available, or modify the existing method to suit your needs. – Jason May 30 '23 at 22:34
  • @Jason, I'm not sure why it would return a file path, since the image is coming from the camera. I can definitely convert it to a `byte[]`, but I don't understand why that would be considered "more flexible". Can you elaborate on that or link me to some articles that would explain it? – computercarguy May 30 '23 at 22:42
  • AFAIK you can't extract or convert the data from an `ImageSource`. All you can do is display it. That's not what I would consider flexible. Other camera APIs typically return a `byte[]` or `stream`, or even a path to a temp file containing the image data – Jason May 30 '23 at 22:47
  • also, you didn't provide a link to the nuget you're using, so I may be looking at something different than you are – Jason May 30 '23 at 22:48
  • @Jason, it's https://github.com/hjam40/Camera.MAUI. I'll add it to the question. – computercarguy May 30 '23 at 22:48
  • @Jason, I'm pulling out the data from the `ImageSource` when I'm saving it. Stream stream = await ((StreamImageSource)imgSrc).Stream(CancellationToken.None); It might not be as straightforward as a `byte[]`, but I can save it as a PNG or JPG by passing it through a `Bitmap` and using the `Compress` method to save it. That `Compress` method might be what's killing my resolution, though. IDK. – computercarguy May 30 '23 at 22:55
  • 1
    interesting. Personally, I would write it to a temp file and just pass the path instead of dealing with the headache of passing streams. – Jason May 30 '23 at 23:00
  • @Jason, this is my first time getting and dealing with camera images, so IDK what I'm doing. Lol. Your suggestion sounds just as good, if not better, than what I'm trying to cobble together. – computercarguy May 30 '23 at 23:03
  • @Jason, actually, after rereading my code for saving, I'm not using that stream. I'm doing it more like the OP from https://stackoverflow.com/questions/73816975/how-to-save-imagesource-to-jpg-in-maui. – computercarguy May 30 '23 at 23:10
  • Like Jason said. Since you want re-use without tying up a lot of memory, save it as a file, then use its path as the source. If its temporary, then do this within your app's local file storage, not in a "shared" media storage location. – ToolmakerSteve May 31 '23 at 00:09

1 Answers1

0

Springboarding off the discussion with @Jason, I've converted the ImageSource returned from the GetSnapShot method to a byte array. Subsequently, I've changed variable datatypes and how I've set Image and ImageButton elements to display the pictures.

The "pics" variable is now List<byte[]> instead of List<ImageSource>.

pics.Add(camera.GetSnapShot());

became

pics.Add(await ImageSourceToByteArray(camera.GetSnapShot()));

// https://stackoverflow.com/a/62112246/1836461
private static async Task<byte[]> ImageSourceToByteArray(ImageSource imgsrc)
{
    Stream stream = await ((StreamImageSource)imgsrc).Stream(CancellationToken.None);
    byte[] bytesAvailable = new byte[stream.Length];
    stream.Read(bytesAvailable, 0, bytesAvailable.Length);

    return bytesAvailable;
}

Due to the need for speed, I may separate out the method call into another thread, since I need to take pictures rapidly for this project. (I'd like the ability to take pictures on the order of 2-3 per second, but I'm not sure if this is even attainable yet or not. I may end up asking another question on how to even approach going about that.)

Displaying images went from

foreach (ImageSource pic in pics)
{
    ...
    ImageButton button = new() { Source = pic };
    ...
}
// and
ImagePanel.Source = pics[index];

to

foreach (byte[] pic in pics)
{
    ...
    ImageSource display = ByteArrayToImageSourceConverter(pic);
    ImageButton button = new() { Source = display };
    ...
}
// and
ImagePanel.Source = ByteArrayToImageSourceConverter(pics[index]);

public static ImageSource ByteArrayToImageSourceConverter(byte[] imageArray)
{
    MemoryStream memory = new(imageArray);
    return ImageSource.FromStream(() => memory);
}

This work for what I need, so that's good enough for now. At least until I do more testing. I may end up saving the files and working with them that way. My concern there is that I'll have to manage removing the files when I no longer need them as well as moving them to an easily accessible location when/if the user decides to keep the pictures. But that's for another day and/or question.

FYI, I was using the ImageSource to save images to the "Camera" folder on the phone, or wherever the user selected to save them. I still haven't changed the save methods yet, but that's getting off-topic. My Windows implementation is already used a byte array, so this change will actually make that method simpler. I'm sure there are iOS and Android methods using a byte array, but I haven't researched that yet, but again, that's off-topic for this question.

Edit/Update:
I haven't tested this on iOS or Windows yet, but what I had for saving on Windows seems to work on Android, and doesn't fail compilation for iOS, so I've gotten rid of OS specific classes for a single method. It produces a valid image from an Android phone, at least. I was doing some extra steps before saving for converting between PNG and JPEG, but I've also made changes to how I call the "GetSnapShot" method so now the method provides PNG or JPG file formats without the need for conversion and saving is extremely simple.

public static async void SaveImage(byte[] imageSource, string path)
{
    await File.WriteAllBytesAsync(path, imageSource);
}
computercarguy
  • 2,173
  • 1
  • 13
  • 27