×
Namespaces

Variants
Actions

Editar efeitos da camara em tempo real no Windows Phone 7 e 8

From Nokia Developer Wiki
Jump to: navigation, search
Featured Article
25 Aug
2013

Underconstruction.pngUnder Construction: (20130912200853) This article is under construction and it may have outstanding issues. If you have any comments please use the comments tab.

Underconstruction.pngEm construção: (20130507235054) Este artigo ainda está sendo escrito, podendo ter questões em aberto. Se você tiver comentários sobre o assunto, por favor faça-os na aba de comentários.

Este artigo explica como ler um stream direto da câmara de uma forma eficiente em dispositivos Windows Phone 7 and 8 e como aplicar um filtro com efeitos no stream.

Lumia 1020 main.pngVencedor: Este artigo foi vencedor no Nokia Imaging Wiki Competition 2013Q3.

WP Metro Icon Multimedia.png
WP Metro Icon XNA.png
WP Metro Icon WP8.png
SignpostIcon WP7 70px.png
Article Metadata

Exemplo de código
Testado com
SDK: Windows Phone 7.1 SDK, Windows Phone 8.0 SDK
Aparelho(s): Nokia Lumia 800, Samsung Omnia 7, Nokia Lumia 720, HTC 8X

Compatibilidade
Platform Security
Capabilities: ID_CAP_ISV_CAMERA

Artigo
Tradução:
Por Vitor Pombeiro
Última alteração feita por Vitor Pombeiro em 09 Oct 2013

Contents

Introdução

A maioria das apps de fotografia no market são bastante simples. Elas permitem o utilizador tirar uma fotografia, e depois aplicar um filtro vintage interessante e gravar a fotografia. Mas só apenas algumas aplicações têm um verdadeiro modo de pré visualização em tempo real com um filtro aplicado. Não seria bom ver como é que a foto irá ficar, mesmo antes de carregar no botão para disparar? E que tal outros efeitos em tempo real como do estilo 8-bit pixelizada?

Vamos-lhe mostrar neste artigo como fazer uma app (na linguagem C#) que pode correr nos dispositivos Windows Phone 7 e Windows Phone 8. O nosso objetivo será desenhar uma implementação suficientemente rápida que pode correr sem falhas em dispositivos mais antigos e lentos. Vamos passar por várias ideias e implementações e vamos compara-las.

Ideias Principais

  • Vamos usar o componente VideoBrush inicialmente. Vamos demonstrar as suas limitações.
  • Vamos ler valores ARGB diretamente da câmara, e ver como esses valores podem ser eficazmente empacotados e desempacotados.
  • Vamos descobrir uma solução mais rápida (lendo valores do buffer YCbCr).
  • Vamos discutir como tornar este processamento ainda melhor (trabalhando com uma resolução mais baixa).
  • Vamos ver uma ideia de como fazer uma app para tirar fotografias interessantes do estilo 8-bit com estes métodos.


Vamos demonstrar estes métodos num projecto Windows Phone 7 (WP7). No último capitulo vamos cobrir as diferenças entre o WP7 e o Windows Phone 8 (WP8):

  • Vamos ver como utilizar esta aproximação no WP8 (graças à framework MonoGame)
  • Vamos comprar todos os métodos e medir o tempo de renderização

Comparação com o Nokia Imaging SDK

Existe outra abordagem alternativa para como ler do stream da câmara em tempo real. O Nokia Imaging SDK providencia muitos métodos úteis para manipulação de imagens e aplicar efeitos e filtros. É uma biblioteca simples e eficaz. Pode utilizar alguns filtros pré-preparados para aplicar no stream em tempo real, como descrito no demo do filtro em tempo real (inglês). Pode também combinar estes filtros para criar novos efeitos interessantes: Combining imaging filters to create new real-time camera effects

Criar um filtro completamente novo é uma operação difícil. Se pretende manipular diretamente os dados RGB para cada pixel; se pretende trabalhar com resoluções mais baixas, ou se pretende compor efeitos finais a partir de imagens mais pequenas (efeito do estilo 8-bit), este artigo pode dar uma grande ajuda.

Nokia Imaging SDK está também disponível para o WP8, esta solução corre de uma forma suave em ambos os dispositivos WP7 e WP8.

Demonstração dos efeitos

Effects comparison.png

Imagem 1: Stream direto da câmara. Imagem 2: Efeito 8-bit (uma imagem composto por várias imagens menores). Imagens 3 e 4: Efeito 8-bit com uma palete de cores limitada, limite de cor e aplicar o algoritmo dithering (mais discussão neste artigo).

Componente VideoBrush

A solução mais simples para mostrar um stream direto da câmara é com a API VideoBrush. O código é o mesmo para o WP7 e WP8. Pode adicionar este código na página XAML:

<Rectangle Width="320" Height="320">
<Rectangle.Fill>
<VideoBrush x:Name="viewfinderBrush" />
</Rectangle.Fill>
</Rectangle>

No code-behind pode inicializar o objeto camera, utilizar a classe PhotoCamera e define o stream para o VideoBrush:

PhotoCamera camera;
 
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Initialize a camera object, set stream to the VideoBrush
camera = new PhotoCamera();
viewfinderBrush.SetSource(camera);
}
 
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
camera.Dispose();
}

