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}]
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}]
EdgeDetect
for detecting the edges $\endgroup$FindCurvePath
(orListCurvePathPlot
) orFindShortestTour
with one of its manyMethod
option values ("Greedy"
?) could be of use. I don't have time to figure out a full solution now. $\endgroup$