Extracting, Analyzing and Optimizing a Vector Shape from a BitmapData Shape

Tuesday, April 28th, 2009

This post has been a long time coming, as I've re-written the code more times than I care to recall. There are just too many ways to skin this damn cat! What I'm sharing now is not a final solution by any means. I just need to stop obsessing for a moment, move on and actually use the code for the cool things I've been planning to do with it! Then obsess further at a more suitable time :D

Also, retrospectively, this post is gonna take some courage and dedication to get through without falling asleep. The code was thrilling to conceive, but less so to write about. Apologies.

Anyway, the challenge is : Given extracted "shape outline points" from a BitmapData shape, I want to optimize these points into the minimum amount which correctly define that shapes perimeter.

Rather than going over the ifs, buts, successes and failiures, here's how the process currently works:

1) Remove redundant points from the "shape outline points"

Since the shape is extracted from a bitmap, the "outline" is composed of rows and columns. Any row or column longer than 2 contains "redudant" points (or pixels), between the start and the end of the line. These are easily removed by looping through the points, keeping track of the current row or column direction.

In the case of the F, the process is done, however, much more work is required for those pesky diagonals and curves...

2) Create "Line" Objects which connect the "non redundant" points

The optimization routines use net.sakri.flash.vector.VectorLine, which has the following properties:

  • start_point:Point
  • end_point:Point
  • type:uint VERTICAL, HORIZONTAL or DIAGONAL
  • direction:Point UP, DOWN, LEFT or RIGHT. Only relevant to VERTICAL and HORIZONTAL lines

The code loops through the "non redundant points", creating Lines. At this point, only VERTICAL and HORIZONTAL lines are present (again, due to the "rows and columns" nature of bitmaps). The loop sets each "line direction", based on the lines start and end positions. See the little red arrows below:

3) Use the directions of the "Line" Objects to discover "break points" or "turning points"

Essentially the goal is to isolate Lines, Diagonals and Curves. I have tried approaches ad infinitum, and so far the following delivers the most Bang For my Buck:

  • Loop through the lines, minding a "vertical anchor line" and a "horizontal anchor line".
  • Whenever the direction of lines changes from the current anchors, this can be seen as a "break" or a "turning point".
  • Store these "break points" in a list.
  • Use the "break point" as the current anchor, and continue looping.

The image below is a "single clockwise pass", the "turning points" are represented as blue pixels accentuated by totally awesome green circles.

Following the lines starting from the top left corner, you should "discover" the same "break points".

The first "horizontal direction change" takes place at the bottom right corner of the M. Up until this point all the horizontal lines go "RIGHT".

The first "vertical direction change" occurs at the bottom of the first "V" shape between the M's legs. Up until this point, all the verticals point DOWN, and after the turn point UP.

This first "pass" uncovers 10 break points, which could be used to analyze the "sub components" (deal with diagonals and curves). However, it's painfully clear that more break points are needed. In the "M", there are 13 which define the shape. So, a "second" pass sounds like a good idea, this time counterclockwise (or in reverse order). This results in different "break points" than the clockwise pass, (again just follow the lines with your eyes...).

Below is a "single counter-clockwise pass", the "turning points" are again visualized as blue pixels surrounded by gratuitous neon green pixels:

Putting these two together, running clockwise and counter-clockwise gives us:

Now we've got 16 break points for "M"! This is good enough, the main lines and diagonals are isolated. There was much rejoicement!

Here's the same for a character with some curves:

Nifty as this might sound, the approach isn't without it's shortcomings. "Staircases" and "Curve Staircases" are a thorn in my side. In such cases, there are "clear" changes in visual direction, yet, neither the HORIZONTAL nor the VERTICAL direction changes in a "staircase". Experience the phenomena in characters including T,F,4,S,a, etc. etc. Witness the F below:

At this point, we say "F it", and move on.

4) Use the "break points" to cut the shape up into segments, and analyze

Loop again. This time looking at the sets of VectorLines separated by "break points":

  • Any set of 1 must be a Vertical or a Horizontal line.
  • Detect Diagonals
  • Handle Curves and "complex shapes"

Here's my "CPU Cheap" approach to detecting a Diagonal in a set:

Actionscript:
  1. protected var _diagonal_buffer:Number=.15;
  2.         protected function isDiagonal(lines_vect:Vector.<VectorLine>,index1:uint,index2:uint):Boolean{
  3.             var first:Point=VectorLine(lines_vect[index1]).start_point;
  4.             var middle:Point=getMiddlePointInVLineSegment(lines_vect,index1,index2);
  5.             var last:Point=VectorLine(lines_vect[index2]).start_point;
  6.             var diff:Number=Point.distance(first,last)-Point.distance(first,middle)-Point.distance(middle,last);
  7.             return Math.abs(diff)<_diagonal_buffer;
  8.         }

The code grabs the "middle point" within a set of Lines. Then it compares the distance from the "sets start point to finish point", with the added distance of "start TO midpoint+midpoint TO endpoint". If the difference is less than "_diagonal_buffer", it's a diagonal. Accept it.

The remaining shapes fall under the categories of "Curves" and "Complex Shapes". Both can actually be treated the same : Based on a parametrized "Curve Accuracy", the remaining shapes can just be divided into "sub segments".

Let's say a Curve has 200 pixels, which might translate to 50 "Lines" once "redundant points" are removed. If "Curve Accuracy" is set to 15, it's relatively easy to loop through the 50 lines, and isolate them into "Sub Lines" grouped by this "Curve Accuracy". Maybe an easier way to picture this is just to chop up a curve by choosing every X points along it's path. If that makes no sense, just check out the code.

There's a few more optimizations, but I can't be arsed to go into it right now. This post is long enough as it is.


Click here for the demo (Comic Sans pictured below):

Download the Source (Requires FlexBuilder, with sdk 3.2+)

Congrats, give yourself a hearty pat on the back for making it all the way through! You have our Gratitude! I personally guarantee future posts with better entertainment value using this code base and it's oncoming descendants :D

Extract shape outline points from BitmapData

Monday, April 13th, 2009

On my quest to generate 3D text from non-embedded fonts, after extracting positive and negative shapes from characters, the next step is to analyze and extract the points which define the pixel boundary surrounding the extracted shapes.

There is a "brute" way to do this, simply by looping through every pixel in a BitmapData, and checking weather any surrounding pixels are transparent. This works, but the discovered pixels are not in a meaningful order.

The goal is to create an algorithm which circumnavigates the extremities of shapes, storing the traversed points in sequence, eventually doing this:

My original approach was to grab the first transparent pixel, then loop around its 8 surrounding pixels, run through some rules, then determine the next pixel to move to, until the "3x3 grid" would arrive at its starting point. The "3x3" grid would move along the shape clockwise.

As a side note, it was a relief to see Marios presentation in Milan, where he had pretty much the same approach, except his was moving counter clockwise. One very important "innovation" that I learned from Mario was to double the size of the analyzed bitmap. This prevents "single pixel" lines, which, if you think about it, are very tricky for the "3x3 grid" to analyze.

My implementation was sluggish, and had a few bugs... So I came up with another approach. I guessed that the rules of moving clockwise never change, so I figured a lookup table approach might just work. The idea is to represent the "3x3 grid" surrounding a pixel as a String using zeros for transparent pixels, ones for non-transparent ones. The lookup table would then map the "next position" to the various "3x3 grids". Here's a short snippet of my lookup table:

Actionscript:
  1. protected var _variations:Object={
  2.     "000110110":new Point(0,1),
  3.     "111110000":new Point(-1,0),
  4.     "111011011":new Point(0,-1),
  5.     "111111110":new Point(0,1),
  6.     "100111111":new Point(1,0)}

It's still not the Usain Bolt of algorithms, but a big improvement on my previous attempt. So where did this magical lookup table come from? If you look at the animated gif above, it's actually a screenshot of a "outline pixels generator" tool which I wrote just for this purpose. This allowed me to move along the edge of a shape, as the eye would follow it, and store the "moves" as "rules". You can find the entire lookup table in the zip below.

To test and demo the algorithm, I wrote a quick flex app, click to try it out:



Choose any font, pick a character, then extract the outlines. There is one thorny topic of annoyance. Flash player adds a weird anti-alias to all text above a certain font size. Small non-embedded text appears as one would expect, but in bigger sizes (which is needed for better curve information), there is a "quasi-anti-alias". In the above image,

  • The left-most image is a non-embedded text field, with the "anti-alias".
  • The second image is a "mono-chrome" representation of it.
  • The third is the generated outline (of the mono-chrome)
  • The last one is the original (first image), with the generated outline superimposed.

The outline is not "ideal", all pixels with an alpha greater than 0 are represented as black, making the image "blockier" than necessary. In the "superimposed" image, particularly with curves, there is a clear difference between the original character and the curve. meh. There is a way to get the "aliased" text with embedded fonts, more here. But since I'm interested in non-embedded fonts...