Não se esqueça de adicionar a capacidade ID_CAP_ISV_CAMERA. Esta é uma solução muito simples. O stream da câmara está bastante distorcido e rodada de uma forma errada, mas pode corrigir isto utilizando uma RelativeTransform (inglês) no retângulo. Mas o que é pena é que, não pode modificar o stream de qualquer forma. Não pode aplicar qualquer filtro ou efeito no objeto VideoBrush, não tem acesso aos dados dos pixeis em bruto.

Ler do buffer

Vamos utilizar uma aproximação diferente. Como pode descobrir, existem alguns métodos GetPreviewBuffer no objeto PhotoCamera. Esta é a forma como podemos obter os dados em bruto da câmara. Mas queremos também mostrar os dados algures, de uma forma rápida e eficiente. O componente WritableBitmap do Silverlight é muito lento, escrever pixel a pixel utilizando este componente pode levar centenas de milissegundos. Temos de encontrar uma outra solução.

Iremos criar um projeto que combine Silverlight/XNA para o Windows Phone 7 (vamos falar do WP8 mais tarde). Iremos combinar a facilidade do XAML e Silverlight com o poder do XNA em uma única solução. Vamos utilizar uma aproximação muito parecida com a descrita no artigo Use Camera with XNA.

Precisa de ter instalado o Windows Phone SDK 7.1. Abra o Visual Studio 2010 (Express ou o Professional/Ultimate) e crie um projeto Windows Phone Silverlight and XNA Application.

New xnasilverlight project.png

Para ter a capacidade de ler da câmara é necessário colocar uma Canvas falsa no nosso código XAML. Adicione na página GamePage.xaml esta linha de código:

<Canvas x:Name="cameraDisplayFake" Width="0" Height="0"></Canvas>

Vamos agora criar um objeto helper. Vamos criar uma nova classe CamReader.cs e colocar na mesma estas declarações:

PhotoCamera camera;
int[] previewBuffer, outputBuffer;
Point previewSize, outputSize;

Como pode ver, existe um buffer de pré-visualização e um buffer de saída. O buffer de pré-visualização irá conter os valores em bruto da câmara que irão ter normalmente a dimensão 640x480 pixeis. Aplicar efeitos a esta resolução seria muito exigente e lento. Nós não necessitamos de uma resolução tão alta para a pré-visualização. O nosso buffer de saída será menor, por exemplo 240x240 pixeis. Todos os cálculos e cálculos para filtros serão efetuados nessa resolução. Podemos aumentar a imagem mais tarde, irá ficar desfocada, mas isso não nos interessa muito agora.

Isto é como o método CamReader.StartCamera irá ficar:

