matplotlib 绘制两条直线之间夹角的最佳方法

k4aesqcs  于 2023-10-24  发布在  其他
关注(0)|答案(5)|浏览(103)

我是一个使用matplotlib的新手,找不到任何显示两条线之间的夹角的例子。
这是我目前的图像:

这是我想要实现的一个例子:

我通常会看一下Matplotlib gallery来了解如何执行某些任务,但似乎没有任何类似的东西。

kuhbmx9i

kuhbmx9i1#

您可以使用matplotlib.patches.Arc绘制相应Angular 测量的圆弧。

绘制Angular 圆弧:

定义一个函数,它可以接受2个matplotlib.lines.Line2D对象,计算Angular 并返回一个matplotlib.patches.Arc对象,您可以将其添加到您的图中,沿着线。

def get_angle_plot(line1, line2, offset = 1, color = None, origin = [0,0], len_x_axis = 1, len_y_axis = 1):

    l1xy = line1.get_xydata()

    # Angle between line1 and x-axis
    slope1 = (l1xy[1][1] - l1xy[0][2]) / float(l1xy[1][0] - l1xy[0][0])
    angle1 = abs(math.degrees(math.atan(slope1))) # Taking only the positive angle

    l2xy = line2.get_xydata()

    # Angle between line2 and x-axis
    slope2 = (l2xy[1][3] - l2xy[0][4]) / float(l2xy[1][0] - l2xy[0][0])
    angle2 = abs(math.degrees(math.atan(slope2)))

    theta1 = min(angle1, angle2)
    theta2 = max(angle1, angle2)

    angle = theta2 - theta1

    if color is None:
        color = line1.get_color() # Uses the color of line 1 if color parameter is not passed.

    return Arc(origin, len_x_axis*offset, len_y_axis*offset, 0, theta1, theta2, color=color, label = str(angle)+u"\u00b0")

打印Angular 值:

如果你想让Angular 值内联显示,请参考this SO Question了解如何在matplotlib中打印内联标签。注意,你必须打印弧的标签。
我做了一个小函数,它提取弧的顶点,并试图计算Angular 文本的坐标。
这可能不是最佳的,并且可能不适用于所有Angular 值。

def get_angle_text(angle_plot):
    angle = angle_plot.get_label()[:-1] # Excluding the degree symbol
    angle = "%0.2f"%float(angle)+u"\u00b0" # Display angle upto 2 decimal places

    # Get the vertices of the angle arc
    vertices = angle_plot.get_verts()

    # Get the midpoint of the arc extremes
    x_width = (vertices[0][0] + vertices[-1][0]) / 2.0
    y_width = (vertices[0][5] + vertices[-1][6]) / 2.0

    #print x_width, y_width

    separation_radius = max(x_width/2.0, y_width/2.0)

    return [ x_width + separation_radius, y_width + separation_radius, angle]

或者你可以手动预先计算标签点,然后使用text来显示Angular 值。你可以使用get_label()方法从Arc对象的label中获取Angular 值(因为我们已经将标签设置为Angular 值+Unicode度数符号)。

以上函数用法示例:

fig = plt.figure()

line_1 = Line2D([0,1], [0,4], linewidth=1, linestyle = "-", color="green")
line_2 = Line2D([0,4.5], [0,3], linewidth=1, linestyle = "-", color="red")

ax = fig.add_subplot(1,1,1)

ax.add_line(line_1)
ax.add_line(line_2)

angle_plot = get_angle_plot(line_1, line_2, 1)
angle_text = get_angle_text(angle_plot) 
# Gets the arguments to be passed to ax.text as a list to display the angle value besides the arc

ax.add_patch(angle_plot) # To display the angle arc
ax.text(*angle_text) # To display the angle value

ax.set_xlim(0,7)
ax.set_ylim(0,5)

如果你不关心Angular 文本的内联位置,你可以使用plt.legend()来打印Angular 值。

最后:

plt.legend()
plt.show()

函数get_angle_plot中的offset参数用于指定弧的伪半径值。
当Angular 弧可能彼此重叠时,这将非常有用。
(在这个图中,就像我说的,我的get_angle_text函数在放置文本值方面不是很理想,但应该给予你一个如何计算点的想法)
添加第三行:

line_3 = Line2D([0,7], [0,1], linewidth=1, linestyle = "-", color="brown")
ax.add_line(line_3)
angle_plot = get_angle_plot(line_1, line_3, 2, color="red") # Second angle arc will be red in color
angle_text = get_angle_text(angle_plot)

ax.add_patch(angle_plot) # To display the 2nd angle arc
ax.text(*angle_text) # To display the 2nd angle value

xbp102n0

xbp102n02#

