# 姿势估计

## 目标

在这一部分，

- 我们会了解到如何利用 calib3d 模块在图像中实现 3D 效果。

## 基础

这将仅是很小一部分。在上一章节(相机校准)，你已经找到了相机矩阵，畸变系数等等参数。给出一个图案图像，我们便可以利用上面的信息用于计算其姿势，或者物体在空间中位于何处，比如如何旋转，如何移动等等问题。对于一个平面物体，我们可以假定 Z = 0，这样，问题现在便转化为了如何放置摄像机才能查看到我们的图案图像。所以如果我们知道物体在空间中的位置，我们便可以绘制一些 2D 图像用以模拟 3D 效果。让我们看一下如何做到这件事情。

我们的问题是，我们想在我们棋盘的第一个角上绘制 3D 坐标系(x, y, z 坐标系)，其中 X 轴是蓝色，Y 轴是绿色，Z 轴是红色。所以从效果上讲，Z 轴应该感觉像是与棋盘垂直的。

首先，让我们读取从上一章节(相机校准)存储的结果中读取出相机矩阵与畸变参数。

```python
import numpy as np
import cv2 as cv
import glob

# 读取事先存好的数据
with np.load('B.npz') as X:
    mtx, dist, _, _ = [X[i] for i in ('mtx','dist','rvecs','tvecs')]
```

现在让我们创建一个函数`draw`，用以利用棋盘角点(利用[**cv.findChessboardCorners()**](https://docs.opencv.org/4.0.0/d9/d0c/group__calib3d.html#ga93efa9b0aa890de240ca32b11253dd4a)函数)和**坐标轴点**绘制 3D 坐标系。

```python
def draw(img, corners, imgpts):
    corner = tuple(corners[0].ravel())
    img = cv.line(img, corner, tuple(imgpts[0].ravel()), (255,0,0), 5)
    img = cv.line(img, corner, tuple(imgpts[1].ravel()), (0,255,0), 5)
    img = cv.line(img, corner, tuple(imgpts[2].ravel()), (0,0,255), 5)
    return img
```

然后正如前面情况那样，我们创建了终止条件，物体点(棋盘上的 3D 角点)和坐标轴点。坐标轴点是 3D 空间中用于绘制轴的点。我们绘制长度为 3 的轴(单位是国际象棋的方形尺寸，我们会根据这个尺寸校准)。所以我们的 X 轴是从(0,0,0)到(3,0,0)绘制的，所以对于 Y 轴。对于 Z 轴，它是从(0,0,0)到(0,0，-3)绘制的。负数表示其接近摄像机。

```python
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
objp = np.zeros((6*7,3), np.float32)
objp[:,:2] = np.mgrid[0:7,0:6].T.reshape(-1,2)

axis = np.float32([[3,0,0], [0,3,0], [0,0,-3]]).reshape(-1,3)
```

现在，像往常一样，我们读取一张图片。搜索 7x6 网格。如果我们找到了，我们用子角像素优化一下它，然后计算旋转和平移，我们使用[**cv.solvePnPRansac()**](https://docs.opencv.org/4.0.0/d9/d0c/group__calib3d.html#ga50620f0e26e02caa2e9adc07b5fbf24e)函数。一旦我们计算完那些旋转矩阵后，我们便用它们来将我们的**坐标轴点**投影到平面图像上。简而言之，我们寻找到平面图象上的点对应 3D 空间里的(3,0,0), (0,3,0), (0,0,3)。一旦我们找到后，我们便可以从第一个角到每个我们找到的坐标轴点之间利用`draw()`函数连线。搞定！！！

```python
for fname in glob.glob('left*.jpg'):
    img = cv.imread(fname)
    gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
    ret, corners = cv.findChessboardCorners(gray, (7,6),None)
    
    if ret == True:
        corners2 = cv.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
        
        # 找到旋转和平移向量
        ret,rvecs, tvecs = cv.solvePnP(objp, corners2, mtx, dist)
        
        # 投射 3D 点到平面图像上
        imgpts, jac = cv.projectPoints(axis, rvecs, tvecs, mtx, dist)
        
        img = draw(img,corners2,imgpts)
        cv.imshow('img',img)
        k = cv.waitKey(0) & 0xFF
        if k == ord('s'):
            cv.imwrite(fname[:6]+'.png', img)
            
cv.destroyAllWindows()
```

请看下面的一些结果。注意，每个轴长 3 个方格：

![pose_1.jpg](img/pose_1.jpg)

<center>pose 1 image</center>

### 渲染立方体

如果你想要绘制一个立方体，你需要根据如下步骤改进`draw()`函数和坐标轴点。

改进`draw()`函数：

```python
def draw(img, corners, imgpts):
    imgpts = np.int32(imgpts).reshape(-1,2)
    
    # 将底面绘制为绿色
    img = cv.drawContours(img, [imgpts[:4]],-1,(0,255,0),-3)
    
    # 将支柱绘制为蓝色
    for i,j in zip(range(4),range(4,8)):
        img = cv.line(img, tuple(imgpts[i]), tuple(imgpts[j]),(255),3)
        
    # 将顶面绘制为红色
    img = cv.drawContours(img, [imgpts[4:]],-1,(0,0,255),3)
    
    return img
```

改进坐标轴点。它们是 3D 空间中立方体的 8 个角：

```python
axis = np.float32([ [0,0,0], [0,3,0], [3,3,0], [3,0,0],
                   [0,0,-3],[0,3,-3],[3,3,-3],[3,0,-3] ])
```

然后看上去就像下面这样：

![pose_2.jpg](img/pose_2.jpg)

<center>pose 2 image</center>

如果你对图形渲染，增强现实等感兴趣，你可以使用 OpenGL 来渲染更加复杂的图像。

## 其他资源

## 练习