Beispiel #1
0
def vpype_text(string, font, size, position, align):
    """
    Generate text using a Hershey font.
    """

    # skip if text is empty
    if string.strip() == "":
        return vp.LineCollection()

    lines = axi.text(string, font=FONTS[font])
    lc = vp.LineCollection()
    for line in lines:
        lc.append([x + 1j * y for x, y in line])

    # by default, axi's font appear to be approx 18px size
    scale_factor = size / 18.0
    lc.scale(scale_factor, scale_factor)

    min_x, _, max_x, _ = lc.bounds()
    if align == "left":
        lc.translate(-min_x, 0)
    elif align == "center":
        lc.translate(-(max_x - min_x) / 2, 0)
    elif align == "right":
        lc.translate(-max_x, 0)
    else:
        logging.warning(f"text: unknown align parameters: {align}")

    lc.translate(position[0], position[1])

    return lc
Beispiel #2
0
def stylize_path(line: np.ndarray, weight: int, pen_width: float,
                 detail: float) -> vp.LineCollection:
    """Implement a heavy stroke weight by buffering multiple times the base path.

    Note: recursive buffering is to be avoided to properly control detail!
    """

    if weight == 1:
        return vp.LineCollection([line])

    lc = vp.LineCollection()

    # path to be used as starting point for buffering
    geom = LineString(vp.as_vector(line))
    if weight % 2 == 0:
        radius = pen_width / 2
        _add_to_line_collection(
            geom.buffer(radius,
                        resolution=_calc_buffer_resolution(radius, detail)),
            lc)
    else:
        radius = 0.0
        _add_to_line_collection(geom, lc)

    for i in range((weight - 1) // 2):
        radius += pen_width
        p = geom.buffer(radius,
                        resolution=_calc_buffer_resolution(radius, detail))
        _add_to_line_collection(p, lc)

    return lc
Beispiel #3
0
def render_module_set(
    img: np.ndarray,
    mset_path: str,
    quantization: float,
    random_mirror: bool,
    return_sizes: bool = False,
) -> Union[vp.LineCollection, Tuple[vp.LineCollection, float, float]]:
    """
    Build a LineCollection from a 2-dimension bool Numpy array and a path to a module set
    """
    modules, tile_w, tile_h = load_module_set(mset_path, quantization)
    lc = vp.LineCollection()
    for idx, mod_id in np.ndenumerate(bitmap_to_module(img)):
        if mod_id != -1:
            mod_lc = vp.LineCollection(modules[mod_id])

            if random_mirror:
                mod_lc.translate(-tile_w / 2, -tile_h / 2)
                if MSET_MIRRORS[mod_id][0] and random.random() < 0.5:
                    mod_lc.scale(-1, 1)
                if MSET_MIRRORS[mod_id][1] and random.random() < 0.5:
                    mod_lc.scale(1, -1)
                mod_lc.translate(tile_w / 2, tile_h / 2)

            mod_lc.translate(idx[1] * tile_w, idx[0] * tile_h)

            lc.extend(mod_lc)

    if return_sizes:
        return lc, tile_w, tile_h
    else:
        return lc
Beispiel #4
0
def iread(document: vp.Document, input_file: str, color, distance: float):
    """
    Image Read and Vectorization.

    This is a pure python polygon producer. The goal of this project is to
    vector trace images according to some given criteria. The default mode
    does black v. white. However, multiple colors can be specified along with
    a color distance and those colors will be extracted and traced.
    """
    image = Image.open(input_file)
    width, height = image.size
    if len(color) == 0:
        if image.mode != 'L':
            image = image.convert('L')
        image = image.point(lambda e: int(e > 127) * 255)
        lc = vp.LineCollection()
        document.add(lc)
        for points in _vectrace(image.load(), width, height):
            lc.append(points)
        return document

    distance_sq = distance * distance

    def dist(c, pixel):
        r = c.red - pixel[0]
        g = c.green - pixel[1]
        b = c.blue - pixel[2]
        return r * r + g * g + b * b <= distance_sq

    if image.mode != "RGBA":
        image = image.convert("RGBA")

    for c in color:
        v = Image.new('L', image.size, 255)
        v_data = v.load()
        new_data = image.load()
        for y in range(height):
            for x in range(width):
                pixel = new_data[x, y]
                if pixel[3] == 0:
                    continue
                if dist(c, pixel):
                    new_data[x, y] = (255, 255, 255, 0)
                    v_data[x, y] = 0

        lc = vp.LineCollection()
        document.add(lc)
        for points in _vectrace(v_data, width, height):
            lc.append(points)
    return document
Beispiel #5
0
def generate_fill(poly: Polygon, pen_width: float,
                  stroke_width: float) -> vp.LineCollection:
    """Draw a fill pattern for the the input polygon.

    The fill pattern should take into account the stroke width.

    Args:
        poly: polygon to fill
        pen_width: pen width on paper
        stroke_width: width of the stroke (accounting for stroke pen width and weight)

    Returns:
        fill paths
    """

    # we draw the boundary, accounting for pen width
    if stroke_width > 0:
        p = poly.buffer(-stroke_width / 2, join_style=2, mitre_limit=10.0)
    else:
        p = poly

    if p.is_empty:
        # too small, nothing to fill
        return vp.LineCollection()

    min_x, min_y, max_x, max_y = p.bounds
    height = max_y - min_y
    line_count = math.ceil(height / pen_width) + 1
    base_seg = np.array([min_x, max_x])
    y_start = min_y + (height - (line_count - 1) * pen_width) / 2

    segs = []
    for n in range(line_count):
        seg = base_seg + (y_start + pen_width * n) * 1j
        segs.append(seg if n % 2 == 0 else np.flip(seg))

    # mls = MultiLineString([[(pt.real, pt.imag) for pt in seg] for seg in segs]).intersection(
    mls = MultiLineString([complex_to_2d(seg) for seg in segs]).intersection(
        p.buffer(-pen_width / 2, join_style=2, mitre_limit=10.0))

    lc = vp.LineCollection(mls)
    lc.merge(tolerance=pen_width * 5, flip=True)

    boundary = p.boundary
    if boundary.geom_type == "MultiLineString":
        lc.extend(boundary)
    else:
        lc.append(boundary)
    return lc
Beispiel #6
0
def fracture(size, pitch):
    """"""

    width = size[0]
    height = size[1]
    count = round(height / pitch + 1)

    white_start = np.clip(
        np.random.normal(scale=0.1 * width, size=count) + 0.4 * width,
        0.05 * width,
        0.65 * width,
    )
    white_stop = np.clip(
        np.random.normal(scale=0.1 * width, size=count) + 0.6 * width,
        0.35 * width,
        0.95 * width,
    )

    # avoid any contact between left-hand and right-hand set of lines
    white_start = np.clip(white_start, a_min=None, a_max=white_stop - 0.05 * width)
    white_stop = np.clip(white_stop, a_min=white_start + 0.05 * width, a_max=None)
    white_start[1:] = np.clip(
        white_start[1:], a_min=None, a_max=white_stop[:-1] - 0.05 * width
    )
    white_stop[1:] = np.clip(white_stop[1:], a_min=white_start[:-1] + 0.05 * width, a_max=None)

    # generate line collection, taking care of line order and start
    return vp.LineCollection(
        [np.array([0, white_start[i]]) + 1j * i * pitch for i in range(count)]
        + [np.array([width, white_stop[i]]) + 1j * i * pitch for i in range(count)]
    )
Beispiel #7
0
def ellipse(x: float, y: float, w: float, h: float, quantization: float):
    """Generate lines approximating an ellipse.

    The ellipse is centered on (X, Y), with a half-width of W and a half-height of H.
    """

    return vp.LineCollection([vp.ellipse(x, y, w, h, quantization)])
def vpype_flow_imager(filename, noise_coeff, n_fields, min_sep, max_sep,
                      min_length, max_length, max_size, seed, flow_seed,
                      search_ef, test_frequency, field_type, transparent_val,
                      edge_field_multiplier, dark_field_multiplier):
    """
    Generate flowline representation from an image.

    The generated flowlines are in the coordinates of the input image,
    resized to have dimensions at most `--max_size` pixels.
    """
    gray_img = cv2.imread(filename, cv2.IMREAD_UNCHANGED)
    with tmp_np_seed(seed):
        numpy_paths = draw_image(
            gray_img,
            mult=noise_coeff,
            n_fields=n_fields,
            min_sep=min_sep,
            max_sep=max_sep,
            min_length=min_length,
            max_length=max_length,
            max_img_size=max_size,
            flow_seed=flow_seed,
            search_ef=search_ef,
            test_frequency=test_frequency,
            field_type=field_type,
            transparent_val=transparent_val,
            edge_field_multiplier=edge_field_multiplier,
            dark_field_multiplier=dark_field_multiplier,
        )

    lc = vp.LineCollection()
    for path in numpy_paths:
        lc.append(path[:, 0] + path[:, 1] * 1.j)
    return lc
Beispiel #9
0
def _generate_fill(poly: Polygon, pen_width: float) -> vp.LineCollection:

    # nasty hack because unary_union() did something weird once
    poly = Polygon(poly.exterior)

    # we draw the boundary, accounting for pen width
    p = poly.buffer(-pen_width / 2)

    min_x, min_y, max_x, max_y = p.bounds
    height = max_y - min_y
    line_count = math.ceil(height / pen_width) + 1
    base_seg = np.array([min_x, max_x])
    y_start = min_y + (height - (line_count - 1) * pen_width) / 2

    segs = []
    for n in range(line_count):
        seg = base_seg + (y_start + pen_width * n) * 1j
        segs.append(seg if n % 2 == 0 else np.flip(seg))

    mls = MultiLineString([[(pt.real, pt.imag) for pt in seg] for seg in segs
                           ]).intersection(p.buffer(-pen_width / 2))

    lc = vp.LineCollection(mls)
    lc.merge(tolerance=pen_width * 5, flip=True)
    print(p.geom_type)

    boundary = p.boundary
    if boundary.geom_type == "MultiLineString":
        lc.extend(boundary)
    else:
        lc.append(boundary)
    return lc
Beispiel #10
0
def msfingerprint(mset, quantization, fingerprint):
    """Generate geometries based on a previously generated fingerprint."""

    parts = fingerprint.split("_")
    if len(parts) < 3 or len(parts) > 4:
        logging.warning(f"msfingerprint: invalid fingerprint {fingerprint}")
        return vp.LineCollection()

    size_x = int(parts[0])
    size_y = int(parts[1])
    data = bytearray.fromhex(parts[2])
    if len(parts) == 4:
        seed = int(parts[3], 16)
    else:
        seed = None

    img = (np.unpackbits(np.array(data), count=size_x * size_y) == 1).reshape(
        (size_y, size_x))

    if seed is not None:
        random.seed(seed)

    return render_module_set(img,
                             mset,
                             quantization,
                             random_mirror=seed is not None)
 def render(self):
     n = self.width() / self.unit_length
     t = np.linspace(0, n * 2 * math.pi, int(n * 50))
     return vp.LineCollection([
         self.elem_to_global_path(t / t[-1] * self.width() + 1j *
                                  (self.dr / 2 + np.sin(t) * self.dr / 2))
     ])
Beispiel #12
0
def linesort(lines: vp.LineCollection, no_flip: bool = True):
    """
    Sort lines to minimize the pen-up travel distance.

    Note: this process can be lengthy depending on the total number of line. Consider using
    `linemerge` before `linesort` to reduce the total number of line and thus significantly
    optimizing the overall plotting time.
    """
    if len(lines) < 2:
        return lines

    index = vp.LineIndex(lines[1:], reverse=not no_flip)
    new_lines = vp.LineCollection([lines[0]])

    while len(index) > 0:
        idx, reverse = index.find_nearest(new_lines[-1][-1])
        line = index.pop(idx)
        if reverse:
            line = np.flip(line)
        new_lines.append(line)

    logging.info(
        f"optimize: reduced pen-up (distance, mean, median) from {lines.pen_up_length()} to "
        f"{new_lines.pen_up_length()}"
    )

    return new_lines
Beispiel #13
0
def rect(x: float, y: float, width: float, height: float) -> vp.LineCollection:
    """
    Generate a rectangle.

    The rectangle is defined by its top left corner (X, Y) and its width and height.
    """
    return vp.LineCollection([vp.rect(x, y, width, height)])
Beispiel #14
0
def rect(
    x: float,
    y: float,
    width: float,
    height: float,
    radii: Tuple[float, float, float, float],
    quantization: float,
) -> vp.LineCollection:
    """Generate a rectangle, with optional rounded angles.

    The rectangle is defined by its top left corner (X, Y) and its width and height.

    Examples:

        Straight-angle rectangle:

            vpype rect 10cm 10cm 3cm 2cm show

        Rounded-angle rectangle:

            vpype rect --radii 5mm 5mm 5mm 5mm 10cm 10cm 3cm 2cm show

        Rounded-angle rectangle with quantization control:

            vpype rect --quantization 0.1mm --radii 5mm 5mm 5mm 5mm 10cm 10cm 3cm 2cm show
    """
    return vp.LineCollection(
        [vp.rect(x, y, width, height, *radii, quantization)])
Beispiel #15
0
def circle(x: float, y: float, r: float, quantization: float):
    """Generate lines approximating a circle.

    The circle is centered on (X, Y) and has a radius of R.
    """

    return vp.LineCollection([vp.circle(x, y, r, quantization)])
Beispiel #16
0
def line(x0: float, y0: float, x1: float, y1: float) -> vp.LineCollection:
    """
    Generate a single line.

    The line starts at (X0, Y0) and ends at (X1, Y1). All arguments understand supported units.
    """
    return vp.LineCollection([vp.line(x0, y0, x1, y1)])
 def render(self):
     w = self.width()
     lc = vp.LineCollection([
         np.linspace(h * 1j, w + h * 1j, math.ceil(w / self.quantization))
         for h in np.linspace(0, self.dr, self.line_count)
     ])
     return self.elem_to_global_lc(lc)
    def render(self):
        w = self.width()
        n = math.ceil(w / self.unit_length)

        lc = vp.LineCollection()
        lc.extend([
            1j * self.dr / 2 + np.linspace(
                (i + 0.1) * w / n,
                (i + 0.9) * w / n, int(w / self.quantization))
            for i in range(n)
        ])
        lc.extend([
            np.array([0.1, 0.4]) * 1j * self.dr + i * w / n
            for i in range(1, n)
        ])
        lc.extend([
            np.array([0.6, 0.9]) * 1j * self.dr + i * w / n
            for i in range(1, n)
        ])
        lc.extend([
            vp.circle(i * w / n, self.dr / 2, DOT_RADIUS, DOT_RADIUS / 15)
            for i in range(1, n)
        ])

        return self.elem_to_global_lc(lc)
Beispiel #19
0
def snap(line_collection: vp.LineCollection,
         pitch: float) -> vp.LineCollection:
    """Snap all points to a grid with with a spacing of PITCH.

    This command moves every point of every paths to the nearest grid point. If sequential
    points of a segment end up on the same grid point, they are deduplicated. If the resulting
    path contains less than 2 points, it is discarded.

    Example:

        Snap all points to a grid of 3x3mm:

            vpype [...] snap 3mm [...]
    """

    line_collection.scale(1 / pitch)

    new_lines = vp.LineCollection()
    for line in line_collection:
        new_line = np.round(line)
        idx = np.ones(len(new_line), dtype=bool)
        idx[1:] = new_line[1:] != new_line[:-1]
        if idx.sum() > 1:
            new_lines.append(np.round(new_line[idx]))

    new_lines.scale(pitch)
    return new_lines
Beispiel #20
0
def efill(document: vp.Document, tolerance: float, distance: float):
    """
    Implements the Eulerian fill algorithm which fills any closed shapes with as few paths as there are contiguous
    regions. With scanlines to fill any shapes, even those with holes, with an even-odd fill order and direct pathing.

    """
    for layer in list(document.layers.values()
                      ):  # Add all the closed paths to the efill.
        efill = EulerianFill(distance)
        for p in layer:
            if np.abs(p[0] - p[-1]) <= tolerance:
                efill += vp.as_vector(p)
        fill = efill.get_fill()  # Get the resulting fill.

        lc = vp.LineCollection()
        cur_line = []
        for pt in fill:
            if pt is None:
                if cur_line:
                    lc.append(cur_line)
                cur_line = []
            else:
                cur_line.append(complex(pt[0], pt[1]))
        if cur_line:
            lc.append(cur_line)
        document.add(lc)
    return document
    def render(self):
        w = self.width()
        lc = vp.LineCollection([
            np.linspace(0, w, math.ceil(w / self.quantization)) +
            1j * self.dr / 2
        ])

        return self.elem_to_global_lc(lc)
Beispiel #22
0
def execute_single_line(pipeline: str, line: vp.LineLike) -> vp.LineCollection:
    """Execute a pipeline on a single line. The pipeline is expected to remain single layer.

    Returns:
        the layer 1's LineCollection
    """
    doc = execute(pipeline, vp.Document(vp.LineCollection([line])))
    assert len(doc.layers) == 1
    return doc.layers[1]
    def render(self):
        w = self.width()
        n = math.ceil(w / self.unit_length)

        x = np.hstack([[0, 0], np.arange(0.5, n - 0.5, 0.5), n - 1]) * w / n
        y = self.dr * np.ones(x.shape)
        y[1::2] = 0

        return vp.LineCollection([self.elem_to_global_path(x + 1j * y)])
Beispiel #24
0
def big_doc():
    random.seed(0)
    doc = vp.Document()
    doc.add(
        vp.LineCollection([(
            random.uniform(0, 100) + random.uniform(0, 100) * 1j,
            random.uniform(0, 100) + random.uniform(0, 100) * 1j,
        ) for _ in range(1000)]))
    return doc
Beispiel #25
0
def arc(x: float, y: float, rw: float, rh: float, start: float, stop: float,
        quantization: float):
    """Generate lines approximating a circular arc.

    The arc is centered on (X, Y) and has a radius of R and spans counter-clockwise from START
    to STOP angles (in degrees). Angular values of zero refer to east of unit circle and
    positive values extend counter-clockwise.
    """
    return vp.LineCollection([vp.arc(x, y, rw, rh, start, stop, quantization)])
 def __post_init__(self):
     super().__post_init__()
     lines = axi.text(self.text, self.font)
     self.item_lc = vp.LineCollection()
     for line in lines:
         self.item_lc.append([x + 1j * y for x, y in line])
     x1, y1, x2, y2 = self.item_lc.bounds()
     self.item_lc.translate(-(x1 + x2) / 2, -(y1 + y2) / 2)
     s = self.unit_length / (y2 - y1)
     self.item_lc.scale(s, -s)
Beispiel #27
0
def vpype_flow_imager(document, layer, filename, noise_coeff, n_fields,
                      min_sep, max_sep, min_length, max_length, max_size, seed,
                      flow_seed, search_ef, test_frequency, field_type,
                      transparent_val, transparent_mask, edge_field_multiplier,
                      dark_field_multiplier, kdtree_searcher, cmyk):
    """
    Generate flowline representation from an image.

    The generated flowlines are in the coordinates of the input image,
    resized to have dimensions at most `--max_size` pixels.
    """
    if kdtree_searcher:
        searcher_class = KDTSearcher
    else:
        searcher_class = HNSWSearcher
    target_layer = vp.single_to_layer_id(layer, document)
    img = cv2.imread(filename, cv2.IMREAD_UNCHANGED)
    logger.debug(f"original img.shape: {img.shape}")
    with tmp_np_seed(seed):
        if cmyk:
            img_layers = split_cmyk(img.copy())
        else:
            img_layers = [img]

        alpha = get_alpha_channel(img)

        for layer_i, img_layer in enumerate(img_layers):
            logger.info(f"computing layer {layer_i+1}")
            numpy_paths = draw_image(
                img_layer,
                alpha,
                mult=noise_coeff,
                n_fields=n_fields,
                min_sep=min_sep,
                max_sep=max_sep,
                min_length=min_length,
                max_length=max_length,
                max_img_size=max_size,
                flow_seed=flow_seed,
                search_ef=search_ef,
                test_frequency=test_frequency,
                field_type=field_type,
                transparent_val=transparent_val,
                transparent_mask=transparent_mask,
                edge_field_multiplier=edge_field_multiplier,
                dark_field_multiplier=dark_field_multiplier,
                searcher_class=searcher_class,
            )

            lc = vp.LineCollection()
            for path in numpy_paths:
                lc.append(path[:, 0] + path[:, 1] * 1.j)

            document.add(lc, target_layer + layer_i)
    return document
 def render(self):
     w = self.width()
     n = max(math.ceil(self.dr / self.quantization), 2)
     m = max(math.ceil(w / self.quantization), 2)
     # noinspection PyTypeChecker
     p = np.hstack([
         np.linspace(0, self.dr * 1j, n),
         np.linspace(self.dr * 1j, self.dr * 1j + w, m),
         np.linspace(self.dr * 1j + w, w, n),
         np.linspace(w, 0, m, dtype=complex),
     ])
     return vp.LineCollection([self.elem_to_global_path(p)])
Beispiel #29
0
def generate_fill(rect: RectType, pen_width: float) -> vp.LineCollection:
    line_count = math.ceil(rect[3] / pen_width)

    base_seg = np.array([pen_width / 2, rect[2] - pen_width / 2]) + rect[0]
    y_start = rect[1] + (rect[3] - (line_count - 1) * pen_width) / 2

    segs = []
    for n in range(line_count):
        seg = base_seg + (y_start + pen_width * n) * 1j
        segs.append(seg if n % 2 == 0 else np.flip(seg))

    return vp.LineCollection([np.hstack(segs)])
 def render(self) -> vp.LineCollection:
     w = self.width()
     n = math.ceil(w / self.unit_length)
     lc = vp.LineCollection()
     for i in range(n):
         x, y = self.elem_to_global_coord((i / n) * w, self.dr / 2)
         item_lc = self.render_item(i, n)
         angle = self.start_angle + (i / n) * (self.stop_angle -
                                               self.start_angle) + 90.0
         item_lc.rotate(-angle * math.pi / 180.0)
         item_lc.translate(x, y)
         lc.extend(item_lc)
     return lc