Take the 2-minute tour ×
Mathematica Stack Exchange is a question and answer site for users of Mathematica. It's 100% free, no registration required.

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];

Test:

Graphics[Point[pdata]]

Mathematica graphics

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

Graphics[Polygon[pdata]]

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?

Additionally,

  • 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

share|improve this question
    
2  
You can also use EdgeDetect for detecting the edges –  rm -rf Jan 24 '12 at 22:49
    
@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 Jan 24 '12 at 23:09
1  
FindCurvePath (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 Jan 24 '12 at 23:24
    
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. Jan 25 '12 at 0:10
add comment

7 Answers 7

up vote 24 down vote accepted

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[
   First[ImportString[
     ExportString[
      Style["\[Euro] 9 M-8 ", Italic, FontSize -> 24, 
       FontFamily -> "Times"], "PDF"], "PDF", 
     "TextMode" -> "Outlines"]]];

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

enter image description here

share|improve this answer
    
I was aware of this possibility (see my earlier question here: mathematica.stackexchange.com/q/563/57). The main reason for my question is I want to go 3D and FilledCurve doesn't do that. –  Sjoerd C. de Vries Jan 24 '12 at 22:30
    
(FilledCurve being the carrier of the above paths) –  Sjoerd C. de Vries Jan 24 '12 at 22:37
    
@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 Jan 24 '12 at 23:11
    
I completely forgot about ListCurvePathPlot. +1! –  Mr.Wizard Jan 25 '12 at 11:43
    
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 May 28 '12 at 1:14
add comment

Possible answer using ClusteringComponents:

image=Rasterize[
 Style["Sjoerd!",Italic,FontSize->24,FontFamily->"Times"],
 "Image",ImageSize->200]

cluster=ClusteringComponents[ImageReflect[image,Left->Right]];

ListPlot3D[cluster,Mesh->False,BoxRatios->{3,1,1},Boxed->False,
 Axes->False,Lighting->"Neutral",
 ViewPoint->{-0.4340374864952124,0.8958897046884841,3.2340366567727856`}]

Mathematica graphics

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

RegionPlot3D[
 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

share|improve this answer
    
That's neat. Is there a similar example in Documentation? –  Vitaliy Kaurov Jan 25 '12 at 0:47
    
@VitaliyKaurov - Not to my knowledge ... –  Arnoud Buzing Jan 25 '12 at 2:21
add comment

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]]] /@ 
  N[pdata]];

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

share|improve this answer
    
This a pretty neat idea - love the span of approach. Could function Norm be used in your 1st snippet of code? –  Vitaliy Kaurov Jan 24 '12 at 23:54
1  
@Vitaly Thanks. Norm - yes, I could have used it. but it would slow things down, since here I used Listability. –  Leonid Shifrin Jan 25 '12 at 7:27
    
I think there may be an extraneous ) lurking in the definiton of distances. –  image_doctor Jan 30 '13 at 8:32
    
@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 Jan 30 '13 at 10:24
    
@LeonidShifrin It seems the unit test was a little delayed this time around :) –  image_doctor Jan 30 '13 at 16:10
add comment

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

EDIT

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

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

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

ListCurvePathPlot[points]

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!

share|improve this answer
add comment

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]];

Manipulate[
   ImagePerspectiveTransformation[sjoerd, 
      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... :)

share|improve this answer
add comment

Posting as an answer per request. See

http://forums.wolfram.com/mathgroup/archive/2011/Mar/msg00529.html

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.

share|improve this answer
add comment

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 @@@ 
    ReplacePart[
     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] :=
 Module[{samples},
  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

share|improve this answer
    
Better late than never! Thanks. –  Sjoerd C. de Vries Jan 30 '13 at 18:33
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.