Оператор Собеля

О реализации некоторых нефотореалистичных эффектов. Часть 9. Оператор Собеля

Оператор Cобеля – это один лучших алгоритмов выделения границ, он часто применяется как один из шагов более сложных и точных алгоритмов (таких как оператор Кенни), так что в этой статье рассмотрим этот оператор.


Но для начала немного я немного изменю текущую реализацию модели освещения Toon. Обычно в такой модели не используется спекулярная (зеркальная) компонента.

Изменим шейдер следующим образом:

float4 DiffuseColor = float4(0,0,1,1);

float kd = 1;

// поставим для kd значение побольше, чем было раньше, чтобы получше осветить
// модель

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float4 Ambient = ka * AmbientColor;

float3 lightDirection = normalize(LightPosition — worldPosition);

float Diffuse = kd * max(0, dot(worldNormal, lightDirection));

float2 pos= float2(0,0);

pos.x = Diffuse;

float4 dColor = tex2D(LigthMaskSampler, pos) * DiffuseColor;

return Color + Ambient + dColor;

}

Результат будет выглядель примерно так:


Дальше немного описания с Википедии:

http://ru.wikipedia.org/wiki/%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80_%D0%A1%D0%BE%D0%B1%D0%B5%D0%BB%D1%8F

Оператор Собеля используется в области обработки изображений. Часто его применяют в алгоритмах выделения границ. По сути, это дискретный дифференциальный оператор, вычисляющий приближенное значение градиента яркости изображения. Результатом применения оператора Собеля в каждой точке изображения является либо вектор градиента яркости в этой точке, либо его норма. Оператор Собеля основан на свёртке изображения небольшими сепарабельными целочисленными фильтрами в вертикальном и горизонтальном направлениях, поэтому его относительно легко вычислять. С другой стороны, используемая им аппроксимация градиента достаточно грубая, особенно это сказывается на высокочастотных колебаниях изображения.

Упрощённое описание

Если проще, то оператор вычисляет градиент яркости изображения в каждой точке. Так находится направление наибольшего увеличения яркости и величина её изменения в этом направлении. Результат показывает, насколько «резко» или «плавно» меняется яркость изображения в каждой точке, а значит, вероятность нахождения точки на грани, а также ориентацию границы. На практике, вычисление величины изменения яркости(вероятности принадлежности к грани)надежнее и проще в интерпретации, чем расчет направления.

Математически, градиент функции двух переменных для каждой точки изображения (которой и является функция яркости) — двумерный вектор, компонентами которого являются производные яркости изображения по горизонтали и вертикали. В каждой точке изображения градиентный вектор ориентирован в направлении наибольшего увеличения яркости, а его длина соответствует величине изменения яркости. Это означает, что результатом оператора Собеля в точке области постоянной яркости будет нулевой вектор, а в точке, лежащей на границе областей различной яркости — вектор, пересекающий границу в направлении увеличения яркости.

Формализация

Строго говоря, оператор использует ядра 3×3, с которыми свёртывают исходное изображение для вычисления приближенных значений производных по горизонтали и по вертикали. Пусть A исходное изображение, а Gx и Gy — два изображения, где каждая точка содержит приближенные производные по x и по y. Они вычисляются следующим образом:


где * обозначает двумерную операцию свертки.

Координата x здесь возрастает «направо», а y — «вниз». В каждой точке изображения приближенное значение величины градиента можно вычислить, используя полученные приближенные значения производных:


Используя эту информацию, мы также можем вычислить направление градиента:


где, к примеру, угол Θ равен нулю для вертикальной границы, у которой тёмная сторона слева.

Поскольку в описании ничего сложного нет, просто реализуем данный эффект для полученного растра. В принципе направление градиента не является слишком важным, так что в данном примере я остановлюсь на том, что буду сравнивать величину градиента (на самом деле квадрат этой величины) с порогом, чтобы получить карту границ.

Так выглядит кусок шейдера, отвечающий за получение границ:

float color00 = dot(tex2D(Sampler, pos-deltaX-deltaY), luminance);