public void StartCamera(Dispatcher dispatcher, Canvas fakeCamCanvas, Point outputSize)
{
this.outputSize = outputSize;
 
if (camera != null)
StopCamera(fakeCamCanvas);
 
// Create a PhotoCamera instance
if (PhotoCamera.IsCameraTypeSupported(CameraType.Primary))
camera = new PhotoCamera(CameraType.Primary);
else if (PhotoCamera.IsCameraTypeSupported(CameraType.FrontFacing))
camera = new PhotoCamera(CameraType.FrontFacing);
else
{
System.Windows.MessageBox.Show("Cannot find a camera on this device");
return;
}
 
// Wait camera initialization before create the buffer image
camera.Initialized += (a, b) =>
{
// Move to UI thread
dispatcher.BeginInvoke(() =>
{
outputBuffer = new int[outputSize.X * outputSize.Y];
previewSize = new Point((int)camera.PreviewResolution.Width, (int)camera.PreviewResolution.Height);
previewBuffer = new int[previewSize.X * previewSize.Y];
}
);
};
 
// Initialize camera
// - we need to initialize VideoBrush to given Canvas, otherwise the stream wouldn't be loaded
var brush = new VideoBrush();
brush.SetSource(camera);
fakeCamCanvas.Background = brush;
}

Ler dados ARGB

Agora podemos desenhar métodos para ler dados ARGB da câmara:

public int[] GetBufferFromARGB()
{
if (camera != null && previewBuffer != null)
{
// Get preview Image buffer (640x480)
try { camera.GetPreviewBufferArgb32(previewBuffer); }
catch { return null; }
 
// Select outputBuffer pixels (outputSize = smaller than 640x480 preview pixels)
CopyValuesToOutputBuffer();
 
// Swap Red and Blue channel (Silverlight -> XNA's texture)
SwapRedBlueChannel(outputBuffer);
return outputBuffer;
}
return null;
}

Este método obtém dados da câmara para o buffer de pré-visualização (640x480). Então os valores selecionados podem ser copiados para o menor buffer de saída (veja o próximo paragrafo para a implementação do método). As colores de saída são trocadas para corrigir o método XNA Texture2D e finalmente o buffer de saída é devolvido.

O buffer de saída é um array linear de valores inteiros. Todos os inteiros têm a informação comprimida sobre o valor RGB do pixel e o valor alpha (transparência). No campo outputSize temos o tamanho de X e Y da imagem de saída.

Métodos Helper

O primeiro método é utilizado para copiar do buffer de pré-visualização para o de saída. Aqui pode encontrar uma variação complexa. Esta implementação corta a imagem automaticamente proporcionalmente ao buffer de saída. A imagem sofre também uma auto-rotação e redimensionada.

private void CopyValuesToOutputBuffer()
{
// Copies data from preview buffer (640x480) to smaller output buffer in correct ratio
Point start = Point.Zero, incr = Point.Zero;
GetOutputParams(ref start, ref incr);
 
// Inserts values to the output buffer
for (int y = 0; y < outputSize.Y; y++)
for (int x = 0; x < outputSize.X; x++)
{
// Auto flip / rotate the image to output
int sourceX = Math.Min(start.X + y * incr.X, previewSize.X - 1);
int sourceY = Math.Min(previewSize.Y - (start.Y + x * incr.Y) - 1, previewSize.Y - 1);
int i = sourceX + sourceY * previewSize.X;
outputBuffer[x + y * outputSize.X] = previewBuffer[i];
}
}
private void GetOutputParams(ref Point start, ref Point incr)
{
// Returns correct params for the output buffer
// = start index & increment, how should we read from the preview buffer
start = Point.Zero;
incr = new Point(previewSize.X / outputSize.Y, previewSize.Y / outputSize.X);
 
if (previewSize.X / (float)previewSize.Y > outputSize.Y / (float)outputSize.X)
{
// Preview is wider, output buffer will be cropped at left/right side
start.X = (int)((previewSize.X - outputSize.Y * previewSize.Y / (float)outputSize.X) / 2);
incr.X = (previewSize.X - 2 * start.X) / outputSize.Y;
}
else
{
// Output buffer is wider (preview is taller, crop at top/bottom)
start.Y = (int)((previewSize.Y - outputSize.X * previewSize.X / (float)outputSize.Y) / 2);
incr.Y = (previewSize.Y - 2 * start.Y) / outputSize.X;
}
}