我一直在寻找一个多合一的解决方案,发现了AngleAnnotation类。我强烈推荐它。
用圆弧标记线条或内部形状之间的Angular 通常很有用。虽然Matplotlib提供了Arc,但直接将其用于此类目的时的固有问题是,数据空间中的圆弧不一定是显示空间中的圆形。此外,弧的半径通常最好在独立于实际数据坐标的坐标系中定义-至少如果您希望能够自由地放大绘图,而注解不会无限大。
你可以在这里找到它https://matplotlib.org/stable/gallery/text_labels_and_annotations/angle_annotation.html我保存它作为AngleAnnotation.py(当然你可以不同的名称)在我的工作目录和导入它在我的代码与

from AngleAnnotation import AngleAnnotation

下面是我如何使用它的一个片段:

...
#intersection of the two lines
center = (0.0,0.0)
#any point (other than center) on one line
p1 = (6,2)
# any point (other than center) on the other line
p2 = (6,0)
# you may need to switch around p1 and p2 if the arc is drawn enclosing the lines instead
# of between
# ax0 is the axes in which your lines exist
# size sets how large the arc will be
# text sets the label for your angle while textposition lets you rougly set where the label is, here "inside"
# you can pass kwargs to the textlabel using text_kw=dict(...)
# especially useful is the xytext argument which lets you customize the relative position of your label more precisely

am1 = AngleAnnotation(center, p1, p2, ax=ax0, size=130, text="some_label", textposition = "inside", text_kw=dict(fontsize=20, xytext = (10,-5)))

你可以在上面的链接中找到更多的细节。它现在在matplotlib 3.4.2上工作。

qojgxg4l

qojgxg4l3#

从@user3197452这里的想法是我使用的。这个版本结合了text,也照顾到比例轴比。

def add_corner_arc(ax, line, radius=.7, color=None, text=None, text_radius=.5, text_rotatation=0, **kwargs):
    ''' display an arc for p0p1p2 angle
    Inputs:
        ax     - axis to add arc to
        line   - MATPLOTLIB line consisting of 3 points of the corner
        radius - radius to add arc
        color  - color of the arc
        text   - text to show on corner
        text_radius     - radius to add text
        text_rotatation - extra rotation for text
        kwargs - other arguments to pass to Arc
    '''

    lxy = line.get_xydata()

    if len(lxy) < 3:
        raise ValueError('at least 3 points in line must be available')

    p0 = lxy[0]
    p1 = lxy[1]
    p2 = lxy[2]

    width = np.ptp([p0[0], p1[0], p2[0]])
    height = np.ptp([p0[1], p1[1], p2[1]])

    n = np.array([width, height]) * 1.0
    p0_ = (p0 - p1) / n
    p1_ = (p1 - p1)
    p2_ = (p2 - p1) / n 

    theta0 = -get_angle(p0_, p1_)
    theta1 = -get_angle(p2_, p1_)

    if color is None:
        # Uses the color line if color parameter is not passed.
        color = line.get_color() 
    arc = ax.add_patch(Arc(p1, width * radius, height * radius, 0, theta0, theta1, color=color, **kwargs))

    if text:
        v = p2_ / np.linalg.norm(p2_)
        if theta0 < 0:
            theta0 = theta0 + 360
        if theta1 < 0:
            theta1 = theta1 + 360
        theta = (theta0 - theta1) / 2 + text_rotatation
        pt = np.dot(rotation_transform(theta), v[:,None]).T * n * text_radius
        pt = pt + p1
        pt = pt.squeeze()
        ax.text(pt[0], pt[1], text,         
                horizontalalignment='left',
                verticalalignment='top',)

    return arc

get_angle函数是我发布的here,但为了完整性再次复制。

def get_angle(p0, p1=np.array([0,0]), p2=None):
    ''' compute angle (in degrees) for p0p1p2 corner
    Inputs:
        p0,p1,p2 - points in the form of [x,y]
    '''
    if p2 is None:
        p2 = p1 + np.array([1, 0])
    v0 = np.array(p0) - np.array(p1)
    v1 = np.array(p2) - np.array(p1)

    angle = np.math.atan2(np.linalg.det([v0,v1]),np.dot(v0,v1))
    return np.degrees(angle)

def rotation_transform(theta):
    ''' rotation matrix given theta
    Inputs:
        theta    - theta (in degrees)
    '''
    theta = np.radians(theta)
    A = [[np.math.cos(theta), -np.math.sin(theta)],
         [np.math.sin(theta), np.math.cos(theta)]]
    return np.array(A)

要使用它,可以这样做:

ax = gca()
line, = ax.plot([0, 0, 2], [-1, 0, 0], 'ro-', lw=2)
add_corner_arc(ax, line, text=u'%d\u00b0' % 90)
wfsdck30

wfsdck304#

