Callibration Fuji 16mm

This commit is contained in:
ChK
2026-05-14 12:14:46 +02:00
parent 69eb747547
commit bc904c1db2
53 changed files with 903 additions and 20 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

View File

@@ -0,0 +1,280 @@
import cv2
import numpy as np
import glob
import os
import rawpy
from concurrent.futures import ThreadPoolExecutor, as_completed
# ============================================================
# CONFIG
# ============================================================
CHECKERBOARD = (10, 7) # inner corners
SQUARE_SIZE = 25.0 / 1000.0 # 25 mm -> meters
IMAGE_PATTERNS = [
"XPro2-12mm56-1mFokus_f8/*.JPG",
"XPro2-12mm56-1mFokus_f8/*.RAF"
]
# Reject blurry images
MIN_SHARPNESS = 380.0
# Downscale for faster detection
MAX_WIDTH = 2000
# ============================================================
# PREPARE OBJECT POINTS
# ============================================================
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[
0:CHECKERBOARD[0],
0:CHECKERBOARD[1]
].T.reshape(-1, 2)
objp *= SQUARE_SIZE
# ============================================================
# IMAGE PROCESSING
# ============================================================
blurry_images = []
failed_images = []
def process_image(fname):
try:
print(f"\nProcessing: {fname}")
ext = os.path.splitext(fname)[1].lower()
if ext == ".raf" or ext == ".RAF":
with rawpy.imread(fname) as raw:
rgb = raw.postprocess(
use_camera_wb=True,
no_auto_bright=True,
output_bps=8
)
img = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
else:
img = cv2.imread(fname)
if img is None:
print(" -> Could not read image")
return None
#gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = img[:, :, 2]
original_size = gray.shape[::-1]
# ----------------------------------------------------
# DOWNSCALE LARGE IMAGES
# ----------------------------------------------------
scale = 1.0
if gray.shape[1] > MAX_WIDTH:
scale = MAX_WIDTH / gray.shape[1]
gray_small = cv2.resize(
gray,
None,
fx=scale,
fy=scale,
interpolation=cv2.INTER_AREA
)
else:
gray_small = gray
# ----------------------------------------------------
# SHARPNESS CHECK
# ----------------------------------------------------
sharpness = cv2.Laplacian(gray_small, cv2.CV_64F).var()
print(f" Sharpness: {sharpness:.1f}")
if sharpness < MIN_SHARPNESS:
print(" -> Rejected (too blurry)")
failed_images.append(fname)
return None
# ----------------------------------------------------
# FIND CHESSBOARD
# ----------------------------------------------------
flags = (
cv2.CALIB_CB_NORMALIZE_IMAGE |
cv2.CALIB_CB_EXHAUSTIVE
)
# Newer and more robust detector
ret, corners = cv2.findChessboardCornersSB(
gray_small,
CHECKERBOARD,
flags
)
if not ret:
print(" -> No corners found")
return None
# ----------------------------------------------------
# SCALE CORNERS BACK
# ----------------------------------------------------
if scale != 1.0:
corners /= scale
# ----------------------------------------------------
# SUBPIX REFINEMENT
# ----------------------------------------------------
corners2 = cv2.cornerSubPix(
gray,
corners.astype(np.float32),
(11, 11),
(-1, -1),
(
cv2.TERM_CRITERIA_EPS +
cv2.TERM_CRITERIA_MAX_ITER,
30,
1e-6
)
)
print(" -> Success")
return {
"objpoints": objp,
"imgpoints": corners2,
"img_size": original_size
}
except Exception as e:
print(f" -> ERROR: {e}")
return None
# ============================================================
# LOAD IMAGES
# ============================================================
images = []
for pattern in IMAGE_PATTERNS:
images.extend(glob.glob(pattern))
print(f"Found images: {len(images)}")
if len(images) == 0:
raise RuntimeError("No images found.")
# ============================================================
# PROCESS IMAGES IN PARALLEL
# ============================================================
results = []
with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
futures = [
executor.submit(process_image, fname)
for fname in images
]
for future in as_completed(futures):
result = future.result()
if result is not None:
results.append(result)
# ============================================================
# COLLECT VALID RESULTS
# ============================================================
if len(results) == 0:
raise RuntimeError("No valid checkerboards detected.")
objpoints = [r["objpoints"] for r in results]
imgpoints = [r["imgpoints"] for r in results]
img_size = results[0]["img_size"]
print(f"\nValid images: {len(results)} / {len(images)}")
# ============================================================
# CALIBRATION
# ============================================================
print("\nRunning calibration...")
ret, K, D, rvecs, tvecs = cv2.calibrateCamera(
objpoints,
imgpoints,
img_size,
None,
None
)
# ============================================================
# RESULTS
# ============================================================
print("\n=== Calibration Results ===")
print(f"RMS reprojection error: {ret:.4f}")
print("\nCamera Matrix:")
print(K)
print("\nDistortion Coefficients:")
print(D)
# ============================================================
# SAVE
# ============================================================
output_file = "calibration_XPro2-12mm56-1mFokus_f8.npz"
np.savez(
output_file,
camera_matrix=K,
dist_coeffs=D
)
print(f"\nSaved calibration to: {output_file}")
# ------------------------------------------------------------
# DELETE COMMANDS
# ------------------------------------------------------------
if blurry_images:
print("\nDelete blurry images:")
delete_cmd = "rm " + " ".join(
f'"{f}"' for f in blurry_images
)
print(delete_cmd)
if failed_images:
print("\nDelete failed corner images:")
delete_cmd = "rm " + " ".join(
f'"{f}"' for f in failed_images
)
print(delete_cmd)

