基于行空板K10的科赫雪花分形算法实现与图形编程实践
2026/7/29 8:44:45 网站建设 项目流程

1. 项目概述:当数学之美遇见开源硬件

最近在捣鼓行空板K10,想找个项目既能展示它的图形能力,又能带点数学的趣味性。思来想去,科赫雪花这个经典的数学分形图案就跳进了脑海。它规则简单,但迭代出来的图形却无比复杂和精美,非常适合用来作为编程和硬件结合的入门实践。这个项目,说白了,就是利用行空板K10的Python环境和屏幕,通过代码“生长”出一朵科赫雪花。

科赫雪花,也叫科赫曲线或雪花曲线,是瑞典数学家海里格·冯·科赫在1904年提出的一种分形。它的构造规则非常直观:从一个等边三角形开始,把每条边中间的三分之一段去掉,然后用一个去掉底边的等边三角形来替代。这个过程无限重复下去,得到的曲线周长会趋向于无穷大,但面积却是有限的,这种反直觉的特性正是分形的魅力所在。对于行空板K10来说,这个项目能很好地锻炼几个核心能力:一是理解递归或迭代的编程思想,二是掌握基本的图形绘制接口,三是将抽象的数学公式转化为屏幕上可见的、动态的视觉艺术。

无论你是对分形数学感兴趣的学生,还是想找点有趣项目练手的硬件爱好者,亦或是STEAM教育的老师,这个项目都很有价值。它不需要复杂的电路连接,核心全在代码逻辑上,但最终呈现的效果却能让人直观感受到数学与编程结合的力量。接下来,我就把从思路拆解到代码实现,再到调试优化的全过程,毫无保留地分享出来。

2. 核心思路与算法设计拆解

2.1 科赫雪花生成原理与算法选择

科赫雪花的生成,本质上是一个递归过程。我们首先要理解其最基础的构造单元:科赫曲线。给定一条线段,我们需要找到它的两个端点,然后计算出在这条线段上“生长”出一个小尖顶所需的四个新点(加上原来的两个端点,共六个点,但实际绘制是连接其中五个点)。

具体计算如下:假设线段起点为P0(x0, y0),终点为P1(x1, y1)

  1. 第一个点:就是起点P0
  2. 第二个点:将线段三等分,第一个等分点P2P2 = P0 + (P1 - P0) / 3
  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))是等边三角形的高(当底边为原线段三分之一长时)。
  4. 第四个点:线段的第二个三等分点P4P4 = P0 + 2 * (P1 - P0) / 3
  5. 第五个点:就是终点P1

递归过程就是:对于一条线段,如果递归深度为0,则直接绘制这条线段;否则,将这条线段看作上述五个点(P0, P2, P3, P4, P1)顺序连接的四条新线段,并对这四条新线段分别进行递归深度减1的相同操作。

对于科赫雪花,我们从一个等边三角形开始,对这个三角形的三条边分别执行上述科赫曲线的递归过程即可。

算法选择:我们采用递归算法来实现。递归代码简洁,直接反映了科赫曲线的分形定义。虽然对于极深的递归可能存在栈溢出风险,但行空板K10的屏幕分辨率有限,递归深度在5-6层时图形细节就已足够丰富,完全在安全范围内。

2.2 行空板K10图形绘制方案选型

行空板K10预装了Python,并提供了丰富的库支持。对于图形绘制,主要有两个选择:Turtle海龟绘图库和PIL(Python Imaging Library) 结合unihiker库的GUI功能。

  1. Turtle库

    • 优点:极其简单直观,命令如forward,left,非常适合教学和理解图形学基础。绘制过程是动态可视的,可以看到雪花“生长”的过程。
    • 缺点:绘制效率相对较低,对于复杂图形或较深递归,速度会变慢。可控性稍弱,例如精确控制绘图位置和缩放需要一些计算。
  2. PIL + unihiker GUI

    • 优点:功能强大且灵活。我们可以先在内存中的图像对象上绘制,然后一次性更新到屏幕上,效率高。可以轻松实现颜色渐变、背景图、动态效果(如旋转、缩放)等高级功能。unihiker库提供了对行空板屏幕、按钮等硬件的直接控制,交互性更强。
    • 缺点:需要手动计算所有点的坐标并进行连线,代码比Turtle稍复杂。

我的选择与理由:为了更充分地展示行空板K10的性能和实现更丰富的视觉效果,我选择PIL+unihikerGUI的方案。虽然入门门槛比Turtle略高一点,但它能让我们更深入地控制绘图过程,并且为后续添加交互功能(如通过按钮改变递归深度、切换颜色)留下了巨大空间。此外,一次性绘制的效率也更高,体验更流畅。

注意:行空板K10的unihiker库已经为我们封装好了屏幕显示和事件处理,我们只需要关注在PIL.ImagePIL.ImageDraw对象上绘图即可,非常方便。

3. 开发环境搭建与核心代码实现

3.1 行空板K10环境准备与依赖安装

