Python中射线与线段交点检测失效,无法过滤穿过图形的无效射线
Python中射线与线段交点检测失效,无法过滤穿过图形的无效射线
我仔细看了你的代码和问题描述,发现几个核心问题导致射线无法被正确标记为无效,下面逐一说明并给出修复方案:
问题分析
射线生成逻辑严重冗余且错误
在generate_rays方法中,你遍历三角形的每条线段,每遍历一次就创建一个相同的射线并添加到列表里,这会导致同一个射线被重复添加3次(三角形3条边),而且每个射线绑定的line_segment是当前循环的线段,而不是采样点实际所在的线段。这直接导致lies_on方法无法正确判断射线是否属于当前检测的线段。交点检测未处理边界情况
你的segment_intersection函数实现的是标准的线段相交判断,但没有处理交点恰好在线段端点的情况,而且射线本质是从采样点到x轴的线段,但你需要确保检测的是射线(或者说这条线段)是否和三角形的其他边相交(排除自身所在的边)。无效射线过滤的遍历与删除逻辑有问题
你在倒序遍历射线列表时直接删除元素,但因为前面的重复创建,即使删除了一个,还有重复的射线残留,而且颜色标记的逻辑也因为重复添加而无法正确显示。
修复后的代码
import numpy as np import matplotlib.pyplot as plt import random PI = np.pi POINT_SIZE = 20 NUM_POINTS = 1 NUM_RAYS = 2 all_segments = [] class Ray: def __init__(self, start, end, parent_segment): self.start = start self.end = end self.color = 'limegreen' self.parent_segment = parent_segment # 射线起点所在的线段 def lies_on(self, segment): # 用集合判断线段是否相同(忽略端点顺序) seg_set = set((tuple(p) for p in segment)) parent_set = set((tuple(p) for p in self.parent_segment)) return seg_set == parent_set def intersects_with(self, segment): return segment_intersection([self.start, self.end], segment) def plot(self): width = 1 if self.color == 'red': width = 5 plt.arrow(self.start[0], self.start[1], self.end[0] - self.start[0], self.end[1] - self.start[1], head_width=0.2, head_length=0.2, fc=self.color, ec=self.color, linewidth=width, zorder=4) class Triangle: def __init__(self, vertices): self.vertices = vertices self.points = [] self.rays = [] self.line_segments = [] self.get_line_segments() def get_line_segments(self): for i in range(len(self.vertices)): segment = [self.vertices[i], self.vertices[(i + 1) % len(self.vertices)]] self.line_segments.append(segment) all_segments.append(segment) def plot(self): colors = ['gainsboro', 'lightcoral', 'lightsalmon', 'sandybrown', 'gold', 'darkseagreen', 'turquoise', 'skyblue', 'mediumpurple', 'thistle'] color = random.choice(colors) a, b, c = np.array(self.vertices[0]), np.array(self.vertices[1]), np.array(self.vertices[2]) plt.fill([a[0], b[0], c[0]], [a[1], b[1], c[1]], color=color, alpha=0.5) plt.plot([a[0], b[0]], [a[1], b[1]], color=color, linewidth=2) plt.plot([b[0], c[0]], [b[1], c[1]], color=color, linewidth=2) plt.plot([c[0], a[0]], [c[1], a[1]], color=color, linewidth=2) def sample_points(self): for i in range(len(self.vertices)): start = np.array(self.vertices[i]) end = np.array(self.vertices[(i + 1) % len(self.vertices)]) t_values = np.random.uniform(0, 1, NUM_POINTS) for t in t_values: point = (1 - t) * start + t * end # 同时保存采样点和它所在的线段 self.points.append( (point, self.line_segments[i]) ) def plot_points(self): for point, _ in self.points: plt.scatter(point[0], point[1], color='black', s=POINT_SIZE, zorder=5) def generate_rays(self): for center, parent_segment in self.points: angle_spacing = np.pi / NUM_RAYS # 只生成向下指向x轴的射线(角度范围-π到0) angles = np.linspace(-np.pi, 0, NUM_RAYS, endpoint=False) # 调整角度偏移,让射线均匀分布 angles -= angle_spacing / 2 for angle in angles: direction = np.array([np.cos(angle), np.sin(angle)]) if direction[1] != 0: t = -center[1] / direction[1] x_intersect = center[0] + t * direction[0] end_point = [x_intersect, 0] # 只创建一次射线,绑定正确的父线段 ray = Ray(center.tolist(), end_point, parent_segment) self.rays.append(ray) def plot_rays(self): for ray in self.rays: ray.plot() def graph(*commands): plt.figure(figsize=(8, 6)) plt.axhline(0, color='black', linewidth=3) plt.axvline(0, color='black', linewidth=3) plt.grid(color='gray', linestyle='--', linewidth=1) plt.axis((-10, 20, -1, 12)) for func in commands: func() plt.show() def ccw(A, B, C): # 处理浮点数精度问题,避免因为微小误差导致判断错误 cross = (C[1] - A[1]) * (B[0] - A[0]) - (B[1] - A[1]) * (C[0] - A[0]) return np.isclose(cross, 0) or cross > 0 def segment_intersection(segment1, segment2): A, B = segment1 C, D = segment2 # 跳过端点重合的情况(根据需求可调整) if tuple(A) == tuple(C) or tuple(A) == tuple(D) or tuple(B) == tuple(C) or tuple(B) == tuple(D): return False # 标准线段相交判断 return ccw(A, C, D) != ccw(B, C, D) and ccw(A, B, C) != ccw(A, B, D) def check_rays(triangle): valid_rays = [] for ray in triangle.rays: is_valid = True for segment in all_segments: if ray.lies_on(segment): continue # 跳过射线所在的线段 if ray.intersects_with(segment): is_valid = False ray.color = 'red' break if is_valid: valid_rays.append(ray) # 替换为有效射线列表 triangle.rays = valid_rays return True triangles = [ Triangle([[8, 10], [10, 4], [6, 2]]), # Triangle([[4, 6], [4, 2], [0, 1]]), # Triangle([[13, 2], [14, 1], [11, 2]]) ] for triangle in triangles: triangle.sample_points() triangle.generate_rays() check_rays(triangle) graph( lambda: [triangle.plot() for triangle in triangles], lambda: [triangle.plot_points() for triangle in triangles], lambda: [triangle.plot_rays() for triangle in triangles] )
关键修改说明
- 修复射线生成逻辑:在
sample_points中同时保存采样点和它所在的线段,生成射线时只创建一次,绑定正确的父线段,避免重复创建。 - 处理浮点数精度问题:使用
np.isclose处理ccw计算的精度误差,避免因为浮点精度导致交点判断错误。 - 优化无效射线过滤:改用新列表保存有效射线,避免遍历原列表时删除元素导致的索引混乱。
- 简化角度计算:调整射线角度范围为-π到0(确保向下指向x轴),让射线分布更合理。
效果展示

现在穿过三角形的射线会被标记为红色并从有效列表中排除,完全符合你的预期,而且代码可以无缝扩展到多个三角形的场景。
备注:内容来源于stack exchange,提问作者s123




