SVG to path sequence, that can be used by Luxor and Javis

Using this template: https://github.com/JuliaAnimators/Javis.jl/blob/main/examples/fourier.jl

We have:

    Drawing() # julialogo needs a drawing
    julialogo(; action = :path, centered = true)
    shapes = pathtopoly()

I want to change it to something like this

    Drawing() # julialogo needs a drawing
    Javis.pathsvg("../../svg-image.svg")
    shapes = pathtopoly()

But, pathsvg doesn’t work as expected. Is there another alternative to transform svg into paths acceptable for the pathtopoly() function?

1 Like

I don’t know about Javis, but as far as Luxor.jl is concerned, SVG files are effectively “opaque” to the user, and they’ll just pass through from source file to target drawing without modification - about all you can do with an SVG source is to place it. I don’t think Rsvg.jl (the underlying mechanism) provides access to any paths at all, even if librsvg does… :frowning:

Any ideas how can I get this idea to form an approximated closed path, which julia will understand?

To do something like textpath("string"), but for the image imagepath("path-to-image").

I want to sample the (x,y) coordinates of n evenly-spaced points along the svg path, with Julia.

I think this is the best way to put it…

Hey @BuddhiLW ! Co-creator of Javis.jl here! Let me CC @Wikunia

I know that when I did something similar like this here: https://youtu.be/ckvsc6ukdOc?t=15 What I did were the following steps:

  1. Load an image with Images.jl
  2. Apply a black and white color scheme to the image
  3. Apply a shelling function to create shell of core shapes
  4. Pass or threshold values in your resulting image to remove noise that would not be part of the outline you want
  5. Convert each point within your image that you want into the appropriate (x, y) coordinate tuples.
  6. Convert each of these points from the outline of your image into a Luxor Point object and store the points in an array.

If you want the code I used for this outlining process, here is the code:

using Javis
using FFTW
using FFTViews
using FileIO
using Images
using TravelingSalesmanHeuristics

function ground(args...)
    background("black")
    sethue("white")
end

function circ(; r = 10, vec = O, action = :stroke, color = "white")
    sethue("black")
    circle(O, r, action)
    # my_arrow(O, vec)
    return vec
end

function my_arrow(start_pos, end_pos)
    arrow(
        start_pos,
        end_pos;
        linewidth = distance(start_pos, end_pos) / 100,
        arrowheadlength = 7,
    )
    return end_pos
end

function draw_line(
    p1 = O,
    p2 = O;
    color = "white",
    action = :stroke,
    edge = "solid",
    linewidth = 3,
)
    sethue(color)
    setdash(edge)
    Luxor.setline(linewidth)
    line(p1, p2, action)
end

function draw_path!(path, pos, color)
    sethue(color)

    push!(path, pos)
    return draw_line.(path[2:end], path[1:(end - 1)]; color = color)
end

function get_points(img)
    findall(x -> x == 1, img) .|> x -> Point(x.I)
end

function texty()
	fontsize(36)
	text("Loading Asset: Jacob Zelko", Point(0, 290); halign = :center)
end

c2p(c::Complex) = Point(real(c), imag(c))

remap_idx(i::Int) = (-1)^i * floor(Int, i / 2)
remap_inv(n::Int) = 2n * sign(n) - 1 * (n > 0)

function animate_fourier(options)
    npoints = options.npoints
    nplay_frames = options.nplay_frames
    nruns = options.nruns
    nframes = nplay_frames + options.nend_frames

    # obtain points from julialogo
    points = get_points(load(File(format"PNG", "jacob_outline.png")))
    npoints = length(points)
    println("#points: $npoints")
    # solve tsp to reduce length of extra edges
    distmat = [distance(points[i], points[j]) for i = 1:npoints, j = 1:npoints]

    path, cost = solve_tsp(distmat; quality_factor = options.tsp_quality_factor)
    println("TSP cost: $cost")
    points = points[path] # tsp saves the last point again

    # optain the fft result and scale
    y = [p.x  - 372.6666666667 for p in points] 
    x = [p.y - 323.3333333333 / 1.25 for p in points] 

    fs = FFTView(fft(complex.(x, y)))
    # normalize the points as fs isn't normalized
    fs ./= npoints
    npoints = length(fs)

    video = Video(options.width, options.height)
    Background(1:nframes, ground)

    circles = Object[]

    for i = 1:npoints
        ridx = remap_idx(i)

        push!(circles, Object((args...) -> circ(; r = abs(fs[ridx]), vec = c2p(fs[ridx]))))

	if i > 1
	    # translate to the tip of the vector of the previous circle
	    act!(circles[i], Action(1:1, anim_translate(circles[i - 1])))
	end
	ridx = remap_idx(i)
	act!(circles[i], Action(1:nplay_frames, anim_rotate(0.0, ridx * 2π * nruns)))
    end

    trace_points = Point[]
    Object(1:nframes, (args...) -> draw_path!(trace_points, pos(circles[end]), "white"))

    loading = Object(1:nframes, (args...) -> texty())
    act!(loading, Action(1:400, sineio(), appear(:draw_text)))

    return render(video; pathname = joinpath(@__DIR__, options.filename))
    # return render(video; liveview = true)
end

function main()
    # hd_options = (
    # npoints = 3001, # rough number of points for the shape => number of circles
    # nplay_frames = 1200, # number of frames for the animation of fourier
    # nruns = 2, # how often it's drawn
    # nend_frames = 200,  # number of frames in the end
    # width = 1920,
    # height = 1080,
    # shape_scale = 2.5, # scale factor for the logo
    # tsp_quality_factor = 50,
    # filename = "julia_hd.mp4",
    # )

    gif_options = (
        npoints = 1001, # rough number of points for the shape => number of circles
        nplay_frames = 600, # number of frames for the animation of fourier
        nruns = 1, # how often it's drawn
        nend_frames = 200,  # number of frames in the end
        width = 520,
        height = 653,
        tsp_quality_factor = 10,
        filename = "jacob_outline.gif",
    )

    # gif_options = (
    # npoints = 651, # rough number of points for the shape => number of circles
    # nplay_frames = 600, # number of frames for the animation of fourier
    # nruns = 2, # how often it's drawn
    # nend_frames = 0,  # number of frames in the end
    # width = 350,
    # height = 219,
    # shape_scale = 0.8, # scale factor for the logo
    # tsp_quality_factor = 80,
    # filename = "julia_logo_dft.gif",
    # )
    return animate_fourier(gif_options)
end

main()

And here is a reference image of my head and shoulders:

Which results in something like this:

jacob_outline

Important to note is that shelling functions and the above steps will only go so far. You may need to instead open up your selected image that you want to outline in a separate image editor like Gimp to manually do something image manipulation. I think I may have had to do that… Although I cannot recall!

@Wikunia , anything else you could add here?

Does that help @BuddhiLW ?

P.S. Thanks for using Javis! As always, please let us know if you have any feedback or how we can help! :smile: We love to see posts about Javis and what you create!

2 Likes

