Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
I
inkscape-centerline-trace
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Commits
Open sidebar
3dmodeling
inkscape-centerline-trace
Commits
840b30c5
Commit
840b30c5
authored
May 11, 2016
by
Juergen Weigert
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Working inkscape extension
parent
449f602d
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
211 additions
and
164 deletions
+211
-164
centerline-trace.inx
centerline-trace.inx
+1
-1
centerline-trace.py
centerline-trace.py
+210
-18
inkscape-centerline-trace.py
inkscape-centerline-trace.py
+0
-145
dolly.png
testdata/dolly.png
+0
-0
No files found.
centerline-trace.inx
View file @
840b30c5
...
...
@@ -11,7 +11,7 @@
https://github.com/fablabnbg/inkscape-centerline-trace
Version 0.
1
</param>
Version 0.
2
</param>
<effect
needs-live-preview=
"false"
>
<object-type>
path
</object-type>
...
...
centerline-trace.py
View file @
840b30c5
...
...
@@ -11,15 +11,55 @@
# - http://code.google.com/p/inkscape2tikz/
# - http://code.google.com/p/eggbotcode/
#
# 2016-05-11 jw, V0.1 -- initial draught
# vectorize strokes in a graymap png file
# as a path along the centerline of the strokes.
#
# This is done with autotrace -centerline, as
# the builtin potrace in inkscape cannot do centerline --
# it would always draw a path around the contour of the
# stroke, resulting in double lines.
#
# We want a stroke represented by a single path (optionally with line-width) ,
# rather than its outline contour.
#
# Algorithm:
#
# The input image is converted to a graymap and histogram normalized with PIL.ImageOps.equalize.
#
# autotrace needs a bi-level bitmap. In order to find the
# best threshold value, we run autotrace at multiple thresholds
# and evaluate the result.
#
# We count the number of line segments produced and
# measure the total path length drawn.
#
# The svg that has the longest path but the least number of
# segments is returned.
#
# Requires:
# apt-get install autotrace python-pil
#
# 2016-05-10 jw, V0.1 -- initial draught
# 2016-05-11 jw, V0.2 -- first usable inkscape-extension
#
__version__
=
'0.
1
'
# Keep in sync with chain_paths.inx ca line 22
__version__
=
'0.
2
'
# Keep in sync with chain_paths.inx ca line 22
__author__
=
'Juergen Weigert <juewei@fabmail.org>'
import
sys
,
os
,
tempfile
,
base64
import
sys
,
os
,
re
,
math
,
tempfile
,
subprocess
,
base64
import
inkex
,
simplestyle
debug
=
True
#debug = False
try
:
from
PIL
import
Image
from
PIL
import
ImageOps
from
PIL
import
ImageStat
except
:
print
>>
sys
.
stderr
,
"Error: Cannot import PIL. Try
\n
apt-get install python-pil"
sys
.
exit
(
1
)
#debug = True
debug
=
False
# search path, so that inkscape libraries are found when we are standalone.
sys_platform
=
sys
.
platform
.
lower
()
...
...
@@ -54,6 +94,7 @@ class TraceCenterline(inkex.Effect):
inkex
.
Effect
.
__init__
(
self
)
self
.
dumpname
=
os
.
path
.
join
(
tempfile
.
gettempdir
(),
"trace-centerline.dump"
)
self
.
autotrace_opts
=
[]
# extra options for autotrace tuning.
try
:
self
.
tty
=
open
(
"/dev/tty"
,
'w'
)
...
...
@@ -73,6 +114,126 @@ class TraceCenterline(inkex.Effect):
def
author
(
self
):
return
__author__
def
svg_centerline_trace
(
self
,
image_file
):
num_attempts
=
15
# min 1, max 255, beware it gets much slower with more attempts.
autotrace_cmd
=
[
'autotrace'
,
'--centerline'
,
'--input-format=pbm'
,
'--output-format=svg'
]
autotrace_cmd
+=
self
.
autotrace_opts
stroke_style_add
=
'stroke-width:
%.2
f; fill:none; stroke-linecap:round;'
if
debug
:
print
>>
self
.
tty
,
image_file
im
=
Image
.
open
(
image_file
)
im
=
im
.
convert
(
mode
=
'L'
,
dither
=
None
)
if
debug
:
print
>>
sys
.
stderr
,
"seen: "
+
str
([
im
.
format
,
im
.
size
,
im
.
mode
])
im
=
ImageOps
.
equalize
(
im
)
# equalize histogram
#im.show()
def
svg_pathstats
(
path_d
):
""" calculate statistics from an svg path:
length (measuring bezier splines as straight lines through the handles).
points (all, including duplicates)
segments (number of not-connected!) path segments.
"""
path_d
=
path_d
.
lower
()
p_points
=
0
p_length
=
0
p_segments
=
0
for
p
in
path_d
.
split
(
'm'
):
pp
=
re
.
sub
(
'[cl,]'
,
' '
,
p
)
pp
,
closed
=
re
.
subn
(
'z
\
s*$'
,
''
,
pp
)
xy
=
pp
.
split
()
if
len
(
xy
)
<
2
:
# print len(pp)
# print "short path error"
continue
x0
=
float
(
xy
[
0
])
y0
=
float
(
xy
[
1
])
p_points
+=
1
x
=
xy
[
2
::
2
]
y
=
xy
[
3
::
2
]
if
len
(
x
):
p_segments
+=
1
if
closed
:
x
.
extend
(
x0
)
y
.
extend
(
y0
)
for
i
in
range
(
len
(
x
)):
p_points
+=
1
dx
=
float
(
x
[
i
])
-
x0
dy
=
float
(
y
[
i
])
-
y0
p_length
+=
math
.
sqrt
(
dx
*
dx
+
dy
*
dy
)
x0
,
y0
=
float
(
x
[
i
]),
float
(
y
[
i
])
return
{
'points'
:
p_points
,
'segments'
:
p_segments
,
'length'
:
p_length
}
# slice with a list of histogram maps
# 1 -> 128
# 3 -> 64,128,192
# ...
candidate
=
{}
for
i
in
range
(
num_attempts
):
threshold
=
int
(
256.
*
(
1
+
i
)
/
(
num_attempts
+
1
))
lut
=
[
255
for
n
in
range
(
threshold
)
]
+
[
0
for
n
in
range
(
threshold
,
256
)
]
bw
=
im
.
point
(
lut
,
mode
=
'1'
)
cand
=
{
'threshold'
:
threshold
,
'img_width'
:
bw
.
size
[
0
],
'img_height'
:
bw
.
size
[
1
],
'mean'
:
ImageStat
.
Stat
(
im
)
.
mean
[
0
]
}
fp
=
tempfile
.
NamedTemporaryFile
(
suffix
=
'.pbm'
,
delete
=
False
)
fp
.
write
(
"P4
\n
%
d
%
d
\n
"
%
(
bw
.
size
[
0
],
bw
.
size
[
1
]))
fp
.
write
(
bw
.
tobytes
())
fp
.
close
()
try
:
p
=
subprocess
.
Popen
(
autotrace_cmd
+
[
fp
.
name
],
stdout
=
subprocess
.
PIPE
)
except
Exception
as
e
:
print
'+ '
+
' '
.
join
(
autotrace_cmd
)
print
e
print
"Try:
\n
sudo apt-get install autotrace"
sys
.
exit
(
1
)
cand
[
'svg'
]
=
p
.
communicate
()[
0
]
os
.
unlink
(
fp
.
name
)
# <?xml version="1.0" standalone="yes"?>\n<svg width="86" height="83">\n<path style="stroke:#000000; fill:none;" d="M36 15C37.9219 18.1496 41.7926 19.6686 43.2585 23.1042C47.9556 34.1128 39.524 32.0995 35.179 37.6034C32.6296 40.8328 34 48.1105 34 52M36 17C32.075 22.4565 31.8375 30.074 35 36M74 42L46 38C45.9991 46.1415 46.7299 56.0825 45.6319 64C44.1349 74.7955 23.7094 77.5566 16.044 72.3966C7.27363 66.4928 8.04426 45.0047 16.2276 38.7384C20.6362 35.3626 27.7809 36.0006 33 36M44 37L45 37"/>\n</svg>
xml
=
inkex
.
etree
.
fromstring
(
cand
[
'svg'
])
p_len
,
p_seg
,
p_pts
=
0
,
0
,
0
for
p
in
xml
.
findall
(
'path'
):
pstat
=
svg_pathstats
(
p
.
attrib
[
'd'
])
p_len
+=
pstat
[
'length'
]
p_seg
+=
pstat
[
'segments'
]
p_pts
+=
pstat
[
'points'
]
cand
[
'length'
]
=
p_len
cand
[
'segments'
]
=
p_seg
cand
[
'points'
]
=
p_pts
if
cand
[
'mean'
]
>
127
:
cand
[
'mean'
]
=
255
-
cand
[
'mean'
]
# should not happen
blackpixels
=
cand
[
'img_width'
]
*
cand
[
'img_height'
]
*
cand
[
'mean'
]
/
255.
cand
[
'strokewidth'
]
=
blackpixels
/
cand
[
'length'
]
candidate
[
i
]
=
cand
def
calc_weight
(
cand
,
idx
):
offset
=
(
num_attempts
/
2.
-
idx
)
*
(
num_attempts
/
2.
-
idx
)
*
(
cand
[
'img_width'
]
+
cand
[
'img_height'
])
w
=
cand
[
'length'
]
*
5
-
offset
*
.005
-
cand
[
'points'
]
*
.2
-
cand
[
'segments'
]
*
20
# print "calc_weight(%d) = rl=%f o=%f p=%f s=%f -> w=%f" % (idx, cand['length']*5, offset*.005, cand['points']*.2, cand['segments']*20, w)
return
w
best_weight_idx
=
0
for
n
in
candidate
.
keys
():
# print "candidate ", n
c
=
candidate
[
n
]
# print "\t mean=%d len=%d seg=%d width=%d" % (c['mean'], c['length'], c['segments'], c['strokewidth'])
if
calc_weight
(
c
,
n
)
>
calc_weight
(
candidate
[
best_weight_idx
],
best_weight_idx
):
best_weight_idx
=
n
if
debug
:
print
>>
sys
.
stderr
,
"best:
%
d/
%
d"
%
(
best_weight_idx
,
num_attempts
)
## if standalone:
# svg = re.sub('stroke:', (stroke_style_add % candidate[best_weight_idx]['strokewidth']) + ' stroke:', candidate[best_weight_idx]['svg'])
# return svg
## inkscape-extension:
return
(
candidate
[
best_weight_idx
][
'svg'
],
candidate
[
best_weight_idx
][
'strokewidth'
],
im
.
size
)
def
calc_unit_factor
(
self
,
units
=
'mm'
):
""" return the scale factor for all dimension conversions.
- The document units are always irrelevant as
...
...
@@ -102,30 +263,61 @@ class TraceCenterline(inkex.Effect):
# <image .. xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAT8AA ..." preserveAspectRatio="none" height="432" width="425" transform="matrix(1,0,-0.52013328,0.85408511,0,0)"/>
# <image .. xlink:href="file:///home/jw/schaf.png"
#
#
# dump the entire svg to file, so that we can examine what an image is.
href
=
str
(
node
.
get
(
inkex
.
addNS
(
'href'
,
'xlink'
)))
f
=
open
(
self
.
dumpname
,
'w'
)
f
.
write
(
href
)
f
.
close
()
print
>>
self
.
tty
,
"Dump written to "
+
self
.
dumpname
# ######################
#
# dump the entire svg to file, so that we can examine what an image is.
# f=open(self.dumpname, 'w')
# f.write(href)
# f.close()
# if debug: print >>self.tty, "Dump written to "+self.dumpname
#
# ######################
if
href
[:
7
]
==
'file://'
:
filename
=
href
[
7
:]
if
debug
:
print
>>
self
.
tty
,
"linked image: ="
+
filename
elif
href
[
0
]
==
'/'
or
href
[
0
]
==
'.'
:
filename
=
href
if
debug
:
print
>>
self
.
tty
,
"linked image path: ="
+
filename
elif
href
[:
15
]
==
'data:image/png;'
:
png
=
base64
.
decodestring
(
href
[
15
+
7
:])
if
debug
:
print
>>
self
.
tty
,
"embedded image: "
+
href
[:
15
+
7
]
png
=
base64
.
decodestring
(
href
[
15
+
7
:])
f
=
tempfile
.
NamedTemporaryFile
(
mode
=
"wb"
,
suffix
=
".png"
,
delete
=
False
)
f
.
write
(
png
)
filename
=
f
.
name
f
.
close
()
else
:
inkex
.
errormsg
(
_
(
"Neither file:// nor data:image/png; prefix. Cannot parse PNG image href "
+
href
))
print
>>
self
.
tty
,
"filename="
+
filename
sys
.
exit
(
1
)
if
debug
:
print
>>
self
.
tty
,
"filename="
+
filename
#
path_svg
,
stroke_width
,
im_size
=
self
.
svg_centerline_trace
(
filename
)
xml
=
inkex
.
etree
.
fromstring
(
path_svg
)
path_d
=
xml
.
find
(
'path'
)
.
attrib
[
'd'
]
x_off
=
float
(
node
.
get
(
'x'
))
y_off
=
float
(
node
.
get
(
'y'
))
sx
=
float
(
node
.
get
(
'width'
))
/
im_size
[
0
]
sy
=
float
(
node
.
get
(
'height'
))
/
im_size
[
1
]
if
debug
:
print
>>
self
.
tty
,
"im_width "
,
node
.
get
(
'width'
),
"sx="
,
sx
if
debug
:
print
>>
self
.
tty
,
"im_height "
,
node
.
get
(
'height'
),
"sy="
,
sy
if
debug
:
print
>>
self
.
tty
,
"im_x "
,
x_off
if
debug
:
print
>>
self
.
tty
,
"im_y "
,
y_off
if
debug
:
print
>>
self
.
tty
,
"pixel_size= "
,
im_size
## map the coordinates of the returned pixel path to the coordinates of the original SVG image.
matrix
=
"translate(
%
g,
%
g) scale(
%
g,
%
g)"
%
(
x_off
,
y_off
,
sx
,
sy
)
#
# do something
if
href
[:
5
]
==
'data:'
:
os
.
unlink
(
filename
)
## it was a temporary file (representing an embedded image).
#
#if href[:5] == 'data:':
# os.unlink(filename)
# Create SVG Path
style
=
{
'stroke'
:
'#000000'
,
'fill'
:
'none'
,
'stroke-linecap'
:
'round'
,
'stroke-width'
:
stroke_width
}
path_attr
=
{
'style'
:
simplestyle
.
formatStyle
(
style
),
'd'
:
path_d
,
'transform'
:
matrix
}
## insert the new path object
inkex
.
etree
.
SubElement
(
self
.
current_layer
,
inkex
.
addNS
(
'path'
,
'svg'
),
path_attr
)
if
__name__
==
'__main__'
:
...
...
inkscape-centerline-trace.py
deleted
100755 → 0
View file @
449f602d
#! /usr/bin/python
#
# vectorize strokes in a graymap png file
# as a path along the centerline of the strokes.
#
# This is done with autotrace -centerline, as
# the builtin potrace in inkscape cannot do centerline --
# it would always draw a path around the contour of the
# stroke, resulting in double lines.
#
# We want a stroke represented by a single path (optionally with line-width) ,
# rather than its outline contour.
#
# Algorithm:
# autotrace needs a bi-level bitmap. In order to find the
# best threshold value, we run autotrace at multiple thresholds
# and evaluate the result.
#
# We count the number of line segments produced and
# measure the total path length drawn.
#
# The svg that has the longest path but the least number of
# segments is returned.
#
# Requires:
# apt-get install autotrace
import
sys
,
os
,
re
,
math
,
tempfile
,
subprocess
import
xml.etree.ElementTree
as
ET
from
PIL
import
Image
from
PIL
import
ImageOps
from
PIL
import
ImageStat
num_attempts
=
15
# min 1, max 255, beware it gets much slower with more attempts.
autotrace_cmd
=
[
'autotrace'
,
'--centerline'
,
'--input-format=pbm'
,
'--output-format=svg'
]
autotrace_cmd
+=
sys
.
argv
[
2
:]
stroke_style_add
=
'stroke-width:
%.2
f; fill:none; stroke-linecap:round;'
im
=
Image
.
open
(
sys
.
argv
[
1
])
.
convert
(
mode
=
'L'
,
dither
=
None
)
# print [im.format, im.size, im.mode]
im
=
ImageOps
.
equalize
(
im
)
# equalize histogram
#im.show()
def
svg_pathstats
(
path_d
):
""" calculate statistics from an svg path:
length (measuring bezier splines as straight lines through the handles).
points (all, including duplicates)
segments (number of not-connected!) path segments.
"""
path_d
=
path_d
.
lower
()
p_points
=
0
p_length
=
0
p_segments
=
0
for
p
in
path_d
.
split
(
'm'
):
# print "xxxx",p
pp
=
re
.
sub
(
'[cl,]'
,
' '
,
p
)
pp
,
closed
=
re
.
subn
(
'z
\
s*$'
,
''
,
pp
)
xy
=
pp
.
split
()
if
len
(
xy
)
<
2
:
# print len(pp)
# print "short path error"
continue
x0
=
float
(
xy
[
0
])
y0
=
float
(
xy
[
1
])
p_points
+=
1
x
=
xy
[
2
::
2
]
y
=
xy
[
3
::
2
]
if
len
(
x
):
p_segments
+=
1
if
closed
:
x
.
extend
(
x0
)
y
.
extend
(
y0
)
for
i
in
range
(
len
(
x
)):
p_points
+=
1
dx
=
float
(
x
[
i
])
-
x0
dy
=
float
(
y
[
i
])
-
y0
p_length
+=
math
.
sqrt
(
dx
*
dx
+
dy
*
dy
)
x0
,
y0
=
float
(
x
[
i
]),
float
(
y
[
i
])
return
{
'points'
:
p_points
,
'segments'
:
p_segments
,
'length'
:
p_length
}
# slice with a list of histogram maps
# 1 -> 128
# 3 -> 64,128,192
# ...
candidate
=
{}
for
i
in
range
(
num_attempts
):
threshold
=
int
(
256.
*
(
1
+
i
)
/
(
num_attempts
+
1
))
lut
=
[
255
for
n
in
range
(
threshold
)
]
+
[
0
for
n
in
range
(
threshold
,
256
)
]
bw
=
im
.
point
(
lut
,
mode
=
'1'
)
cand
=
{
'threshold'
:
threshold
,
'img_width'
:
bw
.
size
[
0
],
'img_height'
:
bw
.
size
[
1
],
'mean'
:
ImageStat
.
Stat
(
im
)
.
mean
[
0
]
}
fp
=
tempfile
.
NamedTemporaryFile
(
suffix
=
'.pbm'
,
delete
=
False
)
fp
.
write
(
"P4
\n
%
d
%
d
\n
"
%
(
bw
.
size
[
0
],
bw
.
size
[
1
]))
fp
.
write
(
bw
.
tobytes
())
fp
.
close
()
try
:
p
=
subprocess
.
Popen
(
autotrace_cmd
+
[
fp
.
name
],
stdout
=
subprocess
.
PIPE
)
except
Exception
as
e
:
print
'+ '
+
' '
.
join
(
autotrace_cmd
)
print
e
print
"Try:
\n
sudo apt-get install autotrace"
sys
.
exit
(
1
)
cand
[
'svg'
]
=
p
.
communicate
()[
0
]
os
.
unlink
(
fp
.
name
)
# <?xml version="1.0" standalone="yes"?>\n<svg width="86" height="83">\n<path style="stroke:#000000; fill:none;" d="M36 15C37.9219 18.1496 41.7926 19.6686 43.2585 23.1042C47.9556 34.1128 39.524 32.0995 35.179 37.6034C32.6296 40.8328 34 48.1105 34 52M36 17C32.075 22.4565 31.8375 30.074 35 36M74 42L46 38C45.9991 46.1415 46.7299 56.0825 45.6319 64C44.1349 74.7955 23.7094 77.5566 16.044 72.3966C7.27363 66.4928 8.04426 45.0047 16.2276 38.7384C20.6362 35.3626 27.7809 36.0006 33 36M44 37L45 37"/>\n</svg>
xml
=
ET
.
fromstring
(
cand
[
'svg'
])
p_len
,
p_seg
,
p_pts
=
0
,
0
,
0
for
p
in
xml
.
findall
(
'path'
):
pstat
=
svg_pathstats
(
p
.
attrib
[
'd'
])
p_len
+=
pstat
[
'length'
]
p_seg
+=
pstat
[
'segments'
]
p_pts
+=
pstat
[
'points'
]
cand
[
'length'
]
=
p_len
cand
[
'segments'
]
=
p_seg
cand
[
'points'
]
=
p_pts
if
cand
[
'mean'
]
>
127
:
cand
[
'mean'
]
=
255
-
cand
[
'mean'
]
# should not happen
blackpixels
=
cand
[
'img_width'
]
*
cand
[
'img_height'
]
*
cand
[
'mean'
]
/
255.
cand
[
'strokewidth'
]
=
blackpixels
/
cand
[
'length'
]
candidate
[
i
]
=
cand
def
calc_weight
(
cand
,
idx
):
offset
=
(
num_attempts
/
2.
-
idx
)
*
(
num_attempts
/
2.
-
idx
)
*
(
cand
[
'img_width'
]
+
cand
[
'img_height'
])
w
=
cand
[
'length'
]
*
5
-
offset
*
.005
-
cand
[
'points'
]
*
.2
-
cand
[
'segments'
]
*
20
# print "calc_weight(%d) = rl=%f o=%f p=%f s=%f -> w=%f" % (idx, cand['length']*5, offset*.005, cand['points']*.2, cand['segments']*20, w)
return
w
best_weight_idx
=
0
for
n
in
candidate
.
keys
():
# print "candidate ", n
c
=
candidate
[
n
]
# print "\t mean=%d len=%d seg=%d width=%d" % (c['mean'], c['length'], c['segments'], c['strokewidth'])
if
calc_weight
(
c
,
n
)
>
calc_weight
(
candidate
[
best_weight_idx
],
best_weight_idx
):
best_weight_idx
=
n
print
>>
sys
.
stderr
,
"best:
%
d/
%
d"
%
(
best_weight_idx
,
num_attempts
)
svg
=
re
.
sub
(
'stroke:'
,
(
stroke_style_add
%
candidate
[
best_weight_idx
][
'strokewidth'
])
+
' stroke:'
,
candidate
[
best_weight_idx
][
'svg'
])
print
svg
testdata/dolly.png
0 → 100644
View file @
840b30c5
3.74 KB
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment