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

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

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

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

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

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

1.成像几何理论

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

现在我们知道世界坐标中的 3D 点 ( X w , Y w , Z w ) (X_w,Y_w,Z_w) (Xw​,Yw​,Zw​)根据以下等式映射到其对应的像素坐标,其中 P 是相机投影矩阵。


2.它是如何工作的 ?

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

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

下图可能会帮助您更好地理解这些步骤。

创建哈哈镜所涉及的步骤。创建一个 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是任意常数。下面的图显示了一些可以生成的镜面的例子。

现在,当我们清楚地了解如何定义 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 mapx​和mapy​ 没有给我们一个给定的 (x,y) 对的整数值呢?我们根据最近的整数值将 (x,y) 处的像素强度扩展到相邻像素。这会在重新映射或生成的图像中产生孔洞,像素强度未知并设置为 0。我们如何避免这些孔洞?

我们使用反弯曲。这意味着现在 m a p x 和 m a p y map_x 和 map_y mapx​和mapy​ 将为我们提供源图像中旧像素位置,用于目标图像中的给定像素位置 (x,y)。它可以用数学表示如下:

太棒了!现在我们知道了如何执行重映射。为了产生有趣的镜像效果,我们将对原始输入帧应用重映射。但我们需要 m a p x 和 m a p y map_x 和 map_y mapx​和mapy​,对吧?在这个例子中,我们如何定义 m a p x 和 m a p y map_x 和 map_y mapx​和mapy​ ?我们已经计算了映射函数。

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)


基 于 正 弦 函 数 的 哈 哈 镜 效 果 的 输 入 和 相 应 输 出 图 像 基于正弦函数的哈哈镜效果的输入和相应输出图像 基于正弦函数的哈哈镜效果的输入和相应输出图像
惊人的 !让我们尝试创建一个更有趣的镜子以获得更好的主意。在此之后,您将能够制作自己的有趣镜子。

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)


输 入 ( 左 ) 和 输 出 ( 右 ) 图 像 显 示 由 上 述 代 码 创 建 的 搞 笑 镜 效 果 。 输入(左)和输出(右)图像显示由上述代码创建的搞笑镜效果。 输入(左)和输出(右)图像显示由上述代码创建的搞笑镜效果。
所以现在我们知道,通过将 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))


输 入 ( 左 ) 和 输 出 ( 右 ) 图 像 显 示 了 使 用 上 述 函 数 为 3 D 表 面 的 Z 坐 标 创 建 的 失 真 效 果 。 输入(左)和输出(右)图像显示了使用上述函数为 3D 表面的 Z 坐标创建的失真效果。 输入(左)和输出(右)图像显示了使用上述函数为3D表面的Z坐标创建的失真效果。
让我们使用正弦函数创建一些东西!

# 我们生成一面镜子,其中对于每个 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)

7.代码实现

# FunnyMirrorsImages.py
import cv2
import numpy as np
import math
from vcam import vcam, meshGenpaths = ["./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 syscap = 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:breakelse:break

参考目录

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

OpenCV进阶(11)使用 OpenCV实现哈哈镜相关推荐

  1. opencv进阶学习笔记11:cannny边缘检测,直线检测,圆检测

    基础版笔记传送门 python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) cannny边缘检测 基础版边 ...

  2. OpenCV进阶篇视频

    OpenCV进阶篇01 第14章 视频处理 OpenCV不仅能够处理图像,还能够处理视频.视频是由大量的图像构成的,这些图像以固定的时间间隔从视频中获取.这样,就能够使用图像处理的方法对这些图像进行处 ...

  3. 36篇博文带你学完opencv :python+opencv进阶版学习笔记目录

    基础版学习笔记传送门 36篇博文带你学完opencv :python3+opencv学习笔记汇总目录(基础版) 进阶版笔记 项目 opencv进阶学习笔记1: 调用摄像头用法大全(打开摄像头,打开摄像 ...

  4. opencv进阶学习笔记5:图像模糊操作,图像锐化,边缘保留滤波EPF(图像滤镜)

    基础版传送门: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 模糊操作 方法:均值模糊,中值模 ...

  5. OpenCV进阶之路:神经网络识别车牌字符

    1. 关于OpenCV进阶之路 前段时间写过一些关于OpenCV基础知识方面的系列文章,主要内容是面向OpenCV初学者,介绍OpenCV中一些常用的函数的接口和调用方法,相关的内容在OpenCV的手 ...

  6. opencv进阶学习笔记14:分水岭算法 实现图像分割

    基础版学习笔记目录: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 分水岭算法原理 分水岭算法 ...

  7. opencv进阶学习笔记13:图像形态学操作大全(膨胀,腐蚀,开闭,黑帽,顶帽,梯度)python版

    基础版学习笔记: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 基础版形态学: opencv学 ...

  8. opencv进阶学习笔记12:轮廓发现和对象测量

    基础版笔记目录: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 轮廓发现 1轮廓发现介绍 基础 ...

  9. opencv进阶学习笔记10:图像金字塔和图像梯度

    基础版笔记传送门: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 图像金字塔 变小 变大 原理 ...

  10. opencv进阶学习9:图像阈值大全,图像二值化,超大图像二值化

    基础版笔记链接: python3+opencv学习笔记汇总目录(适合基础入门学习) 进阶版笔记目录链接: python+opencv进阶版学习笔记目录(适合有一定基础) 基础版二值化讲解 opencv ...

最新文章

  1. 第12章:项目采购管理(2)-章节重点
  2. C/Cpp / 构造函数种类
  3. 分布式架构的水平和垂直扩容
  4. C#如何操作另一个窗体:[2]子窗体操作主窗体(转)
  5. 学习ui设计_如果您想学习UI设计,该怎么办
  6. 关闭浏览器网页触发事件_浅析浏览器渲染和 script 加载
  7. 查看Oracle有哪些表或者视图
  8. linux 查看握手时间,实战:tcpdump抓包分析三次握手四次挥手
  9. web服务减少服务器TIME_WAIT
  10. 中国石油计算机第三次在线作业,最新中国石油大学北京计算机应用基础第三次在线作业1(10页)-原创力文档...
  11. 蓝桥杯 大整数乘法 试题 算法训练 P0805
  12. java重命名文件(附道客巴巴文档下载方法)
  13. MATLAB的cat函数
  14. SVN重定向svn switch
  15. 【jvm】8-垃圾回收
  16. 长期在计算机房内会有多大辐射,机房辐射范围和预防辐射?计算机房里面适用的屏蔽隔离防辐射材料是什么?...
  17. 转账功能怎么测试?以支付宝转账到银行卡为例
  18. C# DLL HRESULT:0x8007000B
  19. NEFU 262 贪吃的九头龙(树形背包,4级)
  20. 多摩川绝对值编码器CPLD FPGA通信源码(VHDL格式+协议+说明书)

热门文章

  1. 2021全年净利润暴涨超412% 京东方OLED产能已达全国第一
  2. vue中proxyTable配置方法
  3. python计算机视觉第五次实验
  4. 内衣罩杯,A`B`C`D`的真正含义
  5. Chrome 谷歌浏览器清除HTTPS证书缓存
  6. 量子比特-智能财税大数据生态网络,8月8日首发上线FFEX
  7. 换行导致无法模拟SQL注入
  8. 电池容量足够低如何触发自动关机
  9. android notification 的总结分析,Android中Notification用法实例总结
  10. 2022-2027年中国钩端螺旋体疫苗市场竞争态势及行业投资前景预测报告