基于行空板K10的科赫雪花分形算法实现与图形编程实践
1. 项目概述当数学之美遇见开源硬件最近在捣鼓行空板K10想找个项目既能展示它的图形能力又能带点数学的趣味性。思来想去科赫雪花这个经典的数学分形图案就跳进了脑海。它规则简单但迭代出来的图形却无比复杂和精美非常适合用来作为编程和硬件结合的入门实践。这个项目说白了就是利用行空板K10的Python环境和屏幕通过代码“生长”出一朵科赫雪花。科赫雪花也叫科赫曲线或雪花曲线是瑞典数学家海里格·冯·科赫在1904年提出的一种分形。它的构造规则非常直观从一个等边三角形开始把每条边中间的三分之一段去掉然后用一个去掉底边的等边三角形来替代。这个过程无限重复下去得到的曲线周长会趋向于无穷大但面积却是有限的这种反直觉的特性正是分形的魅力所在。对于行空板K10来说这个项目能很好地锻炼几个核心能力一是理解递归或迭代的编程思想二是掌握基本的图形绘制接口三是将抽象的数学公式转化为屏幕上可见的、动态的视觉艺术。无论你是对分形数学感兴趣的学生还是想找点有趣项目练手的硬件爱好者亦或是STEAM教育的老师这个项目都很有价值。它不需要复杂的电路连接核心全在代码逻辑上但最终呈现的效果却能让人直观感受到数学与编程结合的力量。接下来我就把从思路拆解到代码实现再到调试优化的全过程毫无保留地分享出来。2. 核心思路与算法设计拆解2.1 科赫雪花生成原理与算法选择科赫雪花的生成本质上是一个递归过程。我们首先要理解其最基础的构造单元科赫曲线。给定一条线段我们需要找到它的两个端点然后计算出在这条线段上“生长”出一个小尖顶所需的四个新点加上原来的两个端点共六个点但实际绘制是连接其中五个点。具体计算如下假设线段起点为P0(x0, y0)终点为P1(x1, y1)。第一个点就是起点P0。第二个点将线段三等分第一个等分点P2。P2 P0 (P1 - P0) / 3。第三个点这是最关键的“尖顶”点。我们需要在线段中间三分之一段的上方对于构建向外凸起的雪花而言构造一个等边三角形的顶点。计算过程稍复杂先求出线段P0P1的中点M。求出线段的方向向量v P1 - P0。求出与该向量垂直的单位法向量n。对于画布坐标系y轴向下要确保法向量指向“外侧”对于初始三角形的边外侧就是远离中心的方向。一个常用的方法是n (-v.y, v.x) / length(v)然后根据需求调整正负号以确保方向正确。尖顶点P3 M n * (sqrt(3)/6 * length(v))。这里的(sqrt(3)/6 * length(v))是等边三角形的高当底边为原线段三分之一长时。第四个点线段的第二个三等分点P4。P4 P0 2 * (P1 - P0) / 3。第五个点就是终点P1。递归过程就是对于一条线段如果递归深度为0则直接绘制这条线段否则将这条线段看作上述五个点P0, P2, P3, P4, P1顺序连接的四条新线段并对这四条新线段分别进行递归深度减1的相同操作。对于科赫雪花我们从一个等边三角形开始对这个三角形的三条边分别执行上述科赫曲线的递归过程即可。算法选择我们采用递归算法来实现。递归代码简洁直接反映了科赫曲线的分形定义。虽然对于极深的递归可能存在栈溢出风险但行空板K10的屏幕分辨率有限递归深度在5-6层时图形细节就已足够丰富完全在安全范围内。2.2 行空板K10图形绘制方案选型行空板K10预装了Python并提供了丰富的库支持。对于图形绘制主要有两个选择Turtle海龟绘图库和PIL(Python Imaging Library) 结合unihiker库的GUI功能。Turtle库优点极其简单直观命令如forward,left非常适合教学和理解图形学基础。绘制过程是动态可视的可以看到雪花“生长”的过程。缺点绘制效率相对较低对于复杂图形或较深递归速度会变慢。可控性稍弱例如精确控制绘图位置和缩放需要一些计算。PIL unihiker GUI优点功能强大且灵活。我们可以先在内存中的图像对象上绘制然后一次性更新到屏幕上效率高。可以轻松实现颜色渐变、背景图、动态效果如旋转、缩放等高级功能。unihiker库提供了对行空板屏幕、按钮等硬件的直接控制交互性更强。缺点需要手动计算所有点的坐标并进行连线代码比Turtle稍复杂。我的选择与理由为了更充分地展示行空板K10的性能和实现更丰富的视觉效果我选择PILunihikerGUI的方案。虽然入门门槛比Turtle略高一点但它能让我们更深入地控制绘图过程并且为后续添加交互功能如通过按钮改变递归深度、切换颜色留下了巨大空间。此外一次性绘制的效率也更高体验更流畅。注意行空板K10的unihiker库已经为我们封装好了屏幕显示和事件处理我们只需要关注在PIL.Image和PIL.ImageDraw对象上绘图即可非常方便。3. 开发环境搭建与核心代码实现3.1 行空板K10环境准备与依赖安装行空板K10开机即用其基于Linux系统并预装了完整的Python环境。我们主要通过SSH或者直接使用板载的Jupyter Notebook服务来编写和运行代码。这里我推荐使用Jupyter Notebook因为它可以边写边运行即时看到图形结果调试非常方便。连接行空板将行空板通过USB线连接到电脑或者使其连接到与电脑相同的Wi-Fi网络。在电脑浏览器中输入行空板屏幕上显示的IP地址和端口号通常是http://192.168.x.x:8888即可访问Jupyter Notebook。确认库可用新建一个Notebook文件。首先我们检查并导入必要的库。# 检查unihiker和PIL库 import unihiker from PIL import Image, ImageDraw import math import time如果unihiker或PIL导入失败极少数情况可能需要通过终端安装。在Jupyter Notebook的单元格中运行!pip install unihiker -i https://mirrors.aliyun.com/pypi/simple/ !pip install Pillow -i https://mirrors.aliyun.com/pypi/simple/Pillow是PIL的一个友好分支功能完全兼容。使用国内镜像源可以加速下载。3.2 科赫雪花核心绘制函数详解接下来是核心部分实现科赫曲线的递归绘制函数。我们将创建一个函数draw_koch它接收画布对象、起点、终点、当前递归深度和最大递归深度作为参数。def draw_koch(draw, p0, p1, depth, max_depth): 递归绘制科赫曲线 :param draw: PIL.ImageDraw.Draw 对象 :param p0: 起点坐标 (x0, y0) :param p1: 终点坐标 (x1, y1) :param depth: 当前递归深度 :param max_depth: 最大递归深度 # 如果达到最大深度直接绘制当前线段 if depth max_depth: draw.line([p0, p1], fillline_color, widthline_width) return # 计算线段的三等分点 x0, y0 p0 x1, y1 p1 # 计算向量 dx (x1 - x0) / 3.0 dy (y1 - y0) / 3.0 # 点 P2: 第一个三等分点 p2 (x0 dx, y0 dy) # 点 P4: 第二个三等分点 p4 (x0 2 * dx, y0 2 * dy) # 计算尖顶点 P3 # 首先计算线段 P2P4 的中点 M mx (p2[0] p4[0]) / 2.0 my (p2[1] p4[1]) / 2.0 # 计算向量 P2-P4 并求其垂直向量法向量 vx p4[0] - p2[0] vy p4[1] - p2[1] # 法向量将(vx, vy)逆时针旋转90度得到(-vy, vx)然后单位化 length_v math.sqrt(vx**2 vy**2) if length_v 0: return # 避免除零错误理论上不会发生 nx -vy / length_v ny vx / length_v # 等边三角形的高 h (sqrt(3)/2) * (P2P4的长度) / 2? 这里需要仔细推导。 # 实际上P2P4的长度是原线段长度的 1/3。 # 我们需要构造一个以P2P4为底边的等边三角形其高 h (sqrt(3)/2) * |P2P4| # 但注意我们构造的尖顶方向需要一致对于雪花所有尖顶朝外。 # 通过向量点乘判断方向确保法向量指向外侧这里简化处理通过初始三角形顶点顺序保证。 # 更通用的方法是使用一个固定的“旋转方向”参数。 h (math.sqrt(3) / 2.0) * length_v # 尖顶点 P3 p3 (mx nx * h, my ny * h) # 递归绘制四段新的线段 new_depth depth 1 draw_koch(draw, p0, p2, new_depth, max_depth) # P0 - P2 draw_koch(draw, p2, p3, new_depth, max_depth) # P2 - P3 draw_koch(draw, p3, p4, new_depth, max_depth) # P3 - P4 draw_koch(draw, p4, p1, new_depth, max_depth) # P4 - P1代码关键点解析递归终止条件if depth max_depth:当当前深度达到设定的最大深度时不再继续分割直接绘制这条线段。这是所有递归函数的出口必不可少。坐标计算全部使用浮点数进行计算 (/ 3.0)以保证精度避免在多次递归后坐标误差累积。法向量计算(-vy, vx)实现了将向量(vx, vy)逆时针旋转90度。这是计算垂直向量的标准方法之一。单位化 (/ length_v) 是为了确保后续乘以高度h时位移量是正确的。高度计算h (math.sqrt(3) / 2.0) * length_v。这里的length_v是底边P2P4的长度对于等边三角形高等于(根号3 / 2) * 边长。递归调用函数最后调用了自身四次分别处理新生长的四条线段。这是分形绘制的精髓所在。3.3 整合与主程序在行空板屏幕上绘制雪花有了核心的draw_koch函数我们现在需要初始化行空板屏幕定义一个等边三角形作为起点并调用函数绘制三条边。# 主程序 def main(): global line_color, line_width # 声明为全局变量以便调整 line_color (0, 0, 255) # 线条颜色RGB格式这里是蓝色 line_width 2 # 线条宽度 # 初始化行空板GUI gui unihiker.GUI() # 获取屏幕尺寸行空板K10为240*320 screen_width 240 screen_height 320 # 创建一个与屏幕大小相同的空白图像白色背景 img Image.new(RGB, (screen_width, screen_height), colorwhite) draw ImageDraw.Draw(img) # 定义初始等边三角形的顶点 # 为了使雪花居中并大小合适我们需要计算顶点坐标 center_x screen_width // 2 center_y screen_height // 2 radius 100 # 三角形外接圆半径 # 计算等边三角形的三个顶点顶点朝上 # 第一个顶点在正上方 pA (center_x, center_y - radius) # 第二个顶点在左下旋转120度 angle math.radians(120) pB (center_x radius * math.sin(angle), center_y - radius * math.cos(angle)) # 第三个顶点在右下旋转240度 angle math.radians(240) pC (center_x radius * math.sin(angle), center_y - radius * math.cos(angle)) max_depth 4 # 设置递归深度从0开始。4或5的效果就很好 # 绘制三条边构成科赫雪花 print(开始绘制科赫雪花深度:, max_depth) start_time time.time() draw_koch(draw, pA, pB, 0, max_depth) # 边 AB draw_koch(draw, pB, pC, 0, max_depth) # 边 BC draw_koch(draw, pC, pA, 0, max_depth) # 边 CA end_time time.time() print(f绘制完成耗时: {end_time - start_time:.2f} 秒) # 将绘制好的图像显示在行空板屏幕上 gui.show_image(img) # 保持程序运行直到触摸屏幕退出 print(触摸屏幕退出程序) gui.wait_touch() print(程序结束) if __name__ __main__: main()主程序要点初始化创建unihiker.GUI()对象和PIL.Image画布。计算顶点通过三角函数计算一个位于屏幕中央、顶点朝上的等边三角形的三个顶点。这是科赫雪花的“种子”。设置深度max_depth控制雪花的复杂程度。深度为0就是原始三角形为1是第一次迭代后的六角星为4或5时已经具有非常丰富的细节。可以根据性能和个人喜好调整。调用绘制分别对三角形的三条边调用draw_koch函数起始深度均为0。显示与交互使用gui.show_image(img)将最终图像一次性显示到屏幕上。gui.wait_touch()让程序等待用户触摸屏幕后再退出这样我们可以从容欣赏绘制结果。将以上所有代码块按顺序放入Jupyter Notebook的一个单元格或一个Python脚本文件中运行即可在行空板K10的屏幕上看到生成的蓝色科赫雪花。4. 效果优化与功能扩展实战基础的雪花绘制完成后我们可以从视觉和交互上进行多种优化让项目更有趣。4.1 视觉增强颜色渐变与动画效果单一的蓝色线条看久了会有些单调。我们可以根据递归深度来改变线条颜色创造出从中心到边缘的渐变效果。修改draw_koch函数中的绘制部分def draw_koch(draw, p0, p1, depth, max_depth): if depth max_depth: # 根据深度计算颜色实现渐变 # 例如从深蓝(0,0,100)渐变到浅蓝(135, 206, 250) ratio depth / max_depth # 注意这里用depth/max_depth越深的递归颜色越浅 # 更合理的可能是越接近最终层depth大颜色越深或越亮。 # 方案一颜色随当前depth变化最终层都一样 # r int(0 ratio * 135) # g int(0 ratio * 206) # b int(100 ratio * 150) # 方案二颜色随递归“代际”变化给draw函数传递一个基础色参数更好。 # 这里采用一个简单方案根据 (max_depth - depth) 来变化 color_ratio (max_depth - depth) / max_depth r int(30 color_ratio * 200) g int(60 color_ratio * 150) b int(200 - color_ratio * 100) current_color (r, g, b) draw.line([p0, p1], fillcurrent_color, widthline_width) return # ... 其余递归计算代码不变 ...同时移除全局的line_color定义因为颜色现在由函数内部动态计算。添加生长动画 如果想看到雪花一步步“生长”的过程而不是瞬间出现我们可以修改主程序在每次递归调用后都更新一次屏幕。但这会严重降低绘制速度因为涉及大量的屏幕刷新。一个折中的办法是每完成一条完整边的绘制即从深度0递归到最大深度就更新一次屏幕。# 在主程序绘制循环中修改 import time gui unihiker.GUI() img Image.new(RGB, (screen_width, screen_height), colorwhite) draw ImageDraw.Draw(img) # 先显示空白背景 gui.show_image(img) time.sleep(0.5) max_depth 4 # 我们可以分边绘制每画完一条边就更新一次屏幕 edges [(pA, pB), (pB, pC), (pC, pA)] for i, (start, end) in enumerate(edges): draw_koch(draw, start, end, 0, max_depth) gui.show_image(img) # 更新屏幕显示 print(f已完成第 {i1} 条边) time.sleep(0.3) # 短暂停顿让生长过程可见这样用户就能看到雪花从一条边开始逐步“生长”完整的过程体验感更强。4.2 交互功能利用硬件按钮控制参数行空板K10侧面有A、B两个物理按钮我们可以利用它们来增加交互性例如切换递归深度或改变颜色模式。首先需要导入按钮事件处理模块并在主程序中设置事件监听。from unihiker import GUI import time def on_button_a_pressed(): 按钮A按下事件增加递归深度 global max_depth, img, draw, gui if max_depth 6: # 设置一个上限避免递归太深导致卡顿或内存问题 max_depth 1 redraw_snowflake() def on_button_b_pressed(): 按钮B按下事件切换颜色模式 global color_mode, img, draw, gui color_mode (color_mode 1) % 3 # 假设有3种颜色模式 redraw_snowflake() def redraw_snowflake(): 清空画布并重新绘制雪花 global img, draw, gui, max_depth, color_mode, pA, pB, pC # 用白色填充清空画布 draw.rectangle([(0,0), (screen_width, screen_height)], fillwhite) # 根据新的max_depth或color_mode重新绘制 # 这里需要修改draw_koch函数使其能接收color_mode参数并影响颜色计算 # 为了简化示例我们假设有一个新的绘制函数 draw_koch_advanced draw_koch_advanced(draw, pA, pB, 0, max_depth, color_mode) draw_koch_advanced(draw, pB, pC, 0, max_depth, color_mode) draw_koch_advanced(draw, pC, pA, 0, max_depth, color_mode) gui.show_image(img) print(f已更新深度{max_depth}, 颜色模式{color_mode}) def main(): global max_depth, color_mode, screen_width, screen_height, pA, pB, pC, gui, img, draw # 初始化变量 max_depth 3 color_mode 0 # 0:蓝色渐变 1:红色渐变 2:彩虹色 screen_width 240 screen_height 320 gui unihiker.GUI() img Image.new(RGB, (screen_width, screen_height), colorwhite) draw ImageDraw.Draw(img) # 计算三角形顶点同上 center_x screen_width // 2 center_y screen_height // 2 radius 100 pA (center_x, center_y - radius) angle math.radians(120) pB (center_x radius * math.sin(angle), center_y - radius * math.cos(angle)) angle math.radians(240) pC (center_x radius * math.sin(angle), center_y - radius * math.cos(angle)) # 绑定按钮事件 gui.on_button_a_pressed(on_button_a_pressed) gui.on_button_b_pressed(on_button_b_pressed) # 初始绘制 redraw_snowflake() print(使用按钮A增加深度按钮B切换颜色。触摸屏幕退出。) # 这里不再使用 gui.wait_touch()而是用一个循环保持程序运行 try: while True: time.sleep(0.1) # 降低CPU占用 except KeyboardInterrupt: print(程序被中断)draw_koch_advanced函数需要整合之前提到的根据深度和color_mode计算颜色的逻辑。通过这样的改造我们就得到了一个可以通过物理按钮交互的动态科赫雪花生成器可玩性大大提升。5. 常见问题排查与性能调优在实际操作中你可能会遇到以下问题。这里我把自己踩过的坑和解决方案总结一下。5.1 绘图异常与坐标计算纠偏问题1雪花形状扭曲不像等边三角形排查这几乎总是由于初始三角形顶点计算错误或者屏幕坐标系Y轴向下与数学坐标系Y轴向上的差异导致的。检查计算顶点时使用的sin和cos函数是否正确。在我的代码中pA (center_x, center_y - radius)是正上方因为屏幕坐标Y值增加是向下。解决确保三角函数参数是弧度制并使用math.radians()进行转换。仔细核对pB和pC的计算公式。可以先将max_depth设为0只画初始三角形看是否为一个端正的等边三角形。问题2递归深度稍大如5程序卡死或无响应排查这是递归算法固有的问题。科赫曲线的递归调用次数呈指数增长4^n。深度为5时最终需要绘制的线段数量是 3 * 4^5 3072 条。深度为6时是12288条。计算和绘制都需要时间。解决限制深度在交互设置中将最大深度限制在5或6。优化绘制使用draw.line一次绘制多条线段传入点列表的效率可能低于多次调用draw.line绘制单条线段因为PIL内部可能优化了连续线段绘制。但我们的递归结构是分散的不易合并。可以尝试先将所有需要绘制的线段端点收集到一个列表中最后一次性用draw.line绘制。但这会消耗更多内存。视觉妥协对于行空板K10的小屏幕深度4的细节已经非常丰富深度5是视觉和性能的较好平衡点。问题3线条有锯齿不够平滑排查这是低分辨率屏幕和整数坐标绘制的必然结果。PIL的draw.line在低分辨率下抗锯齿能力有限。解决增加虚拟分辨率我们可以在一个尺寸更大的Image上绘制例如480x640然后使用Image.resize方法缩放到屏幕尺寸240x320并指定Image.ANTIALIAS(Pillow老版本) 或Image.Resampling.LANCZOS(新版本) 重采样滤波器可以有效抗锯齿。large_width, large_height 480, 640 img_large Image.new(RGB, (large_width, large_height), white) draw_large ImageDraw.Draw(img_large) # ... 在 draw_large 上绘制所有坐标按比例放大 ... # 绘制完成后 img_small img_large.resize((screen_width, screen_height), Image.Resampling.LANCZOS) gui.show_image(img_small)调整线条宽度将line_width设置为1有时观感上锯齿感会减弱。5.2 行空板特定问题与资源管理问题4导入unihiker失败排查行空板K10的官方系统镜像应该预装了unihiker。如果是从头配置的环境可能缺失。解决通过终端执行pip install unihiker安装。务必使用行空板官方的pip源或国内镜像源加速。问题5程序退出后屏幕残留或板子反应迟钝排查可能是事件监听没有正确取消或者循环没有正常退出导致资源未释放。解决确保使用try...except...finally结构来捕获异常并在finally块或异常处理中执行必要的清理。对于使用了gui.on_button_pressed的事件监听在程序退出前可以考虑使用gui.clear_button_event()来清除事件绑定如果库提供此方法。最简单的办法是直接重启行空板K10的程序在Jupyter中中断内核并重新运行这是开发调试时的常用操作。问题6想保存生成的雪花图片解决在调用gui.show_image(img)之后可以使用PIL的save方法。img.save(/home/pi/koch_snowflake_depth4.png) # 保存到指定路径行空板K10连接网络后可以通过SFTP或文件共享服务将图片传输到电脑上。这个项目从数学原理出发到代码实现再到硬件交互完成了一个完整的闭环。它不仅仅是画出一幅图更是理解递归、坐标变换、图形编程和硬件交互的绝佳练习。你可以尝试修改初始形状比如从正方形开始生成科赫“岛”或者尝试其他分形如谢尔宾斯基三角形、曼德博集行空板K10完全有能力成为你探索数学可视化世界的画板。