简体   繁体   English

如何用Python绘制有图案的曲线

[英]How to draw a patterned curve with Python

Let's say I have a set of coordinates that when plotted looks like this:假设我有一组坐标,绘制时看起来像这样:

积分

I can turn the dots into a smooth-ish line by simply drawing lines from adjacent pair of points:我可以通过简单地从相邻的一对点画线,将这些点变成一条平滑的线:

点之间的线

That one's easy.那个很容易。

However, I need to draw a line with a pattern because it represents a railroad track, so it should look like this:但是,我需要画一条带有图案的线,因为它代表一条铁轨,所以它应该是这样的:

在此处输入图像描述

(This is simulated using Paint.Net, hence the non-uniform spacing. I would like the spacing between pairs of black pips to be uniform, of course.) (这是使用 Paint.Net 模拟的,因此间距不均匀。当然,我希望黑色点对之间的间距是均匀的。)

That is where I'm stumped.这就是我难过的地方。 How do I paint such a patterned line?我如何绘制这样的图案线?

I currently only know how to use pillow , but if need be I will learn how to use other packages.我目前只知道如何使用pillow ,但如果需要我会学习如何使用其他包。

Edited to Add: Do note that pillow is UNABLE to draw a patterned line natively.编辑添加:请注意pillow本身无法绘制图案线。

I got it!我知道了!

Okay first a bit of maths theory.好的,首先是一些数学理论。 There are several ways of depicting a line in geometry.在几何中有几种描绘直线的方法。

The first is the "slope-intercept" form: y = mx + c第一种是“斜截距”形式: y = mx + c
Then there's the "point-slope" form: y = y1 + m * (x - x1)然后是“点斜率”形式: y = y1 + m * (x - x1)
And finally there's the "generalized form":最后是“广义形式”:

广义直线方程

None of these forms are practical for several reasons:由于以下几个原因,这些 forms 都不实用:

  • For the first 2 forms, there's the edge case of vertical lines which means y increases even though x stays the same.对于前 2 个 forms,存在垂直线的边缘情况,这意味着即使x保持不变, y也会增加。
  • Near-verticals mean I have to advance x reaaally slowly or else y increases too fast接近垂直意味着我必须非常缓慢地推进x否则y增加得太快
  • Even with the "generalized form", to make the segment lengths uniform, I have to handle the iterations differently for horizontally-oriented lines (iterate on x ) with vertically-oriented lines (iterate on y )即使使用“广义形式”,为了使线段长度统一,我也必须以不同的方式处理水平方向的线(在x上迭代)和垂直方向的线(在y上迭代)的迭代

However, just this morning I got reminded that there's yet another form, the "parametric form":然而,就在今天早上,我被提醒还有另一种形式,即“参数形式”:

    R = P + tD

Where D is the "displacement vector", P is the "starting point", and R is the "resultant vector".其中D为“位移矢量”, P为“起点”, R为“合成矢量”。 t is a parameter that can be defined any which way you want, depending on D 's dimension. t是一个参数,可以根据D的维度以任何方式定义。

By adjusting D and/or t 's steps, I can get as precise as I want, and I don't have to concern myself with special cases!通过调整D和/或t的步骤,我可以得到我想要的精确度,而且我不必担心特殊情况!

With this concept, I can imagine someone walking down the line segment with a marker, and whenever they have traversed a certain distance, replace the marker with another one, and continue.有了这个概念,我可以想象有人带着标记沿着线段走,每当他们走过一定距离时,用另一个标记替换标记,然后继续。

Based on this principle, here's the (quick-n-dirty) program:基于这个原则,这里是(quick-n-dirty)程序:

import math
from itertools import pairwise, cycle
from math import sqrt, isclose
from typing import NamedTuple
from PIL import Image, ImageDraw


class Point(NamedTuple):
    x: float
    y: float

    def rounded(self) -> tuple[int, int]:
        return round(self.x), round(self.y)


# Example data points
points: list[Point] = [
    Point(108.0, 272.0),
    Point(150.0, 227.0),
    Point(171.0, 218.0),
    Point(187.0, 221.0),
    Point(192.0, 234.0),
    Point(205, 315),
    Point(216, 402),
    Point(275, 565),
    Point(289, 586),
    Point(312, 603),
    Point(343, 609),
    Point(387, 601),
    Point(420, 577),
    Point(484, 513),
    Point(505, 500),
    Point(526, 500),
    Point(551, 509),
    Point(575, 550),
    Point(575, 594),
    Point(546, 656),
    Point(496, 686),
    Point(409, 712),
    Point(329, 715),
    Point(287, 701),
]


