Недавно мы начали рассматривать API, выход которых был анонсирован на выставке MWC — 2013. В сегодняшней статье мы рассмотрим практические примеры применения Advanced Camera API.
Windows Phone 8 Advanced Photo Capture API
Windows Phone 8 позволяет очень тонко конфигурировать параметры камеры. Основной входной точкой для новых возможностей камеры является класс Windows.Phone.Media.Capture.PhotoCaptureDevice
, который предоставляет следующие методы:
— Запрос и регулирование настроек поддерживаемых параметров камеры.
• static CameraCapturePropertyRange SupportedPropertyRange(CameraSensorLocation sensor, Guid propertyId)
• static IReadOnlyList<object> GetSupportedPropertyValues(CameraSensorLocation sensor, Guid propertyId)
• object GetProperty(Guid propertyId)
• void SetProperty(Guid propertyId, object value)
— Выполнение автофокусировки, настройка баланса белого и автовыдержки.
• static bool IsFocusSupported(CameraSensorLocation sensor)
• static bool IsFocusRegionSupported(CameraSensorLocation sensor)
• IAsyncOperation<CameraFocusStatus> FocusAsync()
• IAsyncOperation<CameraFocusStatus> ResetFocusAsync()
— Проверка и установка разрешения предпросмотра и конечного снимка.
• static IReadOnlyList<Size> GetAvailablePreviewResolutions()
• static IReadOnlyList<Size> GetAvailableCaptureResolutions()
• IAsyncAction SetPreviewResolutionAsync(Size value)
• IAsyncAction SetCaptureResolutionAsync(Size value)
— Считывание буфера предпросмотра.
• event TypedEventHandler<ICameraCaptureDevice, Object> PreviewFrameAvailable
• void GetPreviewBufferArgb(out int[] pixels)
• void GetPreviewBufferY(out byte[] pixels)
• void GetPreviewBufferYCbCr(out byte[] pixels)
— Создание серии снимков.
• CameraCaptureSequence CreateCaptureSequence(uint numberOfFrames)
• IAsyncAction PrepareCaptureSequenceAsync(CameraCaptureSequence sequence)
Доступ к настройкам камеры производится в основном тремя способами — в зависимости от параметров. Разрешение предпросмотра и снимка задается и считывается с помощью специального асинхронного метода. Аналогично специальные методы в API существуют и для операций фокусировки. Специфические параметры фотографии регулируются с помощью GUID. Он определяет их с помощью методов, которые сообщают поддерживаемый диапазон или набор значений в запросе и метод для активации одного из поддерживаемых значений.
Стоит иметь в виду, что приложению требуется ID_CAP_ISV_CAMERA
в WMAppManifest.xml для работы вышеописанных функций. Дополнительно необходима ID_CAP_MEDIALIB_PHOTO
для возможности сохранения в библиотеку медиа.
Свойства камеры в смартфонах Lumia
Ниже приведена таблица наиболее важных параметров камеры в Nokia Lumia 820 и 920.
Программа Camera Explorer — пример того, как может быть использован PhotoCaptureDevice
на практике. Приложение включает в себя видоискатель, страницу настроек с некоторым количеством регулируемых параметров и страницу просмотра готового снимка с возможностью сохранить сделанный кадр.
Прежде чем смотреть исходный код приложения, давайте посмотрим на архитектуру Camera Explorer.
В дополнение к четырем основным страницам приложения Windows Phone есть еще синглтон DataContext
для владения широко используемыми объектами, такими как экземпляр Settings
или экземпляр PhotoCaptureDevice
.
Самая интересная часть приложения — это инициализация кода PhotoCaptureDevice
в DataContext, параметры, которые были созданы для этого приложения, для доступа к свойствам PhotoCaptureDevice
, а также процесс захвата изображения, помещенный в MainPage, куда входит живой видоискатель.
Для описания в этой статье из всех настроек в приложении Camera Explorer мы выбрали параметры двух разных типов.
Давайте посмотрим, как PhotoCaptureDevice может быть использован на практике, используя выдержки из кода Camera Explorer. Стоит учитывать, что в данной статье не описывается весь код приложения, а потому некоторые части кода сознательно упрощены для целей статьи. С полным кодом приложения можно ознакомиться на странице проекта.
Имейте в виду DataContext, который содержит примеры, необходимые на нескольких разных разделах.
...
class DataContext : INotifyPropertyChanged
{
...
public static DataContext Singleton { ... }
public ObservableCollection<Parameter> Parameters { ... }
public PhotoCaptureDevice Device { ... }
public MemoryStream ImageStream { ... }
}
Захват изображения камерой
Рассмотрим, как PhotoCaptureDevice
может быть запущен с использованием живого видоискателя и возможностью сделать снимок. В MainPage xaml есть элемент Canvas
с элементом VideoBrush
, который рендерит фон.
<phone:PhoneApplicationPage x:Class="CameraExplorer.MainPage" ... >
...
<Grid x:Name="LayoutRoot" ... >
...
<Grid x:Name="ContentPanel" ... >
<Canvas x:Name="VideoCanvas">
<Canvas.Background>
<VideoBrush x:Name="videoBrush"/>
</Canvas.Background>
</Canvas>
...
</Grid>
</Grid>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text="capture" Click="captureButton_Click" ... />
...
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
...
</phone:PhoneApplicationPage>
В коде C# MainPage PhotoCaptureDevice
запускается методом InitializeCamera
, примеры камеры при этом сохраняются в DataContext. В обработчике событий для кнопки съемки, который был заявлен в XAML, мы видим, как фото может быть снято.
...
public partial class MainPage : PhoneApplicationPage
{
private CameraExplorer.DataContext _dataContext = CameraExplorer.DataContext.Singleton;
...
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
if (_dataContext.Device == null)
{
...
await InitializeCamera(CameraSensorLocation.Back);
...
}
videoBrush.RelativeTransform = new CompositeTransform()
{
CenterX = 0.5,
CenterY = 0.5,
Rotation = _dataContext.Device.SensorLocation == CameraSensorLocation.Back ?
_dataContext.Device.SensorRotationInDegrees :
- _dataContext.Device.SensorRotationInDegrees
};
videoBrush.SetSource(_dataContext.Device);
...
}
private async Task InitializeCamera(CameraSensorLocation sensorLocation)
{
Windows.Foundation.Size initialResolution = new Windows.Foundation.Size(640, 480);
Windows.Foundation.Size previewResolution = new Windows.Foundation.Size(640, 480);
Windows.Foundation.Size captureResolution = new Windows.Foundation.Size(640, 480);
PhotoCaptureDevice d = await PhotoCaptureDevice.OpenAsync(sensorLocation, initialResolution);
await d.SetPreviewResolutionAsync(previewResolution);
await d.SetCaptureResolutionAsync(captureResolution);
d.SetProperty(KnownCameraGeneralProperties.EncodeWithOrientation,
d.SensorLocation == CameraSensorLocation.Back ?
d.SensorRotationInDegrees : -d.SensorRotationInDegrees);
_dataContext.Device = d;
}
private async void captureButton_Click(object sender, EventArgs e)
{
if (!_manuallyFocused)
{
await AutoFocus();
}
await Capture();
}
private async Task AutoFocus()
{
if (!_capturing && PhotoCaptureDevice.IsFocusSupported(_dataContext.Device.SensorLocation))
{
...
await _dataContext.Device.FocusAsync();
...
_capturing = false;
}
}
private async Task Capture()
{
if (!_capturing)
{
_capturing = true;
MemoryStream stream = new MemoryStream();
CameraCaptureSequence sequence = _dataContext.Device.CreateCaptureSequence(1);
sequence.Frames[0].CaptureStream = stream.AsOutputStream();
await _dataContext.Device.PrepareCaptureSequenceAsync(sequence);
await sequence.StartCaptureAsync();
_dataContext.ImageStream = stream;
...
}
...
}
...
}
Показываем предпросмотр и сохраняем в альбом камеры
Отрисовка предпросмотра сделанного снимка довольно проста. Приложение Camera Explorer делает это, используя элемент XAML Image с объектом BitmapImage
. Сохранить сделанный снимок в альбом камеры тоже достаточно просто, стоит только помнить, что поток изображений, являющийся аргументом для void MediaLibrary.SavePictureToCameraRoll(string name, Stream source)
, должен располагаться перед началом данных изображения.
<phone:PhoneApplicationPage x:Class="CameraExplorer.PreviewPage" ... >
...
<Grid x:Name="LayoutRoot" ... >
...
<Grid x:Name="ContentPanel" ... >
<Image x:Name="image" Stretch="UniformToFill"/>
</Grid>
</Grid>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text="save" Click="saveButton_Click" ... />
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
...
</phone:PhoneApplicationPage>
...
public partial class PreviewPage : PhoneApplicationPage
{
private CameraExplorer.DataContext _dataContext = CameraExplorer.DataContext.Singleton;
private BitmapImage _bitmap = new BitmapImage();
protected override void OnNavigatedTo(NavigationEventArgs e)
{
...
_bitmap.SetSource(_dataContext.ImageStream);
image.Source = _bitmap;
...
}
private void saveButton_Click(object sender, EventArgs e)
{
try
{
_dataContext.ImageStream.Position = 0;
MediaLibrary library = new MediaLibrary();
library.SavePictureToCameraRoll("CameraExplorer_" + DateTime.Now.ToString() + ".jpg",
_dataContext.ImageStream);
}
catch (Exception)
{
...
}
...
}
}
Архитектура приложения Camera Explorer такова, что параметры камеры делятся на ArrayParameters
и RangeParameters
, производные от базового класса Parameters
. Такая архитектура класса позволяет легко и гибко управлять параметрами и отображать их на экране, связывая объекты параметров напрямую с элементами управления интерфейса, такими как ComboBox
, Slider
и Text
. В данной статье не рассматривается сопоставление, но вы можете найти примеры на странице проекта.
Вот упрощенный пример базового класса Parameter
с несколькими элементами, соответствующими обоим типам параметров.
...
public abstract class Parameter : INotifyPropertyChanged
{
...
protected Parameter(PhotoCaptureDevice device, string name)
{
...
}
public PhotoCaptureDevice Device { ... }
public string Name { ... }
public bool Supported { ... }
public abstract void Refresh();
...
}
Теперь давайте посмотрим, как можно работать с массивом свойств. Чтение минимума и максимума массива для свойства определяется _propertyId
, который выполняется запросом метода static CameraCapturePropertyRange PhotoCaptureDevice.GetSupportedPropertyRange(CameraSensorLocation sensor, Guid propertyId)
и проверкой полученного значения. Это значение свойства читается запросом к методу примера object PhotoCaptureDevice.GetProperty(Guid propertyId)
. Аналогично установка нового значения для свойства может быть произведена вызовом void PhotoCaptureDevice.SetProperty(Guid propertyId, object value)
.
...
public abstract class RangeParameter<T> : Parameter
{
private Guid _propertyId;
private T _value;
...
protected RangeParameter(PhotoCaptureDevice device, Guid propertyId, string name)
: base(device, name)
{
...
}
public override void Refresh()
{
try
{
CameraCapturePropertyRange range = PhotoCaptureDevice.GetSupportedPropertyRange(
Device.SensorLocation, _propertyId);
if (range == null)
{
Supported = false;
}
else
{
Minimum = (T)range.Min;
Maximum = (T)range.Max;
_value = (T)Device.GetProperty(_propertyId);
Supported = true;
}
}
catch (Exception)
{
Supported = false;
}
...
}
public T Minimum { ... }
public T Maximum { ... }
public T Value
{
...
set
{
if (!_value.Equals(value))
{
try
{
Device.SetProperty(_propertyId, (T)value);
_value = value;
...
}
catch (Exception)
{
...
}
}
}
}
...
}
Вот определение ExposureCompensationParameter
, где Guid RangeParameter
установлен в KnownCameraPhotoProperties.ExposureCompensation
, — значение, прописанное в платформе в качестве конкретного примера диапазона параметров. Также из документации платформы мы знаем, что конкретный тип экспокоррекции владеет Int32
, так что это может быть использовано как шаблонный аргумент для реализации диапазона параметров.
...
public class ExposureCompensationParameter : RangeParameter<Int32>
{
public ExposureCompensationParameter(PhotoCaptureDevice device)
: base(device, KnownCameraPhotoProperties.ExposureCompensation, "Exposure compensation")
{
}
...
}
Приложение Camera Explorer работает с массивом параметров с помощью ArrayParameter
. Вот его упрощенное определение для настроек в контексте кода. Действительно интересные части — это защищенные абстрактные методы void PopulateOptions()
и void SetOption(ArrayParameterOption option)
, в которых мы разберемся сразу после этого отрывка кода.
...
public class ArrayParameterOption
{
...
public ArrayParameterOption(dynamic value, string name)
{
...
}
public dynamic Value { ... }
public string Name { ... }
...
}
public abstract class ArrayParameter : Parameter, IReadOnlyCollection<ArrayParameterOption>
{
private List<ArrayParameterOption> _options = new List<ArrayParameterOption>();
private ArrayParameterOption _selectedOption;
...
public ArrayParameter(PhotoCaptureDevice device, string name)
: base(device, name)
{
...
}
public override void Refresh()
{
...
_options.Clear();
_selectedOption = null;
try
{
PopulateOptions();
Supported = _options.Count > 0;
}
catch (Exception)
{
Supported = false;
}
...
}
protected List<ArrayParameterOption> Options { ... }
public ArrayParameterOption SelectedOption
{
...
set
{
if (_selectedOption != value)
{
...
if (_selectedOption != null))
{
SetOption(value);
}
_selectedOption = value;
}
}
}
protected abstract void PopulateOptions();
protected abstract void SetOption(ArrayParameterOption option);
...
}
Теперь, когда у нас есть выгруженная основа для всех типов массива параметров, давайте посмотрим на конкретный фрагмент кода, который отвечает за считывание и установку параметров камеры в сюжетном режиме. Считывание поддерживаемых значений и выбор текущего активного значения осуществляет метод void PopulateOptions()
, в то время как установка нового значения, согласно изменениям в ArrayParameterOption
SelectedOption
, осуществляется методом void SetOption(ArrayParameterOption option)
. Обратите внимание, как static IReadOnlyList<object> PhotoCaptureDevice.GetSupportedPropertyValues(CameraSensorLocation sensor, Guid propertyId)
используется для считывания поддерживаемых значений, а KnownCameraPhotoProperties.SceneMode
и object PhotoCaptureDevice.GetProperty(Guid propertyId)
используется для считывания текущего значения. Установка нового значения сюжетного режима достигается простым запросом метода void PhotoCaptureDevice.SetProperty(Guid propertyId, object value)
.
...
public class SceneModeParameter : ArrayParameter
{
public SceneModeParameter(PhotoCaptureDevice device)
: base(device, KnownCameraPhotoProperties.SceneMode, "Scene mode")
{
}
protected override void PopulateOptions()
{
IReadOnlyList<object> supportedValues = PhotoCaptureDevice.GetSupportedPropertyValues(
Device.SensorLocation, PropertyId);
object value = Device.GetProperty(PropertyId);
foreach (dynamic i in supportedValues)
{
CameraSceneMode csm = (CameraSceneMode)i;
ArrayParameterOption option = new ArrayParameterOption(csm, csm.ToString());
Options.Add(option);
if (i.Equals(value))
{
SelectedOption = option;
}
}
}
protected override void SetOption(ArrayParameterOption option)
{
Device.SetProperty(PropertyId, option.Value);
}
...
}
Tap-to-focus
По умолчанию приложение фокусируется и рассчитывает сцену по нажатию кнопки «Съемка». Tap-to-focus является альтернативным вариантом поведения, когда касание устанавливает область фокусировки камеры, запускает саму фокусировку, экспозицию и настраивает баланс белого для следующего снимка. Индикатор фокусировки отображается, если режим Tap-to-focus активен.
Индикатор фокуса является элементом Rectangle
внутри Canvas
, где отображается картинка в видоискателе:
<Rectangle x:Name="FocusIndicator"
Stroke="Red" Opacity="0.7" Width="80" Height="80" StrokeThickness="5"
Visibility="Collapsed"/>
Свернутая видимость означает, что элемент изначально скрыт.
Касание определяется с помощью обработчика событий для события Canvas Tap:
public MainPage()
{
...
VideoCanvas.Tap += new EventHandler<GestureEventArgs>(videoCanvas_Tap);
...
}
Обработчик событий должен произвести дополнительные действия для определения зоны фокусировки. Это связано с несовместимыми типами используемых API-интерфейсов и тем фактом, что VideoCanvas
, который используется для отображения видоискателя, не соответствует ему по размеру.
Обработчик компенсирует разницу разрешения, применяя постоянные множители к координатам. Когда область фокусировки определена, при попытке выбрать область фокусировки, которая не попадает полностью в зону предпросмотра изображения, приложение будет закрываться с ошибкой.
Наконец, индикатор фокуса передвигается на правильную позицию и отображается красным цветом, пока идет фокусировка, и зеленым, когда фокус захвачен.
private async void videoCanvas_Tap(object sender, GestureEventArgs e)
{
System.Windows.Point uiTapPoint = e.GetPosition(VideoCanvas);
if (PhotoCaptureDevice.IsFocusRegionSupported(_dataContext.Device.SensorLocation)
&& _focusSemaphore.WaitOne(0))
{
// Получаем координаты нажатия для отправной точки.
Windows.Foundation.Point tapPoint = new Windows.Foundation.Point(uiTapPoint.X, uiTapPoint.Y);
double xRatio = VideoCanvas.ActualWidth / _dataContext.Device.PreviewResolution.Width;
double yRatio = VideoCanvas.ActualHeight / _dataContext.Device.PreviewResolution.Height;
// Регулируем фокус, основываясь на точке касания
Windows.Foundation.Point displayOrigin = new Windows.Foundation.Point(
tapPoint.X - _focusRegionSize.Width / 2,
tapPoint.Y - _focusRegionSize.Height / 2);
// Регулируем разницу разрешения между картинкой предпросмотра и canvas
Windows.Foundation.Point viewFinderOrigin = new Windows.Foundation.Point(
displayOrigin.X / xRatio, displayOrigin.Y / yRatio);
Windows.Foundation.Rect focusrect = new Windows.Foundation.Rect(
viewFinderOrigin, _focusRegionSize);
// Уменьшаем разрешение предпросмотра
Windows.Foundation.Rect viewPortRect = new Windows.Foundation.Rect(
0, 0,
_dataContext.Device.PreviewResolution.Width,
_dataContext.Device.PreviewResolution.Height);
focusrect.Intersect(viewPortRect);
_dataContext.Device.FocusRegion = focusrect;
// Показываем индикатор фокусировки
FocusIndicator.SetValue(Shape.StrokeProperty, _notFocusedBrush);
FocusIndicator.SetValue(Canvas.LeftProperty, uiTapPoint.X - _focusRegionSize.Width / 2);
FocusIndicator.SetValue(Canvas.TopProperty, uiTapPoint.Y - _focusRegionSize.Height / 2);
FocusIndicator.SetValue(Canvas.VisibilityProperty, Visibility.Visible);
CameraFocusStatus status = await _dataContext.Device.FocusAsync();
if (status == CameraFocusStatus.Locked)
{
FocusIndicator.SetValue(Shape.StrokeProperty, _focusedBrush);
_manuallyFocused = true;
_dataContext.Device.SetProperty(
KnownCameraPhotoProperties.LockedAutoFocusParameters,
AutoFocusParameters.Exposure & AutoFocusParameters.Focus & AutoFocusParameters.WhiteBalance);
}
else
{
_manuallyFocused = false;
_dataContext.Device.SetProperty(
KnownCameraPhotoProperties.LockedAutoFocusParameters,
AutoFocusParameters.None);
}
_focusSemaphore.Release();
}
}
Фокусировка также блокируется семафором, так что автофокусировка происходит только один раз. Если мы попытаемся вызвать FocusAsync
, пока предыдущая операция выполняется, приложение закроется с ошибкой. Нулевое значение, заданное методу семафора WaitOne
, означает, что если семафор недоступен, то запрос вернет false
немедленно — вместо ожидания, пока семафор станет доступен.
Интеграция выбора фотоприложений
Стандартное приложение камеры позволяет запускать дополнения, так называемые «Фотоприложения», с помощью Lens Picker. «Фотоприложения» являются полноценными приложениями для работы с камерой и обычно предоставляют более широкие возможности, чем стандартное приложение. Для того чтобы сделать приложение доступным в Lens Picker, следующий фрагмент XML должен быть добавлен в манифест приложения вместе с элементом App:
<Extensions>
<Extension ExtensionName="Camera_Capture_App"
ConsumerId="{5B04B775-356B-4AA0-AAF8-6491FFEA5631]"
TaskId="_default" />
</Extensions>
Таким образом, когда приложение выбрано в Lens Picker, оно запускается на своем стартовом экране. Кроме того, иконки для Lens Picker располагаются в папке Assets
. Иконка должна быть определенного размера и иметь определенное имя, о чем сказано в статье Lens design guidelines for Windows Phone.
В следующей статье цикла мы рассмотрим пример использования Maps API.
Автор: nokiaman