OpenCV进阶(11)使用 OpenCV实现哈哈镜

我们都记得那些我们经常去游乐园或县集市的美好童年时光。这些游乐园中我最喜欢的元素之一是游乐园镜室。

哈哈镜不是平面镜,而是凸/凹反射面的组合,当我们在这些镜子前移动时,会产生扭曲效果,看起来很有趣。

在这篇文章中,我们将学习使用 OpenCV 创建我们自己的哈哈镜。

这篇文章的代码灵感来自项目 FunMirrors 和 VirtualCam。

这篇文章的主要动机是鼓励我们的读者学习基本概念,如成像几何,相机投影矩阵,相机的内在和外在参数。

在这篇文章结束时,您将能够意识到这样一个事实,即在对基本概念和理论有了清晰的认识之后,人们可以创造出真正有趣的东西。

1.成像几何理论

要了解世界中的 3D 点如何投影到相机的图像帧的理论,请阅读有关成像几何和相机校准这些帖子。

现在我们知道世界坐标中的 3D 点 ( X w , Y w , Z w ) (X_w,Y_w,Z_w) (Xw,Yw,Zw)根据以下等式映射到其对应的像素坐标,其中 P 是相机投影矩阵。
OpenCV进阶(11)使用 OpenCV实现哈哈镜_第1张图片
在这里插入图片描述
在这里插入图片描述

2.它是如何工作的 ?

整个项目可以分为三个主要步骤:

  • 1.创建一个虚拟摄像机。
  • 2.定义一个 3D 表面(镜面)并使用合适的投影矩阵值将其投影到虚拟相机中。
  • 3.利用三维表面投影点的图像坐标,应用基于网格的扭曲来获得哈哈镜的理想效果。

下图可能会帮助您更好地理解这些步骤。
OpenCV进阶(11)使用 OpenCV实现哈哈镜_第2张图片
创建哈哈镜所涉及的步骤。创建一个 3D 表面,即镜子(左),在虚拟相机中捕捉平面以获得相应的 2D 点,使用获得的 2D 点将基于网格的扭曲应用于图像,从而产生像一个哈哈镜的效果。

如果您不了解上述步骤,请不要担心。我们将详细解释每个步骤。

3.创建一个虚拟相机

基于上述理论,我们清楚地知道一个 3D 点是如何与其对应的图像坐标相关联的。

现在让我们了解虚拟相机的含义以及如何使用该虚拟相机捕捉图像。

虚拟相机本质上是矩阵 P,因为它告诉我们 3D 世界坐标和相应的图像像素坐标之间的关系。让我们看看如何使用 python 创建我们的虚拟相机。

我们将首先创建外部参数矩阵 (M1) 和内部参数矩阵 (K),并使用它们来创建相机投影矩阵 §。

import numpy as np

# 定义平移矩阵
# Tx,Ty,Tz 代表我们的虚拟相机在世界坐标系中的位置
T = np.array([[1,0,0,-Tx],[0,1,0,-Ty],[0,0,1,-Tz]])

# 定义旋转矩阵
# alpha,beta,gamma 定义虚拟摄像机的方向

Rx = np.array([[1, 0, 0], [0, math.cos(alpha), -math.sin(alpha)], [0, math.sin(alpha), math.cos(alpha)]])

Ry = np.array([[math.cos(beta), 0, -math.sin(beta)],[0, 1, 0],[math.sin(beta),0,math.cos(beta)]])

Rz = np.array([[math.cos(gamma), -math.sin(gamma), 0],[math.sin(gamma),math.cos(gamma), 0],[0, 0, 1]])

R = np.matmul(Rx, np.matmul(Ry, Rz))

# 计算外部相机参数矩阵 M1
M1 = np.matmul(R,T)

# 计算内在相机参数矩阵 K
# sx and sy 是 x 和 y 方向的表观像素长度
# ox and oy 是像平面中光学中心的坐标。
K = np.array([[-focus/sx,sh,ox],[0,focus/sy,oy],[0,0,1]])

P = np.matmul(K,RT)

请注意,您必须为上述矩阵中的所有参数设置合适的值,例如 focus、sx、sy、ox、oy 等。

那么我们如何用这个虚拟相机捕捉图像呢?

