K230如何在CANMV IDE中实现镜头参数标定

Viewed 149

期待结果和实际结果

在单目摄像头测距中需要实现镜头的畸变参数标定和去畸变,在正常python中使用的是opencv库,但在CANMV IDE中没有这个库,我应该如何使用现有API进行摄像头去畸变?

尝试解决过程

我通过询问AI得到了下列手动进行摄像头标定的方法,想请教一下大佬们这是否正确?这样实现镜头去畸变是否可行?

# 读取标定参数
with open("calib.json", "r") as f:
    calib = json.load(f)
K = calib["K"]
fx, _, cx = K[0]
fy, _, cy = K[1]
k1, k2, p1, p2, k3 = calib["D"]

# 去畸变函数
def undistort_pixel(u, v):
    x = (u - cx) / fx
    y = (v - cy) / fy

    r2 = x*x + y*y
    if r2 > 0:
        rad_k = 1 + k1*r2 + k2*r2*r2 + k3*r2*r2*r2
        x_ud = x * rad_k + 2*p1*x*y + p2*(r2 + 2*x*x)
        y_ud = y * rad_k + p1*(r2 + 2*y*y) + 2*p2*x*y
        u_ud = int(x_ud * fx + cx)
        v_ud = int(y_ud * fy + cy)
        return u_ud, v_ud
    else:
        return u, v

# 主循环
while True:
    clock.tick()
    img = sensor.snapshot()

    # 去畸变处理(逐像素)
    undistorted = image.Image(img.width(), img.height())

    for u in range(img.width()):
        for v in range(img.height()):
            u_ud, v_ud = undistort_pixel(u, v)
            if 0 <= u_ud < img.width() and 0 <= v_ud < img.height():
                pixel = img.get_pixel(u_ud, v_ud)
                undistorted.set_pixel(u, v, pixel)

    undistorted.draw_string(0, 0, "Undistorted", color=image.Color.WHITE)
    undistorted.push_to_lcd()

    print(clock.fps())

其中的json文件格式如下:

{
  "K": [[684.5948, 0, 403.6014],
        [0, 684.5948, 240],
        [0, 0, 1]],
  "D": [0.1019, -0.3, 0, 0, 0]
}
1 Answers

你好,可以使用K230拍棋盘格图之后,使用opencv在pc上进行校准。

20250731-155940.jpg

参考脚本

K230采集图片

from media.display import *
from media.media import *
from media.sensor import *
import time, os, sys, gc
from machine import TOUCH
from machine import Pin
from machine import FPIOA

#显示的宽高
DISPLAY_WIDTH = ALIGN_UP(800, 16)
DISPLAY_HEIGHT = 480

#采集图片的分辨率
VIDEO_WIDTH = 1920
VIDEO_HEIGHT = 1080

brd = os.uname()[-1]
if brd == "k230_canmv_01studio":
    PRESS_KEY_NUM = 21
elif brd == "k230_canmv_lckfb":
    PRESS_KEY_NUM = 53
else:
    #按键GPIO Number,根据硬件修改
    PRESS_KEY_NUM = 21

del brd

#保存图片的起始编号,可以修改
save_num = 0

#数据采集标准框,采集的物体最好在这个框框内
grab_x = 0
grab_y = 0
grab_w = 0
grab_h = 0

#保存图片的位置和图片名称开头,根据需要修改
IMG_SAVE_PATH="/sdcard/examples/data/"
IMG_SAVE_NAME_BEGIN="1_"

LOGO_FILE="/sdcard/examples/16-AI-Cube/logo.jpg"

FONT_X = int(DISPLAY_WIDTH/2-50)
FONT_Y = int(DISPLAY_HEIGHT/2-10)

#sensor = None

def ensure_directory(path):
    parts = path.strip('/').split('/')
    current = '/'
    for part in parts:
        current += part + '/'
        try:
            os.listdir(current)  # check if directory exists
        except OSError:
            try:
                os.mkdir(current)
            except Exception as e:
                print("Failed to create:", current, e)

def media_init():
    global sensor
    # 根据硬件选择显示的方法,默认为IDE显示
    # use LCD for display
    Display.init(Display.ST7701, width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT, to_ide = True, osd_num=1)

    # use hdmi for display
    # Display.init(Display.LT9611, width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT, to_ide = True, osd_num=1)

    # use IDE for display
    #Display.init(Display.VIRT, width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT, fps = 60)

    sensor = Sensor(fps=30)
    sensor.reset()
    
    # 这里设置的frame_size方法与实际使用时必现保持一致,否则校准的参数会不准确
    sensor.set_framesize(w=VIDEO_WIDTH, h=VIDEO_HEIGHT,chn=CAM_CHN_ID_0)
#    sensor.set_framesize(w=640, h=480,chn=CAM_CHN_ID_0, crop = ((1920-1280) // 2, (1080 - 960) // 2, 1280, 960))
    sensor.set_pixformat(Sensor.RGB888)
    sensor.set_framesize(w=DISPLAY_WIDTH, h=DISPLAY_HEIGHT, chn=CAM_CHN_ID_2)
    sensor.set_pixformat(Sensor.RGB888, chn=CAM_CHN_ID_2)
    MediaManager.init()
    sensor.run()