Note.pngNote: Estamos a utilizar divisões inteiras para os calcular o incremento (por razões de performance). Isto irá funcionar bem se o buffer de saída for suficientemente menor do que a pré-visualização da câmara. Se o tamanho do buffer de saída for semelhante ao da pré-visualização, este método pode retornar fracos resultados.

Note.pngNote: Pode encontrar um novo objeto para ler da câmara na API do WP8: PhotoCaptureDevice. É possível definir a dimensão do buffer diretamente (portanto não necessita e chamar novamente este método). Infelizmente esta API não está disponível em dispositivos antigos com o WP7.


O próximo método converte a imagem do buffer no formato XNA:

private void SwapRedBlueChannel(int[] buffer)
{
for (int i = 0; i < buffer.Length; ++i)
{
// Converts a packed RGB integer value from Silverlight's to XNA format
// - we need to switch a red and blue channel
buffer[i] = (int)((uint)buffer[i] & 0xFF00FF00)
| (buffer[i] >> 16 & 0xFF)
| (buffer[i] & 0xFF) << 16;
}
}

E finalmente, devemos incluir na nossa classe CamReader o método para parar a câmara. Irá ser chamado quando deixamos a app, or quando a app passa para plano de fundo:

public void StopCamera(Canvas fakeCamCanvas)
{
if (camera != null)
camera.Dispose();
camera = null;
previewBuffer = null;
fakeCamCanvas.Background = null;
}

Reproduzir a pré-visualização

Agora temos preparada uma classe helper CamReader.cs. Gostariamos de integrar esta lógica na nossa app.

Vamos abrir o ficheiro GamePage.xaml.cs. Este é o sitio onde podemos escrever o código para a framework XNA.

Adicione estas declarações a este ficheiro:

Point outputSize;
CamReader camReader;
Texture2D texture;

E adicione esta lógica de inicialização ao método OnNavigatedTo:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
//...
// Initialize the camera reader + XNA's texture (of the correct size)
GraphicsDevice device = SharedGraphicsDeviceManager.Current.GraphicsDevice;
outputSize = new Point(240, 240);
texture = new Texture2D(device, outputSize.X, outputSize.Y);
 
camReader = new CamReader();
camReader.StartCamera(this.Dispatcher, cameraDisplayFake, outputSize);
//...
}

O nosso objeto CamReader e a textura serão inicializados. Note que estamos a passar o objeto cameraDisplayFake para o método StartCamera. Este é o componente que já adicionamos ao nosso código XAML.

Podemos inserir uma lógica semelhante no OnNavigatedFrom:

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
camReader.StopCamera(cameraDisplayFake);
//...
}

Agora podemos finalmente visualizar o nosso stream no ecrã! O método OnDraw é chamado 30 vezes por segundo por defeito. Obtemos os dados do objeto CamReader para cada frame, depois copiamos os dados para a texture e desenhamos esta texture.

private void OnDraw(object sender, GameTimerEventArgs e)
{
GraphicsDevice device = SharedGraphicsDeviceManager.Current.GraphicsDevice;
device.Clear(Color.CornflowerBlue);
 
// Get camera values, draw image directly on screen by SpriteBatch (by converting into Texture2D)
int[] buffer = camReader.GetBufferFromARGB();
if (buffer != null)
{
//ApplyEffect(buffer, outputSize, effect); // Applies special filter on image
device.Textures[0] = null;
texture.SetData<int>(buffer);
 
spriteBatch.Begin();
spriteBatch.Draw(texture, new Vector2(0, 0), Color.White);
spriteBatch.End();
}
}

Note.pngNote: Podemos alterar o intervalo do ciclo de visualização de 30 frames por segundo (FPS) para um valor diferente. Só precisa de alterar o timer.UpdateInterval no construtor GamePage para TimeSpan.FromTicks(166666); e a visualização irá correr a uns espantosos 60 FPS!

