Search code examples
cameracomputer-visioncamera-calibration

Is focal length in pixel unit a linear measurment


I have a pan-tilt-zoom camera (changing focal length over time). There is no idea about its base focal length (e.g. focal length in time point 0). However, It is possible to track the change in focal length between frame and another based on some known constraints and assumptions (Doing a SLAM).

If I assume a random focal length (in pixel unit), for example, 1000 pixel. Then, the new focal lengths are tracked frame by frame. Would I get correct results relatively? Would the results (focal lengths) in each frame be correct up to scale to the ground truth focal length?

For pan and tilt, assuming 0 at start would be valid. Although it is not correct, The estimated values of new tili-pan will be correct up to an offset. However, I suspect the estimated focal length will not be even correct up to scale or offset.. Is it correct or not?


Solution

  • For a quick short answer - if pan-tilt-zoom camera is approximated as a thin lens, then this is the relation between distance (z) and focal length (f):

    enter image description here

    This is just an approximation. Not fully correct. For more precise calculations, see the camera matrix. Focal length is an intrinsic parameter in the camera matrix. Even if not known, it can be calculated using some camera calibration method such as DLT, Zhang's Method and RANSAC. Once you have the camera matrix, focal length is just a small part of it. You get many more useful things along with it.

    OpenCV has an inbuilt implementation of Zhang's method. (Look at this documentation for explanations, but code is old and unusable. New up-to-date code below.) You need to take some pictures of a chess board through your camera. Here is some helper code:

    import cv2
    from matplotlib import pyplot as plt
    import numpy as np
    from glob import glob
    from scipy import linalg
    
    x,y = np.meshgrid(range(6),range(8))
    
    world_points=np.hstack((x.reshape(48,1),y.reshape(48,1),np.zeros((48,1)))).astype(np.float32)
    
    _3d_points=[]
    _2d_points=[]
    
    img_paths=glob('./*.JPG') #get paths of all checkerboard images
    
    for path in img_paths:
        im=cv2.imread(path)
        
        ret, corners = cv2.findChessboardCorners(im, (6,8))
        
        if ret: #add points only if checkerboard was correctly detected:
            _2d_points.append(corners) #append current 2D points
            _3d_points.append(world_points) #3D points are always the same
    
    
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(_3d_points, _2d_points, (im.shape[1],im.shape[0]), None, None)
    
    print ("Ret:\n",ret)
    print ("Mtx:\n",mtx)
    print ("Dist:\n",dist)
    

    You might want Undistortion: Correcting for Radial Distortion

    # termination criteria
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    
    # prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
    objp = np.zeros((6*8,3), np.float32)
    objp[:,:2] = np.mgrid[0:6,0:8].T.reshape(-1,2)
    
    # Arrays to store object points and image points from all the images.
    objpoints = [] # 3d point in real world space
    imgpoints = [] # 2d points in image plane.
    
    for fname in img_paths:
        img = cv2.imread(fname)
        gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    
        # Find the chess board corners
        ret, corners = cv2.findChessboardCorners(gray, (6,8),None)    
        
        # If found, add object points, image points (after refining them)
        if ret == True:
            objpoints.append(objp)
    
            cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
            imgpoints.append(corners)
    
        if 'IMG_5456.JPG' in fname:
            plt.figure(figsize=(20,10))
            img_vis=img.copy()
            cv2.drawChessboardCorners(img_vis, (6,8), corners, ret) 
            plt.imshow(img_vis)
            plt.show() 
    

    enter image description here

    #Calibration
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)
    
    # Reprojection Error
    tot_error = 0
    for i in range(len(objpoints)):
        imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        error = cv2.norm(imgpoints[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2)
        tot_error += error
    
    print ("Mean Reprojection error: ", tot_error/len(objpoints))
    
    # undistort
    mapx,mapy = cv2.initUndistortRectifyMap(mtx,dist,None,newcameramtx,(w,h),5)
    dst = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)
    # crop the image
    x,y,w,h = roi
    dst = dst[y:y+h, x:x+w]
    plt.figure(figsize=(20,10))
    #cv2.drawChessboardCorners(dst, (6,8), corners, ret) 
    plt.imshow(dst)
    plt.show() 
    

    enter image description here

    # Reprojection Error
    tot_error = 0
    for i in range(len(objpoints)):
        imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        error = cv2.norm(imgpoints[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2)
        tot_error += error
    
    print ("Mean Reprojection error: ", tot_error/len(objpoints))