Hi, I am getting this error while running code in Jupyter Notebook . Julia is new to me. Actually, I want to create exactly like this animation for my organization’s logo (https://youtu.be/rrmx2Q3sO1Y): Kindly, help me to solve the issue.

Code:

using Javis
using FFTW
using FFTViews
using FileIO
using Images
using TravelingSalesmanHeuristics

function ground(args…)
background(“black”)
sethue(“white”)
end

function circ(; r = 10, vec = O, action = :stroke, color = “white”)
sethue(“black”)
circle(O, r, action)
# my_arrow(O, vec)
return vec
end

function my_arrow(start_pos, end_pos)
arrow(
start_pos,
end_pos;
linewidth = distance(start_pos, end_pos) / 100,
arrowheadlength = 7,
)
return end_pos
end

function draw_line(
p1 = O,
p2 = O;
color = “white”,
action = :stroke,
edge = “solid”,
linewidth = 3,
)
sethue(color)
setdash(edge)
Luxor.setline(linewidth)
line(p1, p2, action)
end

function draw_path!(path, pos, color)
sethue(color)

push!(path, pos)
return draw_line.(path[2:end], path[1:(end - 1)]; color = color)

end

function get_points(img)
findall(x → x == 1, img) .|> x → Point(x.I)
end

function texty()
fontsize(36)
text(“Loading Asset: Jacob Zelko”, Point(0, 290); halign = :center)
end

c2p(c::Complex) = Point(real(c), imag(c))

remap_idx(i::Int) = (-1)^i * floor(Int, i / 2)
remap_inv(n::Int) = 2n * sign(n) - 1 * (n > 0)

function animate_fourier(options)
npoints = options.npoints
nplay_frames = options.nplay_frames
nruns = options.nruns
nframes = nplay_frames + options.nend_frames

# obtain points from julialogo
points = get_points(load(File(format"PNG", "BazmeBMEQR.png")))
npoints = length(points)
println("#points: $npoints")
# solve tsp to reduce length of extra edges
distmat = [distance(points[i], points[j]) for i = 1:npoints, j = 1:npoints]

path, cost = solve_tsp(distmat; quality_factor = options.tsp_quality_factor)
println("TSP cost: $cost")
points = points[path] # tsp saves the last point again

# optain the fft result and scale
y = [p.x  - 372.6666666667 for p in points] 
x = [p.y - 323.3333333333 / 1.25 for p in points] 

fs = FFTView(fft(complex.(x, y)))
# normalize the points as fs isn't normalized
fs ./= npoints
npoints = length(fs)

video = Video(options.width, options.height)
Background(1:nframes, ground)

circles = Object[]

for i = 1:npoints
    ridx = remap_idx(i)

    push!(circles, Object((args...) -> circ(; r = abs(fs[ridx]), vec = c2p(fs[ridx]))))

if i > 1
    # translate to the tip of the vector of the previous circle
    act!(circles[i], Action(1:1, anim_translate(circles[i - 1])))
end
ridx = remap_idx(i)
act!(circles[i], Action(1:nplay_frames, anim_rotate(0.0, ridx * 2π * nruns)))
end

trace_points = Point[]
Object(1:nframes, (args...) -> draw_path!(trace_points, pos(circles[end]), "white"))

loading = Object(1:nframes, (args...) -> texty())
act!(loading, Action(1:400, sineio(), appear(:draw_text)))

return render(video; pathname = joinpath(@__DIR__, options.filename))
# return render(video; liveview = true)

end

function main()
# hd_options = (
# npoints = 3001, # rough number of points for the shape => number of circles
# nplay_frames = 1200, # number of frames for the animation of fourier
# nruns = 2, # how often it’s drawn
# nend_frames = 200, # number of frames in the end
# width = 1920,
# height = 1080,
# shape_scale = 2.5, # scale factor for the logo
# tsp_quality_factor = 50,
# filename = “julia_hd.mp4”,
# )

gif_options = (
    npoints = 1001, # rough number of points for the shape => number of circles
    nplay_frames = 600, # number of frames for the animation of fourier
    nruns = 1, # how often it's drawn
    nend_frames = 200,  # number of frames in the end
    width = 520,
    height = 653,
    tsp_quality_factor = 10,
    filename = "BazmeBME.gif",
)

# gif_options = (
# npoints = 651, # rough number of points for the shape => number of circles
# nplay_frames = 600, # number of frames for the animation of fourier
# nruns = 2, # how often it's drawn
# nend_frames = 0,  # number of frames in the end
# width = 350,
# height = 219,
# shape_scale = 0.8, # scale factor for the logo
# tsp_quality_factor = 80,
# filename = "julia_logo_dft.gif",
# )
return animate_fourier(gif_options)

end

main()

Error:
#points: 0
MethodError: no method matching solve_tsp(::Matrix{Any}; quality_factor::Int64)

Closest candidates are:
solve_tsp(::AbstractMatrix{T}; quality_factor) where T<:Real
@ TravelingSalesmanHeuristics C:\Users\admin.julia\packages\TravelingSalesmanHeuristics\ryoSo\src\TravelingSalesmanHeuristics.jl:31

Stacktrace:
[1] animate_fourier(options::@NamedTuple{npoints::Int64, nplay_frames::Int64, nruns::Int64, nend_frames::Int64, width::Int64, height::Int64, tsp_quality_factor::Int64, filename::String})
@ Main .\In[11]:78
[2] main()
@ Main .\In[11]:154
[3] top-level scope
@ In[11]:157

You can format the code in your post by enclosing it in three backticks:

    ```
    code
    ```

Try your code after converting your image to an outline in white on black - I think getpoints is looking for white pixels?

For animation, you need a line draw. The logo contains filled areas, you have first to decide what to do with them. Further, it contains highly detailed stuff that would be difficult to draw. Still further, the logo contains gaps, which obviously cannot be animated by drawing a continuous line. So, the very first thing would be to decide how you would like to change/simplify the logo.

For the picture conversion personally I would prefer an interactive tool, I’d take ImageJ / FIJI. Here is what came out (convert to grayscale |> threshold |> outline), but I do not think that is what you want to have.

BME2-outline

Have you actually watched the highly instructive video linked in the other topic?