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

0 阅读5分钟

这些错觉以清晰而明确的方式告诉我们:我们并非直接体验这个世界。

我们常常相信“眼见为实”,但知觉错觉告诉我们:事实并非如此。

我们的大脑并非直接复制世界,而是在构建一个基于经验与期望的“最佳猜测模型”。

今天,我们将通过 5 种经典的知觉错觉,来探索视觉的奥秘。

前三种是静态图像错觉,后两种则是动态错觉,我们将尝试用Manim来重现它们的动态效果。

1. 静态的欺骗

这三种错觉不需要动画,仅仅通过静态的排列和色彩对比,就能欺骗我们的大脑。

1.1. 彩纸屑错觉

这是David Novick创作的Munker错觉的变体。

下面图中所有的圆圈颜色完全相同,唯一不同的是围绕它们的线条颜色。

这个错觉生动地证明:我们并非直接感知物体在现实中的颜色。相反,知觉系统会根据物体周围的环境,做出一个有根据的"猜测"。

1.2. 米饭波浪错觉

这看起来像是一个动态GIF,但其实不是。所有的"运动"都发生在你的大脑中。

它的作者:Akiyoshi Kitaoka

黄色斑块的阴影和排列顺序会触发大脑的运动感知区域,从而在一个实际静止的图像中产生运动的知觉。有趣的是,大约5%的人似乎对这个错觉"免疫"。

1.3. 倾斜道路错觉

这看起来像是从不同角度拍摄的同一道路的两张照片。但实际上,这只是同一张照片复制了两次。

显然,视觉系统将这张图像当作两张独立道路的照片来处理。在二维图像中,两条道路的轮廓是相互平行的。

如果现实世界中的两条道路在图像中呈现这种效果,那么它们在现实中必须是强烈地向外倾斜的。因此,视觉系统便做出了这样的推断。

2. 动态的魔法

接下来,我们使用 Manim 来制作后两种动态错觉。

2.1. 动态艾宾浩斯错觉

图中的橙色圆圈实际上并没有改变大小。

与颜色和明度一样,我们并非直接感知物体的大小。知觉系统会根据感官数据中的线索(包括附近其他物体的相对大小)来推断物体的尺寸。

Manim代码:

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([np.cos(angle), np.sin(angle), 0])
            )
            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([np.cos(angle), np.sin(angle), 0])
            )
            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)

2.2. 动态穆勒-莱尔错觉

这是我见过最棒的错觉之一。蓝色和红色的线条长度完全相同;没有任何线条在移动或改变大小,它们都处于同一水平线上。只有两端的箭头在移动。

这个错觉是经典"穆勒-莱尔错觉"的新变体。关于它的原理有许多理论,但没有人能100%确定。甚至还存在争议:这种错觉是适用于全人类,还是某种特定文化下的现象?

Manim代码:

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) -> 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[i][0], angle=90 * DEGREES, about_point=vertexes[i]
                ).set_run_time(run_times[i]),
                Rotate(
                    wings[i][1], angle=-90 * DEGREES, about_point=vertexes[i]
                ).set_run_time(run_times[i]),
            )
            ag2 = AnimationGroup(
                Rotate(
                    wings[i][0], angle=-90 * DEGREES, about_point=vertexes[i]
                ).set_run_time(run_times[i]),
                Rotate(
                    wings[i][1], angle=90 * DEGREES, about_point=vertexes[i]
                ).set_run_time(run_times[i]),
            )

            anim = Succession([ag1, ag2] * repeat)
            anims.append(anim)

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

3. 总结

这些错觉共同揭示了一个深刻的事实——我们的知觉并非对世界的“直接复制”,而是大脑基于有限感官信息、结合经验与期望所构建的“最佳猜测模型”。

通过 Manim 重现这些错觉,我们不仅理解了视觉心理学,也掌握了如何用代码精确控制视觉元素来传达信息。

理解这一点,不仅能让我们更谦逊地看待自己的认知,也能帮助我们在日常生活中更理性地判断所见所闻。