Статьи

Как сделать фотографию в приложении Windows Phone 8.1 Runtime — Часть III: захват и сохранение фотографии

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

Самый простой способ — запечатлеть как есть:

Самый простой способ сделать снимок — это использовать метод CapturePhotoToStorageFileAsync () MediaCapture. Этот метод показывает вам, как это сделать:

//declare image format
            ImageEncodingProperties format = ImageEncodingProperties.CreateJpeg();

            //generate file in local folder:
            StorageFile capturefile = await ApplicationData.Current.LocalFolder.CreateFileAsync("photo_" + DateTime.Now.Ticks.ToString(), CreationCollisionOption.ReplaceExisting);

            ////take & save photo
            await captureManager.CapturePhotoToStorageFileAsync(format, capturefile);

            //show captured photo
            BitmapImage img = new BitmapImage(new Uri(capturefile.Path));
            takenImage.Source = img;
            takenImage.Visibility = Visibility.Visible;

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

Уважая вращение на захваченном фото:

В нашем текущем примере мы используем поворот на 90 градусов для отображения элемента предварительного просмотра в портретном режиме. Естественно, мы хотим перенести эту ориентацию на наше захваченное изображение.

Есть два способа добиться этого. Мы могли бы захватить фотографию в WriteableBitmap и манипулировать ею, или мы могли бы манипулировать потоком изображений напрямую с  помощью  классов BitmapDecoder  и  BitmapEncoder . Мы сделаем последний.

Во-первых, нам нужно открыть  InMemoryRandomAccessStream  для нашей захваченной фотографии. Мы фиксируем фотографию в потоке с помощью метода CapturePhotoToStreamAsync () MediaCapture,  указывая  имя потока и формат изображения.

Следующим шагом является декодирование потока с помощью нашего BitmapDecoder. Если мы выполняем только ротацию, мы можем напрямую перекодировать InMemoryRandomAccessStream, который мы используем. Поворот захваченной фотографии очень прост: достаточно просто  повернуть  свойство BitmapTransform.Rotation на 90 градусов, почти так же легко, как и превью предварительного просмотра.

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

//declare string for filename
            string captureFileName = string.Empty;
            //declare image format
            ImageEncodingProperties format = ImageEncodingProperties.CreateJpeg();

            //rotate and save the image
            using (var imageStream = new InMemoryRandomAccessStream())
            {
                //generate stream from MediaCapture
                await captureManager.CapturePhotoToStreamAsync(format, imageStream);

                //create decoder and encoder
                BitmapDecoder dec = await BitmapDecoder.CreateAsync(imageStream);
                BitmapEncoder enc = await BitmapEncoder.CreateForTranscodingAsync(imageStream, dec);

                //roate the image
                enc.BitmapTransform.Rotation = BitmapRotation.Clockwise90Degrees;

                //write changes to the image stream
                await enc.FlushAsync();

                //save the image
                StorageFolder folder = KnownFolders.SavedPictures;
                StorageFile capturefile = await folder.CreateFileAsync("photo_" + DateTime.Now.Ticks.ToString() + ".jpg", CreationCollisionOption.ReplaceExisting);
                captureFileName = capturefile.Name;

                //store stream in file
                using (var fileStream = await capturefile.OpenStreamForWriteAsync())
                {
                    try
                    {
                        //because of using statement stream will be closed automatically after copying finished
                        await RandomAccessStream.CopyAsync(imageStream, fileStream.AsOutputStream());
                    }
                    catch 
                    {

                    }
                }
            }

Конечно, нам нужно остановить предварительный просмотр после того, как мы сделали снимок. Также имеет смысл загрузить сохраненное изображение и отобразить его пользователю. Это код для остановки предварительного просмотра:

private async void CleanCapture()
        {

            if (captureManager != null)
            {
                if (isPreviewing == true)
                {
                    await captureManager.StopPreviewAsync();
                    isPreviewing = false;
                }
                captureManager.Dispose();

                previewElement.Source = null;
                previewElement.Visibility = Visibility.Collapsed;
                takenImage.Source = null;
                takenImage.Visibility = Visibility.Collapsed;
                captureButton.Content = "capture";
            }

        }

Результат вышеупомянутого кода (скриншот предварительного просмотра слева, снимок справа):

16by9Photo

Обрезка захваченного фото