View File

@@ -0,0 +1,278 @@
import cv2
import numpy as np
import glob
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
# ============================================================
# CONFIG
# ============================================================
CHECKERBOARD = (10, 7) # inner corners
SQUARE_SIZE = 25.0 / 1000.0 # 25 mm -> meters
IMAGE_PATTERNS = [
"XT1-16mm28-1mFokus_f8/*.JPG"
]
# Reject blurry images
MIN_SHARPNESS = 80.0
# Downscale for faster detection
MAX_WIDTH = 2000
# ============================================================
# PREPARE OBJECT POINTS
# ============================================================
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[
0:CHECKERBOARD[0],
0:CHECKERBOARD[1]
].T.reshape(-1, 2)
objp *= SQUARE_SIZE
# ============================================================
# IMAGE PROCESSING
# ============================================================
blurry_images = []
failed_images = []
def process_image(fname):
try:
print(f"\nProcessing: {fname}")
ext = os.path.splitext(fname)[1].lower()
if ext == ".raf" or ext == ".RAF":
with rawpy.imread(fname) as raw:
rgb = raw.postprocess(
use_camera_wb=True,
no_auto_bright=True,
output_bps=8
)
img = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
else:
img = cv2.imread(fname)
if img is None:
print(" -> Could not read image")
return None
#gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = img[:, :, 2]
original_size = gray.shape[::-1]
# ----------------------------------------------------
# DOWNSCALE LARGE IMAGES
# ----------------------------------------------------
scale = 1.0
if gray.shape[1] > MAX_WIDTH:
scale = MAX_WIDTH / gray.shape[1]
gray_small = cv2.resize(
gray,
None,
fx=scale,
fy=scale,
interpolation=cv2.INTER_AREA
)
else:
gray_small = gray
# ----------------------------------------------------
# SHARPNESS CHECK
# ----------------------------------------------------
sharpness = cv2.Laplacian(gray_small, cv2.CV_64F).var()
print(f" Sharpness: {sharpness:.1f}")
if sharpness < MIN_SHARPNESS:
print(" -> Rejected (too blurry)")
failed_images.append(fname)
return None
# ----------------------------------------------------
# FIND CHESSBOARD
# ----------------------------------------------------
flags = (
cv2.CALIB_CB_NORMALIZE_IMAGE |
cv2.CALIB_CB_EXHAUSTIVE
)
# Newer and more robust detector
ret, corners = cv2.findChessboardCornersSB(
gray_small,
CHECKERBOARD,
flags
)
if not ret:
print(" -> No corners found")
return None
# ----------------------------------------------------
# SCALE CORNERS BACK
# ----------------------------------------------------
if scale != 1.0:
corners /= scale
# ----------------------------------------------------
# SUBPIX REFINEMENT
# ----------------------------------------------------
corners2 = cv2.cornerSubPix(
gray,
corners.astype(np.float32),
(11, 11),
(-1, -1),
(
cv2.TERM_CRITERIA_EPS +
cv2.TERM_CRITERIA_MAX_ITER,
30,
1e-6
)
)
print(" -> Success")
return {
"objpoints": objp,
"imgpoints": corners2,
"img_size": original_size
}
except Exception as e:
print(f" -> ERROR: {e}")
return None
# ============================================================
# LOAD IMAGES
# ============================================================
images = []
for pattern in IMAGE_PATTERNS:
images.extend(glob.glob(pattern))
print(f"Found images: {len(images)}")
if len(images) == 0:
raise RuntimeError("No images found.")
# ============================================================
# PROCESS IMAGES IN PARALLEL
# ============================================================
results = []
with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
futures = [
executor.submit(process_image, fname)
for fname in images
]
for future in as_completed(futures):
result = future.result()
if result is not None:
results.append(result)
# ============================================================
# COLLECT VALID RESULTS
# ============================================================
if len(results) == 0:
raise RuntimeError("No valid checkerboards detected.")
objpoints = [r["objpoints"] for r in results]
imgpoints = [r["imgpoints"] for r in results]
img_size = results[0]["img_size"]
print(f"\nValid images: {len(results)} / {len(images)}")
# ============================================================
# CALIBRATION
# ============================================================
print("\nRunning calibration...")
ret, K, D, rvecs, tvecs = cv2.calibrateCamera(
objpoints,
imgpoints,
img_size,
None,
None
)
# ============================================================
# RESULTS
# ============================================================
print("\n=== Calibration Results ===")
print(f"RMS reprojection error: {ret:.4f}")
print("\nCamera Matrix:")
print(K)
print("\nDistortion Coefficients:")
print(D)
# ============================================================
# SAVE
# ============================================================
output_file = "calibration_XT1-16mm28-1mFokus_f8.npz"
np.savez(
output_file,
camera_matrix=K,
dist_coeffs=D
)
print(f"\nSaved calibration to: {output_file}")
# ------------------------------------------------------------
# DELETE COMMANDS
# ------------------------------------------------------------
if blurry_images:
print("\nDelete blurry images:")
delete_cmd = "rm " + " ".join(
f'"{f}"' for f in blurry_images
)
print(delete_cmd)
if failed_images:
print("\nDelete failed corner images:")
delete_cmd = "rm " + " ".join(
f'"{f}"' for f in failed_images
)
print(delete_cmd)

