Hi,

I've been attempting to determine the maximum field of view (FOV) for the OAK-D Pro W model, but I'm encountering discrepancies between the FOV I calculate and the FOV stated in both the documentation and the product page.

Using ColorCamera with with ISP on 12MP

According to the information provided:

  • In the shop, it's written as: DFOV / HFOV / VFOV: 120° / 108° / 93°
    Shop link
  • In the documentation, it's listed as: DFOV / HFOV / VFOV: 120° / 95° / 74°
    Documentation link

(I guess the documentation dimensions are the correct one)

For my tests on the original (distorted) image:

  • I placed a ruler at a specific distance from the camera, 29 cm, and observed how much of the ruler was visible, measuring it to be 60 cm. Using this data, I applied the formula:
    2 * atan((width/2) / height), which gave me 2 * atan(30 / 29) = 91.9420... degrees.

For the test with the undistorted image:

  • Following the same procedure with the same dimensions (58 cm height and 26 cm width):
    I calculated the FOV to be 2 * atan(26 / 29) = 83.7557... degrees.

With some testing I found for undistorted frames approximately 85 degrees is the golden spot for the horizontal FOV.

I'm puzzled by the discrepancy between my calculations and the documented FOV. Any insights or suggestions would be greatly appreciated.

Hi @vkmNuller
I was sure I updated the docs to match the FOV to shop but I guess I missed some.
Typically I'd go for the shop specs, but if you calculated the distance to 91deg, perhaps the spec given by sony is wrong.

I'll have to check myself.

Thanks,
Jaka

Hi @vkmNuller
I got about 110deg which conforms to the shop specs. Are you sure you haven't made any measurements error? I'm using IMX378 AF sensor.

