【问题标题】:How to draw a patterned curve with Python如何使用 Python 绘制有图案的曲线
【发布时间】:2022-11-30 14:14:14
【问题描述】:

假设我有一组坐标,绘制时看起来像这样:

我可以通过简单地从相邻的一对点画线,将这些点变成一条平滑的线:

那个很容易。

但是,我需要画一条带有图案的线,因为它代表一条铁轨,所以它应该是这样的:

(这是使用 Paint.Net 模拟的,因此间距不均匀。当然,我希望黑色点对之间的间距是均匀的。)

这就是我难过的地方。我如何绘制这样的图案线?

我目前只知道如何使用pillow,但如果需要我会学习如何使用其他包。

【问题讨论】:

  • 好像你会被设置为使用pillow。你用过ImageDraw模块吗? Something like this 可能对虚线部分有帮助。
  • 嗯,不是真的,但由于这些答案,我想起了线方程的“参数”形式,我将对此进行一些探索。
  • 看起来这个问题的目标是找出一个算法或方案来告诉你点应该去哪里,而不是将算法实际翻译成代码。这似乎使它成为一个数学问题,而不是一个编程问题。
  • @KarlKnechtel 最终需要将其转换为数据的可视化表示,因为这是所需的最终结果。然而,由于线性方程的“参数形式”,我设法为此目的编造了一个算法。看我的回答:-)

标签: python curve


【解决方案1】:

我知道了!

好的,首先是一些数学理论。在几何中有几种描绘直线的方法。

第一种是“斜截距”形式:y = mx + c
然后是“点-斜”形式:y = y1 + m * (x - x1)
最后是“广义形式”:

然而,还有另一种形式,“参数形式”

    R = P + tD

其中D是“位移矢量”,P是“起点”,R是“合成矢量”。

有了这个概念,我可以想象有人带着标记沿着线段走,每当他们走过一定距离时,用另一个标记替换标记,然后继续。

基于这个原则,这里是(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()

结果如下:

有点粗糙,但可用于我的目的。

这个解决方案的美妙之处在于,通过改变draw()(以及limits的内容)中发生的事情,我的解决方案也可以很容易地处理虚线;只需让 limits"dash""blank" 之间来回切换,而在 draw() 中,实际上只在 phase == "dash" 时画一条线。

注意:我 100% 确定算法可以进一步优化/整理。截至目前,我很高兴它能正常工作。我可能会偷偷跑到 CodeReview SE 上寻求优化建议。

【讨论】:

    猜你喜欢
    • 2015-06-23
    • 2015-09-10
    • 2016-09-03
    • 2010-09-19
    • 2018-12-08
    • 1970-01-01
    • 2019-10-17
    • 2018-05-17
    • 2014-09-20
    相关资源
    最近更新 更多