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

  1. Using the Windows Phone SDK, create a new project using the Windows Phone App template.

  2. 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.

  3. 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.

  4. 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.

  5. 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
    
  6. 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
    
  7. 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.

  1. 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.

  2. 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.

  3. 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

  1. 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.

  1. 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.

  1. 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

Capturing video for Windows Phone 8

Lenses for Windows Phone 8

How to use the camera capture task for Windows Phone 8