280 lines
6.9 KiB
Python
Executable File
280 lines
6.9 KiB
Python
Executable File
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) |