Не все устройства Windows Phone имеют соотношение сторон 16: 9. Фактически, большинство устройств на рынке имеют соотношение сторон 15: 9 из-за того, что они являются устройствами WVGA или WXGA (об этом я уже говорил во втором посте). Если мы просто делаем снимок описанным выше способом, у нас будут те же черные полосы на изображении, что и в нашем предварительном просмотре. Чтобы обойти это и сделать фотографию с истинным разрешением 15: 9 (имеет смысл для фотографий, которые повторно используются в приложениях, но меньше для реальных фотографий), необходим дополнительный код.

Как и в случае получения правильного решения для камеры, я сгенерировал перечисление, в котором хранятся все возможные значения, а также вспомогательный метод для определения соотношения сторон, используемого в настоящее время используемым устройством:

public enum DisplayAspectRatio
        {
            Unknown = -1,

            FifteenByNine = 0,

            SixteenByNine = 1
        }

        private DisplayAspectRatio GetDisplayAspectRatio()
        {
            DisplayAspectRatio result = DisplayAspectRatio.Unknown;

            //WP8.1 uses logical pixel dimensions, we need to convert this to raw pixel dimensions
            double logicalPixelWidth = Windows.UI.Xaml.Window.Current.Bounds.Width;
            double logicalPixelHeight = Windows.UI.Xaml.Window.Current.Bounds.Height;

            double rawPerViewPixels = DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;
            double rawPixelHeight = logicalPixelHeight * rawPerViewPixels;
            double rawPixelWidth = logicalPixelWidth * rawPerViewPixels;

            //calculate and return screen format
            double relation = Math.Max(rawPixelWidth, rawPixelHeight) / Math.Min(rawPixelWidth, rawPixelHeight);
            if (Math.Abs(relation - (15.0 / 9.0)) < 0.01)
            {
                result = DisplayAspectRatio.FifteenByNine;
            }
            else if (Math.Abs(relation - (16.0 / 9.0)) < 0.01)
            {
                result = DisplayAspectRatio.SixteenByNine;
            }

            return result;
        }

В Windows Phone 8.1 все элементы используют размер логического пикселя. Чтобы получить значения, к которым привыкло большинство из нас, нам нужно вычислить необработанные пиксели из логических пикселей. После этого мы используем те же математические операции, которые я уже использовал для определения соотношения разрешения камеры (см. Пост 2). Я также пытался вычислить значения с помощью логических пикселей, но это привело к некоторому странному поведению округления, а не к желаемым результатам. Вот почему я использую необработанные размеры пикселей.

Прежде чем мы продолжим захват фотографии, мы добавим границу, которая отображается и показывает область, которая была захвачена пользователем в XAML:

Когда мы обрезаем нашу фотографию, нам нужно обрабатывать BitmapEncoder и BitmapDecoder отдельно. Чтобы обрезать изображение, нам нужно установить границы и новые ширину и высоту фотографии через   свойство BitmapTransform.Bounds . Нам также необходимо прочитать PixelData с помощью  метода GetPixelDataAsync () , применить к нему измененные границы и передать их BitmapEncoder с помощью  метода SetPixelData ()  .

В конце мы сбрасываем измененные данные потока непосредственно в файловый поток нашего StorageFile. Вот как:

//declare string for filename
            string captureFileName = string.Empty;
            //declare image format
            ImageEncodingProperties format = ImageEncodingProperties.CreateJpeg();

            using (var imageStream = new InMemoryRandomAccessStream())
            {
                //generate stream from MediaCapture
                await captureManager.CapturePhotoToStreamAsync(format, imageStream);

                //create decoder and transform
                BitmapDecoder dec = await BitmapDecoder.CreateAsync(imageStream);
                BitmapTransform transform = new BitmapTransform();

                //roate the image
                transform.Rotation = BitmapRotation.Clockwise90Degrees;
                transform.Bounds = GetFifteenByNineBounds();

                //get the conversion data that we need to save the cropped and rotated image
                BitmapPixelFormat pixelFormat = dec.BitmapPixelFormat;
                BitmapAlphaMode alpha = dec.BitmapAlphaMode;

                //read the PixelData
                PixelDataProvider pixelProvider = await dec.GetPixelDataAsync(
                    pixelFormat,
                    alpha,
                    transform,
                    ExifOrientationMode.RespectExifOrientation,
                    ColorManagementMode.ColorManageToSRgb
                    );
                byte[] pixels = pixelProvider.DetachPixelData();

                //generate the file
                StorageFolder folder = KnownFolders.SavedPictures;
                StorageFile capturefile = await folder.CreateFileAsync("photo_" + DateTime.Now.Ticks.ToString() + ".jpg", CreationCollisionOption.ReplaceExisting);
                captureFileName = capturefile.Name;

                //writing directly into the file stream
                using (IRandomAccessStream convertedImageStream = await capturefile.OpenAsync(FileAccessMode.ReadWrite))
                {
                    //write changes to the BitmapEncoder
                    BitmapEncoder enc = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, convertedImageStream);
                    enc.SetPixelData(
                        pixelFormat,
                        alpha,
                        transform.Bounds.Width,
                        transform.Bounds.Height,
                        dec.DpiX,
                        dec.DpiY,
                        pixels
                        );

                    await enc.FlushAsync();
                }
            }