首先,我们假设原始图像或视频帧是 3D 平面。当然,我们知道场景在 3D 中实际上不是平面,但是我们没有图像中每个像素的深度信息。所以,我们简单地假设场景是平面的。请记住,我们的目标不是为了科学目的准确地模拟一个有趣的镜子。我们只是想近似它以供娱乐。

一旦我们将图像定义为3D中的平面,我们就可以简单地将矩阵P乘以世界坐标并得到像素坐标 ( u , v ) (u,v) (u,v)。应用这个变换与使用我们的虚拟相机捕捉3D点的图像是一样的。

我们如何决定捕获图像中像素的颜色?场景中物体的材质属性呢?

在渲染逼真的 3D 场景时,上述所有要点绝对很重要,但我们不必渲染逼真的场景。我们只是想做一些看起来很有趣的东西。

我们需要做的就是捕捉(投影)首先将原始图像(或视频帧)表示为虚拟相机中的3D平面,然后使用投影矩阵将这个平面上的每个点投影到虚拟相机的图像平面上。

那么我们该怎么做呢?简单的解决方案是使用嵌套的for循环,然后在所有点上循环,并执行这个转换。在python中,这是计算代价很高的。

因此我们使用 numpy 来做这样的计算。您可能知道,numpy 允许我们执行矢量化操作并消除使用循环的需要。这在计算上比使用嵌套 for 循环非常有效。

因此,我们将 3D 坐标存储为 numpy 数组 (W),将相机矩阵存储为 numpy 数组 § 并执行矩阵乘法 P*W 以捕获 3D 点。

在我们编写使用虚拟相机捕捉 3D 表面的代码之前,我们首先需要定义 3D 表面。

4.定义 3D 表面(镜子)

为了定义一个3D表面,我们形成一个X和Y坐标网格,然后计算每个点的Z坐标作为X和Y的函数。因此,对于平面镜,我们将定义Z = K,其中K是任意常数。下面的图显示了一些可以生成的镜面的例子。
OpenCV进阶(11)使用 OpenCV实现哈哈镜_第3张图片
现在,当我们清楚地了解如何定义 3D 表面并在我们的虚拟相机中捕获它时,让我们看看如何在 python 中对其进行编码。

# 确定输入图像的高度和宽度
H,W = image.shape[:2]
# 分别在 (-W/2 到 W/2) 和 (-H/2 到 H/2) 范围内定义 x 和 y 坐标值

x = np.linspace(-W/2, W/2, W)
y = np.linspace(-H/2, H/2, H)

# 使用上面定义的 x 和 y 坐标范围创建网格。
xv,yv = np.meshgrid(x,y)

# 生成平面的 X、Y 和 Z 坐标
# 这里我们定义 Z = 1 平面
X = xv.reshape(-1,1)
Y = yv.reshape(-1,1)
Z = X*0+1 # 网格将位于 Z = 1 平面上

pts3d = np.concatenate(([X],[Y],[Z],[X*0+1]))[:,:,0]

pts2d = np.matmul(P,pts3d)
u = pts2d[0,:]/(pts2d[2,:]+0.00000001)
v = pts2d[1,:]/(pts2d[2,:]+0.00000001)

这就是我们生成充当镜子的 3D 表面的方式。

5.VCAM : 虚拟摄像机

我们需要每次都写上面的代码吗?如果我们想动态改变相机的一些参数怎么办?为了简化创建此类 3D 表面、定义虚拟相机、设置所有参数以及找到它们的投影的任务,我们可以使用名为 vcam 的 Python 库。您可以在其文档中找到使用该库的不同方式的各种插图。它减少了我们每次创建虚拟相机、定义 3D 点和查找 2D 投影的工作量。此外,该库还负责设置合适的内部和外部参数值并处理各种异常,使其易于使用。

您可以使用 pip 安装该库。

pip install vcam

以下是如何使用该库编写与我们迄今为止编写的代码类似但只有几行代码的代码。

import cv2
import numpy as np
import math
from vcam import vcam,meshGen

# 创建一个虚拟相机对象。这里 H,W 对应于输入图像帧的高度和宽度。
c1 = vcam(H=H,W=W)

# 创建曲面对象
plane = meshGen(H,W)

