How to work with grayscale in a camera app for Windows Phone 8
[ This article is for Windows Phone 8 developers. If you’re developing for Windows 10, see the latest documentation. ]
Starting with Windows Phone OS 7.1, you can programmatically access the phone’s camera using the Microsoft.Devices..::.PhotoCamera class. This topic describes how to alter live video frames from the camera preview buffer. The app described in this topic demonstrates how to process alpha, red, green, and blue (ARGB) frames from the camera and convert them to grayscale. This topic corresponds to the Camera Grayscale Sample.
Tip
If your Windows Phone 8 app needs to process grayscale frames, consider using the GetPreviewBufferY(array<Byte>[]) method. This method uses the efficient YCbCr format to capture only the luminance (Y) information from the camera preview buffer. For more info about using PhotoCaptureDevice class, see Advanced photo capture for Windows Phone 8.
This topic is divided into two parts:
Creating the camera UI and base functionality
Creating the ARGB frame pump
Important Note: |
---|
When upgrading Windows Phone OS 7.0 apps to use the capabilities in Windows Phone OS 7.1, the camera capability ID_CAP_ISV_CAMERA is not automatically added to the app manifest file, WMAppManifest.xml. Without ID_CAP_ISV_CAMERA, apps using the camera API won’t function. In new Windows Phone OS 7.1 projects, this capability is included in the app manifest file. |
The following image illustrates the camera app created in this topic.
Creating the camera UI and base functionality
In this section, you create the camera UI, which consists of a viewfinder region, a button StackPanel control for toggling between both color and grayscale modes, and an Image control that overlays the viewfinder region for grayscale viewing.
To create the camera UI and base functionality
Using the Windows Phone SDK, create a new project using the Windows Phone App template.
After your project has been created, on the Project menu, select Add Reference. On the .NET tab, choose Microsoft.XNA.Framework, and then click OK.
In the MainPage.xaml file, update the phone:PhoneApplicationPage element as shown in the following code.
SupportedOrientations="Landscape" Orientation="LandscapeLeft" shell:SystemTray.IsVisible="False"
This configures the page for landscape orientation and hides the system tray.
On MainPage.xaml, replace the Grid named LayoutRoot with the following code.
<!--LayoutRoot is the root grid where all page content is placed--> <Grid x:Name="LayoutRoot" Background="Transparent"> <Grid.ColumnDefinitions> <ColumnDefinition Width="640" /> <ColumnDefinition Width="160*" /> </Grid.ColumnDefinitions> <!--Camera viewfinder >--> <Rectangle Width="640" Height="480" HorizontalAlignment="Left" > <Rectangle.Fill> <VideoBrush x:Name="viewfinderBrush" /> </Rectangle.Fill> </Rectangle> <!--Overlay for the viewfinder region to display grayscale WriteableBitmap objects--> <Image x:Name="MainImage" Width="320" Height="240" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="16,0,0,16" Stretch="Uniform"/> <!--Button StackPanel to the right of viewfinder>--> <StackPanel Grid.Column="1" > <Button Content="Gray: ON" Name="GrayscaleOnButton" Click="GrayOn_Clicked" /> <Button Content="Gray: OFF" Name="GrayscaleOffButton" Click="GrayOff_Clicked" /> </StackPanel> <!--Used for debugging >--> <TextBlock Height="40" HorizontalAlignment="Left" Margin="8,428,0,0" Name="txtDebug" VerticalAlignment="Top" Width="626" FontSize="24" FontWeight="ExtraBold" /> </Grid>
The code creates a 640×480 viewfinder region that has a StackPanel control to contain the grayscale on and off buttons. Again, the purpose of the Image control is to overlay the viewfinder region when grayscale mode is selected. This creates an alternate viewing region for grayscale WriteableBitmap objects provided by the frame pump.
Open MainPage.xaml.cs, the code-behind file for the main page, and then add the following directives at the top of the page.
// Directives using Microsoft.Devices; using System.Windows.Media.Imaging; using System.Threading;
' Directives Imports Microsoft.Devices Imports System.Windows.Media.Imaging Imports System.Threading
In MainPage.xaml.cs, in the MainPage class, add the following variable declarations before the MainPage class constructor.
// Variables PhotoCamera cam = new PhotoCamera(); private static ManualResetEvent pauseFramesEvent = new ManualResetEvent(true); private WriteableBitmap wb; private Thread ARGBFramesThread; private bool pumpARGBFrames;
' Variables Private cam As New PhotoCamera() Private Shared pauseFramesEvent As New ManualResetEvent(True) Private wb As WriteableBitmap Private ARGBFramesThread As Thread Private pumpARGBFramesCheck As Boolean
In MainPage.xaml.cs, add the following code to the MainPage class.
Note
Until you complete the following steps of this procedure, Visual Studio may list errors about methods that do not exist in the current context. These methods will be added in the following steps.
``` csharp
//Code for camera initialization event, and setting the source for the viewfinder
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
// Check to see if the camera is available on the phone.
if ((PhotoCamera.IsCameraTypeSupported(CameraType.Primary) == true) ||
(PhotoCamera.IsCameraTypeSupported(CameraType.FrontFacing) == true))
{
// Initialize the default camera.
cam = new Microsoft.Devices.PhotoCamera();
//Event is fired when the PhotoCamera object has been initialized
cam.Initialized += new EventHandler<Microsoft.Devices.CameraOperationCompletedEventArgs>(cam_Initialized);
//Set the VideoBrush source to the camera
viewfinderBrush.SetSource(cam);
}
else
{
// The camera is not supported on the phone.
this.Dispatcher.BeginInvoke(delegate()
{
// Write message.
txtDebug.Text = "A Camera is not available on this phone.";
});
// Disable UI.
GrayscaleOnButton.IsEnabled = false;
GrayscaleOffButton.IsEnabled = false;
}
}
protected override void OnNavigatingFrom(System.Windows.Navigation.NavigatingCancelEventArgs e)
{
if (cam != null)
{
// Dispose of the camera to minimize power consumption and to expedite shutdown.
cam.Dispose();
// Release memory, ensure garbage collection.
cam.Initialized -= cam_Initialized;
}
}
```
``` vb
'Code for camera initialization event, and setting the source for the viewfinder
Protected Overrides Sub OnNavigatedTo(ByVal e As System.Windows.Navigation.NavigationEventArgs)
' Check to see if the camera is available on the phone.
If (PhotoCamera.IsCameraTypeSupported(CameraType.Primary) = True Or
PhotoCamera.IsCameraTypeSupported(CameraType.FrontFacing) = True) Then
' Initialize the default camera.
cam = New Microsoft.Devices.PhotoCamera()
'Event is fired when the PhotoCamera object has been initialized
AddHandler cam.Initialized, AddressOf cam_Initialized
'Set the VideoBrush source to the camera
viewfinderBrush.SetSource(cam)
Else
' The camera is not supported on the phone.
Me.Dispatcher.BeginInvoke(Sub()
' Write message.
txtDebug.Text = "A Camera is not available on this phone."
End Sub)
' Disable UI.
GrayscaleOnButton.IsEnabled = False
GrayscaleOffButton.IsEnabled = False
End If
End Sub
Protected Overrides Sub OnNavigatingFrom(e As System.Windows.Navigation.NavigatingCancelEventArgs)
If cam IsNot Nothing Then
' Dispose of the camera to minimize power consumption and to expedite shutdown.
cam.Dispose()
' Release memory, ensure garbage collection.
RemoveHandler cam.Initialized, AddressOf cam_Initialized
End If
End Sub
```
This code uses the [OnNavigatedTo(NavigationEventArgs)](https://msdn.microsoft.com/en-us/library/system.windows.controls.page.onnavigatedto\(system.windows.navigation.navigationeventargs\)\(v=VS.105\)) method to create a PhotoCamera object named cam and add an event handler. This code also sets the VideoBrush source to the phone camera object, cam. If a camera is not available on the phone, the buttons are disabled and a message is displayed in the UI.
Note
To pull video frames from the camera, as shown later with the GetPreviewBufferArgb32(array<Int32>[]) method, the PhotoCamera object needs to be set to display a video preview to a VideoBrush control.
In MainPage.xaml.cs, add the following code to the MainPage class.
protected override void OnNavigatingFrom(System.Windows.Navigation.NavigatingCancelEventArgs e) { // Dispose camera to minimize power consumption and to expedite shutdown. cam.Dispose(); // Release memory, ensure garbage collection. cam.Initialized -= cam_Initialized; }
Protected Overrides Sub OnNavigatingFrom(e As System.Windows.Navigation.NavigatingCancelEventArgs) ' Dispose camera to minimize power consumption and to expedite shutdown. cam.Dispose() ' Release memory, ensure garbage collection. RemoveHandler cam.Initialized, AddressOf cam_Initialized End Sub
This code helps release memory related to the camera.
In MainPage.xaml.cs, add the following code to the MainPage class.
//Update UI if initialization succeeds void cam_Initialized(object sender, Microsoft.Devices.CameraOperationCompletedEventArgs e) { if (e.Succeeded) { this.Dispatcher.BeginInvoke(delegate() { txtDebug.Text = "Camera initialized"; }); } }
Private Sub cam_Initialized(ByVal sender As Object, ByVal e As Microsoft.Devices.CameraOperationCompletedEventArgs) If e.Succeeded Then Me.Dispatcher.BeginInvoke(Sub() txtDebug.Text = "Camera initialized") End If End Sub
This code uses the camera Initialized event to update the TextBlock named txtDebug. The BeginInvoke method is required for updating the status because the app UI runs on a different thread.
To create a camera app, the camera capability must be declared in the app manifest file. Without it, the app will not function. Open WMAppManifest.xml and confirm that the following capabilities element is present.
<Capability Name="ID_CAP_ISV_CAMERA"/>
For more info about app capabilities and requirements, see App capabilities and hardware requirements for Windows Phone 8.
Creating the ARGB frame pump
In this section, you create two methods that are collectively responsible for taking ARGB (Alpha, Red, Green, Blue) frames from the camera and converting them to grayscale.
To create the ARGB frame pump
In MainPage.xaml.cs, add the following code to the MainPage class.
// ARGB frame pump void PumpARGBFrames() { // Create capture buffer. int[] ARGBPx = new int[(int) cam.PreviewResolution.Width * (int) cam.PreviewResolution.Height]; try { PhotoCamera phCam = (PhotoCamera)cam; while (pumpARGBFrames) { pauseFramesEvent.WaitOne(); // Copies the current viewfinder frame into a buffer for further manipulation. phCam.GetPreviewBufferArgb32(ARGBPx); // Conversion to grayscale. for (int i = 0; i < ARGBPx.Length; i++) { ARGBPx[i] = ColorToGray(ARGBPx[i]); } pauseFramesEvent.Reset(); Deployment.Current.Dispatcher.BeginInvoke(delegate() { // Copy to WriteableBitmap. ARGBPx.CopyTo(wb.Pixels, 0); wb.Invalidate(); pauseFramesEvent.Set(); }); } } catch (Exception e) { this.Dispatcher.BeginInvoke(delegate() { // Display error message. txtDebug.Text = e.Message; }); } } internal int ColorToGray(int color) { int gray = 0; int a = color >> 24; int r = (color & 0x00ff0000) >> 16; int g = (color & 0x0000ff00) >> 8; int b = (color & 0x000000ff); if ((r == g) && (g == b)) { gray = color; } else { // Calculate for the illumination. // I =(int)(0.109375*R + 0.59375*G + 0.296875*B + 0.5) int i = (7 * r + 38 * g + 19 * b + 32) >> 6; gray = ((a & 0xFF) << 24) | ((i & 0xFF) << 16) | ((i & 0xFF) << 8) | (i & 0xFF); } return gray; }
'ARGB frame pump Private Sub PumpARGBFrames() ' Create capture buffer. Dim ARGBPx(CInt(cam.PreviewResolution.Width) * CInt(cam.PreviewResolution.Height) - 1) As Integer Try Dim phCam As PhotoCamera = CType(cam, PhotoCamera) Do While pumpARGBFramesCheck pauseFramesEvent.WaitOne() ' Copies the current viewfinder frame into a buffer for further manipulation. phCam.GetPreviewBufferArgb32(ARGBPx) ' Conversion to grayscale. For i As Integer = 0 To ARGBPx.Length - 1 ARGBPx(i) = ColorToGray(ARGBPx(i)) Next i pauseFramesEvent.Reset() ' Copy to WriteableBitmap. Deployment.Current.Dispatcher.BeginInvoke(Sub() ARGBPx.CopyTo(wb.Pixels, 0) wb.Invalidate() pauseFramesEvent.Set() End Sub) Loop Catch e As Exception ' Display error message. Me.Dispatcher.BeginInvoke(Sub() txtDebug.Text = e.Message) End Try End Sub Friend Function ColorToGray(ByVal color As Integer) As Integer Dim gray As Integer = 0 Dim a As Integer = color >> 24 Dim r As Integer = (color And &HFF0000) >> 16 Dim g As Integer = (color And &HFF00) >> 8 Dim b As Integer = (color And &HFF) If (r = g) AndAlso (g = b) Then gray = color Else ' Calculate for the illumination. ' I =(int)(0.109375*R + 0.59375*G + 0.296875*B + 0.5) Dim i As Integer = (7 * r + 38 * g + 19 * b + 32) >> 6 gray = ((a And &HFF) << 24) Or ((i And &HFF) << 16) Or ((i And &HFF) << 8) Or (i And &HFF) End If Return gray End Function
In this code, a method named PumpARGBFrames (the ARGB frame pump), copies ARGB frames into a buffer for manipulation. Then, while each frame is in the buffer, it uses the ColorToGray method to convert the frame to grayscale. Finally, PumpARGBFrames copies the converted frame to a WriteableBitmap object named wb. The continuous processing of the frame pump is used to produce a live black-and-white video that is displayed in the UI.
Note
To pull video frames from the camera, as shown with the GetPreviewBufferArgb32(array<Int32>[]) method, the PhotoCamera object needs to be set to display a video preview to a VideoBrush control.
In MainPage.xaml.cs, add the following code to the MainPage class.
// Start ARGB to grayscale pump. private void GrayOn_Clicked(object sender, RoutedEventArgs e) { MainImage.Visibility = Visibility.Visible; pumpARGBFrames = true; ARGBFramesThread = new System.Threading.Thread(PumpARGBFrames); wb = new WriteableBitmap((int) cam.PreviewResolution.Width, (int) cam.PreviewResolution.Height); this.MainImage.Source = wb; // Start pump. ARGBFramesThread.Start(); this.Dispatcher.BeginInvoke(delegate() { txtDebug.Text = "ARGB to Grayscale"; }); } // Stop ARGB to grayscale pump. private void GrayOff_Clicked(object sender, RoutedEventArgs e) { MainImage.Visibility = Visibility.Collapsed; pumpARGBFrames = false; this.Dispatcher.BeginInvoke(delegate() { txtDebug.Text = ""; }); }
' Start ARGB to grayscale pump. Private Sub GrayOn_Clicked(ByVal sender As Object, ByVal e As RoutedEventArgs) MainImage.Visibility = Visibility.Visible pumpARGBFramesCheck = True ARGBFramesThread = New System.Threading.Thread(AddressOf PumpARGBFrames) wb = New WriteableBitmap(CInt(cam.PreviewResolution.Width), CInt(cam.PreviewResolution.Height)) Me.MainImage.Source = wb ' Start pump. ARGBFramesThread.Start() Me.Dispatcher.BeginInvoke(Sub() txtDebug.Text = "ARGB to Grayscale") End Sub ' Stop ARGB to grayscale pump. Private Sub GrayOff_Clicked(ByVal sender As Object, ByVal e As RoutedEventArgs) MainImage.Visibility = Visibility.Collapsed pumpARGBFramesCheck = False Me.Dispatcher.BeginInvoke(Sub() txtDebug.Text = "") End Sub
In this code, the GrayOn_Clicked method resizes the Image control named MainImage to cover the screen and sets it to display the writeable bitmap named wb, which is updated by the ARGB frame pump. The GrayOff_Clicked event collapses the MainImage control and stops the frame pump.
Note
Notice that the Image control visibility is collapsed in certain situations. Again, this control overlays the standard color viewfinder implementation. It is active and visible for grayscale viewing.
- On a phone, run the app by selecting the Debug | Start Debugging menu command.
See Also
Other Resources
Advanced photo capture for Windows Phone 8