刘志礼 發表於 2026-3-7 10:40:00

探索视觉的边界:用 Manim 重现有趣的知觉错觉

<blockquote>
<p>这些错觉以清晰而明确的方式告诉我们:我们并非直接体验这个世界。</p>
</blockquote>
<p>我们常常相信“眼见为实”,但知觉错觉告诉我们:事实并非如此。</p>
<p>我们的大脑并非直接复制世界,而是在构建一个基于经验与期望的“最佳猜测模型”。</p>
<p>今天,我们将通过 5 种经典的知觉错觉,来探索视觉的奥秘。</p>
<p>前三种是静态图像错觉,后两种则是动态错觉,我们将尝试用<code>Manim</code>来重现它们的动态效果。</p>
<h1 id="1-静态的欺骗">1. 静态的欺骗</h1>
<p>这三种错觉不需要动画,仅仅通过静态的排列和色彩对比,就能欺骗我们的大脑。</p>
<h2 id="11-彩纸屑错觉">1.1. 彩纸屑错觉</h2>
<p>这是<code>David Novick</code>创作的<code>Munker</code>错觉的变体。</p>
<p>下面图中所有的圆圈颜色完全相同,唯一不同的是围绕它们的线条颜色。</p>

<p><img src="https://img2024.cnblogs.com/blog/83005/202603/83005-20260307103600195-1933624528.png" alt="" loading="lazy"></p>
<p>这个错觉生动地证明:我们并非直接感知物体在现实中的颜色。相反,知觉系统会根据物体周围的环境,做出一个有根据的"猜测"。</p>
<h2 id="12-米饭波浪错觉">1.2. 米饭波浪错觉</h2>
<p>这看起来像是一个动态GIF,但其实不是。所有的"运动"都发生在你的大脑中。</p>
<p>它的作者:<code>Akiyoshi Kitaoka</code>。</p>

<p><img src="https://img2024.cnblogs.com/blog/83005/202603/83005-20260307103600152-1739593920.png" alt="" loading="lazy"></p>
<p>黄色斑块的阴影和排列顺序会触发大脑的运动感知区域,从而在一个实际静止的图像中产生运动的知觉。有趣的是,大约5%的人似乎对这个错觉"免疫"。</p>
<h2 id="13-倾斜道路错觉">1.3. 倾斜道路错觉</h2>
<p>这看起来像是从不同角度拍摄的同一道路的两张照片。但实际上,这只是同一张照片复制了两次。</p>

<p><img src="https://img2024.cnblogs.com/blog/83005/202603/83005-20260307103600229-136614659.png" alt="" loading="lazy"></p>
<p>显然,视觉系统将这张图像当作两张独立道路的照片来处理。在二维图像中,两条道路的轮廓是相互平行的。</p>
<p>如果现实世界中的两条道路在图像中呈现这种效果,那么它们在现实中必须是强烈地向外倾斜的。因此,视觉系统便做出了这样的推断。</p>
<h1 id="2-动态的魔法">2. 动态的魔法</h1>
<p>接下来,我们使用 <code>Manim</code> 来制作后两种动态错觉。</p>
<h2 id="21-动态艾宾浩斯错觉">2.1. 动态艾宾浩斯错觉</h2>
<p>图中的橙色圆圈实际上并没有改变大小。</p>

<p><img src="https://img2024.cnblogs.com/blog/83005/202603/83005-20260307103600201-1673076250.gif" alt="" loading="lazy"></p>
<p>与颜色和明度一样,我们并非直接感知物体的大小。知觉系统会根据感官数据中的线索(包括附近其他物体的相对大小)来推断物体的尺寸。</p>
<p><code>Manim</code>代码:</p>
<pre><code class="language-python">from manim import *

config.background_color = WHITE


