Les comparto este artículo escrito originalmente en mi blog:
http://juank.black-byte.com/xna-shaders-convertir-imagen-grises/
----------
Hola, continuando con el tema del procesamiento de imágenes, este es el tercer artículo relacionado con la conversión de una imagen a escala de grises.
En los dos artículos anteriores:
http://juank.black-byte.com/xna-convertir-imagen-escala-grises/
http://juank.black-byte.com/c-bitmap-convertir-imagen-escala-grises/
Se reviso como convertir una imagen a escala de grises utilizando XNA/Texture2D y también como hacerlo utilizando un objeto Bitmap del .net Framework.
Sin embargo hubo un tema que no toqué y honestamente no pretendía tocar, al menos no por ahora, pero uno de mis lectores dejo sembrada en mi esa inquietud, y aquí lo tengo este artículo que muestra como convertir una imagen en escala de grises utilizando el método que es, de lejos, el más eficiente de todos y también el más sencillo una vez se sabe acerca de los Shaders.
Como trabajar los Shaders en XNA?
Para programar el shader he preparado un código de ejemplo inicial, el cual sencillamente crea un rectángulo y lo pone en un entorno 3D y le aplica una textura, la cual será a la que le modificaremos la información por medio del shader. Acá esta el código inicial sin Shaders.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace BlogHLSL
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
VertexPositionTexture[] verts;
BasicEffect effect;
Texture2D textura;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
GraphicsDevice.RenderState.CullMode = CullMode.None;
GraphicsDevice.VertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionTexture.VertexElements);
effect = new BasicEffect(GraphicsDevice, null);
effect.View = Matrix.CreateLookAt(new Vector3(0,0,-10), new Vector3(0,0,1), Vector3.Up);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(0.5f,
Window.ClientBounds.Width/ Window.ClientBounds.Height,
1, 10);
effect.World = Matrix.Identity * Matrix.CreateTranslation(1.7f,1.7f,0);
effect.TextureEnabled = true;
base.Initialize();
}
protected override void LoadContent()
{
textura = Content.Load<texture2d>("rabbid");
effect.Texture = textura;
verts = new VertexPositionTexture[4];
verts[0] = new VertexPositionTexture(new Vector3(-1,1,0), new Vector2(0,0));
verts[1] = new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1, 0));
verts[2] = new VertexPositionTexture(new Vector3(-1, -2.7f, 0),new Vector2(0, 1));
verts[3] = new VertexPositionTexture(new Vector3(1, -2.7f, 0), new Vector2(1, 1));
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
GraphicsDevice.DrawUserPrimitives<vertexpositiontexture>(PrimitiveType.TriangleStrip, verts, 0, 2);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
}
Ahora a programar con Shaders!!!
Tomando el código anterior como base lo que haré será dibujar la textura en otra posición, simplemente cambiando la posicion del world, la diferencia será que el efecto que usaré para dibujar la textura no será un BasicEffect sino unEffect.
Al utilizar Effect se tiene todo el poder de los Shaders en las manos, pero se debe asumir un pequeño costo y es que dejar la aplicación haciendo exactamente lo que hacia es un poco más complicado de hacer de lo que era con unBasicEffect , si bien la funcionalidad del BasicEffect se puede considerar muy limitada.
Shader para dibujar una textura
BasicEffect es un shader pre implementado que viene con XNA el cual sencillamente nos permite comenzar a trabajar sin preocuparnos por saber o no de Shaders. En su lugar vamos a trabajar con Effect. Para inicializar Effect necesitamos un archivo .fx que es el que contendrá el código del shader, así que en el Content se debe agregar un nuevo archivo de tipoEffect y lo llamaré Efecto.fx.
Este es el shader con el que iniciaré a trabajar, es decir este es el Shader mínimo que permite hacer exactamente lo mismo que con BasicEffect:
float4x4 WorldViewProjection;
Texture textura;
sampler muestreador = sampler_state
{
Texture = <textura>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = mirror;
AddressV = mirror;
};
struct VertexShaderInput
{
float4 Position : POSITION0;
float2 Tex : TEXCOORD0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float2 Tex : TEXCOORD0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output = (VertexShaderOutput)0;
output.Position = mul(input.Position , WorldViewProjection);
output.Tex= input.Tex;
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return tex2D(muestreador, input.Tex.xy);
}
technique Technique1
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
No es complicado entender el código pero no entrare en mayores detalles porque recuerden que mi intención no es hacer un curso de Shaders, ya habrá tiempo para ello, por ahora el objetivo es aprender a aplicar el efecto de escala de grises y brindaré la información suficiente para que el artículo sea suficientemente entendible para todos.
Una vez hecho el shader se debe preparar el código para utilizarlo, así que se debe declarar una referencia a un objetoEffect el cual he llamado shaderEffect, y en el método LoadContent adiciono este código de inicialización justo después de cargar la textura:
Matrix tmpWorld = Matrix.Identity * Matrix.CreateTranslation(-0.5f, 1.7f, 0);
Matrix tmpView = Matrix.CreateLookAt(new Vector3(0,0,-10), new Vector3(0,0,1), Vector3.Up);
Matrix tmpProjection = Matrix.CreatePerspectiveFieldOfView(0.5f,
Window.ClientBounds.Width/ Window.ClientBounds.Height,
1, 10);
shaderEffect = Content.Load<effect>("Efecto");
shaderEffect.CurrentTechnique = shaderEffect.Techniques["Technique1"];
shaderEffect.Parameters["WorldViewProjection"].SetValue(tmpWorld * tmpView * tmpProjection);
shaderEffect.Parameters["textura"].SetValue(textura);
Prácticamente es el mismo código que para BasicEffect pero con algunas diferencias menores, es importante recordar que el world ha sido inicializado con una traslación diferente para que la imagen de la textura quede justo al frente de la original. Otra cosa importante de notar es que a diferencia de los dos artículos anteriores donde lo que hice fue crear una copia de la textura, en este la copia solo se realiza a nivel de la memoria de video por lo cual no crearemos un nuevo Texture2D. (Esto en realidad es lo que lo hace increíblemente más rápido ya que todo el procesamiento se hace en la GPU)
El código para dibujar en el método draw es básicamente replicar el anterior, adiciono esto justo debajo de effect.End():
shaderEffect.Begin();
foreach (EffectPass pass in shaderEffect.CurrentTechnique.Passes)
{
pass.Begin();
GraphicsDevice.DrawUserPrimitives<vertexpositiontexture>(PrimitiveType.TriangleStrip, verts, 0, 2);
pass.End();
}
shaderEffect.End();
Ahora tengo esto:

