Contour and Valley detection using GLSL

Today, I managed to perform contour/suggestive contour rendering in imagespace – thus working on a rendered diffuse shaded image. After using the Sobel edge detection filter  to detect regular contours, I looked for an efficient algorithm to detect valleys and creases, in order to draw suggestive contours as well.

As the original paper from D. Decarlo indicates, I needed to exploit the fact that the algorithm should just work for smooth rendered images, whereas most valley detection algorithms are complicated because they provide noise robustness.

While a pixel in a valley is not necessarily the minimum intensity value in a neighborhood, it will be among a thin set of dark pixels that cuts across the neighborhood.If the valley is steep, the neighborhood will also contain significantly brighter pixels away from the valley; We can require a sufficient intensity difference that the surface must be turned meaningfully away.

The good news is that this method also detects regular contours, thus eliminating the need for the seperate sobel filter pass! GLSL code and results behind the cut.

The main program should just render a diffuse-shaded image, copy that image to a texture, clear the screen and then render a fullscreen quad, with texture coordinates ranging from 0.0 to 1.0. The vertex shader is also quite trivial: it does the projection transform and copies the texture coordinates to a GLSL variable.

void main()
{
	gl_TexCoord[0] = gl_MultiTexCoord0;
	gl_Position = ftransform();
}

The fragment shader needs some uniform variables:

  • color: the texture containing your rendered image.
  • radius: the search radius for every pixel. Larger radii provide thicker contour lines but less noise.
  • renderwidth: the width of your rendered image (only square images are supported, but extending to rectangular is trivial)
// texture
uniform sampler2D color;
// radius for valley detection
uniform int radius;
// rendered image width
uniform int renderwidth;

float intensity(in vec4 color)
{
	return sqrt((color.x*color.x)+(color.y*color.y)+(color.z*color.z));
}

vec3 simple_edge_detection(in float step, in vec2 center)
{
	// let's learn more about our center pixel
	float center_intensity = intensity(texture2D(color, center));
	// counters we need
	int darker_count = 0;
	float max_intensity = center_intensity;
	// let's look at our neighbouring points
	for(int i = -radius; i <= radius; i++)
	{
		for(int j = -radius; j<= radius; j++)
		{
			vec2 current_location = center + vec2(i*step, j*step);
			float current_intensity = intensity(texture2D(color,current_location));
			if(current_intensity < center_intensity) 			{ 				darker_count++; 			} 			if(current_intensity > max_intensity)
			{
				max_intensity = current_intensity;
			}
		}
	}
	// do we have a valley pixel?
	if((max_intensity - center_intensity) > 0.01*radius)
	{
		if(darker_count/(radius*radius) < (1-(1/radius)))
		{
			return vec3(0.0,0.0,0.0); // yep, it's a valley pixel.
		}
	}
	return vec3(1.0,1.0,1.0); // no, it's not.

}

void main(void)
{
	float step = 1.0/renderwidth;
	vec2 center_color = gl_TexCoord[0].st;
	gl_FragColor.xyz = simple_edge_detection(step,center_color);
    gl_FragColor.a = 0.0;
}

As you can see, rendering the Stanford Dragon (+200k polygons) at a very interactive 30 fps.

Was this code useful to you? Then don’t hesitate to flattr me!

Leave a Reply