Возможно, вы заметили метод GetFifteenByNineBounds () в приведенном выше коде. Поскольку нам нужно рассчитать некоторые значения для обрезки изображения, я решил разделить их. Они не только предоставляют значения для обрезаемого изображения, но также и значения размера для нашей ранее добавленной границы, которая используется в моем образце (ссылка на скачивание в конце проекта), чтобы показать размер, который будет иметь фотография после обрезки. (в нашем случае это автоматический процесс). Вот код:

private BitmapBounds GetFifteenByNineBounds()
        {
            BitmapBounds bounds = new BitmapBounds();

            //image size is raw pixels, so we need also here raw pixels
            double logicalPixelWidth = Windows.UI.Xaml.Window.Current.Bounds.Width;
            double logicalPixelHeight = Windows.UI.Xaml.Window.Current.Bounds.Height;

            double rawPerViewPixels = DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;
            double rawPixelHeight = logicalPixelHeight * rawPerViewPixels;
            double rawPixelWidth = logicalPixelWidth * rawPerViewPixels;

            //calculate scale factor of UniformToFill Height (remember, we rotated the preview)
            double scaleFactorVisualHeight = maxResolution().Width / rawPixelHeight;

            //calculate the visual Width
            //(because UniFormToFill scaled the previewElement Width down to match the previewElement Height)
            double visualWidth = maxResolution().Height / scaleFactorVisualHeight;

            //calculate cropping area for 15:9
            uint scaledBoundsWidth = maxResolution().Height;
            uint scaledBoundsHeight = (scaledBoundsWidth / 9) * 15;

            //we are starting at the top of the image
            bounds.Y = 0;
            //cropping the image width
            bounds.X = 0;
            bounds.Height = scaledBoundsHeight;
            bounds.Width = scaledBoundsWidth;

            //set finalPhotoAreaBorder values that shows the user the area that is captured
            finalPhotoAreaBorder.Width = (scaledBoundsWidth / scaleFactorVisualHeight) / rawPerViewPixels;
            finalPhotoAreaBorder.Height = (scaledBoundsHeight / scaleFactorVisualHeight) / rawPerViewPixels;
            finalPhotoAreaBorder.Margin = new Thickness(
                                            Math.Floor(((rawPixelWidth - visualWidth) / 2) / rawPerViewPixels), 
                                            0,
                                            Math.Floor(((rawPixelWidth - visualWidth) / 2) / rawPerViewPixels), 
                                            0);
            finalPhotoAreaBorder.Visibility = Visibility.Visible;

            return bounds;
        }

Опять же, нам нужно применить необработанные пиксели для достижения наилучших результатов (я просто вставил эти строки для этого примера). Чтобы вычислить правильные значения для нашей Границы, нам нужен масштабный коэффициент между экраном и разрешением предварительного просмотра, которое мы использовали (который является двойным scaleFactorVisualHeight). Прежде чем мы вычислим значения границ, мы устанавливаем ширину для высоты разрешения (мы повернули, помните?) И вычисляем соответствующую высоту 15: 9.

Значения границ основаны на ширине и высоте обрезанного изображения, но уменьшены до значения scaleFactorVisualHeight и преобразованы в необработанный пиксель. Margin размещает границу соответственно над элементом предварительного просмотра.

Это результат вышеупомянутого кода (скриншот предварительного просмотра слева, снимок справа):

15by9Photo

Это все, что вам нужно знать, чтобы приступить к базовому захвату фотографий из приложения Windows Phone 8.1 Runtime. Конечно, есть и другие модификации, которые вы можете применить, и я уже упоминал большинство классов, которые приводят вас к подходящим методам и свойствам (нажмите на ссылки, чтобы перейти к документации)

Кстати, большую часть кода можно адаптировать и в приложении для Windows 8.1 (с некоторыми отличиями, конечно).

Пример проекта

Как и было обещано, вы можете  скачать образец здесь . Он содержит все фрагменты кода, которые я вам показал, и может работать при его сборке и развертывании.

Как всегда, отзывы приветствуются, и я надеюсь, что этот пост будет полезен для некоторых из вас.

До следующего раза, счастливого кодирования!