again stuck with contour tracing :(

Monkey Forums/Monkey Programming/again stuck with contour tracing :(

GC-Martijn(Posted 2015) [#1]
I created a contour tracing code for PNG files, and there are special cases that its not working 100% correct.

several weeks later, still looking for a solution to trace a PNG image to get the full outline clockwise, so I can create a polygon and/or do other stuff later.

I found many methods and did try everything.

This is the not 100% correct one:
http://www.monkey-x.com/Community/posts.php?topic=9748
Using marching squares.

And below is the last one using this algorithm
http://en.wikipedia.org/wiki/Moore_neighborhood

But i'm missing 4 pixels using that code.


Using this image png image:



ignore the other functions in it.
they only work after the points are clockwise order.
Strict

Import mojo
Import opengl.gles11
Import classes.pngtracer

Function Main:Int()
	New MyApp()
	Return 0
End

Class MyApp Extends App
	Field thing:Sprite
		
	Method OnCreate:Int()		

		SetUpdateRate(60)
		thing = New Sprite()
		Return 0
	End
	
	Method OnUpdate:Int()
		thing.Update()
		Return 0
	End
	
	Method OnRender:Int()
		Cls(255,255,255)

		thing.Draw()
		Return 0
	End
End



Class Sprite
	Field visible_outline:Stack<Point> = New Stack<Point>()
	Field simplyfied_outline:Stack<Point> = New Stack<Point>()
	Field verts:Float[]

	Field hit:Bool
	Field imgSlice:Image

	Field PNGTrace:Pngtracer = New Pngtracer()
	Method New()
		

		PNGTrace.Trace("monkey://data/bord.png",1,True)
		visible_outline = PNGTrace.outline
		simplyfied_outline = PNGTrace.simplyfied_outline
		verts = PNGTrace.verts

		' or atlas file
		'PNGTrace.Trace("monkey://data/atlas-world1.png",1,True,2715,417,242,132)
		'visible_outline = PNGTrace.outline
		'simplyfied_outline = PNGTrace.simplyfied_outline
		'verts = PNGTrace.verts

		'imgSlice = CreateImage(242, 132)
		'imgSlice.WritePixels(PNGTrace.data, 0, 0, 242, 132)

		' ! I WILL USE IT LIKE THIS
		' I save the verts in a json file, so I don't have to calculate it before
	
		' Monkey has a bug or it yust don't want to work with my poly data, I don't know why yet.
		' DrawPoly(verts) don't work, but hit detection and other poly math do work.
		' 
	End
	
	Method Draw:Void()
	''	DrawImage(imgSlice,0,0)

	''	If hit Then
	''		SetColor(255,0,0)
	''		For Local point:Point = Eachin simplyfied_outline
	''			DrawPoint(point.x,point.y)
	''		Next
	''	Else	
			SetColor(0,0,255)
			For Local point:Point = Eachin visible_outline
				DrawPoint(point.x,point.y)
			Next
	''	End
	End
	
	Method Update:Void()
	''	If PointInPoly(MouseX(),MouseY(),verts) Then
	''		hit = True
	''	Else
	''		hit = False
	''	End
	End

	

	Function PointInPoly:Bool(x:Float, y:Float, poly:Float[])
		Local i:Int, j:Int, c:Bool
		Local v1:Bool, v2:Bool, v3:Bool, v4:Bool, v5#, v6#, v7#
		c = False
		Local p_count% = (poly.Length() / 2)
		
		For i = 0 To p_count-1
			j = (i+1) Mod p_count
			v1 = (poly[i*2+1] <= y)
			v2 = (y < poly[j*2+1])
			v3 = (poly[j*2+1] <= y)
			v4 = (y < poly[i*2+1])
			v5 = (poly[j*2]-poly[i*2]) * (y-poly[i*2+1])
			v6 = (poly[j*2+1]-poly[i*2+1])
			If v6 = 0.0 Then v6 = 0.0001
			v7 = poly[i*2]
			If (((v1 And v2) Or (v3 And v4)) And (x < v5 / v6 + v7)) Then c = Not c
		Next
		Return c
	End
End

Strict

Import mojo
Import monkey.math
Import opengl.gles11


Function AllocateArrayInt:Int[][]( i:Int, j:Int)
	Local arr:Int[][] = New Int[i][]
	For Local ind:Int = 0 Until i
	    arr[ind] = New Int[j]
	Next
	Return arr		
End

Class Pngtracer
	Field outline:Stack<Point>
	Field simplyfied_outline:Stack<Point>
	Field data:Int[][]
	Field verts:Float[]

	Method Trace:Stack<Point>(path:String, tolerance:Float=1,highestQuality:Bool=True, startX:Int=0, startY:Int=0, w:Int=-1, h:Int=-1)
		Local info:Int[2]
		Local db:DataBuffer = LoadImageData(path, info)
		Local startPixelX:Int = -1
		Local startPixelY:Int = -1

		If w=-1 Then w=info[0]
		If h=-1 Then h=info[1]

		data = AllocateArrayInt(info[0],info[1])

		For Local x:Int = 0 Until w
			For Local y:Int = 0 Until h
				Local j:Int = db.PeekInt( ( y * info[0] + x ) * 4)
				data[x][y] = (j & $ff000000) | ( (j & $00ff0000) Shr 16) | (j & $0000ff00) | ( (j & $000000ff) Shl 16)
			Next
		Next
			
		outline = moorNeighbor(w,h)
		db = Null
		If highestQuality=False Then
			simplifyRadialDistance(outline,tolerance)
			simplifyDouglasPeucker(simplyfied_outline,tolerance)
		Else
			simplifyDouglasPeucker(outline,tolerance)
		End

		toPolyData(outline)
		Return outline
	End


	Method isPixelSolid:Bool(x:int, y:int,w:Int, h:Int)
		Return (data[x][y] shr 24) & $FF > 0
	End

	Method moorNeighbor:Stack<Point>(w:Int,h:Int)
		Local clockwiseOffset:StringMap<Point> = New StringMap<Point>()
		clockwiseOffset.Set("1,0",New Point(1,-1))
		clockwiseOffset.Set("1,-1",New Point(0,-1))
		clockwiseOffset.Set("0,-1",New Point(-1,-1))
		clockwiseOffset.Set("-1,-1",New Point(-1,0))
		clockwiseOffset.Set("-1,0",New Point(-1,1))
		clockwiseOffset.Set("-1,1",New Point(0,1))
		clockwiseOffset.Set("0,1",New Point(1,1))
		clockwiseOffset.Set("1,1",New Point(1,0))

		Local out:Stack<Point> = New Stack<Point>()
		Local prev:Point
        Local curr:Point
        Local boundary:Point
        Local first:Point
        Local firstPrev:Point

        ' find first pixel
		For Local y:Int = h-1 To 0 Step -1
			firstPrev = New Point(0,y-1)
			For Local x:Int = 0 Until w-1
				If isPixelSolid(x,y,w,h) Then
					first = New Point(x,y)
					Exit ' quite this for loop
				End
				firstPrev = New Point(x,y)
			Next
			If first <> Null Then
				Exit ' found the first pixel quit this for loop
			End
		Next

		prev = firstPrev
		out.Push(first)
		boundary = first

		curr = Clockwise(clockwiseOffset,boundary,prev)
		Local stopLoop:Bool = False
 		While stopLoop = False
            If curr.y >= 0 And curr.x >= 0 And
            	curr.y < h And curr.x < w And 
            	isPixelSolid(curr.x,curr.y,w,h) Then
            	out.Push(curr)
            	prev = boundary
            	boundary = curr
            	curr = Clockwise(clockwiseOffset,boundary, prev)
            Else
            	prev = curr
            	curr = Clockwise(clockwiseOffset,boundary, prev)
           	End
           	stopLoop = ((curr.x = first.x And curr.y = first.y) OR (prev.x = firstPrev.x And prev.y = firstPrev.y))
        Wend

	
        Return out
	End

	Method Clockwise:Point(_clockwiseOffset:StringMap<Point>,target:Point,prev:Point)
		Local clock:Point = _clockwiseOffset.Get((prev.x - target.x)+","+(prev.y - target.y))
		Return New Point(clock.x+target.x,clock.y+target.y) 
	End


	Method getSquareDistance:Float(p1:Point, p2:Point)
    	Local dx:Float = p1.x - p2.x
    	Local dy:Float = p1.y - p2.y
    	Return dx * dx + dy * dy
    End
	Method simplifyRadialDistance:Stack<Point>(points:Stack<Point>, tolerance:Float=1)
		Local length:Int = points.Length()
		Local prev_point:Point = points.Get(0)
		Local new_points:Stack<Point> = New Stack<Point>()
		new_points.Push(prev_point)

		Local point:Point

		For Local i:Int = 0 Until length
			point = points.Get(i)
			If getSquareDistance(point, prev_point) > tolerance Then
            	new_points.Push(point)
            	prev_point = point
            End
		Next

		If prev_point <> point Then
        	new_points.Push(point)
        End

        simplyfied_outline = new_points
        Return new_points
	End

	Method getSquareSegmentDistance:Float(p:Point, p1:Point, p2:Point)
	    Local x:Float = p1.x
	    Local y:Float = p1.y

	    Local dx:Float = p2.x - x
	    Local dy:Float = p2.y - y

	    If dx <> 0 or dy <> 0 Then
	        Local t:Float = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy)

	        If t > 1 Then
	            x = p2.x
	            y = p2.y
	        Elseif t > 0 Then
	            x += dx * t
	            y += dy * t
	        End
	    End

	    dx = p.x - x
	    dy = p.y - y

	    Return dx * dx + dy * dy
    End

	Method simplifyDouglasPeucker:Stack<Point>(points:Stack<Point>, tolerance:Float=1)
		Local length:Int = points.Length()
		Local markers:Int[] = New Int[length]

		Local f:Int = 0
		Local l:Int = length-1

		Local first_stack:IntStack = New IntStack()
		Local last_stack:IntStack = New IntStack()
		Local new_points:Stack<Point> = New Stack<Point>()

		markers[f] = 1
		markers[l] = 1

		Local index:Int = 0
		Local max_sqdist:Float = 0
		Local stop:Bool = False

		While stop=False
			max_sqdist = 0

			For Local i:Int = f Until l
				Local sqdist:Float = getSquareSegmentDistance(points.Get(i), points.Get(f), points.Get(l))
				If sqdist > max_sqdist Then
                	index = i
                	max_sqdist = sqdist
                End
			Next

			If max_sqdist > tolerance Then
	            markers[index] = 1

	            first_stack.Push(f)
	            last_stack.Push(index)

	            first_stack.Push(index)
	            last_stack.Push(l)
	        End

	        If first_stack.IsEmpty() Then
	        	f = 0
	        Else
	        	f = first_stack.Pop()
	        End

	        If last_stack.IsEmpty() Then
	        	l = 0
	        	stop = True
	        Else
	        	l = last_stack.Pop()
	        End
		End

		For Local i:Int = 0 Until length
        	If markers[i] Then
            	new_points.Push(points.Get(i))
           	End
        Next

       	simplyfied_outline = new_points
       	Return new_points
	End


	Method toPolyData:Float[](points:Stack<Point>)
		verts = verts.Resize(points.Length*2)
		Local tmpI:Int = 0

		For Local point:Point = Eachin points
			verts[tmpI] = point.x
			tmpI=tmpI+1
			verts[tmpI] = point.y
			tmpI=tmpI+1
		Next

		Return verts
	End
End


Class Point
	Field x:Int
	Field y:Int

	Method New(_x:Int,_y:Int)
		x=_x
		y=_y
	End
End



But maybe its very simple, because I can get the full outline with this simple code below.

But the problem with that is that the coordinate order is not clockwise, so I can't create a polygon and do other things with it.

For example create less points using the method simplifyDouglasPeucker(outline,tolerance)

This is because i'm scanning from x (left right) to y (top bottom)

Method Trace:Stack<Point>(path:String, tolerance:Float=1,highestQuality:Bool=True, startX:Int=0, startY:Int=0, w:Int=-1, h:Int=-1)
		Local info:Int[2]
		Local db:DataBuffer = LoadImageData(path, info)
		Local startPixelX:Int = -1
		Local startPixelY:Int = -1

		If w=-1 Then w=info[0]
		If h=-1 Then h=info[1]

		data = AllocateArrayInt(info[0],info[1])

		For Local x:Int = 0 Until w
			For Local y:Int = 0 Until h
				Local j:Int = db.PeekInt( ( y * info[0] + x ) * 4)
				data[x][y] = (j & $ff000000) | ( (j & $00ff0000) Shr 16) | (j & $0000ff00) | ( (j & $000000ff) Shl 16)
			Next
		Next
		
		Local data2:Int[][] = AllocateArrayInt(info[0],info[1])
		outline = New Stack<Point>()
		For Local x:Int = 0 Until w
			For Local y:Int = 0 Until h
				If isPixelSolid(x,y,w,h) Then
					data2[x][y]=1
				Else
					data2[x][y]=0
				End
			Next
		Next
		For Local x:Int = 0 Until w
			For Local y:Int = 0 Until h
				If ((x = 0) Or (x = w - 1) Or (y = 0) Or (y = h - 1)) Then
                	If (data2[x][y]) Then
                		outline.Push(New Point(x,y))
                    End
                Else
                	If (data2[x][y] And
                		 (data2[x - 1][y - 1]=0 Or data2[x][y - 1]=0 Or
                                 data2[x + 1][y - 1]=0 Or
                                 data2[x - 1][y]=0 Or data2[x + 1][y]=0 Or
                                 data2[x - 1][y + 1]=0 Or
                                 data2[x][y + 1]=0 Or data2[x + 1][y + 1]=0)) Then
                    	outline.Push(New Point(x,y))
                    End
            	End
			Next
		Next
		db = Null
		Return outline
	End



Other option maybe...
I can create a map with data2 like this
[
0000000000000
0111111111110
0100000000010
0101111111010
0101000001010
0101000001010
0101000001010
0101000001010
0111000001110
]

As you can see the 1 is the image contour line
But what is then the algorithm to get the coordinates clockwise ?


Gerry Quinn(Posted 2015) [#2]
Just test whether they are anti-clockwise (unless they always are) and reverse them if they are!

ABC -> CBA


GC-Martijn(Posted 2015) [#3]
but the they arent clockwise and not anti clokwise in the last example, they are from x top left to bottom right. in that rare order.


Gerry Quinn(Posted 2015) [#4]
Ah, there's no magic way to do it. You have to put them in in the right order.

How about making four rows: furthest left, furthest right, topmost, and bottom-most pixels, by moving in from the edges / top / bottom until you hit the object. It will work for most simple shapes. Then put all four rows in the right order and direction to get a polygon.

If you want something more general, you might have to 'trace' around the edge in code. Possibly after magnifying the image and smoothing the edges, to avoid corners that make that harder.


GC-Martijn(Posted 2015) [#5]
i was had a idea about a analog clock.
using the middle and scan the 12 hours.

with using the the map above.
but dont know how to make this clock haha.


Gerry Quinn(Posted 2015) [#6]
Maybe if you expanded the image by a factor of 3 and applied a smoothing step to get rid of corners, you could apply an algorithm similar to 'marching squares' to walk around it without any problems from awkward spots.


Raph(Posted 2015) [#7]
I put my old code up in the original post here: http://www.monkey-x.com/Community/posts.php?topic=8626#105178 Maybe it will help.


GC-Martijn(Posted 2015) [#8]
Oke I will have a look , thanks !


GC-Martijn(Posted 2015) [#9]
@Raph,
Can't run your code because its missing one class:
Global edgePoints:List<Edge> = New List<Edge>



Gerry Quinn(Posted 2015) [#10]
Is this for detecting mouseclicks in a game? If so, you can always fall back to making a boolean array from the rectangle containing the image, and checking if the click hit a valid pixel. That will be 32 times smaller than the image, and you can always compress it further by shrinking the image by a factor of 2 or 3. So whether you do it in-game or separately, it won't make your memory consumption grow out of control.


Raph(Posted 2015) [#11]
Huh, it should have been at the bottom. I will update in the other post.

Edit: I take it back. The Edge class IS at the bottom. Did you not copy everything into your file, perhaps?


nullterm(Posted 2015) [#12]
Just a shot in the dark, but maybe try this? Just the two changes



Essentially, skip the 'x' test for lines that have y1=y2, ie. perfectly horizontal. Otherwise you might get unpredictable results.


GC-Martijn(Posted 2015) [#13]
@gerry its in the first place to automatic create a png border, that can be used to detect collision. For example using point in poly
That boolean thing sounds like a good idea, but there are many possibilitys when i get the polygon.

@raph
Ow sorry, i see it now.

@nullterm
Pointinpoly did work oke for me, but only for valid poly points.
Or is this a pointinToXToBottomLeftArea now?


Raph(Posted 2015) [#14]
GC-Martijn, if what you want is to do collision, you probably DON'T want an algorithm that gives you an accurate poly contour. You want a simpler hull for collision that you can use PointInPoly with. My code generates actual outlines, concave hulls -- I used it for shadowcasters.

You might want to try Warpy's convex hull code: http://www.monkey-x.com/Community/posts.php?topic=709


GC-Martijn(Posted 2015) [#15]
@Raph
The next step is to simplify the point to only the corner points (that code is working already)
And in my code I already made the convex hull thing, but the problem with that is that its not working for that image.
I guess because I need a concave hull then.
But with all those idea's and hints I can try another month now haha :)

Then I have
- transparent png [done]
- fast contour scan [done]
- reorder contour to clockwise (or something)
- simplify points [done]
- point in polygon [done]

The simplify points are only the corners in clockwise order, and I save them in a xml file.
Then when the game loads, I reload them to use them for collision.

And I can use the contour from step 2 to create extra FX things.