行空板K10开机即用,其基于Linux系统并预装了完整的Python环境。我们主要通过SSH或者直接使用板载的Jupyter Notebook服务来编写和运行代码。这里我推荐使用Jupyter Notebook,因为它可以边写边运行,即时看到图形结果,调试非常方便。

  1. 连接行空板:将行空板通过USB线连接到电脑,或者使其连接到与电脑相同的Wi-Fi网络。在电脑浏览器中输入行空板屏幕上显示的IP地址和端口号(通常是http://192.168.x.x:8888),即可访问Jupyter Notebook。
  2. 确认库可用:新建一个Notebook文件。首先,我们检查并导入必要的库。
    # 检查unihiker和PIL库 import unihiker from PIL import Image, ImageDraw import math import time
    如果unihikerPIL导入失败(极少数情况),可能需要通过终端安装。在Jupyter Notebook的单元格中运行:
    !pip install unihiker -i https://mirrors.aliyun.com/pypi/simple/ !pip install Pillow -i https://mirrors.aliyun.com/pypi/simple/
    PillowPIL的一个友好分支,功能完全兼容。使用国内镜像源可以加速下载。

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], fill=line_color, width=line_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), color='white') 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()

主程序要点

  1. 初始化:创建unihiker.GUI()对象和PIL.Image画布。
  2. 计算顶点:通过三角函数计算一个位于屏幕中央、顶点朝上的等边三角形的三个顶点。这是科赫雪花的“种子”。
  3. 设置深度max_depth控制雪花的复杂程度。深度为0就是原始三角形,为1是第一次迭代后的六角星,为4或5时已经具有非常丰富的细节。可以根据性能和个人喜好调整。
  4. 调用绘制:分别对三角形的三条边调用draw_koch函数,起始深度均为0。
  5. 显示与交互:使用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], fill=current_color, width=line_width) return # ... 其余递归计算代码不变 ...

同时,移除全局的line_color定义,因为颜色现在由函数内部动态计算。

添加生长动画: 如果想看到雪花一步步“生长”的过程,而不是瞬间出现,我们可以修改主程序,在每次递归调用后都更新一次屏幕。但这会严重降低绘制速度,因为涉及大量的屏幕刷新。一个折中的办法是,每完成一条完整边的绘制(即从深度0递归到最大深度),就更新一次屏幕。

# 在主程序绘制循环中修改 import time gui = unihiker.GUI() img = Image.new('RGB', (screen_width, screen_height), color='white') 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"已完成第 {i+1} 条边") 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)], fill='white') # 根据新的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), color='white') 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轴向上)的差异导致的。检查计算顶点时使用的sincos函数是否正确。在我的代码中,pA = (center_x, center_y - radius)是正上方,因为屏幕坐标Y值增加是向下。
    • 解决:确保三角函数参数是弧度制,并使用math.radians()进行转换。仔细核对pBpC的计算公式。可以先将max_depth设为0,只画初始三角形,看是否为一个端正的等边三角形。
  • 问题2:递归深度稍大(如>5)程序卡死或无响应

    • 排查:这是递归算法固有的问题。科赫曲线的递归调用次数呈指数增长(4^n)。深度为5时,最终需要绘制的线段数量是 3 * 4^5 = 3072 条。深度为6时,是12288条。计算和绘制都需要时间。
    • 解决
      1. 限制深度:在交互设置中,将最大深度限制在5或6。
      2. 优化绘制:使用draw.line一次绘制多条线段(传入点列表)的效率,可能低于多次调用draw.line绘制单条线段,因为PIL内部可能优化了连续线段绘制。但我们的递归结构是分散的,不易合并。可以尝试先将所有需要绘制的线段端点收集到一个列表中,最后一次性用draw.line绘制。但这会消耗更多内存。
      3. 视觉妥协:对于行空板K10的小屏幕,深度4的细节已经非常丰富,深度5是视觉和性能的较好平衡点。
  • 问题3:线条有锯齿,不够平滑

    • 排查:这是低分辨率屏幕和整数坐标绘制的必然结果。PIL的draw.line在低分辨率下抗锯齿能力有限。
    • 解决
      1. 增加虚拟分辨率:我们可以在一个尺寸更大的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)
      1. 调整线条宽度:将line_width设置为1,有时观感上锯齿感会减弱。

5.2 行空板特定问题与资源管理

  • 问题4:导入unihiker失败

    • 排查:行空板K10的官方系统镜像应该预装了unihiker。如果是从头配置的环境,可能缺失。
    • 解决:通过终端执行pip install unihiker安装。务必使用行空板官方的pip源或国内镜像源加速。
  • 问题5:程序退出后,屏幕残留或板子反应迟钝

    • 排查:可能是事件监听没有正确取消,或者循环没有正常退出,导致资源未释放。
    • 解决
      1. 确保使用try...except...finally结构来捕获异常,并在finally块或异常处理中执行必要的清理。
      2. 对于使用了gui.on_button_pressed的事件监听,在程序退出前,可以考虑使用gui.clear_button_event()来清除事件绑定(如果库提供此方法)。
      3. 最简单的办法是:直接重启行空板K10的程序(在Jupyter中中断内核并重新运行),这是开发调试时的常用操作。
  • 问题6:想保存生成的雪花图片

    • 解决:在调用gui.show_image(img)之后,可以使用PIL的save方法。
      img.save('/home/pi/koch_snowflake_depth4.png') # 保存到指定路径
      行空板K10连接网络后,可以通过SFTP或文件共享服务将图片传输到电脑上。

这个项目从数学原理出发,到代码实现,再到硬件交互,完成了一个完整的闭环。它不仅仅是画出一幅图,更是理解递归、坐标变换、图形编程和硬件交互的绝佳练习。你可以尝试修改初始形状(比如从正方形开始生成科赫“岛”),或者尝试其他分形如谢尔宾斯基三角形、曼德博集,行空板K10完全有能力成为你探索数学可视化世界的画板。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询