Это третий и последний пост из этой серии. В первых двух постах я показал вам, как запустить предварительный просмотр 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"; } }
Результат вышеупомянутого кода (скриншот предварительного просмотра слева, снимок справа):
Обрезка захваченного фото
Не все устройства 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 размещает границу соответственно над элементом предварительного просмотра.
Это результат вышеупомянутого кода (скриншот предварительного просмотра слева, снимок справа):
Это все, что вам нужно знать, чтобы приступить к базовому захвату фотографий из приложения Windows Phone 8.1 Runtime. Конечно, есть и другие модификации, которые вы можете применить, и я уже упоминал большинство классов, которые приводят вас к подходящим методам и свойствам (нажмите на ссылки, чтобы перейти к документации)
Кстати, большую часть кода можно адаптировать и в приложении для Windows 8.1 (с некоторыми отличиями, конечно).
Пример проекта
Как и было обещано, вы можете скачать образец здесь . Он содержит все фрагменты кода, которые я вам показал, и может работать при его сборке и развертывании.
Как всегда, отзывы приветствуются, и я надеюсь, что этот пост будет полезен для некоторых из вас.
До следующего раза, счастливого кодирования!