# 更改 Z 坐标。默认 Z 设置为 1
# 我们生成一面镜子,其中对于每个 3D 点,其 Z 坐标定义为 Z = 10*sin(2*pi[x/w]*10)
plane.Z = 10*np.sin((plane.X/plane.W)*2*np.pi*10)

# 获取曲面修改后的 3D 点
pts3d = plane.getPlane()

# 使用我们的虚拟相机对象 c1 投影 3D 点并获得相应的 2D 图像坐标
pts2d = c1.project(pts3d)

可以很容易地看到 vcam 库如何使定义虚拟摄像机、创建 3D 平面以及将其投影到虚拟摄像机中变得容易。

投影的 2D 点现在可用于基于网格的重新映射。这是创建我们哈哈镜的最后一步。

6.图像重映射

重映射基本上是通过将输入图像的每个像素从其原始位置移动到由重映射函数定义的新位置来生成新图像。因此在数学上它可以写成如下:
在这里插入图片描述
上述方法称为前向重映射或前向变形,其中 map_x 和 map_y 函数为我们提供像素的新位置,该位置最初位于 (x,y)。

现在如果 m a p x 和 m a p y map_x 和 map_y mapxmapy 没有给我们一个给定的 (x,y) 对的整数值呢?我们根据最近的整数值将 (x,y) 处的像素强度扩展到相邻像素。这会在重新映射或生成的图像中产生孔洞,像素强度未知并设置为 0。我们如何避免这些孔洞?

我们使用反弯曲。这意味着现在 m a p x 和 m a p y map_x 和 map_y mapxmapy 将为我们提供源图像中旧像素位置,用于目标图像中的给定像素位置 (x,y)。它可以用数学表示如下:
在这里插入图片描述
太棒了!现在我们知道了如何执行重映射。为了产生有趣的镜像效果,我们将对原始输入帧应用重映射。但我们需要 m a p x 和 m a p y map_x 和 map_y mapxmapy,对吧?在这个例子中,我们如何定义 m a p x 和 m a p y map_x 和 map_y mapxmapy ?我们已经计算了映射函数。

2D 投影点 (pts2d),相当于我们理论解释中的 (u,v),是我们可以传递给重映射函数的所需映射。现在让我们看看从投影的 2D 点中提取映射并应用重映射功能(基于网格的扭曲)来生成哈哈镜效果的代码。

# 从二维投影点获取 mapx 和 mapy
map_x,map_y = c1.getMaps(pts2d)

# 将重映射功能应用于输入图像(img)以生成有趣的镜像效果
output = cv2.remap(img,map_x,map_y,interpolation=cv2.INTER_LINEAR)

cv2.imshow("Funny mirror",output)
cv2.waitKey(0)

OpenCV进阶(11)使用 OpenCV实现哈哈镜_第4张图片
基 于 正 弦 函 数 的 哈 哈 镜 效 果 的 输 入 和 相 应 输 出 图 像 基于正弦函数的哈哈镜效果的输入和相应输出图像
惊人的 !让我们尝试创建一个更有趣的镜子以获得更好的主意。在此之后,您将能够制作自己的有趣镜子。

import cv2
import numpy as np
import math
from vcam import vcam,meshGen

# 读取输入图像。
img = cv2.imread("chess.png")
H,W = img.shape[:2]

# 创建虚拟相机对象
c1 = vcam(H=H,W=W)

# 创建表面对象
plane = meshGen(H,W)

# 我们生成一面镜子,其中对于每个 3D 点,其 Z 坐标定义为 Z = 20*exp^((x/w)^2 / 2*0.1*sqrt(2*pi))
plane.Z += 20*np.exp(-0.5*((plane.X*1.0/plane.W)/0.1)**2)/(0.1*np.sqrt(2*np.pi))
pts3d = plane.getPlane()

pts2d = c1.project(pts3d)
map_x,map_y = c1.getMaps(pts2d)

output = cv2.remap(img,map_x,map_y,interpolation=cv2.INTER_LINEAR)

cv2.imshow("Funny Mirror",output)
cv2.imshow("Input and output",np.hstack((img,output)))
cv2.waitKey(0)

