again stuck with contour tracing :(
Monkey Forums/Monkey Programming/again stuck with contour tracing :(
| ||
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 ? |
| ||
Just test whether they are anti-clockwise (unless they always are) and reverse them if they are! ABC -> CBA |
| ||
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. |
| ||
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. |
| ||
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. |
| ||
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. |
| ||
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. |
| ||
Oke I will have a look , thanks ! |
| ||
@Raph, Can't run your code because its missing one class: Global edgePoints:List<Edge> = New List<Edge> |
| ||
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. |
| ||
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? |
| ||
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. |
| ||
@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? |
| ||
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 |
| ||
@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. |