class DynamicEbbinghaus(Scene):
    def construct(self):
      # 中心圆圈(实际大小不变)
      center_circle = Circle(radius=0.3, color=ORANGE, fill_opacity=1)
      center_circle.set_stroke(width=0)
      center_circle.move_to(LEFT * 2 + UP * 2)

      center_circle2 = center_circle.copy()
      center_circle2.move_to(ORIGIN)

      # 周围圆圈
      surrounding_circles = VGroup()
      surrounding_circles2 = VGroup()
      num_circles = 6
      radius = 0.1
      distance = 0.4
      radius2 = 0.7
      distance2 = 1.5

      for i in range(num_circles):
            angle = i * (360 / num_circles) * DEGREES
            circle = Circle(radius=radius, color=PURE_BLUE, fill_opacity=1)
            circle.set_stroke(width=0)
            circle.move_to(
                center_circle.get_center()
                + distance * np.array()
            )
            surrounding_circles.add(circle)

            circle2 = Circle(radius=radius2, color=PURE_BLUE, fill_opacity=1)
            circle2.set_stroke(width=0)
            circle2.move_to(
                center_circle2.get_center()
                + distance2 * np.array()
            )
            surrounding_circles2.add(circle2)

      self.add(center_circle, surrounding_circles)
      self.wait(0.5)

      a_group = VGroup(center_circle, surrounding_circles)
      a_group2 = a_group.copy()
      b_group = VGroup(center_circle2, surrounding_circles2)

      # 正常移动
      self.play(a_group.animate.move_to(b_group.get_center()), run_time=2)
      self.play(a_group.animate.move_to(a_group2.get_center()), run_time=2)
      self.wait(1)

      # 放大蓝色小圆
      # 动画:周围圆圈变大,使中心圆圈看起来变小
      self.play(
            ReplacementTransform(a_group, b_group),
            run_time=2,
      )

      # 动画:周围圆圈变小,使中心圆圈看起来变大
      self.play(
            ReplacementTransform(b_group, a_group2),
            run_time=2,
      )
      self.wait(1)

</code></pre>
<h2 id="22-动态穆勒-莱尔错觉">2.2. 动态穆勒-莱尔错觉</h2>
<p>这是我见过最棒的错觉之一。蓝色和红色的线条长度完全相同;没有任何线条在移动或改变大小,它们都处于同一水平线上。只有两端的箭头在移动。</p>
<p>这个错觉是经典"穆勒-莱尔错觉"的新变体。关于它的原理有许多理论,但没有人能100%确定。甚至还存在争议:这种错觉是适用于全人类,还是某种特定文化下的现象?</p>

<p><img src="https://img2024.cnblogs.com/blog/83005/202603/83005-20260307103600286-704719054.gif" alt="" loading="lazy"></p>
<p><code>Manim</code>代码:</p>
<pre><code class="language-python">from manim import *
import numpy as np

config.background_color = WHITE