OpenCV进阶(11)使用 OpenCV实现哈哈镜_第5张图片
输 入 ( 左 ) 和 输 出 ( 右 ) 图 像 显 示 由 上 述 代 码 创 建 的 搞 笑 镜 效 果 。 输入(左)和输出(右)图像显示由上述代码创建的搞笑镜效果。
所以现在我们知道,通过将 Z 定义为 X 和 Y 的函数,我们可以创建不同类型的失真效果。让我们使用上面的代码创建更多效果。我们只需要更改将 Z 定义为 X 和 Y 函数的行。这将进一步帮助您创建自己的效果。

# 我们生成一面镜子,其中对于每个 3D 点,其 Z 坐标定义为 Z = 20*exp^((y/h)^2 / 2*0.1*sqrt(2*pi))

plane.Z += 20*np.exp(-0.5*((plane.Y*1.0/plane.H)/0.1)**2)/(0.1*np.sqrt(2*np.pi))

OpenCV进阶(11)使用 OpenCV实现哈哈镜_第6张图片
输 入 ( 左 ) 和 输 出 ( 右 ) 图 像 显 示 了 使 用 上 述 函 数 为 3 D 表 面 的 Z 坐 标 创 建 的 失 真 效 果 。 输入(左)和输出(右)图像显示了使用上述函数为 3D 表面的 Z 坐标创建的失真效果。 使3DZ
让我们使用正弦函数创建一些东西!

# 我们生成一面镜子,其中对于每个 3D 点,其 Z 坐标定义为 Z = 20*[ sin(2*pi*(x/w-1/4))) + sin(2*pi*(y/h-1/4))) ]

plane.Z += 20*np.sin(2*np.pi*((plane.X-plane.W/4.0)/plane.W)) + 20*np.sin(2*np.pi*((plane.Y-plane.H/4.0)/plane.H))

一些径向失真效果怎么样?

# 我们生成一面镜子,其中对于每个 3D 点,其 Z 坐标定义为 Z = -100*sqrt[(x/w)^2 + (y/h)^2]

plane.Z -= 100*np.sqrt((plane.X*1.0/plane.W)**2+(plane.Y*1.0/plane.H)**2)

OpenCV进阶(11)使用 OpenCV实现哈哈镜_第7张图片

7.代码实现

# FunnyMirrorsImages.py
import cv2
import numpy as np
import math
from vcam import vcam, meshGen

paths = ["./data/chess.png", "./data/im2.jpeg", "./data/img3.jpg"]

for mode in range(8):
    for i, path in enumerate(paths):
        # 读取输入图像
        img = cv2.imread(path)
        img = cv2.resize(img, (300, 300))
        H, W = img.shape[:2]

        # 创建虚拟相机对象
        c1 = vcam(H=H, W=W)

        # 创建表面对象
        plane = meshGen(H, W)

        # 我们生成一面镜子,其中对于每个 3D 点,其 Z 坐标定义为 Z = F(X,Y)

        if mode == 0:
            plane.Z += 20 * np.exp(-0.5 * ((plane.X * 1.0 / plane.W) / 0.1) ** 2) / (0.1 * np.sqrt(2 * np.pi))
        elif mode == 1:
            plane.Z += 20 * np.exp(-0.5 * ((plane.Y * 1.0 / plane.H) / 0.1) ** 2) / (0.1 * np.sqrt(2 * np.pi))
        elif mode == 2:
            plane.Z -= 10 * np.exp(-0.5 * ((plane.X * 1.0 / plane.W) / 0.1) ** 2) / (0.1 * np.sqrt(2 * np.pi))
        elif mode == 3:
            plane.Z -= 10 * np.exp(-0.5 * ((plane.Y * 1.0 / plane.W) / 0.1) ** 2) / (0.1 * np.sqrt(2 * np.pi))
        elif mode == 4:
            plane.Z += 20 * np.sin(2 * np.pi * ((plane.X - plane.W / 4.0) / plane.W)) + 20 * np.sin(
                2 * np.pi * ((plane.Y - plane.H / 4.0) / plane.H))
        elif mode == 5:
            plane.Z -= 20 * np.sin(2 * np.pi * ((plane.X - plane.W / 4.0) / plane.W)) - 20 * np.sin(
                2 * np.pi * ((plane.Y - plane.H / 4.0) / plane.H))
        elif mode == 6:
            plane.Z += 100 * np.sqrt((plane.X * 1.0 / plane.W) ** 2 + (plane.Y * 1.0 / plane.H) ** 2)
        elif mode == 7:
            plane.Z -= 100 * np.sqrt((plane.X * 1.0 / plane.W) ** 2 + (plane.Y * 1.0 / plane.H) ** 2)
        else:
            print("Wrong mode selected")
            exit(-1)

        # 提取生成的 3D 平面
        pts3d = plane.getPlane()

        # 在虚拟相机中投影(捕捉)平面
        pts2d = c1.project(pts3d)

        # 为基于网格的扭曲生成映射函数。
        map_x, map_y = c1.getMaps(pts2d)

        # 生成输出
        output = cv2.remap(img, map_x, map_y, interpolation=cv2.INTER_LINEAR)
        output = cv2.flip(output, 1)

        cv2.imshow("Funny Mirror", output)
        cv2.imshow("Input and output", np.hstack((img, np.zeros((H, 2, 3), dtype=np.uint8), output)))
        # 取消注释以下行以保存输出
        # cv2.imwrite("Mirror-effect-%d-image-%d.jpg"%(mode+1,i+1),np.hstack((img,np.zeros((H,2,3),dtype=np.uint8),output)))
        cv2.waitKey(0)
