使用OpenCV实现哈哈镜效果
点击上方“小白学视觉”,选择加"星标"或“置顶”
重磅干货,第一时间送达
我们都记得那些曾经去游乐园或县集市的童年时代。这些游乐园中我最喜欢的是哈哈镜室。
有趣的镜子不是平面镜子,而是凸/凹反射表面的组合,它们会产生扭曲效果,当我们在这些镜子前面移动时,这些效果看起来很有趣。
在本文中,我们将学习使用OpenCV创建属于自己的哈哈镜。
视频
我们可以看到上面视频中的孩子如何享受不同的有趣镜子。我们都想拥有这样的乐趣,但是为此,我们必须在现实中找到一面哈哈镜。
现在,我们无需去哈哈镜室即可在家中享受这种有趣的效果。在本文中,我们将学习如何使用OpenCV制作这些有趣的镜子的数字版本。我们先来看一下具体的效果。
视频
图像形成理
我们首先需要了解如何将世界上的3D点投影到相机的图像坐标系中,这部分内容我们默认小伙伴们已经了解,如果不了解,可以简单搜索一下,会有很多讲解的文章。这里我们只做一个简单的介绍。
世界坐标中的3D点和图像中的像素点具有以下等式映射关系。其中P是相机投影矩阵。
项目的主要内容
整个项目可以分为三个主要步骤:
创建一个虚拟相机。 定义3D表面(镜面),并使用合适的投影矩阵值将其投影到虚拟相机中。 使用3D曲面的投影点的图像坐标来应用基于网格的变形以获得有趣的镜子的所需效果。
下图可能会帮助我们更好地理解步骤。
图1:创建数字滑稽镜像所涉及的步骤。创建一个3D表面,即镜子(左),在虚拟相机中捕获平面以获取相应的2D点,使用获得的2D点将基于网格的变形应用于图像,从而产生类似于滑稽镜子的效果。
接下来我们将详细的介绍每一个步骤
创建一个虚拟相机
基于上述理论,我们清楚地知道3D点如何与其对应的图像坐标相关。现在让我们了解虚拟相机的含义以及如何使用该虚拟相机捕获图像。
虚拟相机本质上是矩阵P,因为它告诉我们3D世界坐标与相应图像像素坐标之间的关系。让我们看看如何使用python创建虚拟相机。
我们将首先创建外部参数矩阵(M1)和内部参数矩阵(K),然后使用它们创建相机投影矩阵(P)。
import numpy as np
# Defining the translation matrix
# Tx,Ty,Tz represent the position of our virtual camera in the world coordinate system
T = np.array([[1,0,0,-Tx],[0,1,0,-Ty],[0,0,1,-Tz]])
# Defining the rotation matrix
# alpha,beta,gamma define the orientation of the virtual camera
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))
# Calculating the extrinsic camera parameter matrix M1
M1 = np.matmul(R,T)
# Calculating the intrinsic camera parameter matrix K
# sx and sy are apparent pixel length in x and y direction
# ox and oy are the coordinates of the optical center in the image plane.
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)。应用此转换与使用我们的虚拟相机捕获3D点的图像相同!
我们如何确定捕获图像中像素的颜色?场景中物体的材质属性如何?
在渲染逼真的3D场景时,以上所有这些点绝对重要,但是我们不必渲染逼真的场景。我们只是想做一些看起来很有趣的事情。
我们需要做的就是捕获(投影),首先将原始图像(或视频帧)表示为虚拟相机中的3D平面,然后使用投影矩阵将该平面上的每个点投影到虚拟相机的图像平面上。
我们将3D坐标存储为numpy数组(W),将相机矩阵存储为numpy数组(P),然后执行矩阵乘法P * W捕获3D点。
但是,在编写代码以使用虚拟相机捕获3D表面之前,我们首先需要定义3D表面。
定义3D表面(镜子)
为了定义3D曲面,我们形成X和Y坐标的网格,然后针对每个点计算Z坐标作为X和Y的函数。因此,对于平面镜,我们将定义Z = K,其中K为任何常数。下图显示了可以生成的镜面的一些示例。
3D表面的一些示例可用于创建哈哈镜镜子
现在,由于我们对如何定义3D曲面并将其捕获到虚拟相机中有了清晰的思路,让我们看看如何在python中进行程序书写。
# Determine height and width of input image
H,W = image.shape[:2]
# Define x and y coordinate values in range (-W/2 to W/2) and (-H/2 to H/2) respectively
x = np.linspace(-W/2, W/2, W)
y = np.linspace(-H/2, H/2, H)
# Creating a mesh grid using the x and y coordinate range defined above.
xv,yv = np.meshgrid(x,y)
# Generating the X,Y and Z coordinates of the plane
# Here we define Z = 1 plane
X = xv.reshape(-1,1)
Y = yv.reshape(-1,1)
Z = X*0+1 # The mesh will be located on Z = 1 plane
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)
VCAM:虚拟摄像机
我们是否需要每次编写以上代码?如果我们想动态更改摄像机的某些参数怎么办?为了简化创建此类3D曲面,定义虚拟相机,设置所有参数并查找其投影的任务,我们可以使用一个名为vcam的python库。我们可以在其文档中找到使用此库的不同方式的各种插图。它减少了我们每次创建虚拟相机,定义3D点和查找2D投影的工作。此外,该库还负责设置适当的内在和外在参数值,并处理各种异常,从而使其易于使用。存储库中还提供了安装库的说明。
我们可以使用pip安装该库。
pip3 install vcam
下面是我们可以使用该库编写代码的方式,该代码的工作方式与我们到目前为止编写的代码类似,但只有几行。
import cv2
import numpy as np
import math
from vcam import vcam,meshGen
# Create a virtual camera object. Here H,W correspond to height and width of the input image frame.
c1 = vcam(H=H,W=W)
# Create surface object
plane = meshGen(H,W)
# Change the Z coordinate. By default Z is set to 1
# We generate a mirror where for each 3D point, its Z coordinate is defined as Z = 10*sin(2*pi[x/w]*10)
plane.Z = 10*np.sin((plane.X/plane.W)*2*np.pi*10)
# Get modified 3D points of the surface
pts3d = plane.getPlane()
# Project the 3D points and get corresponding 2D image coordinates using our virtual camera object c1
pts2d = c1.project(pts3d)
可以很容易地看到vcam库如何使定义虚拟摄像机,创建3D平面以及将其投影到虚拟摄像机中变得容易。
现在可以将投影的2D点用于基于网格的重新映射。这是创建哈哈镜镜面效果的最后一步。
图像重映射
重映射基本上是通过将输入图像的每个像素从其原始位置移动到由重映射功能定义的新位置来生成新图像。因此,在数学上可以这样写:
上面的方法称为前向重映射或前向扭曲,其中map_x和map_y函数为我们提供了像素的新位置,该位置最初位于(x,y)。
现在,如果map_x和map_y没有为我们给定的(x,y)对提供整数值怎么办?我们基于最接近的整数值将(x,y)处的像素强度扩展到相邻像素。这会在重新映射或生成的图像中创建孔,这些像素的强度未知且设置为0。如何避免这些孔?
我们使用反翘曲。这意味着现在map_x和map_y将为我们提供源图像中目标图像中给定像素位置(x,y)的旧像素位置。它可以用数学方式表示如下:
我们现在知道如何执行重新映射。为了产生有趣的镜像效果,我们将对原始输入帧应用重新映射。但是为此,我们需要map_x和map_y。在这种情况下,我们如何定义map_x和map_y?
相当于我们理论解释中的(u,v)的2D投影点(pts2d)是可以传递给remap函数的所需地图。现在,让我们来看一下从投影的2D点提取地图并应用remap函数(基于网格的变形)以生成有趣的镜像效果的代码。
# Get mapx and mapy from the 2d projected points
map_x,map_y = c1.getMaps(pts2d)
# Applying remap function to input image (img) to generate the funny mirror effect
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
# Reading the input image. Pass the path of image you would like to use as input image.
img = cv2.imread("chess.png")
H,W = img.shape[:2]
# Creating the virtual camera object
c1 = vcam(H=H,W=W)
# Creating the surface object
plane = meshGen(H,W)
# We generate a mirror where for each 3D point, its Z coordinate is defined as 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的函数的行即可。这将进一步帮助您创建自己的效果。
# We generate a mirror where for each 3D point, its Z coordinate is defined as 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))
# We generate a mirror where for each 3D point, its Z coordinate is defined as 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))
# We generate a mirror where for each 3D point, its Z coordinate is defined as 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)
项目源码:https://github.com/spmallick/learnopencv/tree/master/FunnyMirrors
交流群
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~