OpenCV 标定摄像头(Python 版本代码,视频中标定,亲测可用)

时间:2024-05-20 22:15:46

在机器视觉领域,摄像头的标定指通过技术手段拿到相机的内参、外参及畸变参数。

相机内参长这样,利用针孔模型,将 3d 物体透视投影到 2d 的相机屏幕上。

P=[fx0cx0fycy001] P = \begin{bmatrix} f_{x} & 0 & c_{x} \\ 0 & f_{y} & c_{y}\\ 0 & 0 & 1 \end{bmatrix}

畸变参数包括 2 类,径向畸变和切向畸变
OpenCV 标定摄像头(Python 版本代码,视频中标定,亲测可用)
径向畸变最明显的例子就是鱼眼相机的效果。

大家仔细观察上面的图片,它就能很好地介绍径向畸变。越往镜头边缘,线条弯曲的越明显,本来是直线,现在都变成了曲线,消除畸变就是为了把这些曲线尽量还原成本来的样子。

径向畸变可以被纠正,公式如下。

OpenCV 标定摄像头(Python 版本代码,视频中标定,亲测可用)

除了径向畸变外,还有一个畸变就是切向畸变

切向畸变一般来说,是因为相机镜头制造工艺精度不够,透镜和感光器原件没有平行。从而造成了图像的变形。

矫正公式如下:

OpenCV 标定摄像头(Python 版本代码,视频中标定,亲测可用)

两个畸变的参数通常用一个向量表示。

OpenCV 标定摄像头(Python 版本代码,视频中标定,亲测可用)
但一般只用 4 个参数。
[k1,k2,p1,p2] [k_{1} ,k_{2},p_{1},p_{2}]

如果用 5 个参数,畸变后的相片就成球状了。

我们的目标就是为了标定出相机内参和外参。

OpenCV 官网上有标定代码示例,但是是基于图片的,并且只有一张图片,我们知道一般要得到一个比较好的标定效果的话,大概需要标定 20 张图片左右。

所以,我想改良一下,我就想到了用相机拍摄视频,然后在视频中完成操作。

标定物我选择了传统的棋盘格,源文件在此

OpenCV 标定摄像头(Python 版本代码,视频中标定,亲测可用)
我用 A4 纸打印了出来,然后粘贴在一张硬纸板上。

OpenCV 标定摄像头(Python 版本代码,视频中标定,亲测可用)

接下来就可以编写代码了。

代码的流程其实非常简单。

  1. 打开摄像头,获取画面,并监听键盘事件。
  2. 如果检测到空格键,执行棋盘格检测代码。
  3. 如果检测成功,将棋盘格角点信息绘制在画面上,并将结果保存到列表当中。同时更新棋盘格检测成功次数。
  4. 如果棋盘格检测成功次数达到指定值,比如 20,又或者是用户按下 Q 键,退出棋盘格的检测。
  5. 将棋盘格角点信息送入标定函数,获取标定结果并保存。
  6. 标定结果可以用来去畸变。
def calibrate():
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    Nx_cor = 9
    Ny_cor = 6

    objp = np.zeros((Nx_cor * Ny_cor, 3), np.float32)
    objp[:, :2] = np.mgrid[0:Nx_cor, 0:Ny_cor].T.reshape(-1, 2)
    objpoints = []  # 3d points in real world space
    imgpoints = []  # 2d points in image plane.

    count = 0  # count 用来标志成功检测到的棋盘格画面数量
    while (True):

        ret, frame = cap.read()

        if cv2.waitKey(1) & 0xFF == ord(' '):

            # Our operations on the frame come here
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

            ret, corners = cv2.findChessboardCorners(gray, (Nx_cor, Ny_cor), None)  # Find the corners
            # If found, add object points, image points
            if ret == True:
                corners = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
                objpoints.append(objp)
                imgpoints.append(corners)
                cv2.drawChessboardCorners(frame, (Nx_cor, Ny_cor), corners, ret)
                count += 1

                if count > 20:
                    break

        # Display the resulting frame
        cv2.imshow('frame', frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    global mtx, dist

    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
    print(mtx, dist)

    mean_error = 0
    for i in xrange(len(objpoints)):
        imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
        mean_error += error

    print "total error: ", mean_error / len(objpoints)

上面是标定的函数。核心是利用了 OpenCV 的几个关键的 API.

# 查找棋盘格角点信息
ret, corners = cv2.findChessboardCorners(gray, (Nx_cor, Ny_cor), None)
# 精细化角点信息
corners = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
# 绘制查找到的角点
cv2.drawChessboardCorners(frame, (Nx_cor, Ny_cor), corners, ret)
# 标定,mtx 是相机内参,dist 是畸变,rvecs,tvecs 分别是旋转矩阵和平移矩阵代表外参
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)