# FunnyMirrorsVideo.py
import cv2
import numpy as np
import math
from vcam import vcam,meshGen
import sys


cap = cv2.VideoCapture(sys.argv[1])
ret, img = cap.read()

H,W = img.shape[:2]
fps = 30

# 创建虚拟相机对象
c1 = vcam(H=H,W=W)

# 创建表面对象
plane = meshGen(H,W)

mode = int(sys.argv[2])

# 我们生成一面镜子,其中对于每个 3D 点,其 Z 坐标定义为 Z = F(X,Y)
if mode == 0:
	plane.Z += 20*np.exp(-0.5*((plane.X*1.0/plane.W)/0.1)**2)/(0.1*np.sqrt(2*np.pi))
elif mode == 1:
	plane.Z += 20*np.exp(-0.5*((plane.Y*1.0/plane.H)/0.1)**2)/(0.1*np.sqrt(2*np.pi))
elif mode == 2:
	plane.Z -= 10*np.exp(-0.5*((plane.X*1.0/plane.W)/0.1)**2)/(0.1*np.sqrt(2*np.pi))
elif mode == 3:
	plane.Z -= 10*np.exp(-0.5*((plane.Y*1.0/plane.W)/0.1)**2)/(0.1*np.sqrt(2*np.pi))
elif mode == 4:
	plane.Z += 20*np.sin(2*np.pi*((plane.X-plane.W/4.0)/plane.W)) + 20*np.sin(2*np.pi*((plane.Y-plane.H/4.0)/plane.H))
elif mode == 5:
	plane.Z -= 20*np.sin(2*np.pi*((plane.X-plane.W/4.0)/plane.W)) - 20*np.sin(2*np.pi*((plane.Y-plane.H/4.0)/plane.H))
elif mode == 6:
	plane.Z += 100*np.sqrt((plane.X*1.0/plane.W)**2+(plane.Y*1.0/plane.H)**2)
elif mode == 7:
	plane.Z -= 100*np.sqrt((plane.X*1.0/plane.W)**2+(plane.Y*1.0/plane.H)**2)
else:
	print("Wrong mode selected")
	exit(-1)

# 提取生成的 3D 平面
pts3d = plane.getPlane()

# 在虚拟相机中投影(捕捉)平面
pts2d = c1.project(pts3d)

# 为基于网格的扭曲导出映射函数。
map_x,map_y = c1.getMaps(pts2d)

ret, img = cap.read()

while 1:
	ret, img = cap.read()
	if ret:
		output = cv2.remap(img,map_x,map_y,interpolation=cv2.INTER_LINEAR,borderMode=4)
		output = cv2.flip(output,1)
		out1 = np.hstack((img,output))
		out1 = cv2.resize(out1,(700,350))
		cv2.imshow("output",out1)
		if cv2.waitKey(1)&0xFF == 27:
			break
	else:
		break

参考目录

https://learnopencv.com/funny-mirrors-using-opencv/

你可能感兴趣的:(OpenCV,opencv)