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

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

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

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

畸变参数包括 2 类,径向畸变和切向畸变
在这里插入图片描述
径向畸变最明显的例子就是鱼眼相机的效果。

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

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

在这里插入图片描述

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

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

矫正公式如下:

在这里插入图片描述

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

在这里插入图片描述
但一般只用 4 个参数。
[ k 1 , k 2 , p 1 , p 2 ] [k_{1} ,k_{2},p_{1},p_{2}] [k1,k2,p1,p2]

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

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

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

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

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

在这里插入图片描述
我用 A4 纸打印了出来,然后粘贴在一张硬纸板上。

在这里插入图片描述

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

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

  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 号

frank909 CSDN认证博客专家 CV(computer vision)
爱阅读的程序员,专注于技术思考和分享。关注架构设计、Android 开发、AI、数学、自动驾驶领域,个人公号:Frankcall
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值