class ParametricLine:
    def __init__(self, p1: Point, p2: Point):
        self.p1 = p1
        self.x1, self.y1 = p1
        self.p2 = p2
        self.x2, self.y2 = p2
        self._len = -1.0

    @property
    def length(self):
        if self._len < 0.0:
            dx, dy = self.displacement
            self._len = sqrt(dx ** 2 + dy ** 2)
        return self._len

    @property
    def displacement(self):
        return (self.x2 - self.x1), (self.y2 - self.y1)

    def replace_start(self, p: Point):
        self.p1 = p
        self.x1, self.y1 = p
        self._len = -1.0

    def get_point(self, t: float) -> Point:
        dx, dy = self.displacement
        xr = self.x1 + (t / self.length) * dx
        xy = self.y1 + (t / self.length) * dy
        return Point(xr, xy)


image = Image.new("RGBA", (1000, 1000))
idraw = ImageDraw.Draw(image)


def draw(segments: list[tuple[Point, Point]], phase: str):
    drawpoints = []
    prev_p2 = segments[0][0]
    p2 = None
    for p1, p2 in segments:
        assert isclose(p1.x, prev_p2.x)
        assert isclose(p1.y, prev_p2.y)
        drawpoints.append(p1.rounded())
        prev_p2 = p2
    drawpoints.append(p2.rounded())
    if phase == "dash" or phase == "gapp":
        idraw.line(drawpoints, fill=(255, 255, 0), width=10, joint="curve")
    elif phase == "pip1" or phase == "pip2":
        idraw.line(drawpoints, fill=(0, 0, 0), width=10, joint="curve")


def main():
    limits: dict[str, float] = {
        "dash": 40.0,
        "pip1": 8.0,
        "gapp": 8.0,
        "pip2": 8.0,
    }

    pointpairs = pairwise(points)
    climit = cycle(limits.items())

    phase, tleft = next(climit)
    segments: list[tuple[Point, Point]] = []

    pline: ParametricLine | None = None
    p1 = p2 = Point(math.nan, math.nan)
    while True:
        if pline is None:
            try:
                p1, p2 = next(pointpairs)
            except StopIteration:
                break
            pline = ParametricLine(p1, p2)
        if pline.length > tleft:
            # The line segment is longer than our leftover budget.
            # Find where we should truncate the line and draw the
            # segments until the truncation point.
            p3 = pline.get_point(tleft)
            segments.append((p1, p3))
            draw(segments, phase)
            segments.clear()
            pline.replace_start(p3)
            p1 = p3
            phase, tleft = next(climit)
        else:
            # The segment is shorter than our leftover budget.
            # Record that and reduce the budget.
            segments.append((p1, p2))
            tleft -= pline.length
            pline = None
            if abs(tleft) < 0.01:
                # The leftover is too small, let's just assume that
                # this is insignificant and go to the next phase.
                draw(segments, phase)
                segments.clear()
                phase, tleft = next(climit)
    if segments:
        draw(segments, phase)

    image.save("results.png")


if __name__ == '__main__':
    main()

And here's the result:结果如下:

在此处输入图像描述

A bit rough, but usable for my purposes.有点粗糙,但可用于我的目的。

And the beauty of this solution is that by varying what happens in draw() (and the contents of limits ), my solution can also handle dashed lines quite easily;这个解决方案的美妙之处在于,通过改变draw()中发生的事情(以及limits的内容),我的解决方案也可以很容易地处理虚线; just make the limits toggle back and forth between, say, "dash" and "blank" , and in draw() only actually draw a line when phase == "dash" .只需让limits在例如"dash""blank"之间来回切换,并且在draw()中实际上只在phase == "dash"时画一条线。

Note: I am 100% certain that the algorithm can be optimized / tidied up further.注意:我 100% 确定算法可以进一步优化/整理。 As of now I'm happy that it works at all.截至目前,我很高兴它能正常工作。 I'll probably skedaddle over to CodeReview SE for suggestions on optimization.我可能会偷偷跑到 CodeReview SE 上寻求优化建议。

Edit: The final version of the code is live and open for review on CodeReview SE .编辑:代码的最终版本是实时的,并在 CodeReview SE 上开放供审查 If you arrived here via a search engine because you're looking for a way to draw a patterned line, please use the version on CodeReview SE instead.如果您是通过搜索引擎到达这里的,因为您正在寻找一种绘制图案线的方法,请改用 CodeReview SE 上的版本。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM