ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

两圆相交完全指南:交点坐标与重叠面积推导及代码实现

两圆相交完全指南:交点坐标与重叠面积推导及代码实现 上一次被这类看起来“简单”的几何问题折磨是在写一个配送范围重叠判断。背景是外卖平台要算出两个商家配送圈的重合区域有多大用来评估客流分流风险。当时第一反应是“两个圆重叠还不好算”真动手推公式才发现要从两圆交点坐标一步步算到交叉区域的精确面积中间全是细节相切怎么办、包含怎么办、浮点误差怎么办、角度象限对不对。后来在游戏碰撞检测、地理围栏、信号覆盖分析里又连续遇到同一个问题这才意识到两个重叠的圆不是一道初中平面几何题而是一整套工程计算套路。这篇文章打算把这个套路完整拆开从最基础的圆与圆位置关系开始推导交点坐标和重叠面积的数学公式再给出可直接复制的 Python、JavaScript 实现最后补充边界条件、数值验证和常见坑。适合正在做 2D 游戏、地图服务、信号覆盖分析或者 Canvas 可视化的同学收藏下来可以直接套用。1. 两个重叠的圆到底要算什么“两个重叠的圆”在工程里很少只是一个静态图形更多时候是三类计算需求第一类是交点坐标计算。比如在 Canvas 上画两个相交圆要标出它们的切点或者拖拽一个圆时实时更新交点的位置。这类需求需要的是解析解——给定两个圆心坐标和半径直接算出来两个交点落在哪里。第二类是重叠面积计算。这是更硬核的需求。地理围栏里要算两个圆形区域的交集多大决定是否触发合并告警无线覆盖里要评估两个信号塔覆盖范围的冗余程度图像处理里可能需要计算两个圆形 ROI 的交集。这类需求需要的是精确面积而不是“看着大概重叠了五分之一”。第三类是碰撞检测和关系判定。两个圆是相交、相切、包含还是外离这组判断是游戏碰撞检测和物理引擎的地基。它不涉及复杂的公式但需要写对边界条件。这三种计算本质上都从同一个源问题出发给定圆心 O1(x1, y1)、O2(x2, y2) 和半径 r1、r2如何完整描述这两个圆的几何关系很多资料会直接给出结论公式却没有解释公式推导和使用条件。结果就是新手抄完公式遇到“小圆完全在大圆内部”这种边界情况得出的面积直接变成负数。这篇文章会把推导逻辑讲清楚并给出健壮的代码实现。2. 圆与圆位置关系所有计算的判断前提2.1 圆心距是一切判断的起点设两圆圆心分别为 O1 和 O2圆心距记为d sqrt((x2 - x1)^2 (y2 - y1)^2)不管后续要计算什么第一步都是先算出这个 d。它是全部判断的基础。两个圆的平面位置关系有五种由 d 与 r1、r2 的大小关系决定位置关系判断条件交点数量重叠面积外离d r1 r200外切d r1 r210相交r1 - r2 d r1 r2内切d r1 - r2且 d ≠ 0内含d r1 - r2同心d 00小圆面积这里要注意两个边界同心但半径不同时小圆在大圆内部重叠面积是小圆面积。同心且半径相等时两圆完全重合重叠面积理论上是大圆面积但在工程计算中这种精确重合通常视为异常或按全等处理。2.2 一个容易忽略的细节浮点相等判断代码里不能直接写d r1 r2因为浮点数计算有精度误差。实际工程中建议使用误差阈值def is_tangent(d, r1, r2, eps1e-9): return abs(d - (r1 r2)) eps or abs(d - abs(r1 - r2)) eps这个细节在后续代码里会反复出现。3. 两圆交点坐标的数学推导这是整个计算的核心。很多资料直接给公式但没有说明公式怎么来的导致读者不理解什么情况下公式会失效。这里我们完整推导一遍。3.1 问题描述已知两个圆圆1方程 (x - x1)^2 (y - y1)^2 r1^2 圆2方程 (x - x2)^2 (y - y2)^2 r2^2要求这两个方程组成的方程组的解即两个交点的坐标。3.2 第一步把“求交点”转换成“求位置”观察两圆相交时的几何结构。设两个交点分别为 I1、I2线段 I1I2 是两圆的公共弦。这个公共弦与圆心连线 O1O2 互相垂直且交点 P 是公共弦的中点。这个结构相当有用它把二维的交点求解放到了一条直线上先求出 P 在 O1O2 连线上距离 O1 多远记为 a。再求出从 P 到 I1或 I2的垂直距离 h。最后在 P 点处沿垂直方向平移 h得到两个交点。3.3 第二步求 a在三角形 O1-O2-I1 中三条边分别是O1I1 r1O2I1 r2O1O2 dP 是 I1I2 与 O1O2 的垂足所以三角形 O1-P-I1 是直角三角形。设 O1P a则 O2P d - a。由勾股定理r1^2 - a^2 r2^2 - (d - a)^2展开整理r1^2 - a^2 r2^2 - (d^2 - 2da a^2) r1^2 - a^2 r2^2 - d^2 2da - a^2 r1^2 r2^2 - d^2 2da a (r1^2 - r2^2 d^2) / (2d)这就是计算 a 的标准公式。3.4 第三步求 h在直角三角形 O1-P-I1 中斜边是 r1一条直角边是 a另一条直角边是 h。所以h sqrt(r1^2 - a^2)当 r1^2 - a^2 0 时说明两圆不相交这个公式在实数范围内无解。3.5 第四步从向量方向求交点坐标现在有了单位向量 u方向从 O1 指向 O2u ((x2 - x1)/d, (y2 - y1)/d)P 点坐标P O1 a * u垂直单位向量 v (-u.y, u.x)两个交点分别为I1 P h * v I2 P - h * v写成完整坐标形式I1.x x1 a * (x2 - x1) / d - h * (y2 - y1) / d I1.y y1 a * (y2 - y1) / d h * (x2 - x1) / d I2.x x1 a * (x2 - x1) / d h * (y2 - y1) / d I2.y y1 a * (y2 - y1) / d - h * (x2 - x1) / d用向量方式实现不容易出错比直接套坐标公式更清晰也更便于理解。4. 两圆重叠面积的推导重叠面积是最容易写错的部分。网上常见公式版本很多如果不理解推导边界条件写错了很难排查。4.1 弓形分解法在两圆相交的情况下公共区域可以被分解为两个“弓形”之和圆1被公共弦切出的弓形弦对应的弓形区域圆2被公共弦切出的弓形每个弓形的面积 对应扇形面积 - 对应三角形面积。设圆1中公共弦所对的圆心角为 2α圆2中为 2β。从前面推导可知在圆1中α 满足 cos(α) a / r1因此α acos((d^2 r1^2 - r2^2) / (2 * d * r1))在圆2中同理β acos((d^2 r2^2 - r1^2) / (2 * d * r2))4.2 弓形面积公式圆1的弓形面积弓形1 (1/2) * r1^2 * 2α - (1/2) * r1^2 * sin(2α) r1^2 * (α - sinα * cosα)同理圆2的弓形面积弓形2 r2^2 * (β - sinβ * cosβ)因此重叠面积S r1^2 * (α - sinα * cosα) r2^2 * (β - sinβ * cosβ)这就是不用开根号的重叠面积公式。它只依赖 α、β 两个角实现起来更简单。4.3 教科书版本的等价公式更常见的表示方式是用海伦公式处理后面那个三角形面积项S r1^2 * acos((d^2 r1^2 - r2^2)/(2*d*r1)) r2^2 * acos((d^2 r2^2 - r1^2)/(2*d*r2)) - 0.5 * sqrt((-dr1r2) * (dr1-r2) * (d-r1r2) * (dr1r2))这个公式与弓形公式等价但多了一个带负号的根号项更容易因为浮点误差出现负面积。如果只是实现功能推荐使用弓形公式。4.4 边界条件不能丢写代码时必须在主公式前处理这些情况d r1 r2不重叠面积 0。d abs(r1 - r2)小圆被大圆完全包含面积为 min(πr1^2, πr2^2)。d 0且半径不同面积同样是较小圆的面积。半径相同且d 0完全重合视为异常或返回一个大圆面积。5. 完整代码实现Python 与前端方案5.1 Python 版本交点计算下面的代码实现了两圆交点计算并处理了相切、无交点和包含等边界情况。# 文件路径circle_intersection.py import math def circle_intersection_points(x1, y1, r1, x2, y2, r2): 计算两个圆的交点坐标。 参数 x1, y1, r1: 圆1的圆心横坐标、纵坐标、半径 x2, y2, r2: 圆2的圆心横坐标、纵坐标、半径 返回值 交点列表 [(x, y), ...]包含 0-2 个点。 dx x2 - x1 dy y2 - y1 d math.hypot(dx, dy) # 同心圆无交点 if d 1e-12: return [] # 外离无交点 if d r1 r2 1e-12: return [] # 内含无交点 if d abs(r1 - r2) - 1e-12: return [] # 相切情况只有一个交点 # 外切 if abs(d - (r1 r2)) 1e-9: t r1 / d return [(x1 dx * t, y1 dy * t)] # 内切 if abs(d - abs(r1 - r2)) 1e-9: if r1 r2: t r1 / d return [(x1 dx * t, y1 dy * t)] else: t 1 - r2 / d return [(x1 dx * t, y1 dy * t)] # 标准的两交点情况 a (r1 * r1 - r2 * r2 d * d) / (2 * d) h2 r1 * r1 - a * a h math.sqrt(max(h2, 0.0)) # 中点 P px x1 a * dx / d py y1 a * dy / d # 垂直单位向量 vx -dy / d vy dx / d i1 (px h * vx, py h * vy) i2 (px - h * vx, py - h * vy) return [i1, i2]这段代码的关键点是使用math.hypot计算距离避免dx * dx dy * dy在极端数值下溢出。把“外切”和“内切”单独处理防止后续h计算出现接近 0 的值导致精度不稳定。用max(h2, 0.0)防止浮点误差导致根号下出现微小的负数。5.2 Python 版本重叠面积计算# 文件路径circle_overlap.py import math def circle_overlap_area(x1, y1, r1, x2, y2, r2): 计算两个圆的重叠面积。 参数 x1, y1, r1: 圆1的圆心横坐标、纵坐标、半径 x2, y2, r2: 圆2的圆心横坐标、纵坐标、半径 返回值 重叠面积浮点数。 d math.hypot(x2 - x1, y2 - y1) # 不重叠 if d r1 r2: return 0.0 # 含或被含 if d abs(r1 - r2): return math.pi * min(r1, r2) ** 2 # 同心且半径不等d 已经等于 0 时 if d 1e-12: return math.pi * min(r1, r2) ** 2 # 半角公式 alpha math.acos((d * d r1 * r1 - r2 * r2) / (2 * d * r1)) beta math.acos((d * d r2 * r2 - r1 * r1) / (2 * d * r2)) area ( r1 * r1 * (alpha - math.sin(alpha) * math.cos(alpha)) r2 * r2 * (beta - math.sin(beta) * math.cos(beta)) ) return area5.3 JavaScript 版本Canvas 实时可视化前端场景同样常用。这里给出一个可直接运行的 HTML 页面展示两个重叠圆的绘制和实时面积显示。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleTwo Overlapping Circles/title style body { font-family: monospace; margin: 40px; background: #fafafa; } #cv { border: 1px solid #ddd; background: #fff; margin-bottom: 16px; } /style /head body canvas idcv width600 height400/canvas div idinfo/div script const canvas document.getElementById(cv); const ctx canvas.getContext(2d); const info document.getElementById(info); // 参数圆A和圆B const c1 { x: 240, y: 200, r: 120 }; const c2 { x: 360, y: 200, r: 100 }; function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 先画圆1 ctx.beginPath(); ctx.arc(c1.x, c1.y, c1.r, 0, Math.PI * 2); ctx.fillStyle rgba(66, 133, 244, 0.25); ctx.fill(); ctx.strokeStyle #4285f4; ctx.lineWidth 2; ctx.stroke(); // 再画圆2 ctx.beginPath(); ctx.arc(c2.x, c2.y, c2.r, 0, Math.PI * 2); ctx.fillStyle rgba(234, 67, 53, 0.25); ctx.fill(); ctx.strokeStyle #ea4335; ctx.lineWidth 2; ctx.stroke(); // 计算交点和面积 const area overlapArea(c1, c2); const points intersectionPoints(c1, c2); // 画交点 points.forEach(p { ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, Math.PI * 2); ctx.fillStyle #222; ctx.fill(); }); info.textContent 重叠面积: area.toFixed(4) px², 交点数量: points.length; } // JS 版本交点计算 function intersectionPoints(a, b) { const dx b.x - a.x; const dy b.y - a.y; const d Math.hypot(dx, dy); if (d 1e-12 || d a.r b.r 1e-12 || d Math.abs(a.r - b.r) - 1e-12) { return []; } if (Math.abs(d - (a.r b.r)) 1e-9) { const t a.r / d; return [{ x: a.x dx * t, y: a.y dy * t }]; } const t (a.r * a.r - b.r * b.r d * d) / (2 * d); const h Math.sqrt(Math.max(a.r * a.r - t * t, 0)); const px a.x t * dx / d; const py a.y t * dy / d; const vx -dy / d; const vy dx / d; return [ { x: px h * vx, y: py h * vy }, { x: px - h * vx, y: py - h * vy } ]; } // JS 版本重叠面积 function overlapArea(a, b) { const d Math.hypot(b.x - a.x, b.y - a.y); if (d a.r b.r) return 0; if (d Math.abs(a.r - b.r)) return Math.PI * Math.min(a.r, b.r) ** 2; const alpha Math.acos((d * d a.r * a.r - b.r * b.r) / (2 * d * a.r)); const beta Math.acos((d * d b.r * b.r - a.r * a.r) / (2 * d * b.r)); return a.r * a.r * (alpha - Math.sin(alpha) * Math.cos(alpha)) b.r * b.r * (beta - Math.sin(beta) * Math.cos(beta)); } // 初始绘制 draw(); /script /body /html这个页面有两个值得注意的点面积计算在浏览器里直接实时更新适合用来理解位置关系的变化。如果以后要拖拽圆只需要在mousemove事件里更新c1或c2的位置再调用draw()。6. 运行结果与数值验证6.1 用已知几何关系验证代码写数学相关代码最重要的一件事是用已知结果验证。下面是三个标准测试用例。测试用例1等半径外离。# r1 5, r2 5, d 12外离 area circle_overlap_area(0, 0, 5, 12, 0, 5) print(area) # 期望 0.0测试用例2小圆完全在大圆内部。# r1 5, r2 2, d 1内含 area circle_overlap_area(0, 0, 5, 1, 0, 2) print(area) # 期望 12.566370614359172π * 4测试用例3两圆正交相交r1 r2 5d 6。# 手工推导结果22.365... area circle_overlap_area(0, 0, 5, 6, 0, 5) print(area) # 期望约 22.365813第三个用例我们可以手工验证α acos(0.6) ≈ 0.927295 sinα 0.8cosα 0.6 弓形面积 25 * (0.927295 - 0.48) ≈ 11.1824 重叠面积 22.3648如果代码输出与这个数字匹配说明实现正确。6.2 用蒙特卡洛方法做交叉验证解析公式算完最好用蒙特卡洛模拟验证一次。思路是向大包围盒里随机撒点统计落在两个圆内的点的比例乘以包围盒面积。# 文件路径monte_carlo_check.py import math import random def monte_carlo_area(x1, y1, r1, x2, y2, r2, samples300000): # 计算包围盒 min_x min(x1 - r1, x2 - r2) max_x max(x1 r1, x2 r2) min_y min(y1 - r1, y2 - r2) max_y max(y1 r1, y2 r2) box_area (max_x - min_x) * (max_y - min_y) count 0 for _ in range(samples): px random.uniform(min_x, max_x) py random.uniform(min_y, max_y) in_1 (px - x1) ** 2 (py - y1) ** 2 r1 ** 2 in_2 (px - x2) ** 2 (py - y2) ** 2 r2 ** 2 if in_1 and in_2: count 1 return box_area * count / samples # 对比 analytic circle_overlap_area(0, 0, 5, 6, 0, 5) mc monte_carlo_area(0, 0, 5, 6, 0, 5) print(f解析公式结果: {analytic:.4f}) print(f蒙特卡洛结果: {mc:.4f})蒙特卡洛的结果会根据随机种子有波动但通常样本量 30 万时误差可以控制在 0.5% 以内。如果两条曲线数值差距很大说明解析公式或代码有问题优先检查边界条件和 acos 的输入是否越界。6.3 运行失败的排查顺序如果代码输出与预期不一致按这个顺序排查先检查 d 的数值确认圆心距计算没有错误。检查 acos 的输入参数是否落在 [-1, 1] 区间。浮点误差会导致分母乘出来的结果偏出 1比如 1.0000000002此时要 clamp 到 1。检查边界条件是否覆盖了“内含”的情况否则小圆在大圆内时面积会算错。比较两个等价公式的结果如果弓形公式和教科书公式不一致通常是三解项符号问题。7. 常见问题与故障排查问题现象可能原因排查方式解决方案相交时交点坐标明显不对垂直向量方向取反打印 P 点和本地方向向量手工验算一个直角特例确认 v (-dy/d, dx/d) 与 u (dx/d, dy/d) 垂直面积计算结果偏大或为负未处理“内含”边界构造 r2 远小于 r1 的用例在公式前增加d abs(r1 - r2)判断acos 报 ValueError浮点误差导致输入超过 1打印公式分子和分母对 acos 参数做 clamp(-1, 1)相切时交点数量抖动浮点比较d r1 r2不稳定打印 d 和 r1r2 的差值使用误差阈值比较同心圆时除零d 为 0代码未做保护检查圆心距离判断在计算 a 之前先返回内侧圆面积蒙特卡洛验证不一致采样量不足或边界条件写错增大采样量到 100 万打印正反例先修复解析公式再用蒙特卡洛复核前端 Canvas 显示重叠区域不明显透明度叠加颜色不明显检查 fillStyle 透明度使用半透明高对比颜色8. 工程最佳实践与扩展思考8.1 写成独立的工具模块两圆相交计算是高度通用的基础能力建议封装成独立模块而不是散落在业务代码里。推荐 API 设计Circle(center_x, center_y, radius) - distance_to(other) - intersection_points(other) - overlap_area(other) - relation(other)这样业务层不需要关心几何公式只需要调用overlap_area或intersection_points。8.2 使用误差阈值而不是精确相等圆心距和半径相加的相等判断在浮点体系下几乎不可能精确命中。务必引入误差阈值。在 Python 中可以使用math.isclose在 JavaScript 中自行定义EPSILON。推荐默认误差阈值1e-9或相对误差方式。对于像素级的 Canvas 应用1e-3通常足够对于地理坐标或高精度计算需要更严格的判断。8.3 acos 输入越界的防御性处理acos的合法输入范围是[-1, 1]。浮点舍入可能导致传入参数为1.0000000002在 Python 中会直接抛异常在 JavaScript 中返回NaN。最稳妥的做法是def safe_acos(value): return math.acos(max(-1.0, min(1.0, value)))8.4 在工程场景中的扩展两圆相交只是“圆形对象”运算的起点。很多真实问题最终会扩展成多个圆叠加覆盖区域计算合并所有圆形的覆盖范围判断总覆盖面积不能用两两相交简单相加需要平面扫描或网格法。移动圆的连续碰撞检测考虑时间维度后问题从静态相交变成“质点在单位时间内的扫掠路径与另一个圆是否相交”。圆周与线段的相交游戏里角色搜索范围是一个圆墙面是线段此时两圆相交公式不够用需要几何求交的升级版。旋转坐标系下的椭圆相交两圆正交相交公式可以直接推广到“轴对齐椭圆”但任意角度椭圆相交就需要代数求解了。8.5 性能与精度平衡单次两圆相交计算是常数时间复杂度性能不需要优化。但如果做批量计算比如上万次碰撞检测先用粗粒度包围盒AABB过滤明显不相交的圆对。再对候选对执行精确计算。使用平方距离比较避免开根号带来的开销。对于面积计算acos和sin开销比sqrt稍高但在绝大多数场景中可忽略。如果需要在移动端每帧计算多次可以考虑提前表格化或近似计算。9. 一些额外的思考“两个重叠的圆”这个几何问题看起来是一个再基础不过的数学知识点但越深入越会发现它连接着许多实际工程方向。从学校到工程中间隔着的其实不是公式记忆而是对边界情况的敏感度。第一次实现时我花了很长时间在“正常相交”的主逻辑上结果一跑起来就崩在“内含”和“外切”上。两个圆的几何关系只有五种情况主公式却只有“相交”这一种能用其他四种都要靠边界条件拦截。这大概是这类几何算法最核心的经验先判断关系再套用公式。如果你最近也在做类似的功能建议按这个顺序实践先跑通单独的交点计算函数输入几个已知几何关系验证。再实现重叠面积注意边界分支和 acos 越界。最后在你的业务场景里接入并保留测试用例。几何算法的测试用例应该保留下来它们比任何文档都更能说明问题外离、外切、相交、内切、内含、同心六个用例各写一个断言以后重构或换语言实现跑一遍测试就知道有没有改坏。这一步很基础但在实际项目里带来很多便利。希望这篇整理对你也有用特别是正准备在项目里写圆相关计算的时候能少走一点弯路。如果你在实践中有更好的边界处理技巧也欢迎在评论区交流。
返回列表