class DynamicMullerLyer(Scene):
    def construct(self):
      self.vertexes = []
      count = 11

      # 所有线都一样长,蓝色和红色的线段也是一样长。
      lines = self.create_lines(count)
      self.play(Create(lines))
      self.wait(1)
      self.clear()

      wings = self.create_wings(self.vertexes)
      self.add(*wings)
      self.rotate_wings(
            wings,
            self.vertexes,
            list(np.random.uniform(0.5, 1.5, len(wings))),
            repeat=4,
      )
      self.wait(1)

      # 放在一起
      self.add(lines)
      self.rotate_wings(
            wings,
            self.vertexes,
            list(np.random.uniform(0.5, 1.5, len(wings))),
            repeat=8,
      )

      self.wait(0.5)

    def create_lines(self, count=11, interval=0.4) -&gt; VGroup:
      l_group = VGroup()

      for i in range(count // 2 + 1):
            vertical_l_group = VGroup()
            vertical_l_group.add(
                Line(UP * 2.5, UP * 1.5, stroke_width=2, color=PURE_BLUE)
            )
            vertical_l_group.add(
                Line(UP * 1.5, UP * 0.5, stroke_width=2, color=PURE_RED)
            )
            vertical_l_group.add(
                Line(UP * 0.5, DOWN * 0.5, stroke_width=2, color=PURE_BLUE)
            )
            vertical_l_group.add(
                Line(DOWN * 0.5, DOWN * 1.5, stroke_width=2, color=PURE_RED)
            )
            vertical_l_group.add(
                Line(DOWN * 1.5, DOWN * 2.5, stroke_width=2, color=PURE_BLUE)
            )
            vertical_l_group.shift(LEFT * i * interval)
            self.vertexes.append(UP * 2.5 + LEFT * i * interval)
            self.vertexes.append(UP * 1.5 + LEFT * i * interval)
            self.vertexes.append(UP * 0.5 + LEFT * i * interval)
            self.vertexes.append(DOWN * 0.5 + LEFT * i * interval)
            self.vertexes.append(DOWN * 1.5 + LEFT * i * interval)
            self.vertexes.append(DOWN * 2.5 + LEFT * i * interval)
            l_group.add(vertical_l_group)

      for i in range(1, count // 2 + 1):
            vertical_l_group = VGroup()
            vertical_l_group.add(
                Line(UP * 2.5, UP * 1.5, stroke_width=2, color=PURE_BLUE)
            )
            vertical_l_group.add(
                Line(UP * 1.5, UP * 0.5, stroke_width=2, color=PURE_RED)
            )
            vertical_l_group.add(
                Line(UP * 0.5, DOWN * 0.5, stroke_width=2, color=PURE_BLUE)
            )
            vertical_l_group.add(
                Line(DOWN * 0.5, DOWN * 1.5, stroke_width=2, color=PURE_RED)
            )
            vertical_l_group.add(
                Line(DOWN * 1.5, DOWN * 2.5, stroke_width=2, color=PURE_BLUE)
            )
            vertical_l_group.shift(RIGHT * i * interval)
            self.vertexes.append(UP * 2.5 + RIGHT * i * interval)
            self.vertexes.append(UP * 1.5 + RIGHT * i * interval)
            self.vertexes.append(UP * 0.5 + RIGHT * i * interval)
            self.vertexes.append(DOWN * 0.5 + RIGHT * i * interval)
            self.vertexes.append(DOWN * 1.5 + RIGHT * i * interval)
            self.vertexes.append(DOWN * 2.5 + RIGHT * i * interval)
            l_group.add(vertical_l_group)

      return l_group

    def create_wings(self, vertexes, wing_radio=0.1):
      groups = []
      # 创建两条线,呈V字形
      for vertex in vertexes:
            left_line = Line(
                vertex, vertex + (UP + LEFT) * wing_radio, stroke_width=2, color=BLACK
            )
            right_line = Line(
                vertex, vertex + (UP + RIGHT) * wing_radio, stroke_width=2, color=BLACK
            )

            groups.append(VGroup(left_line, right_line))

      return groups

    def rotate_wings(self, wings, vertexes, run_times, repeat=4):

      anims = []
      for i in range(len(wings)):
            ag1 = AnimationGroup(
                Rotate(
                  wings, angle=90 * DEGREES, about_point=vertexes
                ).set_run_time(run_times),
                Rotate(
                  wings, angle=-90 * DEGREES, about_point=vertexes
                ).set_run_time(run_times),
            )
            ag2 = AnimationGroup(
                Rotate(
                  wings, angle=-90 * DEGREES, about_point=vertexes
                ).set_run_time(run_times),
                Rotate(
                  wings, angle=90 * DEGREES, about_point=vertexes
                ).set_run_time(run_times),
            )

            anim = Succession( * repeat)
            anims.append(anim)

      self.play(
            AnimationGroup(*anims),
            run_time=max(run_times) * repeat,
      )

</code></pre>
<h1 id="3-总结">3. 总结</h1>
<p>这些错觉共同揭示了一个深刻的事实——我们的知觉并非对世界的“直接复制”,而是大脑基于有限感官信息、结合经验与期望所构建的“最佳猜测模型”。</p>
<p>通过 <code>Manim</code> 重现这些错觉,我们不仅理解了视觉心理学,也掌握了如何用代码精确控制视觉元素来传达信息。</p>
<p>理解这一点,不仅能让我们更谦逊地看待自己的认知,也能帮助我们在日常生活中更理性地判断所见所闻。</p><br><br>
来源:https://www.cnblogs.com/wang_yb/p/19682287
頁: [1]
查看完整版本: 探索视觉的边界:用 Manim 重现有趣的知觉错觉