Note.pngNote: Podemos combinar código XAML (Silverlight) e conteúdo XNA numa página. Elementos XAML podem ser desenhados em texturas e visualizados mesmo por cima de conteúdo proveniente de XNA. Situação similar com o WP8, conteúdo C#/XAML podem ser também combinados com a framework MonoGame na mesma página.

Trabalhar com os dados da imagem

XNA é uma framework muito poderosa para manipulação de imagens. Neste exemplo vamos desenhar a textura no ponto 0,0 (no canto superior escerdo), com o zoom a 100%. Podemos facilmente alterar o código de renderização para desenhar esta imagem com o dobro do tamanho (= zoom de 200% ):

spriteBatch.Draw(texture, Vector2.Zero, null, Color.White, 0f, Vector2.Zero, 2f, SpriteEffects.None, 0);

Para esta textura - podemos também definir a escala, a rotação, transparência. Antes de converter o nosso buffer para a textura, podemos também aplicar qualquer filtro especial nestes dados, por exemplo: escala de cinzas, sepia, lomo, vintage... Qualquer coisa que possamos imaginar.

8-bit style renderer

So, we have a raw data from the camera. We do not need to convert them to the texture directly. We can work with this values and, for example, we can draw an extra image for every pixel!

We can make an interesting 8-bit looking app with a real live preview. The power of the XNA Framework will handle it (the slowest Windows Phones can draw around 120x120 non-transparent textures at once, in more than 20 FPS).

This 8-bit effect can look like this:

8bit live effect demonstration mini.png

Note.pngNote: These sample images have been generated with some advanced filters. Colors were restricted to a limited palette (for example only 16 specific colors) with a specified threshold. Variants of the Ordered Dithering and Floyd-Steinberg algorithm were applied.

Rendering pixels from images

We will show here the most simple version of this 8-bit renderer. Every pixel obtained from the camera will be drawn as a small image, colored with the RGB data. For example - our sample image will be 6x6 pixels big. If we set the output buffer of the camera to 80x80 pixels, the final image will be 480x480 px big.

You can download the sample image here: Sample sprite.png

And add this image to your Content project:

Xna content2.png

Now we can load this image into our app. Add to your GamePage.xaml.cs this declaration:

Texture2D sprite;

Modify the OnNavigated method like this:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
//...
// Initialize the camera reader + small Sprite texture
sprite = contentManager.Load<Texture2D>("sample_sprite");
outputSize = new Point(80, 80);
camReader = new CamReader();
camReader.StartCamera(this.Dispatcher, cameraDisplayFake, outputSize);
//...
}

The image will be loaded to the sprite variable.

Note.pngNote: We are using a standard XNA Content pipeline, the image is converted to the uncompressed .xnb file, then loaded into the texture. We can also load this image directly from .png file.

Now we can draw the values. Modify the OnDraw method like this:

private void OnDraw(object sender, GameTimerEventArgs e)
{
//...
// Get camera values, draw image
int[] buffer = camReader.GetBufferFromARGB();
if (buffer != null)
{
// Draw a pixelated 8-bit effect
spriteBatch.Begin();
Draw8BitStyle(spriteBatch, buffer);
spriteBatch.End();
}
}

Our method Draw8BitStyle will simply read the values from the buffer, extract the color data and draw the sprite for every pixel:

private void Draw8BitStyle(SpriteBatch spriteBatch, int[] buffer)
{
// Draw a pixelated 8-bit effect from the buffer
for (int y = 0; y < outputSize.Y; y++)
for (int x = 0; x < outputSize.X; x++)
{
int val = buffer[y * outputSize.X + x];
 
// Convert value into color, draw the sprite
int a = val >> 24;
int b = (val & 0x00ff0000) >> 16;
int g = (val & 0x0000ff00) >> 8;
int r = (val & 0x000000ff);
 
Vector2 position = new Vector2(x * sprite.Width, y * sprite.Height);
spriteBatch.Draw(sprite, position, new Color(r, g, b));
}
}

Our live camera stream will look like this now. This is awesome!

8bit effect.png

Tip: If you want to know how to convert RGB values back into one integer value, this method will help you:

private int PackRGBValue(byte r, byte g, byte b)
{
// Pack individual RGB components into one single integer
uint output = 0xff000000; // Alpha
output |= (uint)b;
output |= (uint)(g << 8);
output |= (uint)(r << 16);
return unchecked((int)output);
}

Note.pngNote: You can find both variants of the algorithm in the attached sample project. The component for measuring frames per second (FPS) from the XNA is also attached.

Reading from the YCbCr buffer

Our photo stream from the camera looks great. But it's still not perfect. This implementation is horribly slow on the first generation of Windows Phone devices. For example on the Samsung Omnia 7 Device, the 120x120 pixels large scene composed of 4x4 px sprites runs only at 6 FPS! Can we fix it?

Fortunately there is a second method on the PhotoCamera object for reading raw values from the camera: GetPreviewBufferYCbCr. It is little more complicated to use, but gives us much better results. The main difference between these methods is - when we read an ARGB data from the camera, the whole 640x480 buffer is internally converted into RGB values. It doesn't matter that we wanted only a 120x120 px small array. All values had to be converted.

Method GetPreviewBufferYCbCr returns a buffer from the camera in the "more raw" format. We will convert to RGB values only those pixels that we really want. I can reveal you in advance that this implementation will run on the Samsung Omnia 7 Device faster than 20 FPS!

Note.pngNote: Do not worry about Nokia Lumia product line. Both implementations are sufficiently fast even on the Nokia Lumia 710 or 800 Device. The ARGB method runs at a stable 15 FPS, the YCbCr method runs at 22 FPS.

Note.pngNote: Windows Phone 8 devices will be even faster, on the Windows Phone 8X Device by HTC the ARGB method runs at 27 FPS and the faster YCbCr method is attacking 30 FPS (we are talking about WP7 application in a compatible mode, the native WP8 solution will be discussed in the last chapter of this article).

Implementation

We will extend our CamReader class. Add here these declarations:

// Special buffer for the YCbCr reader
YCbCrPixelLayout bufferLayout;
byte[] yCbCrPreviewBuffer;

The StartCamera method will also be slightly modified. Add these initialization logic to the dispatcher.BeginInvoke() call:

// Buffer initialization for reading from the YCbCr buffer
bufferLayout = camera.YCbCrPixelLayout;
yCbCrPreviewBuffer = new byte[bufferLayout.RequiredBufferSize];

And finally we can add here a new method - GetBufferFromYCbCr. It is the alternative to the GetBufferFromARGB, it returns data in the same format (but it is faster).

public int[] GetBufferFromYCbCr()
{
if (camera != null && yCbCrPreviewBuffer != null)
{
// Get preview Image buffer
try { camera.GetPreviewBufferYCbCr(yCbCrPreviewBuffer); }
catch { return null; }
 
// Select outputBuffer pixels (outputSize = smaller than 640x480 preview pixels)
CopyYCbCrValuesToOutputBuffer();
SwapRedBlueChannel(outputBuffer);
return outputBuffer;
}
return null;
}

YCbCr to RGB conversion

YCbCr is a pretty interesting color model. Y stands for the Luminance (brightness), Cr and Cb are Chrominance values (they represents the color difference). If we load to our app only a Luminance (Y) value, we will obtain a grayscale image.

Ycbcr2.png

Now we need to implement the last few helper methods. We want to select the specific pixels from the YCbCr buffer, extract the correct Y, Cr and Cb values, convert them back to the ARGB and finally to save the value of each pixel to the output buffer.

private void CopyYCbCrValuesToOutputBuffer()
{
// Copies data from YCbCr buffer to smaller output buffer (in correct ratio)
Point start = Point.Zero, incr = Point.Zero;
GetOutputParams(ref start, ref incr);
 
for (int y = 0; y < outputSize.Y; y++)
for (int x = 0; x < outputSize.X; x++)
{
// Auto flip / rotate the image to output
int sourceX = Math.Min(start.X + y * incr.X, previewSize.X - 1);
int sourceY = Math.Min(previewSize.Y - (start.Y + x * incr.Y) - 1, previewSize.Y - 1);
 
// Read Y, Cb and Cr values of the pixel
byte yValue;
int cr, cb;
GetYCbCrFromPixel(bufferLayout, yCbCrPreviewBuffer, sourceX, sourceY, out yValue, out cr, out cb);
 
// Convert to RGB and write to output buffer
int rgbPackedValue = YCbCrToArgb(yValue, cb, cr);
outputBuffer[x + y * outputSize.X] = rgbPackedValue;
}
}
private void GetYCbCrFromPixel(YCbCrPixelLayout layout, byte[] currentPreviewBuffer, int xFramePos, int yFramePos, out byte y, out int cr, out int cb)
{
// Parse YCbCr value from given pixel
// - finds the bytes corresponding to the pixel location in the frame.
int yBufferIndex = layout.YOffset + yFramePos * layout.YPitch + xFramePos * layout.YXPitch;
int crBufferIndex = layout.CrOffset + (yFramePos / 2) * layout.CrPitch + (xFramePos / 2) * layout.CrXPitch;
int cbBufferIndex = layout.CbOffset + (yFramePos / 2) * layout.CbPitch + (xFramePos / 2) * layout.CbXPitch;
 
y = currentPreviewBuffer[yBufferIndex];
cr = currentPreviewBuffer[crBufferIndex];
cb = currentPreviewBuffer[cbBufferIndex];
 
// Cr and Cb will be from -128 to 127, Y (luminance) from 0 to 255
cr -= 128;
cb -= 128;
}

Note.pngNote: These methods for unpacking YCbCr values and converting them into RGB were inspired by the former article on the MSDN (Windows Phone 7 Camera Color Conversion). The content is no longer available there.

Especially the next method for converting YCbCr value to the RGB looks totally devilish. But don't worry, it's a pretty standard algorithm. You can find lots of implementations of it on the internet. This conversion uses the integer-only divisions again (for the performance reasons).

private int YCbCrToArgb(byte y, int cb, int cr)
{
// Converts YCbCr to packed RGB
int r, g, b;
uint argbPixel;
 
// Integer-only division.
r = y + cr + (cr >> 2) + (cr >> 3) + (cr >> 5);
g = y - ((cb >> 2) + (cb >> 4) + (cb >> 5)) - ((cr >> 1) + (cr >> 3) + (cr >> 4) + (cr >> 5));
b = y + cb + (cb >> 1) + (cb >> 2) + (cb >> 6);
 
// Clamp values to 8-bit RGB range between 0 and 255.
r = r <= 255 ? r : 255;
r = r >= 0 ? r : 0;
g = g <= 255 ? g : 255;
g = g >= 0 ? g : 0;
b = b <= 255 ? b : 255;
b = b >= 0 ? b : 0;
 
// Pack individual components into a single pixel.
argbPixel = 0xff000000; // Alpha
argbPixel |= (uint)b;
argbPixel |= (uint)(g << 8);
argbPixel |= (uint)(r << 16);
 
// Return the ARGB pixel.
return unchecked((int)argbPixel);
}

Now if you switch your rendering logic in the GamePage.xaml.cs file to this line, you should see an acceleration in your frame rate speed:

int[] buffer = camReader.GetBufferFromYCbCr();

Windows Phone 8

We were talking about Windows Phone 7 devices so far. But what about newer WP8 phones? Every WP8 device can run older WP7 applications in the compatibility mode. If you run this sample on your WP8 device, it will work correctly.

But there are some limitations. Such an obsolete app runs only in fixed 800x480 resolution, we don't have an access to bigger resolutions of the HD screens. On some devices with a different screen ratio (HTC 8X), the black border at the top is displayed. We can access some newer APIs from the old app via reflection, but this is also limited. If we want to take full advantage of the new WP8 API (while still supporting the older WP7 devices), we need to make two apps. The Windows Phone Store is prepared for this scenario, we can upload two .xap files into one app submission, the correct version will be automatically downloaded for user.