Thanks,
Jaka

    7 days later

    Hi jakaskerl
    Can you please send me the script you are using, because i tried to get/calculate the FOV with OpenCV and I got similar results. The code i used for calibration (aka. get FOV):

    def _calibrate(self, framesCount, save = False, path = "None"):
            self.debug = True
            self.windowSize = (1290, 1080)
            self.exit = False
    
            print("Calibrating...")
            print(f"Frame count: {framesCount} | Save: {save} | Path: {path}")
    
            if self.debug:
                cv2.namedWindow("color", cv2.WINDOW_NORMAL)
                cv2.resizeWindow("color", self.windowSize)
    
            frames = []
    
            if path != "None":
                if save and not os.path.exists(path):
                    print(f"Path '{path}' does not exist, creating...")
                    os.mkdir(path)
    
                elif not os.path.exists(path):
                    print(f"Path '{path}' does not exist")
                    return
    
            if path != "None" and not save:
                for idx, filename in enumerate(os.listdir(path)):
    
                    if idx >= framesCount:
                        break
    
                    if filename.startswith("color") and filename.endswith(".png"):
                        print(f"File: {filename}")
    
                        frame = cv2.imread(os.path.join(path, filename))
    
                        # frame = cv2.resize(frame, (4056, 3040))
    
                        frames.append(frame)
    
                print()
            else:
                calibFB = FrameBuffer(size = framesCount)
    
                while not self.exit:
                    colorBuffer = self.colorQueue.tryGet()
    
                    if not colorBuffer:
                        continue
    
                    colorFrame = colorBuffer.getCvFrame()
    
                    if calibFB.isReady():
                        self.exit = True
                        continue
    
                    if self.debug:
                        cv2.imshow('color', colorFrame)
    
                    key = cv2.waitKey(1)
    
                    if key == ord(" "):
                        frameIdx = calibFB.getLength() + 1
                        print(f"Capturing frame: #{frameIdx}")
    
                        if save:
                            cv2.imwrite(f"{path}/color{frameIdx}.png", colorFrame)
    
                        colorFrame = cv2.resize(colorFrame, (4056, 3040))
    
                        calibFB.addFrames(colorFrame)
    
                frames = calibFB.getFrames()
    
            patternSize = (6,8)
            criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    
            # Creating vector to store vectors of 3D & 3D points for each patternSize image
            objpoints = []
            imgpoints = []
    
            # Defining the world coordinates for 3D points
            objp = np.zeros((1, patternSize[0] * patternSize[1], 3), np.float32)
            objp[0,:,:2] = np.mgrid[0:patternSize[0], 0:patternSize[1]].T.reshape(-1, 2)
    
            outFrame = frames[0]
    
            for idx, frame in enumerate(frames):
                print(f"\rProcessed: {idx+1}/{len(frames)}", end="")
                print("", end="", flush=True)
    
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
                found, corners = chessboard(
                    frame = gray,
                    patternSize = patternSize
                )
    
                if found:
                    objpoints.append(objp)
                    # refining pixel coordinates for given 2d points.
                    corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
    
                    imgpoints.append(corners2)
    
                    if self.debug:
                        outFrame = cv2.drawChessboardCorners(outFrame, patternSize, corners2, found)
                        cv2.imshow('color', outFrame)
                        cv2.waitKey(4)
    
            imageShape = gray.shape[::-1]
    
            # https://en.wikipedia.org/wiki/Image_sensor_format (1/2.3")
            sensorWdith  = 6.17
            sensorHeight = 4.55
    
            ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, imageShape, None, None)
            fovH, fovV, focalLength, principalPoint, aspectRatio = cv2.calibrationMatrixValues(mtx, imageShape, sensorWdith, sensorHeight)
    
            fov_x = np.rad2deg(2 * np.arctan2(imageShape[0], 2 * mtx[0][0]))
            fov_y = np.rad2deg(2 * np.arctan2(imageShape[1], 2 * mtx[1][1]))
    
            print()
            print(f"Return: {ret}")
            print(f"Matrix:\n{mtx}")
            print(f"Dist. Coeff.:\n{dist}")
            print(f"R vecs:\n{rvecs}")
            print(f"T vecs:\n{tvecs}")
    
            print()
            print(f"FOV H: {fov_x}")
            print(f"FOV V: {fov_y}")
    
            print()
            print(f"FOV H: {fovH}")
            print(f"FOV V: {fovV}")
            print(f"Focal Length: {focalLength}")
            print(f"Principal Point: {principalPoint}")
            print(f"Aspect Ratio: {aspectRatio}")

    Hi @vkmNuller
    I didn't use a script, just manual measurements. Measured camera to ruler distance, checked how much of the ruler I can see, then applied your equation (didn't check but look OK at first glance).

    Thanks,
    Jaka

      Hi jakaskerl
      Could you please provide the code you are using to capture frames for your calculations? I'd like to compare the frames I obtain using my code with yours

      Thanks

      Hi @vkmNuller
      I modified the rgb_isp_scale.py example to show full FOV:

      #!/usr/bin/env python3
      
      import cv2
      import depthai as dai
      
      # Create pipeline
      pipeline = dai.Pipeline()
      
      # Define source and output
      camRgb = pipeline.create(dai.node.ColorCamera)
      xoutVideo = pipeline.create(dai.node.XLinkOut)
      
      xoutVideo.setStreamName("video")
      
      # Properties
      camRgb.setBoardSocket(dai.CameraBoardSocket.CAM_A)
      camRgb.setResolution(dai.ColorCameraProperties.SensorResolution.THE_12_MP)
      
      xoutVideo.input.setBlocking(False)
      xoutVideo.input.setQueueSize(1)
      
      # Linking
      camRgb.isp.link(xoutVideo.input)
      
      cv2.namedWindow("video", cv2.WINDOW_NORMAL)
      
      # Connect to device and start pipeline
      with dai.Device(pipeline) as device:
      
          video = device.getOutputQueue(name="video", maxSize=1, blocking=False)
      
          while True:
              videoIn = video.get()
              # Get BGR frame from NV12 encoded video frame to show with opencv
              # Visualizing the frame on slower hosts might have overhead
              cv2.imshow("video", videoIn.getCvFrame())
              print(videoIn.getCvFrame().shape)
      
              if cv2.waitKey(1) == ord('q'):
                  break

      Thanks,
      Jaka