需要注意的是,在这里标定板的的棋盘格是检测内点,所以是横向 9 个,竖向 6 个。

Nx_cor = 9
Ny_cor = 6

标定的时候,还需要角点的物理坐标和图像坐标,这是因为需要通过透视成像的原理,来反向拟合相机的参数,原理比较复杂,这个不做解释,有兴趣的同学可以查看相关书籍和资料。

标定后的结果需要衡量误差,下面是代码。

mean_error = 0
for i in xrange(len(objpoints)):
   imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
   error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
   mean_error += error

print "total error: ", mean_error / len(objpoints)

通过 cv2.projectPoints()方法将角点的物理坐标、标定得到的外参重新投影得到新的角点的图像坐标。

然后将新的图像坐标与之前检测角点时的真实图像坐标对比,以此来衡量标定的精确性。

np.savez('calibrate.npz', mtx=mtx, dist=dist[0:4])

这行代码的用途是为了将标定的结果序列化,保存到本地,以备以后直接使用,畸变参数我只保存了 4 个,原因前面有讲过。

标定得到的相机内参与畸变参数可以用来消除相机原始画面的畸变,代码如下:

def undistortion(img, mtx, dist):
    h, w = img.shape[:2]
    newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))

    dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

    # crop the image
    x, y, w, h = roi
    if roi != (0, 0, 0, 0):
        dst = dst[y:y + h, x:x + w]

    return dst

所以,我们可以编写测试代码,如果本地有标定好的参数,那么就直接加载。如果没有的话,那就标定一次。

拿到内参和畸变参数后,我们可以打开摄像头,然后去畸变,然后你可以直接观察效果。

if __name__ == '__main__':

    cap = cv2.VideoCapture(0)

    mtx = []
    dist = []


    try:
        npzfile = np.load('calibrate.npz')
        mtx = npzfile['mtx']
        dist = npzfile['dist']
    except IOError:
        calibrate()

    print('dist', dist[0:4])
    while (True):

        ret, frame = cap.read()

        frame = undistortion(frame, mtx, dist[0:4])
        # Display the resulting frame
        cv2.imshow('frame', frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

你可以变换姿态,然后按空格键进行图像的抓取。

完整代码如下:

#encoding=utf-8
import numpy as np
import cv2


def undistortion(img, mtx, dist):
    h, w = img.shape[:2]
    newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))

    print('roi ', roi)

    dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

    # crop the image
    x, y, w, h = roi
    if roi != (0, 0, 0, 0):
        dst = dst[y:y + h, x:x + w]

    return dst

def calibrate():
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    Nx_cor = 9
    Ny_cor = 6

    objp = np.zeros((Nx_cor * Ny_cor, 3), np.float32)
    objp[:, :2] = np.mgrid[0:Nx_cor, 0:Ny_cor].T.reshape(-1, 2)
    objpoints = []  # 3d points in real world space
    imgpoints = []  # 2d points in image plane.

    count = 0  # count 用来标志成功检测到的棋盘格画面数量
    while (True):

        ret, frame = cap.read()

        if cv2.waitKey(1) & 0xFF == ord(' '):

            # Our operations on the frame come here
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

            ret, corners = cv2.findChessboardCorners(gray, (Nx_cor, Ny_cor), None)  # Find the corners
            # If found, add object points, image points
            if ret == True:
                corners = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
                objpoints.append(objp)
                imgpoints.append(corners)
                cv2.drawChessboardCorners(frame, (Nx_cor, Ny_cor), corners, ret)
                count += 1

                if count > 20:
                    break

        # Display the resulting frame
        cv2.imshow('frame', frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    global mtx, dist

    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
    print(mtx, dist)

    mean_error = 0
    for i in xrange(len(objpoints)):
        imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
        mean_error += error

    print "total error: ", mean_error / len(objpoints)
        # # When everything done, release the capture

    np.savez('calibrate.npz', mtx=mtx, dist=dist[0:4])

def undistortion(img, mtx, dist):
    h, w = img.shape[:2]
    newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))

    dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

    # crop the image
    x, y, w, h = roi
    if roi != (0, 0, 0, 0):
        dst = dst[y:y + h, x:x + w]

    return dst


if __name__ == '__main__':

    cap = cv2.VideoCapture(0)

    mtx = []
    dist = []


    try:
        npzfile = np.load('calibrate.npz')
        mtx = npzfile['mtx']
        dist = npzfile['dist']
    except IOError:
        calibrate()

    print('dist', dist[0:4])
    while (True):

        ret, frame = cap.read()

        frame = undistortion(frame, mtx, dist[0:4])
        # Display the resulting frame
        cv2.imshow('frame', frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

如果觉得文章有所帮助,可以点击这行字给我博客投票,046 号