View File

@@ -0,0 +1,280 @@
import cv2
import numpy as np
import glob
import os
import rawpy
from concurrent.futures import ThreadPoolExecutor, as_completed
# ============================================================
# CONFIG
# ============================================================
CHECKERBOARD = (10, 7) # inner corners
SQUARE_SIZE = 25.0 / 1000.0 # 25 mm -> meters
IMAGE_PATTERNS = [
"XPro2-16mm28-1mFokus_f8/*.JPG",
"XPro2-16mm28-1mFokus_f8/*.RAF"
]
# Reject blurry images
MIN_SHARPNESS = 80.0
# Downscale for faster detection
MAX_WIDTH = 2000
# ============================================================
# PREPARE OBJECT POINTS
# ============================================================
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[
0:CHECKERBOARD[0],
0:CHECKERBOARD[1]
].T.reshape(-1, 2)
objp *= SQUARE_SIZE
# ============================================================
# IMAGE PROCESSING
# ============================================================
blurry_images = []
failed_images = []
def process_image(fname):
try:
print(f"\nProcessing: {fname}")
ext = os.path.splitext(fname)[1].lower()
if ext == ".raf" or ext == ".RAF":
with rawpy.imread(fname) as raw:
rgb = raw.postprocess(
use_camera_wb=True,
no_auto_bright=True,
output_bps=8
)
img = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
else:
img = cv2.imread(fname)
if img is None:
print(" -> Could not read image")
return None
#gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = img[:, :, 2]
original_size = gray.shape[::-1]
# ----------------------------------------------------
# DOWNSCALE LARGE IMAGES
# ----------------------------------------------------
scale = 1.0
if gray.shape[1] > MAX_WIDTH:
scale = MAX_WIDTH / gray.shape[1]
gray_small = cv2.resize(
gray,
None,
fx=scale,
fy=scale,
interpolation=cv2.INTER_AREA
)
else:
gray_small = gray
# ----------------------------------------------------
# SHARPNESS CHECK
# ----------------------------------------------------
sharpness = cv2.Laplacian(gray_small, cv2.CV_64F).var()
print(f" Sharpness: {sharpness:.1f}")
if sharpness < MIN_SHARPNESS:
print(" -> Rejected (too blurry)")
failed_images.append(fname)
return None
# ----------------------------------------------------
# FIND CHESSBOARD
# ----------------------------------------------------
flags = (
cv2.CALIB_CB_NORMALIZE_IMAGE |
cv2.CALIB_CB_EXHAUSTIVE
)
# Newer and more robust detector
ret, corners = cv2.findChessboardCornersSB(
gray_small,
CHECKERBOARD,
flags
)
if not ret:
print(" -> No corners found")
return None
# ----------------------------------------------------
# SCALE CORNERS BACK
# ----------------------------------------------------
if scale != 1.0:
corners /= scale
# ----------------------------------------------------
# SUBPIX REFINEMENT
# ----------------------------------------------------
corners2 = cv2.cornerSubPix(
gray,
corners.astype(np.float32),
(11, 11),
(-1, -1),
(
cv2.TERM_CRITERIA_EPS +
cv2.TERM_CRITERIA_MAX_ITER,
30,
1e-6
)
)
print(" -> Success")
return {
"objpoints": objp,
"imgpoints": corners2,
"img_size": original_size
}
except Exception as e:
print(f" -> ERROR: {e}")
return None
# ============================================================
# LOAD IMAGES
# ============================================================
images = []
for pattern in IMAGE_PATTERNS:
images.extend(glob.glob(pattern))
print(f"Found images: {len(images)}")
if len(images) == 0:
raise RuntimeError("No images found.")
# ============================================================
# PROCESS IMAGES IN PARALLEL
# ============================================================
results = []
with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
futures = [
executor.submit(process_image, fname)
for fname in images
]
for future in as_completed(futures):
result = future.result()
if result is not None:
results.append(result)
# ============================================================
# COLLECT VALID RESULTS
# ============================================================
if len(results) == 0:
raise RuntimeError("No valid checkerboards detected.")
objpoints = [r["objpoints"] for r in results]
imgpoints = [r["imgpoints"] for r in results]
img_size = results[0]["img_size"]
print(f"\nValid images: {len(results)} / {len(images)}")
# ============================================================
# CALIBRATION
# ============================================================
print("\nRunning calibration...")
ret, K, D, rvecs, tvecs = cv2.calibrateCamera(
objpoints,
imgpoints,
img_size,
None,
None
)
# ============================================================
# RESULTS
# ============================================================
print("\n=== Calibration Results ===")
print(f"RMS reprojection error: {ret:.4f}")
print("\nCamera Matrix:")
print(K)
print("\nDistortion Coefficients:")
print(D)
# ============================================================
# SAVE
# ============================================================
output_file = "calibration_XPro2_16mm_1m_f8.npz"
np.savez(
output_file,
camera_matrix=K,
dist_coeffs=D
)
print(f"\nSaved calibration to: {output_file}")
# ------------------------------------------------------------
# DELETE COMMANDS
# ------------------------------------------------------------
if blurry_images:
print("\nDelete blurry images:")
delete_cmd = "rm " + " ".join(
f'"{f}"' for f in blurry_images
)
print(delete_cmd)
if failed_images:
print("\nDelete failed corner images:")
delete_cmd = "rm " + " ".join(
f'"{f}"' for f in failed_images
)
print(delete_cmd)