def media_deinit():
    global sensor
    os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
    sensor.stop()
    time.sleep_ms(50)
    Display.deinit()
    MediaManager.deinit()

def save_file(img_0):
    global save_num
    img_1 = img_0.to_jpeg()
    img_name = IMG_SAVE_NAME_BEGIN + str(save_num) + ".jpg"
    img_1.save(IMG_SAVE_PATH + img_name, quality = 99)
    print("save img " + IMG_SAVE_PATH + img_name)
    save_num += 1
    gc.collect()
    return img_name

def gpio_init():
    global KEY
    fpioa = FPIOA()
    fpioa.set_function(PRESS_KEY_NUM,FPIOA.GPIO21)
    KEY=Pin(PRESS_KEY_NUM, Pin.IN, Pin.PULL_UP) #构建KEY对象

def show_logo():
    logo_img = image.Image(LOGO_FILE)
    print("show logo w: " + str(logo_img.width()) + ", h: " + str(logo_img.height()))
    Display.show_image(logo_img.to_rgb888())
    time.sleep(2)


def index_init():
    global save_num
    for file in os.listdir(IMG_SAVE_PATH):
        if file is None:
            break
        if file.startswith(IMG_SAVE_NAME_BEGIN):
            index = file.split('_')[1]
            index = int(index.split('.')[0])
            if save_num <= index:
                save_num = index + 1
    print("index_init start " + str(save_num))

def key_handle(img_save, img_display):
    global KEY, grab_x, grab_y, grab_w, grab_h
    if KEY.value()==0:   #按键被按下
        time.sleep_ms(10) #消除抖动
        if KEY.value()==0: #确认按键被按下
            print('Save')
            img_name = save_file(img_save)
            img_display.draw_string_advanced(0, 0, 100, img_name, color = (0, 0, 255))
            time.sleep(0.8)

            while not KEY.value(): #检测按键是否松开
                pass

    Display.show_image(img_display) #显示图片

try:
    ensure_directory(IMG_SAVE_PATH)
    media_init()
    gpio_init()
    index_init()
#    show_logo()
    while True:
        img_save = sensor.snapshot(chn=CAM_CHN_ID_0)
        img_display = sensor.snapshot(chn=CAM_CHN_ID_2)
        key_handle(img_save, img_display)
        time.sleep_ms(10)
except BaseException as e:
    import sys
    sys.print_exception(e)
media_deinit()
gc.collect()

PC校准脚本

import cv2
import numpy as np
import glob
import os
import json

# 棋盘格内角点的行列数(注意是内角点数,不是方格数)
chessboard_size = (9, 6)  # 9列6行内角点
square_size = 2.4  # 每个格子的大小,单位为 cm

# 构建棋盘格在世界坐标中的3D点(Z=0 平面)
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp *= square_size  # 实际世界坐标按格子大小缩放

# 用于存储所有图像中的3D和2D点
obj_points = []  # 3D 点
img_points = []  # 2D 点

# 加载图像路径
images = glob.glob("test/*.jpg")  # 替换为你图像的实际路径

# 遍历每张图像进行角点检测
for fname in images:
    img = cv2.imread(fname)
    if img is None:
        print(f"无法读取图像: {fname}")
        continue
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    size = gray.shape[::-1]

    # 寻找棋盘格角点
    ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)

    if ret:
        # 追加3D点
        obj_points.append(objp)
        # 精细化角点
        corners2 = cv2.cornerSubPix(
            gray, corners, (5, 5), (-1, -1),
            (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        )
        img_points.append(corners2)

        # 绘制并显示角点
        cv2.drawChessboardCorners(img, chessboard_size, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey()
    else:
        print(f"角点检测失败: {fname}")

cv2.destroyAllWindows()

# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, size, None, None)

# 提取相机内参
fx = mtx[0, 0]
fy = mtx[1, 1]
cx = mtx[0, 2]
cy = mtx[1, 2]
f_avg = (fx + fy) / 2

# 显示内参和焦距
print("\nCamera Matrix (mtx):\n", mtx)
print("\nDistortion Coefficients (dist):\n", dist)
print(f"\nFocal Length fx: {fx:.2f}, fy: {fy:.2f}, average f: {f_avg:.2f}")
print(f"Optical Center cx: {cx:.2f}, cy: {cy:.2f}")

# 保存为 JSON 文件
output = {
    "camera_matrix": mtx.tolist(),
    "dist_coeff": dist.tolist(),
    "fx": fx,
    "fy": fy,
    "cx": cx,
    "cy": cy,
    "f_average": f_avg
}
with open("camera_calibration_result.json", "w") as f:
    json.dump(output, f, indent=4)
print("\n标定结果已保存到 camera_calibration_result.json")

# 创建保存文件夹
os.makedirs("undistorted_output", exist_ok=True)

# (可选)保存去畸变效果图
for fname in images:  # 仅处理前3张图
    img = cv2.imread(fname)
    h, w = img.shape[:2]
    newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))

    # 去畸变
    dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

    # 构造保存路径
    base_name = os.path.basename(fname)
    save_path = os.path.join("undistorted_output", f"undistorted_{base_name}")

    # 保存图像
    cv2.imwrite(save_path, dst)
    print(f"保存去畸变图像: {save_path}")

cv2.destroyAllWindows()