The second "demo" within is an animated "blob", which extracts the outline on every frame. I made this mostly just to "test" the algorithm... So far it's not throwing any errors for me. The frame rate is dependent on the number of "separate shapes and negative shapes". If you have a slow machine I don't recommend this one :D

The next step is to analyze these outline paths and optimize them into "vector information", which I'll blog about shortly.

Click here to try out the demo, then click
here to download the zipped flex source folder. The relevant code is in plain as3 classes, in case flex isn't your thing.

FlexCamp Belgium Summary, Slides And Source Code

Friday, December 12th, 2008

We had a pretty good turnout yesterday, it was great to see (and meet) a lot of new faces! I naturally started the event by going to the wrong SISA building, but fortunately the 'real' one was just a few minutes away.

Here are some of the highlights:

James Ward went over some of the new tags, namespace changes and general updates in Gumbo. Amongst other things, he covered the new 3d implementation, pixelbender integration, FXG, skins and new text features.

Chet Haase talked about his recent work in the frameworks animation capabilities. The Tween class is now replaced by a lower level 'Animator' (or was it Animation?) Class, and the implementation was like music to my ears! He also went on the explain the higher level implementations of Effects... very cool...

Peter Elst delivered an interesting overview of the SQLite Features in Air1.5 in his typically coherent and well organized manner, always a pleasure :)

Mr. Spring ActionScript : Christophe Herreman discussed a bit of Inversion Of Control Theory, then went on to showcase his current project, how Spring ActionScript was is implemented in it and what the benefits are. I have to admit that although each time I read about the topic I understand a little bit more, yet, parts of it still flew way over my head. It's a fascinating framework, and I have a great deal of respect for Christophe! He posted a very comprehensive summary of his presentation this morning on his blog.

Finally, my own presentation. Again, I'm not sure what Koen was thinking when he put me on as the last speaker... Talking about Invalidation Routines at 21:45 in the evening was, well, challenging ;) Having said that, the presentation went smoothly, generated some laughs and I received positive feedback at the end, including the "If only you could have told me this one year ago" :D

Here be the slides:

...and probably more interesting, you can download the Flex project with all the source code here

Thanks again, and see you all at the next user group event!

Talking invalidation at FlexCamp08 Belgium

Monday, December 8th, 2008

Koen was kind enough to invite me to speak the FlexCamp at Sisa Antwerp. Given just 10 days headsup, I decided to go with my "Invalidation Routines Pounded into your cranium once and for all!" which I gave at the 360Flex earlier this year in Milan. If you are building Flex Components, and are not 100% clear on the uses and implementation of measure(), commitProperties() and updateDisplayList() are, then this is a must see. The original presentation was 90 minutes, this time it's compressed to just 30, but I'll still try to fit time to "dig a bit deeper" to keep it interesting for even the Flex Masters out there. I still see people getting this wrong, so I figure it's a useful presentation. Apparently the event is pretty much sold out, so if you're not one of the lucky ones, you can try the scalpers outside the door or by bribing Koen ;)

Also, a wonderful case of "ask and thou shall receive", Christophe Herreman blogged about the release of the Spring ActionScript last week, I asked if he might demo it at the FlexCamp, and within a few hours he was on the speaker list! I'm really looking forwards to that one!

See you there!

Trying out flash player 10 3d features

Thursday, October 16th, 2008

Nascom was kind enough to let me spend some work hours investigating the new features of flash player 10. Naturally the first step was to check out the 3d features. I built a quick "testing" environment. Each test is pretty self explanatory, but if that's not enough, I included some notes attached to each experiment in the flex app. Behold the first 3 tests (but install player 10 first)...

Be sure to drag the vanishing point...

basic flash player 10 3d behaviour

The frame rate is not bad, even with a ton of shapes:

basic flash player 10 3d performance test

Creating a cube... lookout papervision ;)

flash player 10 3d cube test with color materia

Ok, so the cube was not so impressive due to the "layered" nature of flash player10 3d. I'm convinced that through the "hackish" nature of the flash community, there will soon be enough "clever" implementations that will be small in filesize, but will fool the eye. Remember, these cubes can be created and tweened on the timeline... Here's a slightly more convincing "wireframe" version :


flash player 10 3d cube test with wireframe material

try out the flash player 10 3d test runner

More tests coming soon. I'll be posting the code as well.