Character edge finding



The following line of code finds the edge of a character:

pic = Binarize[GradientFilter[Rasterize[Style["\[Euro]", FontFamily -> "Times"], 
                              ImageSize -> 200] // Image, 1]]

Mathematica graphics

The coordinates of the edges can be found as follows:

pdata = Position[ImageData[pic], 1];



Mathematica graphics

However, the points are not sorted in an order usable by Line or Polygon:


Mathematica graphics

This brings me to my question:

  • What would be an efficient method to sort the coordinates so that it would plot properly with Line or Polygon?


  • How to thin and smooth the set of points?
  • How to deal with characters with holes in them, like the ones below?

Mathematica graphics or Mathematica graphics

Sjoerd C. de Vries

Posted 2012-01-24T21:57:59.703

Reputation: 63 549


– Daniel Lichtblau – 2012-01-24T22:25:02.680

2You can also use EdgeDetect for detecting the edges – rm -rf – 2012-01-24T22:49:21.563

@R.M. I tried that, but at first sight the gradient filter looked slightly better to me. The other one looked a bit more irregular. However, the edge detected version has a smaller set of points and that's actually good (question 2) – Sjoerd C. de Vries – 2012-01-24T23:09:23.470

1FindCurvePath (or ListCurvePathPlot) or FindShortestTour with one of its many Method option values ("Greedy"?) could be of use. I don't have time to figure out a full solution now. – Szabolcs – 2012-01-24T23:24:48.713

A relevant question from SO. Sadly Graham scans work only for convex polygons, but you can use it if the outline for the character is convex. – J. M.'s ennui – 2012-01-25T00:10:46.577


Related: 75855

– shrx – 2015-04-16T20:12:16.053



I think there is a neat solution. We have curios function ListCurvePathPlot:

pic = Thinning@Binarize[GradientFilter[Rasterize[Style["\[Euro]", 
FontFamily -> "Times"], ImageSize -> 200] // Image, 1]];

pdata = Position[ImageData[pic], 1];

lcp = ListCurvePathPlot[pdata]

enter image description here

Now this is of course Graphics containing Line with set of points

lcp[[1, 1, 3, 2]]

enter image description here

So of course we can do something like

Graphics3D[Table[{Orange, Opacity[.5],Polygon[(#~Join~{10 n})&
/@ lcp[[1, 1, 3, 2, 1]]]}, {n, 10}], Boxed -> False]

enter image description here

I think it works nicely with "8" and Polygon:

pic = Thinning@Binarize[GradientFilter[
Rasterize[Style["8", FontFamily -> "Times"], ImageSize -> 500] //Image, 1]]; 
pdata = Position[ImageData[pic], 1]; lcp = ListCurvePathPlot[pdata]

enter image description here

And you can do polygons 1-by-1 extraction:

Graphics3D[{{Orange, Thick, Polygon[(#~Join~{0}) & /@ lcp[[1, 1, 3, 2, 1]]]},
  {Red, Thick, Polygon[(#~Join~{1}) & /@ lcp[[1, 1, 3, 3, 1]]]},
  {Blue, Thick, Polygon[(#~Join~{200}) & /@ lcp[[1, 1, 3, 4, 1]]]}}]

enter image description here

=> To smooth the curve set ImageSize -> "larger number" in your pic = code.

=> To thin the curve to 1 pixel wide use Thinning:

 Row@{Thinning[#], Identity[#]} &@Binarize[GradientFilter[
 Rasterize[Style["\[Euro]", FontFamily -> "Times"], 
 ImageSize -> 200] // Image, 1]]

enter image description here

You can do curve extraction more efficiently with Mathematica. A simple example would be

text = First[
      Style["\[Euro] 9 M-8 ", Italic, FontSize -> 24, 
       FontFamily -> "Times"], "PDF"], "PDF", 
     "TextMode" -> "Outlines"]]];

Graphics[{EdgeForm[Black], FaceForm[], text}]

enter image description here

Vitaliy Kaurov

Posted 2012-01-24T21:57:59.703

Reputation: 66 672

I was aware of this possibility (see my earlier question here: The main reason for my question is I want to go 3D and FilledCurve doesn't do that.

– Sjoerd C. de Vries – 2012-01-24T22:30:14.847

(FilledCurve being the carrier of the above paths) – Sjoerd C. de Vries – 2012-01-24T22:37:11.013

@R.M. That might be the ultimate goal, but a flat version living in 3D space would be nice for starters. – Sjoerd C. de Vries – 2012-01-24T23:11:00.583

I completely forgot about ListCurvePathPlot. +1! – Mr.Wizard – 2012-01-25T11:43:59.107

I was playing with ImportString for the outlines and was reminded of this... I noticed that "TextMode" -> "Outlines" doesn't really do anything. You could replace it with "TextMode -> "foo" or remove it altogether, and it'll still give you the same result.The documentation uses the above syntax in a couple of places too... do you know if there are cases where setting the "TextMode" changes the result? Do you also know what other options are available for "TextMode", perhaps bounding boxes? – rm -rf – 2012-05-28T01:14:49.873


You can recast this to a problem of finding a Hamiltonian cycle in a graph constructed in a certain way from your points (distance graph). First, compute mutual distances:

distances = 
With[{tr = Transpose[N@pdata]},
 Function[point, Sqrt[Total[(point - tr)^2]]] /@ 

Now, construct an adjacency matrix by stating that two vertices (points) are connected if their disctance is smaller than a certain radius (which I tweaked a bit):

radius = 1;
adj = Unitize@Clip[distances, {0, radius}, {0, 0}];

Now build an adjacency graph:

graph = AdjacencyGraph[adj];

And find the cycle:

cycle = FindHamiltonianCycle[graph];

Finally, the plot:

Graphics[Polygon[pdata[[cycle[[1, All, 1]]]]]]

This probably can be refined further.

enter image description here

Leonid Shifrin

Posted 2012-01-24T21:57:59.703

Reputation: 108 027

This a pretty neat idea - love the span of approach. Could function Norm be used in your 1st snippet of code? – Vitaliy Kaurov – 2012-01-24T23:54:45.357

1@Vitaly Thanks. Norm - yes, I could have used it. but it would slow things down, since here I used Listability. – Leonid Shifrin – 2012-01-25T07:27:30.673

I think there may be an extraneous ) lurking in the definiton of distances. – image_doctor – 2013-01-30T08:32:57.467

@image_doctor Thanks, corrected. It is telling that this has only be noticed now, more than a year after it has been posted :-). – Leonid Shifrin – 2013-01-30T10:24:47.457

@LeonidShifrin It seems the unit test was a little delayed this time around :) – image_doctor – 2013-01-30T16:10:27.310


Possible answer using ClusteringComponents:




Mathematica graphics

Another fun one, using RegionPlot3D on the same cluster data:

 cluster[[Round[x], Round[y]]] > 1.5, {x, 1, 66}, {y, 1, 202}, {z, 0, 1}, 
 PlotPoints -> 80, Mesh -> False, Axes -> False, Boxed -> False, 
 Lighting -> "Neutral"]

Mathematica graphics

Arnoud Buzing

Posted 2012-01-24T21:57:59.703

Reputation: 9 213

That's neat. Is there a similar example in Documentation? – Vitaliy Kaurov – 2012-01-25T00:47:13.740

@VitaliyKaurov - Not to my knowledge ... – Arnoud Buzing – 2012-01-25T02:21:46.517


I tried FindCurvePath and FindShortestTour, but didn't succeed. The latter is too slow to finish on my machine, except when using the "Greedy" method, which does not give a perfect solution.

Here's something different:

matrix = Rasterize[
    Style["\[Euro]", FontFamily -> "Times", Antialiasing -> False], 
    "Data", ImageSize -> 200][[All, All, 1]];

fun = ListInterpolation[matrix]

{{xmin, xmax}, {ymin, ymax}} = fun["Domain"]

g = RegionPlot[fun[x, y] < 128, {x, xmin, xmax}, {y, ymin, ymax}, 
 PlotPoints -> 50, AspectRatio -> Automatic]

Mathematica graphics

The filling of the region is made of a large number of polygons, but the outline is just one or more lines, which we can easily extract ...

Graphics[Cases[Normal[g], Line[___], Infinity]]

Mathematica graphics

... or even make into polygons again:

% /. Line -> Polygon


I noticed Vitaliy's Thinning solution. With that ListCurvePathPlot works:

img = Thinning@
   Rasterize[Style["\[Euro]", FontFamily -> "Times"], "Image", 
    ImageSize -> 200]

points = N@Position[ImageData[img], 1];


Mathematica graphics

FindCurvePath will give you the ordering of points. This won't of course work for the image of number 8 or 9.

We can also apply a slight smoothing:

orderedPoints = points[[First@FindCurvePath[points]]];    
ListLinePlot[MovingAverage[orderedPoints, 3]]

Warning: FindCurvePath may return a number of disconnected curves!


Posted 2012-01-24T21:57:59.703

Reputation: 213 047


A year late, but here are my thoughts:

As Szabolcs showed, extracting the Line primitives from a RegionPlot provides a convenient way to produce a polygon from an image. The function imgToPolys below does just that - it's essentially the same as Szabolcs' code but I use ImageValue instead of creating an interpolating function from the image data.

Of course, some of the polygons produced this way are the "holes" in letters. In order to display the text properly we need to identify which polygons are holes and which are "solids" (for want of a better word). The function findHoles takes a list of polygons and uses the undocumented function Graphics`Mesh`InPolygonQ to test whether the first point of each polygon lies inside any of the other polygons. I've assumed that any "holes" are completely contained within a surrounding "solid", so that testing a single point is sufficient. The expression returned by findHoles is a list of {solids, holes}.

imgToPolys[img_Image] :=
 Module[{w, h, rp},
  {w, h} = ImageDimensions[img];
  rp = RegionPlot[
    ImageValue[img, {x, y}] < 0.5, {x, 0.5, w - 0.5}, {y, 0.5, h - 0.5}, 
    PlotPoints -> 100];
  Cases[Normal[rp], l_Line :> Polygon @@ l, -1]];

findHoles[p : {_Polygon ..}] := Module[{inq, holes, solids},
  inq = Outer[Graphics`Mesh`InPolygonQ, p, p[[All, 1, 1]], 1];
  holes = DeleteCases[inq ~Position~ True , {x_, x_}][[All, 2]];
  solids = Complement[Range[Length[p]], holes];
  {p[[solids]], p[[holes]]}];

Here's an example where I have shown the holes in white, so they look like, well, holes.

input = Style["{##}&/@", Bold, FontFamily -> "Calibri", FontSize -> 12];

img = Rasterize[input, ImageSize -> 800] ~ColorConvert~ "Grayscale" ~
  ImageResize~ 400;

{solids, holes} = findHoles@imgToPolys@img;

Graphics[{EdgeForm[Red], Yellow, solids, White, holes}]

enter image description here

Faster method using FilledCurve

The procedure above is rather slow, due to the RegionPlot. An alternative way to create polygons from text is from the FilledCurve primitives that can be obtained by converting via PDF (as in Vitaliy's answer). Below is some code to convert the filled curves into polygons.

The conversion is a two stage process, first the filled curves are converted to bezier curves, then the bezier curves are sampled at some number of points (default 10), and the coordinates fed into a Polygon expression.

As previously, the polygons can then be split into "solids" and "holes" using findHoles.

filledCurveToBeziers[fc_FilledCurve] := MapThread[processFCdata, List @@ fc];

processFCdata[desc_, pts_] := Module[{r, sd},
  r = Range @@@ 
     Partition[Prepend[Accumulate[desc[[All, 2]]], 0], 2, 1], {1, 1} -> 1];
  sd = desc[[All, 3]] /. {0 -> 1};
  MapThread[BezierCurve[pts[[#1]], SplineDegree -> #2] &, {r, sd}]];

beziersToPolygons[x_, n_: 10] :=
  samples = x /. BezierCurve[data__] :> BezierFunction[data] /@ Range[0, 1, 1/n];
  Polygon /@ Select[Split[Flatten[#, 1]][[All, 1]] & /@ samples, Length[#] > 1 &]];

This approach is much faster, since it completely avoids the rasterization and image processing.

{solids, holes} = 
  With[{fc = Cases[ImportString[ExportString[input, "PDF"]], _FilledCurve, -1]},
   Transpose[findHoles@beziersToPolygons[filledCurveToBeziers@#, 10] & /@ fc]];

Graphics[{EdgeForm[Red], Yellow, solids, White, holes}]

enter image description here

Simon Woods

Posted 2012-01-24T21:57:59.703

Reputation: 81 905

Better late than never! Thanks. – Sjoerd C. de Vries – 2013-01-30T18:33:24.833


From the sublime to the ridiculous. By way of contrast, here's another approach to 3D text rendering:

sjoerd = Image[
     Rasterize[" sjoerd ", RasterSize -> 900, ImageSize -> 900]];

      ScalingMatrix[s, {v1, v2, v3}],  Padding -> "Periodic", 
      Resampling -> "Bicubic"], {{s, .2}, 0.1, 8}, 
         {{v1, -1}, -1, 1}, {{v2, -0.1}, -1, 1}, {{v3, .3}, 0.1, 1}] 

3D lettering here

Perhaps it's more fun to play with than to look at... :)


Posted 2012-01-24T21:57:59.703

Reputation: 23 565


Posting as an answer per request. See

Caveat: It's been almost a year since I've toyed with that code. Strange things can happen to code left on the dusty shelves of Usenet. The basic idea was similar to Leonid's: use some proximity measure to find neighbor pairs. And be prepared for some failure cases that may need further tweaking, that is to say, segment "jumps" to nearby lines.

Daniel Lichtblau

Posted 2012-01-24T21:57:59.703

Reputation: 52 368