Friday, 5 February 2021

How to convert SVG polylines to quickdraw stroke-3 numpy format?

I would like to convert a basic SVG file containing polylines into the stroke-3 format used by sketch-rnn (and the quickdraw dataset).

To my understanding, each polyline point in stroke-3 format would be:

  • stored as [delta_x, delta_y, pen_up], where
  • delta_x, delta_y represent the coordinates relative to the previous point and
  • pen_up is a bit that is 1 when the pen is up (e.g. move_to operation a-la turtle graphics) or 0 when the pen is down (e.g. line_to operation a-la turtle graphics).

I've attempted to write the function and convert an SVG, but I when I render a test of the stroke-3 format I get an extra line.

My input SVG looks like this:

box svg

<?xml version="1.0" encoding="UTF-8"?><svg viewBox="0 0 900 900" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#00c000" stroke-linecap="round"><path d="m324.56 326.77h62.721 62.721 62.721 62.721"/><path d="m575.44 326.77 0.1772 62.891 0.1771 62.891 0.1772 62.891 0.1771 62.891"/><path d="m576.15 578.33h-63.075-63.075l-63.075-1e-4h-63.075"/><path d="m323.85 578.33 0.1772-62.891 0.1772-62.891 0.1772-62.891 0.1771-62.891"/><path d="m575.44 326.77 29.765-32.469 29.765-32.469" stroke="#c00000"/><path d="m634.97 261.83h-92.486-92.486l-92.486-1e-4h-92.486" stroke="#c00000"/><path d="m265.03 261.83 44.647 48.704 14.882 16.235" stroke="#c00000"/><path d="m323.85 578.33-15.092 13.725-30.183 27.45-15.092 13.725" stroke="#c0c000"/><path d="m263.48 633.23h93.258 93.258l93.258 1e-4h93.258" stroke="#c0c000"/><path d="m636.52 633.23-60.366-54.9" stroke="#c0c000"/><path d="m634.97 261.83 0.3863 92.851 0.3862 92.851 0.3863 92.851 0.3863 92.851" stroke="#0000c0"/><path d="m636.52 633.23h-93.258l-93.258-1e-4h-93.258-93.258" stroke="#0000c0"/><path d="m263.48 633.23 0.3863-92.851 0.3863-92.851 0.3863-92.851 0.3862-92.851" stroke="#0000c0"/></g></svg>

Here is a visualisation where the lines parsed from the SVG file are rendered in thick green and the lines drawn from the converted stroke-3 format are rendered in thinner red: box line and strokes

Notice the diagonal line on the right hand side face which isn't present in the original SVG.

I must be doing something wrong marking a line operation instead of a move operation somewhere, but I've been staring at the code for so long I can't spot the error.

This is a minimal example showing my attempt using svg.path:

import xml.etree.ElementTree as ET
import numpy as np
from svg.path import parse_path
from svg.path.path import Line, Move

cubeSVG = '<?xml version="1.0" encoding="UTF-8"?><svg viewBox="0 0 900 900" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#00c000" stroke-linecap="round"><path d="m324.56 326.77h62.721 62.721 62.721 62.721"/><path d="m575.44 326.77 0.1772 62.891 0.1771 62.891 0.1772 62.891 0.1771 62.891"/><path d="m576.15 578.33h-63.075-63.075l-63.075-1e-4h-63.075"/><path d="m323.85 578.33 0.1772-62.891 0.1772-62.891 0.1772-62.891 0.1771-62.891"/><path d="m575.44 326.77 29.765-32.469 29.765-32.469" stroke="#c00000"/><path d="m634.97 261.83h-92.486-92.486l-92.486-1e-4h-92.486" stroke="#c00000"/><path d="m265.03 261.83 44.647 48.704 14.882 16.235" stroke="#c00000"/><path d="m323.85 578.33-15.092 13.725-30.183 27.45-15.092 13.725" stroke="#c0c000"/><path d="m263.48 633.23h93.258 93.258l93.258 1e-4h93.258" stroke="#c0c000"/><path d="m636.52 633.23-60.366-54.9" stroke="#c0c000"/><path d="m634.97 261.83 0.3863 92.851 0.3862 92.851 0.3863 92.851 0.3863 92.851" stroke="#0000c0"/><path d="m636.52 633.23h-93.258l-93.258-1e-4h-93.258-93.258" stroke="#0000c0"/><path d="m263.48 633.23 0.3863-92.851 0.3863-92.851 0.3863-92.851 0.3862-92.851" stroke="#0000c0"/></g></svg>'

def svg_to_stroke3(svg_string):
    # parse the doc
    doc = ET.fromstring(svg_string)
    # get paths
    paths = doc.findall('.//{http://www.w3.org/2000/svg}path')

    strokes = []
    # previous x, y
    px, py = 0, 0

    for path_index, path in enumerate(paths):
        stroke = parse_path(path.attrib['d'])
        was_moving = False
        for operation_index, operation in enumerate(stroke):
            if isinstance(operation, Move):
                mx  = int(operation.start.real)
                my  = int(operation.start.imag)
                # prep this end point for check as next line first point
                was_moving = True

                strokes.append([mx-px, my-py, 1])
                # update previous (absolute) coordinates
                px = mx
                py = my

            if isinstance(operation, Line):
                sx  = int(operation.start.real)
                sy  = int(operation.start.imag)
                ex  = int(operation.end.real)
                ey  = int(operation.end.imag)
                if was_moving:
                    # append delta x, y relative to previous move operation
                    strokes.append([sx-px, sy-py, 0])
                    was_moving = False
                # append delta x,y (line end relative to line start)
                strokes.append([ex-sx, ey-sy, 0])
                # update previous (absolute) coordinates
                px = ex
                py = ey

                # update previous end point
    
    strokes_np = np.array(strokes, dtype=np.int16)
    return strokes_np

print(svg_to_stroke3(cubeSVG))

Additionally I've made the above available as an easy to run Google Colab Notebook.



from How to convert SVG polylines to quickdraw stroke-3 numpy format?

No comments:

Post a Comment