View File

@@ -1,54 +1,99 @@
import cv2 import cv2
import numpy as np import numpy as np
import glob import glob
# #
# Create callibration .npz from checkerboard images. # Create calibration .npz from checkerboard images
#
# #
# Parameters
CHECKERBOARD = (10, 7) # inner corners
square_size = 25.0 / 1000.0 # 25 mm -> meters
# Parameter # Prepare object points
CHECKERBOARD = (9, 6) # innere Ecken objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
square_size = 25.0 / 1000.0 # 25 mm → Meter
# Objektpunkte vorbereiten
objp = np.zeros((CHECKERBOARD[0]*CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
objp *= square_size objp *= square_size
objpoints = [] objpoints = [] # 3D points
imgpoints = [] imgpoints = [] # 2D points
images = glob.glob("calib_images/*.jpg") # Load images
images = glob.glob("XPro2-16mm28-1mFokus/*.JPG")
print("Found images:", len(images))
img_size = None
for fname in images: for fname in images:
img = cv2.imread(fname) img = cv2.imread(fname)
print(f"Processing {fname}...")
if img is None:
print(f"Warning: could not read {fname}")
continue
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Save image size once
if img_size is None:
img_size = gray.shape[::-1]
print(f" Gray")
ret, corners = cv2.findChessboardCorners( ret, corners = cv2.findChessboardCorners(
gray, CHECKERBOARD, gray,
CHECKERBOARD,
flags=cv2.CALIB_CB_ADAPTIVE_THRESH + flags=cv2.CALIB_CB_ADAPTIVE_THRESH +
cv2.CALIB_CB_NORMALIZE_IMAGE + cv2.CALIB_CB_NORMALIZE_IMAGE +
cv2.CALIB_CB_FAST_CHECK cv2.CALIB_CB_FAST_CHECK
) )
print(" Corners found")
if ret: if ret:
corners2 = cv2.cornerSubPix( corners2 = cv2.cornerSubPix(
gray, corners, (11,11), (-1,-1), gray,
corners,
(11, 11),
(-1, -1),
(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6)
) )
objpoints.append(objp) objpoints.append(objp)
imgpoints.append(corners2) imgpoints.append(corners2)
# Kalibrieren print(f"✅ Corners found in {fname}")
else:
print(f"❌ No corners found in {fname}")
print(f"\nTotal valid images: {len(objpoints)} / {len(images)}" )
# Sanity checks
if img_size is None:
raise RuntimeError("No images were successfully loaded.")
if len(objpoints) == 0:
raise RuntimeError("No chessboard corners detected in any image. Calibration failed.")
print("\n=== Sanity Checks Passed ===")
# Calibration
ret, K, D, rvecs, tvecs = cv2.calibrateCamera( ret, K, D, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None objpoints,
imgpoints,
img_size,
None,
None
) )
print("RMS reprojection error:", ret)
# Speichern print("\n=== Calibration Results ===")
np.savez("calibration_cam0.npz", print("RMS reprojection error:", ret)
camera_matrix=K, print("Camera matrix:\n", K)
dist_coeffs=D) print("Distortion coefficients:\n", D)
# Save calibration
np.savez(
"calibration_XPro2_16mm.npz",
camera_matrix=K,
dist_coeffs=D
)
print("\n✅ Calibration saved to calibration_XPro2_16mm.npz")