Code archives/Algorithms/Marching Squares

This code has been declared by its author to be Public Domain code.

Download source code

Marching Squares by TWH2010
the marching squares algorithm traces the outline of a 2d image. This code was heavily inspired by actionscript code by Sakri Rosenstrom:
http://www.sakri.net/blog/2009/05/28/detecting-edge-pixels-with-marching-squares-algorithm/

I imagine msquares could be used to
-make a magic wand tool in an image editor, trace outline of certain pixels (color hues)
-visualize 2d implicit functions like x^2+y^2=5 (circle)
-"ai" for a creature in a game -> it follows the contour of the level or something
-cool effects like watery text
-a 2d water effect if regions are filled and animated
-detect outline of metashapes
'written by TWH aka Torbjoern
'25. aug 
'-fixed multiplie out-of-bounds errors, added getOutlinePixmap( Tpixmap )
'-only the current pixel (pos.x,pos.y) is marked as tracked/visited,

superstrict
graphics 640,480,0,0,2

global fpsCounter:TFPSCounter= new TFPSCounter

type TUIState
	field activeItem:int = -1
	field hotItem:int = -1
end type

global uistate:TUIState = new TUIState
global lowerColor:TColor = TColor.Create(0,0,0)
global upperColor:TColor = TColor.Create(255,255,255)

function mainloop()
	local msquares:TMarchingSquares = new TMarchingSquares
	
	local width:int = 320
	local height:int = 240
	local screen:int[,] = new int[width,height]
	local pointList:tlist = null
	local brushSiz# = 10
	
	local pixmap:TPixmap = LoadPixmap("fry.png")

	
	while not appterminate() and not keyhit(KEY_ESCAPE)
		cls
		'draw our painting surface
		local i:int, j:int
		for i=0 until width
		for j=0 until height
			if( screen[i,j] )
				setcolorARGB( screen[i,j] )
				plot i,j
			endif
		next
		next
	
		'Draw a bitmap using the mouse:
		brushSiz :+ MouseZSpeed() 'change brush size using scrollwheel
		brushSiz = min(30.0, max(2.0, brushSiz) )
		drawCircle(mousex(), mousey(), brushSiz) 'draw our brush
		if mousedown(1) then paintbrush(mousex(), mousey(), brushSiz, $00ffff00, screen)
		
		if keydown(KEY_1) 
			msquares.animated = false
			pointList = msquares.getOutline(screen) 'get a list of outline points
		endif
		
		if keydown(KEY_2) 
			msquares.animated = true 'animate the process
			pointList = msquares.getOutline(screen) 
		endif
		
		if keyhit(KEY_3) 
			msquares.animated = false
			if pixmap then pointList = msquares.getOutlinePixmap(pixmap, lowerColor, upperColor) 
		endif
		
		'Draw all points generated by marching squares
		if pointList<>null
			setcolor 0,255,0
			for local p:vec2 = eachin pointList
				plot p.x, p.y
			next
		endif
		
		setcolor 255,255,255
		DrawLineRect(0,0,width,height)
		if pixmap then drawpixmap(pixmap,width+10,0)
		
		'Begin slider code (lower and upper color threshold for pixmap)
		local sliderLen:int = 255
		local slider_x:int = 15
		local lo_r% = lowerColor.r
		local lo_g% = lowerColor.g 
		local lo_b% = lowerColor.b
		
		local hi_r% = upperColor.r
		local hi_g% = upperColor.g 
		local hi_b% = upperColor.b
		Slider(0, slider_x + (sliderLen+15)*0, height+15*4, sliderLen, $ff0000, lo_r)
		Slider(1, slider_x + (sliderLen+15)*1, height+15*4, sliderLen, $ff0000, hi_r)
		Slider(2, slider_x + (sliderLen+15)*0, height+15*5, sliderLen, $00ff00, lo_g)
		Slider(3, slider_x + (sliderLen+15)*1, height+15*5, sliderLen, $00ff00, hi_g)
		Slider(4, slider_x + (sliderLen+15)*0, height+15*6, sliderLen, $0000ff, lo_b)
		Slider(5, slider_x + (sliderLen+15)*1, height+15*6, sliderLen, $0000ff, hi_b)
		lowerColor.set(lo_r, lo_g, lo_b)
		upperColor.set(hi_r, hi_g, hi_b)
		'End sliders

		local text_y_pos:int = height+100
		drawtext "getOutline()-test, draw here",15, text_y_pos+15*+0
		drawtext "getOutlinePixmap()-test ",15+width*1,  text_y_pos+15*+0
		drawtext "press key 1 update outline",15, text_y_pos+15*+1
		drawtext "press key 2 to see marching squares working",15, text_y_pos+15*+2
		drawtext "Sliders adjust lower lower/upper color threshold for pixmap",15, text_y_pos+15*+3

		drawtext "fps: "+fpsCounter.fps, 15,graphicsheight()-30
		
		fpsCounter.update()
		
	flip
	wend