float color10 = dot(tex2D(Sampler, pos-deltaY), luminance);

float color20 = dot(tex2D(Sampler, pos+deltaX-deltaY), luminance);

float color01 = dot(tex2D(Sampler, pos-deltaX), luminance);

float color21 = dot(tex2D(Sampler, pos+deltaX), luminance);

float color02 = dot(tex2D(Sampler, pos-deltaX+deltaY), luminance);

float color12 = dot(tex2D(Sampler, pos+deltaY), luminance);

float color22 = dot(tex2D(Sampler, pos+deltaX+deltaY), luminance);

float gx = color00*1 + color10*2 + color20*1 +

color01*0 + 0 + color21*0 +

color02*-1 + color12*-2 + color22*-1;

float gy = color00*1 + color10*0 + color20*-1 +

color01*2 + 0 + color21*-2 +

color02*1 + color12*0 + color22*-1;


Далее, как обычно, сравнием с порогом, строим специальную текстуру с границами и складываем изображения, результат получится таким (для порога равного 0.3):


Судя по моим экпериментам, этот оператор показывает отличные результаты (особенно это заметно для обработки растровых изображений), однако требует достаточно много текстурных операций и умножений, что плохо сказывается на производительности.

Так что иногда имеет смысл задуматься над тем, нельзя ли применить более простой алгоритм.

Исходный код:

using System;

using System.Collections.Generic;

using System.Linq;

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Audio;

using Microsoft.Xna.Framework.Content;

using Microsoft.Xna.Framework.GamerServices;

using Microsoft.Xna.Framework.Graphics;

using Microsoft.Xna.Framework.Input;

using Microsoft.Xna.Framework.Media;

using Microsoft.Xna.Framework.Net;

using Microsoft.Xna.Framework.Storage;

using Primitives3D;

namespace NPR_1

{


///
<summary>


/// This is the main type for your game


///
</summary>


public
class
Game1 : Microsoft.Xna.Framework.Game

{


GraphicsDeviceManager graphics;


SpriteBatch spriteBatch;


GeometricPrimitive teapot;


Effect effect;


Effect postEffect;


Texture2D lightMask;


RenderTarget2D target;


Texture2D scene;


int height;


int width;


public Game1()

{

graphics = new
GraphicsDeviceManager(this);

Content.RootDirectory = «Content»;

width = graphics.PreferredBackBufferWidth = 800;

height = graphics.PreferredBackBufferHeight = 600;

graphics.PreferMultiSampling = true;

}


///
<summary>


/// Allows the game to perform any initialization it needs to before starting to run.


/// This is where it can query for any required services and load any non-graphic


/// related content. Calling base.Initialize will enumerate through any components


/// and initialize them as well.


///
</summary>


protected
override
void Initialize()

{


// TODO: Add your initialization logic here

teapot = new
TeapotPrimitive(GraphicsDevice);

target = new
RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth,

graphics.PreferredBackBufferHeight, 1, SurfaceFormat.Color, GraphicsDevice.PresentationParameters.MultiSampleType, GraphicsDevice.PresentationParameters.MultiSampleQuality);


base.Initialize();

}


///
<summary>


/// LoadContent will be called once per game and is the place to load


/// all of your content.


///
</summary>


protected
override
void LoadContent()

{


// Create a new SpriteBatch, which can be used to draw textures.

spriteBatch = new
SpriteBatch(GraphicsDevice);


// TODO: use this.Content to load your game content here

effect = Content.Load<Effect>(«light»);

postEffect = Content.Load<Effect>(«postEdge»);

lightMask = Content.Load<Texture2D>(«lightMask»);

}


///
<summary>


/// UnloadContent will be called once per game and is the place to unload


/// all content.


///
</summary>


protected
override
void UnloadContent()

{


// TODO: Unload any non ContentManager content here

}


///
<summary>


/// Allows the game to run logic such as updating the world,


/// checking for collisions, gathering input, and playing audio.


///
</summary>


///
<param name=»gameTime»>Provides a snapshot of timing values.</param>


protected
override
void Update(GameTime gameTime)

{


// Allows the game to exit


if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)


this.Exit();


// TODO: Add your update logic here


base.Update(gameTime);

}