我写了一个函数来创建一个matplotlib Arc对象,它接受了几个有用的参数。它也适用于在原点不相交的直线。对于给定的两条直线,用户可能想要绘制许多可能的弧。此函数允许用户使用参数指定哪一个。文本绘制在弧和原点之间的中点。评论,或在gist containing this function上。

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
Arc = matplotlib.patches.Arc

def halfangle(a, b):
    "Gets the middle angle between a and b, when increasing from a to b"
    if b < a:
        b += 360
    return (a + b)/2 % 360

def get_arc_patch(lines, radius=None, flip=False, obtuse=False, reverse=False, dec=0, fontsize=8):
    """For two sets of two points, create a matplotlib Arc patch drawing 
    an arc between the two lines.
    
    lines: list of lines, of shape [[(x0, y0), (x1, y1)], [(x0, y0), (x1, y1)]]
    radius: None, float or tuple of floats. If None, is set to half the length
    of the shortest line
    orgio: If True, draws the arc around the point (0,0). If False, estimates 
    the intersection of the lines and uses that point.
    flip: If True, flips the arc to the opposite side by 180 degrees
    obtuse: If True, uses the other set of angles. Often used with reverse=True.
    reverse: If True, reverses the two angles so that the arc is drawn 
    "the opposite way around the circle"
    dec: The number of decimals to round to
    fontsize: fontsize of the angle label
    """
    import numpy as np
    from matplotlib.patches import Arc
    
    linedata = [np.array(line.T) for line in lines]
    scales = [np.diff(line).T[0] for line in linedata]
    scales = [s[1] / s[0] for s in scales]
    
    # Get angle to horizontal
    angles = np.array([np.rad2deg(np.arctan(s/1)) for s in scales])
    if obtuse:
        angles[1] = angles[1] + 180
    if flip:
        angles += 180
    if reverse:
        angles = angles[::-1]
        
    angle = abs(angles[1]-angles[0])
    
    if radius is None:
        lengths = np.linalg.norm(lines, axis=(0,1))
        radius = min(lengths)/2
    
    # Solve the point of intersection between the lines:
    t, s = np.linalg.solve(np.array([line1[1]-line1[0], line2[0]-line2[1]]).T, line2[0]-line1[0])
    intersection = np.array((1-t)*line1[0] + t*line1[1])
    # Check if radius is a single value or a tuple
    try:
        r1, r2 = radius
    except:
        r1 = r2 = radius
    arc = Arc(intersection, 2*r1, 2*r2, theta1=angles[1], theta2=angles[0])
    
    half = halfangle(*angles[::-1])
    sin = np.sin(np.deg2rad(half))
    cos = np.cos(np.deg2rad(half))

    r = r1*r2/(r1**2*sin**2+r2**2*cos**2)**0.5
    xy = np.array((r*cos, r*sin))
    xy =  intersection + xy/2
    
    textangle = half if half > 270 or half < 90 else 180 + half 
    textkwargs = {
        'x':xy[0],
        'y':xy[1],
        's':str(round(angle, dec)) + "°",
        'ha':'center',
        'va':'center',
        'fontsize':fontsize,
        'rotation':textangle
    }
    return arc, textkwargs

它使用附带的脚本创建如下图所示的弧:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
# lines are formatted like this: [(x0, y0), (x1, y1)]
line1 = np.array([(1,-2), (3,2)])
line2 = np.array([(2,2), (2,-2)])
lines = [line1, line2]

fig, AX = plt.subplots(nrows=2, ncols=2)
for ax in AX.flatten():
    for line in lines:
        x,y = line.T
        ax.plot(x,y)
        ax.axis('equal')

ax1, ax2, ax3, ax4 = AX.flatten()

arc, angle_text = get_arc_patch(lines)
ax1.add_artist(arc)
ax1.set(title='Default')
ax1.text(**angle_text)

arc, angle_text = get_arc_patch(lines, flip=True)
ax2.add_artist(arc)
ax2.set(title='flip=True')
ax2.text(**angle_text)

arc, angle_text = get_arc_patch(lines, obtuse=True, reverse=True)
ax3.add_artist(arc)
ax3.set(title='obtuse=True, reverse=True')
ax3.text(**angle_text)

arc, angle_text = get_arc_patch(lines, radius=(2,1))
ax4.add_artist(arc)
ax4.set(title='radius=(2,1)')
ax4.text(**angle_text)
plt.tight_layout()
plt.show()
kadbb459

kadbb4595#

我发现TomNorway的方法更好,它在其他情况下比公认的答案更灵活。我测试了代码,并通过创建一个类进行了一些快速修复,以获得更大的适用性。

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.patches import Arc

class LinesAngles:
    def __init__(self, line1, line2, radius=None, flip=False, obtuse=False, reverse=False, dec=0, fontsize=8, title=""):
        """
        line1: list of two points, of shape [[x0, y0], [x1, y1]]
        line2: list of two points, of shape [[x0, y0], [x1, y1]]
        radius: None, float or tuple of floats. If None, is set to half the length
            of the shortest line orgio: If True, draws the arc around the point (0,0). If False, estimates 
            the intersection of the lines and uses that point.
        flip: If True, flips the arc to the opposite side by 180 degrees
        obtuse: If True, uses the other set of angles. Often used with reverse=True.
        reverse: If True, reverses the two angles so that the arc is drawn "the opposite way around the circle"
        dec: The number of decimals to round to
        fontsize: fontsize of the angle label
        title: Title of the plot
        """
        self.line1 = line1
        self.line2 = line2
        self.lines = [line1, line2]
        self.radius = radius
        self.flip = flip
        self.obtuse = obtuse
        self.reverse = reverse
        self.dec = dec
        self.fontsize = fontsize
        self.title = title

    def halfangle(self,a, b) -> float:
        """
        Gets the middle angle between a and b, when increasing from a to b
        a: float, angle in degrees
        b: float, angle in degrees
        returns: float, angle in degrees
        """
        if b < a:
            b += 360
        return (a + b)/2 % 360

    def get_arc_patch(self, lines: list):
        """
        For two sets of two points, create a matplotlib Arc patch drawing 
        an arc between the two lines. 
        lines: list of lines, of shape [[(x0, y0), (x1, y1)], [(x0, y0), (x1, y1)]]
        returns: Arc patch, and text for the angle label
        """
      
        linedata = [np.array(line.T) for line in lines]
        scales = [np.diff(line).T[0] for line in linedata]
        scales = [s[1] / s[0] for s in scales]
        
        # Get angle to horizontal
        angles = np.array([np.rad2deg(np.arctan(s/1)) for s in scales])
        if self.obtuse:
            angles[1] = angles[1] + 180
        if self.flip:
            angles += 180
        if self.reverse:
            angles = angles[::-1]
            
        angle = abs(angles[1]-angles[0])
        
        if self.radius is None:
            lengths = np.linalg.norm(lines, axis=(0,1))
            self.radius = min(lengths)/2
        
        # Solve the point of intersection between the lines:
        t, s = np.linalg.solve(np.array([line1[1]-line1[0], line2[0]-line2[1]]).T, line2[0]-line1[0])
        intersection = np.array((1-t)*line1[0] + t*line1[1])
        # Check if radius is a single value or a tuple
        try:
            r1, r2 = self.radius
        except:
            r1 = r2 = self.radius
        arc = Arc(intersection, 2*r1, 2*r2, theta1=angles[1], theta2=angles[0])
        
        half = self.halfangle(*angles[::-1])
        sin = np.sin(np.deg2rad(half))
        cos = np.cos(np.deg2rad(half))

        r = r1*r2/(r1**2*sin**2+r2**2*cos**2)**0.5
        xy = np.array((r*cos, r*sin))
        xy =  intersection + xy/2
        
        textangle = half if half > 270 or half < 90 else 180 + half 
        textkwargs = {
            'x':xy[0],
            'y':xy[1],
            's':str(round(angle, self.dec)) + "°",
            'ha':'center',
            'va':'center',
            'fontsize':self.fontsize,
            'rotation':textangle
        }
        return arc, textkwargs

    def plot(self) -> None:
        """!
        Plot the lines and the arc
        """

        fig = plt.figure()
        ax = fig.add_subplot(1,1,1)

        for line in self.lines:
            x,y = line.T
            ax.plot(x,y)
            ax.axis('equal')

        arc, angle_text = self.get_arc_patch(self.lines)
        ax.add_artist(arc)
        ax.set(title=self.title)
        ax.text(**angle_text)
        plt.show()

要使用它,您只需创建示例和plot函数。

# lines are formatted like this: [(x0, y0), (x1, y1)]
line1 = np.array([(1,-2), (3,2)])
line2 = np.array([(2,2), (2,-2)])
default = LinesAngles(line1, line2, title="Default")
#Plot single pair of lines
default.plot()

如果您仍然想绘制多个案例,我创建了一个函数,它接受示例并自动绘制到您需要的子图。

# lines are formatted like this: [(x0, y0), (x1, y1)]
line1 = np.array([(1,-2), (3,2)])
line2 = np.array([(2,2), (2,-2)])
default = LinesAngles(line1, line2, title="Default")
flip = LinesAngles(line1, line2, title='flip=True', flip=True)
obtuse = LinesAngles(line1, line2, title='obtuse=True, reverse=True', obtuse=True, reverse=True)
radius = LinesAngles(line1, line2, title='radius=(2,1)', radius=(2,1))

#Plot single pair of lines
default.plot()
#Plot multiple line pairs
multiple_plot(default, flip, obtuse, radius, num_subplots=4)

感谢TomNorway的回答,我只做了一些修改。

相关问题