end function
mainloop()

function unpackARGB(argb:int, a% var, r% var, g% var, b% var)
	a = argb shr 24 & $ff
	r = (argb shr 16) & $ff
	g = (argb shr 8) & $ff
	b = argb & $ff
end function

function setColorARGB(argb:int)
	local a:int = argb shr 24 & $ff
	local r:int = argb shr 16 & $ff
	local g:int = argb shr 8 & $ff
	local b:int = argb & $ff
	setcolor r,g,b
end function

'taken from Sol's graphics tutorial at sol.gfxile.net
function paintbrush(x#,y#,r:int,c:int,screen:int[,])
   local i#, j:int
   for i=0 until 2*r step 1
      local length:int = int( sqr( cos(0.5*180*(i-r)/r)) * r * 2 );
      for j=0 until length
         local xp:int = floor(x+j-length/2.0 + 0.5);
         local yp:int = floor(y-r+i + 0.5);
			local width:int = screen.Dimensions()[0]
			local height:int = screen.Dimensions()[1]
         if(xp > 0 and  xp < (width-1) and yp > 0 and  yp < (height-1))
            screen[xp,yp] = c;
         endif
      next
   next
end function


type vec2
  field x:int,y:int

  function create:vec2(x:int,y:int)
    local tmp:vec2 = new vec2
    tmp.x = x; tmp.y = y;
    return tmp
  end function

end type

type TMarchingSquares
	field bmd:int[,]
	field width:int, height:int
	field variations_cw:vec2[16]
	field variations_ccw:vec2[16]
	field tracked:int[,] 'used to remember if we've traced the pixel earlier
	field points:TList 'list of traced points. Might contain dupes
	field animated:int = false
	
	field errCase15:int = 0
	field errCase0:int = 0
	field foundLoop:int = 0

  method new()
    variations_cw[ 0] = vec2.create( 0, 0) 'illegal. No way to find direction.
    variations_cw[ 1] = vec2.create( 1, 0) ' r
    variations_cw[ 2] = vec2.create( 0, 1) ' d
    variations_cw[ 3] = vec2.create( 1, 0) ' r
    variations_cw[ 4] = vec2.create( 0,-1) ' l
    variations_cw[ 5] = vec2.create( 0,-1) ' u
    variations_cw[ 6] = vec2.create( 0,-1) ' u
    variations_cw[ 7] = vec2.create( 0,-1) ' u
    variations_cw[ 8] = vec2.create( 1, 0) ' l
    variations_cw[ 9] = vec2.create( 1, 0) ' r
    variations_cw[10] = vec2.create( 0, 1) ' d
    variations_cw[11] = vec2.create( 1, 0) ' r
    variations_cw[12] = vec2.create(-1, 0) ' l
    variations_cw[13] = vec2.create(-1, 0) ' l
    variations_cw[14] = vec2.create( 0, 1) ' u
    variations_cw[15] = vec2.create( 0, 0) ' illegal. 
  
    variations_ccw[ 0] = vec2.create( 0, 0) ' illegal
    variations_ccw[ 1] = vec2.create( 0, 1) ' d v
    variations_ccw[ 2] = vec2.create(-1, 0) ' l <
    variations_ccw[ 3] = vec2.create(-1, 0) ' l <
    variations_ccw[ 4] = vec2.create( 1, 0) ' r >
    variations_ccw[ 5] = vec2.create( 0, 1) ' d v
    variations_ccw[ 6] = vec2.create(-1, 0) ' l <
    variations_ccw[ 7] = vec2.create(-1, 0) ' l <
    variations_ccw[ 8] = vec2.create( 0,-1) ' u ^
    variations_ccw[ 9] = vec2.create( 0,-1) ' u ^
    variations_ccw[10] = vec2.create( 0,-1) ' u ^
    variations_ccw[11] = vec2.create( 0,-1) ' u ^
    variations_ccw[12] = vec2.create( 1, 0) ' r >
    variations_ccw[13] = vec2.create( 0, 1) ' d v
    variations_ccw[14] = vec2.create( 1, 0) ' r >
    variations_ccw[15] = vec2.create( 0, 0) ' illegal
  end method

	method getOutlinePixmap:TList( bitmapdata:TPixmap, lowerThreshold:TColor, upperThreshold:TColor )
		bmd = new int[bitmapdata.width, bitmapdata.height]
		width = bitmapdata.width
		height = bitmapdata.height
		
		for local x:int = 0 until width
		for local y:int = 0 until height
			local pixel:int = readpixel(bitmapdata,x,y)
			local a:int, r:int, g:int, b:int 'alpha isnt used
			unpackARGB(pixel,a,r,g,b)
			local avgColor:int = (r+g+b) / 3

			local color_rule:int = 1
			select color_rule
				case 1 if avgColor > 128 bmd[x,y] = 1 'trace white-ish parts, worked images with a white background
				case 2 if avgColor < 128 bmd[x,y] = 1 'trace black-ish parts, workd well on shapes with black contours
				case 3
					'all values must be witin threshold
					if r > lowerThreshold.r and r < upperThreshold.r or..
						g > lowerThreshold.g and g < upperThreshold.g or..
						b > lowerThreshold.r and b < upperThreshold.b
						bmd[x,y] = 1
					endif
					'values may be witin on or another threshold
				case 4
					if r > lowerThreshold.r and r < upperThreshold.r or..
						g > lowerThreshold.g and g < upperThreshold.g or..
						b > lowerThreshold.r and b < upperThreshold.b
						bmd[x,y] = 1
					endif
			end select
		next
		next		

		return searchEachPixel()
	end method
	
	method getOutline:TList( bitmapdata:int[,])
		bmd = bitmapdata
		width = bitmapdata.dimensions()[0]
		height = bitmapdata.dimensions()[1]
		
		return searchEachPixel()
 	end method

	method searchEachPixel:TList()
		tracked = new int[width,height]
		'memclear(varptr tracked[0,0], 4*width*height) 'is this good practice? Does bmax init arrays to 0?
		points = new TList

		' reset error status
		errCase15 = 0
		errCase0 = 0
		foundLoop = 0
		
		'find pixels that have something in them and are untracked so far, run marching squares on pixel
		'keep boundries witin image (2x2) marching squares area 
		for local y:int=2 until height-2
		for local x:int=2 until width-2
			if(bmd[x,y] > 0)
				if( not tracked[x,y])
					marchingSquares(x,y);
				endif
			endif
		next
		next
	
		return points
	end method

  method marchingSquares(x:int,y:int)
	local pos:vec2 = vec2.create( x-1, y-1 ) 'move back and up from pixel
	local start:vec2 = vec2.create( pos.x, pos.y )
	local outOfBounds:int = pos.x < 1 or pos.x > (width-2) or pos.y < 1 or pos.y > (height-2)		
	assert not outOfBounds
	
	local iters:int = 0
	
	while(true)
		iters:+1
		tracked[pos.x,pos.y] :+ 1
		local dirTableIndex:int = getNextEdgePoint(pos)
		outOfBounds = pos.x < 1 or pos.x > (width-2) or pos.y < 1 or pos.y > (height-2)		
		if outOfBounds then return
		if( dirTableIndex=0 or dirTableIndex=15 ) then return 'quit if illegal marching squares state
		if( pos.x=start.x and pos.y=start.y ) then return 'were back at start, finished trace
		if( tracked[pos.x,pos.y] > 2 )  'tracked this pixel too many times, prevent inf loops caused by bad pixel configurations.
			foundLoop:+1
			return 
		endif
		
		points.addLast( vec2.create(pos.x, pos.y) )
		
		' Visualize marching squares progress
		if(animated)
			setcolor(0,50+5*iters mod 150,0)
			plot(pos.x,pos.y)
			
			if(iters mod 5 = 0)  
				flip()
				delay(5)
			endif
		endif
		
	wend
	
	end method

	'use marching squares look-up-table to decide where to move next
	method getNextEdgePoint:int(pos:vec2)
		local x:int = pos.x; local y:int = pos.y
		local index:int = 0
		if( bmd[x+1,y+1] > 0 ) index :+ 1;
		if( bmd[x,y+1] > 0 ) index :+ 2;	
		if( bmd[x+1,y] > 0 ) index :+ 4;
		if( bmd[x,y] > 0 ) index :+ 8;

		' Error
		if( index=15 ) then errCase15:+1;  
		if( index=0) then errCase0:+1; 
		
		pos.x :+ variations_ccw[index].x; pos.y :+ variations_ccw[index].y
		'pos.x += variations_cw[index].x; pos.y += variations_cw[index].y
		return index;
	end method  

end type

type TColor
	field r:byte, g:byte, b:byte 
	function Create:TColor(r:byte, g:byte, b:byte)
		local tmp:TColor = new TColor
		tmp.r = r; tmp.g = g; tmp.b = b;
		return tmp
	end function
	
	method set(r:byte, g:byte, b:byte)
		self.r = r; self.g = g; self.b = b;
	end method 
end type


'Immidiate mode slider function
'Inspired by Sols tutorial: http://sol.gfxile.net/imgui/ch05.html
'What it does: Draws a GUI slider and modifies the slideValue sent to it
'To use a slider, specifiy an id (0 upto MAXSLIDERS)

function Slider(id:int, x#, y#, length#, color:int, slideValue:int var)
	const MAXSLIDERS:int = 7
	assert id < MAXSLIDERS-1
	global mouseLock:int[MAXSLIDERS]
	global sliderValue:int[MAXSLIDERS]
	global globCtor:int = false
	
	if globCtor = false
		globCtor = true
		for local i:int = 0 until MAXSLIDERS
			mouseLock[i] = false
			sliderValue[i] = length / 2
		next
	endif
	
	
	local handle_size# = 10
	local handle_x_pos# = x+slideValue-handle_size/2
	local handle_y_pos# = y-handle_size/2
	
	if PointInRect(mousex(), mousey(),handle_x_pos, handle_y_pos, handle_size, handle_size)
		uistate.hotitem = id
		if( uistate.activeitem = -1 and mouseDown(1) )
			uistate.activeitem = id
		endif
	endif
	
	if not mouseDown(1) then uistate.activeitem = -1 'release grip of handle
	
	setcolor 127,127,127
	drawrect(x,y-2,length,4)
	
	if uistate.hotitem = id
		setcolor 255,255,255
	else
		setcolor 127,127,127
	endif
	
	if uistate.activeitem = id then setcolorARGB(color)

	drawrect(handle_x_pos, handle_y_pos, handle_size, handle_size)
	
	if uistate.activeitem = id
		drawtext ""+slideValue ,handle_x_pos-5,handle_y_pos-15
		if( mousex() < x+length and mousex() > x ) 'limit movement to slider line
			slideValue = mousex()-x
		endif
	endif

end function 

function PointInRect:int(testx#,testy#,topleftx#,toplefty#,w#,h#)
   if(testx >= topleftx and testx <= topleftx+w and..
		testy >= toplefty and testy <= toplefty+h)
      return true
	endif
	return false 
end function

Type TFPSCounter
	Field frames:Int=0
	Field fps:Int=0
	Field sec:Long = MilliSecs()
	
	Method update()
		If(sec < MilliSecs() )
			sec = MilliSecs() + 100
			fps = frames*10
			frames = 0
		EndIf
		frames :+1
	End Method
End Type

'circle outline func by ImaginaryHuman
'http://www.blitzbasic.com/codearcs/codearcs.php?code=1476
function drawCircle(xCenter:Int=320, yCenter:Int=240, radius:Int)
	Local p:int,x:int,y:Int
	x=0
	y=radius
	Plot xCenter+x,yCenter+y
	Plot xCenter-x,yCenter+y
	Plot xCenter+x,yCenter-y
	Plot xCenter-x,yCenter-y
	Plot xCenter+y,yCenter+x
	Plot xCenter-y,yCenter+x
	Plot xCenter+y,yCenter-x
	Plot xCenter-y,yCenter-x
	p=1-radius
	While x<y
		If p<0
			x:+1
		Else
			x:+1
			y:-1
		EndIf
		If p<0
			p=p+(x Shl 1)+1
		Else
			p=p+((x-y) Shl 1)+1
		EndIf
		Plot xCenter+x,yCenter+y
		Plot xCenter-x,yCenter+y
		Plot xCenter+x,yCenter-y
		Plot xCenter-x,yCenter-y
		Plot xCenter+y,yCenter+x
		Plot xCenter-y,yCenter+x
		Plot xCenter+y,yCenter-x
		Plot xCenter-y,yCenter-x
	wend
End function

'DrawLineRect by drnmr
'http://www.blitzmax.com/codearcs/codearcs.php?code=1674
Function DrawLineRect(x:Int,y:Int,length:Int,height:Int)
	local lowerlefty% = y+height
	local lowerrightx% = x+length
	local lowerrighty% = y+height
	local upperrightx% = x+length
	DrawLine x,y,x,lowerlefty
	DrawLine x,lowerlefty,lowerrightx,lowerrighty
	DrawLine lowerrightx,lowerrighty,upperrightx,y
	DrawLine upperrightx,y,x,y
EndFunction

Comments

Jesse2010
quite nice! and useful. Thanks.


GW2010
This is neat.
BTW, all your array operations overflow the array boundries. I had to wrap a whole bunch of stuff in exception handlers just to get it working with real images.


BlitzSupport2010
That's really cool -- very fast, too.


GW2010
I think there is an error in the code.
It looks like pixels on the top and left are outlining properly, but pixels on the bottom and right are at the same coord as the source.


TWH2010
Thanks for all the feedback! The bounds errors are corrected now. I prototyped this in evaldraw, and I forgot it wraps arrays when porting to Bmax.

I've also got pixmaps working now. It works quite well with cartoon characters :D

GW: I hadn't notice the incorrect bottom and right pixels. I think this is just the way marching squares works. To fix this add +1x or +1y depending on what state getNextEdgePoint state returns of the MarchingSquares states:
http://en.wikipedia.org/wiki/Marching_squares
..hm
or maybe on can just look see if the source has pixels at the current upper left square postion like this when adding points:
if bmd[pos.x, pos.y] and bmd[pos.x, pos.y+1]
points.addLast( vec2.create(pos.x+1, pos.y) )
else
points.addLast( vec2.create(pos.x, pos.y) )
endif


Code Archives Forum