Fortunately the WP8 API is very similar to the older WP7 API. Maybe they don't call it Silverlight anymore, but there is still the same C# and XAML. Except the one missing component... The XNA Framework. A minute of silence, please... If we select a new WP8 project, we have no chance to access this very powerful graphics engine. The whole idea about the efficient drawing images is now gone. So, how to solve this?

MonoGame Framework

MonogameLogo256x256.png

There is a solution! MonoGame framework is an open source alternative to the XNA Framework. It is a light wrapper around the native DirectX calls and provides us the almost identical API to the XNA. It is a project with very active community and a very good support from the developers. This framework allows you to port your apps and games to WP8, Windows 8 or even on the Android and iOS.

I can recommend this framework. At this moment the Windows Phone 8 version is almost without any issues. The fast app switching works. There is already a working WP8 template in the installer. Maybe, I only did not manage how to change a resolution of the graphics buffer in the latest version. But I hope this will be fixed soon.

Note.pngNote: You can also compile this framework by yourself from the source code. There is already one article on this wiki how to set up the MonoGame: XNA Games On Windows Phone 8 with Monogame, but it is a little outdated now. Most of the problems are fixed now. You can just install the installer, set up a new template project (or link a MonoGame .dll) and everything will work fine, like if you are using a real XNA.

Windows Phone 8 port

We can switch this app to the Windows Phone 8 API. You will need to install a MonoGame Framework and create a new Windows Phone Game project in the Visual Studio 2012.

Monogame template.png

Our helper CamReader.cs object will stay the same. We can move this file into another project, or into some Portable Library, if we want to share it between the projects.

XAML code from the original GamePage.xaml will also stay the same. The code-behind in the GamePage.xaml.cs will be slightly different. In the MonoGame WP8 template, this logic was split into two files. We will only keep the main C#/XAML logic in this file, the XNA logic will be moved into Game1.cs file. Take a look into the attached source code: Media:LiveStreamReader.zip

In this package there is a working WP8 project, with the actual compiled MonoGame binaries. There are also both .xap installation files for WP7 and WP8 versions, you can try them on your phone.

Note.pngNote: This approach (with a split logic into two files) is more similar to the standard XNA template. If you ever used the basic XNA WP7 template, there was also the Game1.cs class there. Only the combined XNA/Silverlight WP7 project has a different name convention.

Note.pngNote: Do not forget that MonoGame has still one limitation. The Content Pipeline is not working yet in this framework (converting .png images into the uncompressed .xnb files). You need to copy your .xnb files directly to the project (into the Content folder), or to load these images directly from the stream without the Content Pipeline (as was mentioned before).

The results

We have made two applications, one specific for WP7 and the second for WP8. Both apps share the same core logic, the first app is rendered with the XNA Framework, the WP8 version uses MonoGame. If we start both apps on the same WP8 device, we will see that their performance is almost similar. For example on the Lumia 720 Device both implementations run at 20 FPS.

WP8 version can take advantage of the all new APIs, in runs also in a native resolution of the display. This is the reason why this implementation can be a little slower on devices with HD displays. You need to render more than 2.5x more pixels (than in the 800x480 px WP7 version).

Summary

In this tutorial we have learned how to efficiently read a live stream from the camera and how to display it in the app. Our implementation runs smoothly on both Windows Phone 7 and 8 devices. We have discussed a couple of methods how to make this solution better and faster.

The interesting use of this algorithm was presented, we have learned how to make an original 8-bit looking photo app. We have figured out how we can integrate other live filters and effects to this app.

We have managed to build this app without any line written in C++, all presented code was written in C#. We have learned how to combine a Silverlight and XNA Framework together into one WP7 solution and how to achieve a similar goal with the MonoGame Framework on Windows Phone 8.

This page was last modified on 9 October 2013, at 14:40.
233 page views in the last 30 days.
×