Shader para aplicar el efecto de escala grises
Tal como lo mostré en los artículos anteriores, para convertir una imagen en escala de grises basta con sumar los componentes de color de cada pixel y distribuir la intensidad de color de manera proporcional a como nuestro ojo percibe los diferentes componentes de color, esto es 0.3R 0.59G 0.11B. Para hacer esto desde el shader vasta con modificar el PixelShader (en este caso PixelShaderFunction)de la siguiente forma:
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float4 color = tex2D(muestreador, input.Tex.xy);
color.rgb = color.r * 0.3 + color.g * 0.59 + color.b *.11;
return color;
}
Y Listo! se ejecuta el programa y…:

Hasta pronto!
Este articulo es una copia cruzada de mi blog original, visítalo en:
http://juank.black-byte.com/c-bitmap-convertir-imagen-escala-grises/
-------------------
Hola, hace apenas unos días publiqué un artículo para convertir una imagen a escala de grises utilizando XNA Framework:
C# – XNA- Como Convertir Una Imagen a Escala de Grises
Bien como no todo mundo conoce XNA Framework he
decidido hacer una versión más accesible utilizando el objeto Bitmap de
System.Drawing. Algunas cosas serán un ligeramente más difíciles pero
en esencia es lo mismo.
Nuevamente explicare el tema de la conversión de Color para evitar el ir y venir al link anterior.
Cómo Convertir una Imagen a Escala de Grises
Básicamente para que una imagen sea vea en tonos de
gris se requiere que los tres componentes básicos del color ( en el
computador: rojo, verde, azul – RGB por sus siglas en ingles ) tengan
más o menos la misma intensidad, podemos decir que si queremos
convertir un pixel a su equivalente en escala de grises bastaría con
hacer algo como esto:
- Sumar los valores de los componentes de color del pixel, es decir sumar R + G + B
- Sacar el promedio de esa suma
- El valor hallado se debe asignar a R, G y B
Con estos tres pasos ya logramos que el pixel sea de color gris ya que cada uno de sus componentes tiene el mismo valor.
Hay muchas otras formas de hacerlo, incluso alguien
que haya trabajado previamente con imágenes puede tener su propia
versión de como implementarlo de acuerdo a lo que necesite o al tiempo
que tenga. Pero existe una manera ampliamente conocida y aceptada en el
gremio de las personas que trabajan con imágenes y visión por
computador esa manera es la que aprenderemos a efectuar.
El ojo humano y su sensibilidad
Bien, resulta que el ojo humano es mucho más sensible
a los colores verdes y rojos que al azul, por lo que en cuanto a
precepción de iluminación se trata nuestro ojo reconoce los patrones de
iluminación en color en las siguientes proporciones para cada
componente:
- Rojo:30%
- Verde:59%
- Azul:11%
Así que lo más adecuado es calcular el valor de cada
componente de color con base a esta proporción y de este modo se
obtiene el pixel de color gris con la iluminación adecuada para que
nuestro ojo lo perciba como un mejor equivalente a su versión en color.
Manos A La Obra, Tiempo de Programar
Los Bitmap tienen diversos formatos, el más normal hoy
día es el de color de 24 bit, el cual tambien es el valor por defecto
cuando cargamos imagenes jpg, en este artículo trabajaremos diseñando
un algoritmo para 24 bit de color, a diferencia de como se hizo en el
artículo de XNA donde presumimos que trabajabamos con imagenes de 32
bit. Para visualizar un objeto Bitmap basta con usar un PictureBox y a
este se le asigna la imagen en su atributo Image, tal como lo vemos a
continuación:
private void Form1_Load(object sender, EventArgs e)
{
Bitmap imagen = new Bitmap("conejo.jpg");
pictureBox1.Image = imagen;
}
Ahora lo que haremos es crear una función que reciba
como parámetro un objeto Bitmap ( o imagen ) y devuelva un nuevo Bitmap
( o imagen ) convertido a escala de grises:
private Bitmap CreateGrayScaleBitmap(Bitmap source)
{
Bitmap target = new Bitmap(source.Width, source.Height, source.PixelFormat);
return target;
}
Padding y Stride
El siguiente paso es obtener del bitmap la información
de los pixeles que lo conforman, esta tarea si bien es sencilla es un
poco más complicada de hacer a lo que es con un Texture2D de XNA,
debido a que el objeto Bitmap representa su información de color
fielmente a lo que es un archivo bmp. Por ello se debe tener en cuenta
que no todos los bytes son información de color, algunos de estos bytes
solo son de relleno y ello puede cambiar de un Bitmap a otro segun sus
dimensiones y profundidad de color, esta caracteristica es normalmente
conocida como padding y en su momento se implemento para hacer que la
lectura del archivo bmp fuera mucho mas rápida al poder cargarla en
bloques de enteros.
Asi que lo siguiente a realizar es obtener la
información de la imagen lo cual lo hacemos con el método LockBits, el
cual retorna un objeto BitmapData
private Bitmap CreateGrayScaleBitmap(Bitmap source)
{
Bitmap target = new Bitmap(source.Width, source.Height, source.PixelFormat);
BitmapData bmpData = source.LockBits(new Rectangle(0, 0, source.Width, source.Height),
ImageLockMode.ReadOnly,
source.PixelFormat);
return target;
}
Sin embargo BitmapData no nos sirve para recorrer byte
por byte la información de color, por lo que ahora debemos convertir
bmpData a un byte[]… pero Como?
Debemos apoyarnos en los mecanismos de Marshal
provistos por el .net Framework, la clase Marshal permite por ejemplo
convertir un bloque de memoria no administrado en un bloque
administrado, el objeto bmpData tiene un atributo Scan0 que no es más
que un puntero al arreglo de bytes donde esta la información del color,
así que podemos utilizar ese puntero para crear poner la información en
nuestro array de bytes.
Pero antes debemos crear el array de bytes, el tamaño
del array debe ser básicamente ancho x alto x número de bytes por
pixel… pero… hay que tener en cuenta el padding…los bytes de relleno.
Calcular el padding es muy fácil aunque es dependiente sobre todo de la
profundidad del color, pero el objeto bmpData tiene un atributo llamado
Stride el cual es el ancho en bytes de cada línea de pixeles incluyendo
el stride, así que nuestra formula para hallar el tamaño del array de
bytes se reduce a: bmpData.Stride * alto
private Bitmap CreateGrayScaleBitmap(Bitmap source)
{
Bitmap target = new Bitmap(source.Width, source.Height, source.PixelFormat);
BitmapData bmpData = source.LockBits(new Rectangle(0, 0, source.Width, source.Height),
ImageLockMode.ReadOnly,
source.PixelFormat);
byte[] targetBytes = new byte[bmpData.Stride * source.Height ];
return target;
}
El engorroso sistema de acceso a los pixeles
Ahora utilizando Marshal obtenemos el array de bytes:
private Bitmap CreateGrayScaleBitmap(Bitmap source)
{
Bitmap target = new Bitmap(source.Width, source.Height, source.PixelFormat);
BitmapData bmpData = source.LockBits(new Rectangle(0, 0, source.Width, source.Height),
ImageLockMode.ReadOnly,
source.PixelFormat);
byte[] targetBytes = new byte[bmpData.Stride * source.Height ];
Marshal.Copy(bmpData.Scan0, targetBytes, 0, targetBytes.Length);
return target;
}
como tambien necesitamos acceder a la infromación en
bytes del bmp de destino es necesario repetir las mismas tres
operaciones pero en LockBits ahora colocamos WriteOnly.
private Bitmap CreateGrayScaleBitmap(Bitmap source)
{
Bitmap target = new Bitmap(source.Width, source.Height, source.PixelFormat);
BitmapData bmpData = source.LockBits(new Rectangle(0, 0, source.Width, source.Height),
ImageLockMode.ReadOnly,
source.PixelFormat);
byte[] targetBytes = new byte[bmpData.Stride * source.Height ];
Marshal.Copy(bmpData.Scan0, targetBytes, 0, targetBytes.Length);
BitmapData targetData = target.LockBits(new Rectangle(0, 0, target.Width, target.Height),
ImageLockMode.WriteOnly,
target.PixelFormat);
byte[] targetBytes = new byte[targetData.Stride * targetData.Height];
Marshal.Copy(targetData.Scan0, targetBytes, 0, targetBytes.Length);
return target;
}
Como es casi el mismo código podemos optimizarlo y organizarlo un poco así:
private byte[] GetImageBytes(Bitmap image, ImageLockMode lockMode, out BitmapData bmpData)
{
bmpData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height),
lockMode, image.PixelFormat);
byte[] imageBytes = new byte[bmpData.Stride * image.Height];
Marshal.Copy(bmpData.Scan0, imageBytes, 0, imageBytes.Length);
return imageBytes;
}
private Bitmap CreateGrayScaleBitmap(Bitmap source)
{
Bitmap target = new Bitmap(source.Width, source.Height, source.PixelFormat);
BitmapData targetData, sourceData;
byte[] sourceBytes = GetImageBytes(source, ImageLockMode.ReadOnly, out sourceData);
byte[] targetBytes = GetImageBytes(target, ImageLockMode.ReadWrite,out targetData);
return target;
}
Ahora se debe recorrer el array de bytes para
convertirlo a escala de grises, para poder hacerlo necesitamos recorrer
el arreglo en saltos de a pixel, si trabajamos con BMP de 24 bit el
tamaño de cada pixel es de 3 bytes.
En un Bitmap el formato de color viene en BGR (Blue
Green Red) mientras que en XNA viene en BGRA (Blue Green Red Alpha)
teniendo en cuenta esto la implementación para convertir los bytes a
escala grises queda:
private Bitmap CreateGrayScaleBitmap(Bitmap source)
{
Bitmap target = new Bitmap(source.Width, source.Height, source.PixelFormat);
BitmapData targetData, sourceData;
byte[] sourceBytes = GetImageBytes(source, ImageLockMode.ReadOnly, out sourceData);
byte[] targetBytes = GetImageBytes(target, ImageLockMode.ReadWrite,out targetData);
//recorrer los pixeles
for (int i = 0; i < sourceBytes.Length; i += 3)
{
//ignorar el padding, es decir solo procesar los bytes necesarios
if ( (i + 3) % (source.Width * 3) > 0 )
{
//Hallar tono gris
byte y = (byte)(sourceBytes[i+2] * 0.3f
+ sourceBytes[i + 1] * 0.59f
+ sourceBytes[i] * 0.11f);
//Asignar tono gris a cada byte del pixel
targetBytes[i + 2] = targetBytes[i + 1] = targetBytes[i] = y;
}
}
return target;
}
Finalmente hay que copiar el array de bytes modificado al bitmap de destino, y desbloquear ambos bitmaps:
private Bitmap CreateGrayScaleBitmap(Bitmap source)
{
Bitmap target = new Bitmap(source.Width, source.Height, source.PixelFormat);
BitmapData targetData, sourceData;
byte[] sourceBytes = GetImageBytes(source, ImageLockMode.ReadOnly, out sourceData);
byte[] targetBytes = GetImageBytes(target, ImageLockMode.ReadWrite,out targetData);
//recorrer los pixeles
for (int i = 0; i < sourceBytes.Length; i += 3)
{
//ignorar el padding, es decir solo procesar los bytes necesarios
if ( (i + 3) % (source.Width * 3) > 0 )
{
//Hallar tono gris
byte y = (byte)(sourceBytes[i+2] * 0.11f
+ sourceBytes[i + 1] * 0.59f
+ sourceBytes[i] * 0.3f);
//Asignar tono gris a cada byte del pixel
targetBytes[i + 2] = targetBytes[i + 1] = targetBytes[i] = y;
}
}
Marshal.Copy(targetBytes, 0, targetData.Scan0, targetBytes.Length);
source.UnlockBits(sourceData);
target.UnlockBits(targetData);
return target;
}
Wowfff! finalmente asignamos la imagen al PictureBox:
private void Form1_Load(object sender, EventArgs e)
{
Bitmap imagen = new Bitmap("conejo.jpg");
pictureBox1.Image = imagen;
pictureBox2.Image = CreateGrayScaleBitmap(imagen);
}