///
<summary>


/// This is called when the game should draw itself.


///
</summary>


///
<param name=»gameTime»>Provides a snapshot of timing values.</param>


protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.SetRenderTarget(0, target);

GraphicsDevice.Clear(new
Color(150, 150, 150));


// TODO: Add your drawing code here


Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateTranslation(-1.0f, 0, 0);


Matrix view = Matrix.CreateLookAt(new
Vector3(0, 1, 4), Vector3.Zero, Vector3.Up);


Matrix proj = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10);

effect.CurrentTechnique = effect.Techniques[«Phong»];

effect.Parameters[«World»].SetValue(world);

effect.Parameters[«View»].SetValue(view);

effect.Parameters[«Projection»].SetValue(proj);

effect.Parameters[«Eye»].SetValue(new
Vector3(0, 1, 4));

effect.Parameters[«LightMask»].SetValue(lightMask);

teapot.Draw(effect);

world = Matrix.CreateRotationY(-(float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateTranslation(1.0f, 0, 0);

effect.CurrentTechnique = effect.Techniques[«NPR»];

effect.Parameters[«World»].SetValue(world);

effect.Parameters[«View»].SetValue(view);

effect.Parameters[«Projection»].SetValue(proj);

effect.Parameters[«Eye»].SetValue(new
Vector3(0, 1, 4));

effect.Parameters[«LightMask»].SetValue(lightMask);

teapot.Draw(effect);

GraphicsDevice.SetRenderTarget(0, null);

GraphicsDevice.Clear(Color.White);

scene = target.GetTexture();

spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);

spriteBatch.Draw(scene, new
Rectangle(0, 0, width / 2, height), new
Rectangle(0, 0, width / 2, height), Color.White);

spriteBatch.End();

postEffect.Begin();

spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);

postEffect.CurrentTechnique.Passes[0].Begin();

spriteBatch.Draw(scene, new
Rectangle(width / 2, 0, width / 2, height), new
Rectangle(width / 2, 0, width / 2, height), Color.White);

postEffect.CurrentTechnique.Passes[0].End();

postEffect.End();

spriteBatch.End();


base.Draw(gameTime);

}

}

}

Шейдер:

sampler Sampler;

float4 PixelShaderFunction(float2 pos : TEXCOORD) : COLOR0

{

float3 luminance = float3(0.299, 0.587, 0.114);


// у нас половина изображения

float2 deltaX = float2( 1.0 / 400, 0);

float2 deltaY = float2( 0, 1.0 / 600);


float color00 = dot(tex2D(Sampler, pos-deltaX-deltaY), luminance);

float color10 = dot(tex2D(Sampler, pos-deltaY), luminance);

float color20 = dot(tex2D(Sampler, pos+deltaX-deltaY), luminance);

float color01 = dot(tex2D(Sampler, pos-deltaX), luminance);

float color21 = dot(tex2D(Sampler, pos+deltaX), luminance);

float color02 = dot(tex2D(Sampler, pos-deltaX+deltaY), luminance);

float color12 = dot(tex2D(Sampler, pos+deltaY), luminance);

float color22 = dot(tex2D(Sampler, pos+deltaX+deltaY), luminance);

float gx = color00*1 + color10*2 + color20*1 +

color01*0 + 0 + color21*0 +

color02*-1 + color12*-2 + color22*-1;

float gy = color00*1 + color10*0 + color20*-1 +

color01*2 + 0 + color21*-2 +

color02*1 + color12*0 + color22*-1;


float border = saturate (gx*gx + gy*gy);


float result = 0;

if (border < 0.3)

{

result = 1;

}

float4 drawColor = tex2D(Sampler, pos) * result;

drawColor.a = 1;

return drawColor;

}

technique EdgeDetection

{

pass Pass1

{

// TODO: set renderstates here.

PixelShader = compile ps_2_0 PixelShaderFunction();

}

}

Реклама
Запись опубликована в рубрике Uncategorized с метками , , , , , . Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s