vendor: OpenCV 5.0.0 snapshot at 40738fb16ceddb5fb3fea747585f7ce6abb0605b
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
add_definitions(-D__OPENCV_BUILD=1)
|
||||
add_definitions(-D__OPENCV_APPS=1)
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
file(RELATIVE_PATH __loc_relative "${OpenCV_BINARY_DIR}" "${CMAKE_CURRENT_LIST_DIR}/pattern-tools\n")
|
||||
file(APPEND "${OpenCV_BINARY_DIR}/opencv_apps_python_tests.cfg" "${__loc_relative}")
|
||||
endif()
|
||||
|
||||
string(REPLACE "," ";" OPENCV_INSTALL_APPS_LIST "${OPENCV_INSTALL_APPS_LIST}") # support comma-separated list (,) too
|
||||
|
||||
# Unified function for creating OpenCV applications:
|
||||
# ocv_add_application(tgt [MODULES <m1> [<m2> ...]] SRCS <src1> [<src2> ...])
|
||||
function(ocv_add_application the_target)
|
||||
cmake_parse_arguments(APP "" "" "MODULES;SRCS" ${ARGN})
|
||||
ocv_check_dependencies(${APP_MODULES})
|
||||
if(NOT OCV_DEPENDENCIES_FOUND)
|
||||
return()
|
||||
endif()
|
||||
|
||||
project(${the_target})
|
||||
ocv_target_include_modules_recurse(${the_target} ${APP_MODULES})
|
||||
ocv_target_include_directories(${the_target} PRIVATE "${OpenCV_SOURCE_DIR}/include/opencv")
|
||||
ocv_add_executable(${the_target} ${APP_SRCS})
|
||||
ocv_target_link_libraries(${the_target} ${APP_MODULES})
|
||||
set_target_properties(${the_target} PROPERTIES
|
||||
DEBUG_POSTFIX "${OPENCV_DEBUG_POSTFIX}"
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${LIBRARY_OUTPUT_PATH}
|
||||
RUNTIME_OUTPUT_DIRECTORY ${EXECUTABLE_OUTPUT_PATH}
|
||||
OUTPUT_NAME "${the_target}")
|
||||
|
||||
if(ENABLE_SOLUTION_FOLDERS)
|
||||
set_target_properties(${the_target} PROPERTIES FOLDER "applications")
|
||||
endif()
|
||||
|
||||
if(NOT INSTALL_CREATE_DISTRIB
|
||||
OR (OPENCV_INSTALL_APPS_LIST STREQUAL "all" OR ";${OPENCV_INSTALL_APPS_LIST};" MATCHES ";${the_target};")
|
||||
)
|
||||
install(TARGETS ${the_target} RUNTIME DESTINATION ${OPENCV_BIN_INSTALL_PATH} COMPONENT dev)
|
||||
elseif(INSTALL_CREATE_DISTRIB)
|
||||
if(BUILD_SHARED_LIBS)
|
||||
install(TARGETS ${the_target} RUNTIME DESTINATION ${OPENCV_BIN_INSTALL_PATH} CONFIGURATIONS Release COMPONENT dev)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
link_libraries(${OPENCV_LINKER_LIBS})
|
||||
|
||||
macro(ocv_add_app directory)
|
||||
if(DEFINED BUILD_APPS_LIST)
|
||||
list(FIND BUILD_APPS_LIST ${directory} _index)
|
||||
if (${_index} GREATER -1)
|
||||
add_subdirectory(${directory})
|
||||
else()
|
||||
message(STATUS "Skip OpenCV app: ${directory}")
|
||||
endif()
|
||||
else()
|
||||
add_subdirectory(${directory})
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
ocv_add_app(interactive-calibration)
|
||||
ocv_add_app(version)
|
||||
ocv_add_app(model-diagnostics)
|
||||
@@ -0,0 +1,741 @@
|
||||
# This file is part of OpenCV project.
|
||||
# It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
# of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
'''
|
||||
Camera calibration for chromatic aberration correction
|
||||
The calibration is done of a photo of black discs on white background.
|
||||
The calibration pattern can be found either in
|
||||
opencv_extra/testdata/cv/cameracalibration/chromatic_aberration/chromatic_aberration_pattern_a3.png,
|
||||
or can be replicated using the script for generating patterns:
|
||||
https://github.com/opencv/opencv/blob/4.x/doc/pattern_tools/gen_pattern.py,
|
||||
using the following invocation:
|
||||
|
||||
python doc/pattern_tools/gen_pattern.py \
|
||||
--output fc4_pattern_A3.svg \
|
||||
--type circles \
|
||||
--rows 26 --columns 37 \
|
||||
--units mm \
|
||||
--square_size 11 \
|
||||
--radius_rate 2.75 \
|
||||
--page_width 420 --page_height 297
|
||||
|
||||
And then converted to PNG:
|
||||
|
||||
inkscape fc4_pattern_A3.svg --export-type=png --export-dpi=300 \
|
||||
--export-background=white --export-background-opacity=1 \
|
||||
--export-filename=fc4_pattern_A3.png
|
||||
|
||||
Calibration image is split into b,g,r, and g is used as reference channel.
|
||||
The centres of each circle in red and blue channels are found as centres of ellipses
|
||||
and then calculated on a subpixel level. Each centre in red or blue channel is paired to
|
||||
a respective centre in green channel. Then, a polynomial model of degree 11 is fit onto the image,
|
||||
minimizing the difference between the displacements between centres in green and red/blue
|
||||
and the actual delta computed with polynomial coefficients. The coefficients are then saved in yaml
|
||||
format and can be used in this sample to correct images of the same camera, lens and settings.
|
||||
|
||||
usage:
|
||||
chromatic_calibration.py calibrate [-h] [--degree DEGREE] --coeffs_file YAML_FILE_PATH image [image ...]
|
||||
chromatic_calibration.py correct [-h] --coeffs_file YAML_FILE_PATH [-o OUTPUT] image
|
||||
chromatic_calibration.py full [-h] [--degree DEGREE] --coeffs_file YAML_FILE_PATH [-o OUTPUT] image
|
||||
|
||||
usage example:
|
||||
chromatic_calibration.py calibrate pattern_aberrated.png --coeffs_file calib_result.yaml
|
||||
|
||||
default values:
|
||||
--degree: 11
|
||||
-o, --output: corrected.png
|
||||
'''
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import pathlib
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import yaml
|
||||
from scipy.optimize import minimize
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
|
||||
@dataclass
|
||||
class Polynomial2D:
|
||||
coeffs_x: np.ndarray
|
||||
coeffs_y: np.ndarray
|
||||
degree: int
|
||||
height: int
|
||||
width: int
|
||||
|
||||
def delta(self, x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
mean_x, mean_y = self.width * 0.5, self.height * 0.5
|
||||
inv_std_x, inv_std_y = 1.0 / mean_x, 1.0 / mean_y
|
||||
x_n = (x - mean_x) * inv_std_x
|
||||
y_n = (y - mean_y) * inv_std_y
|
||||
terms = monomial_terms(x_n, y_n, self.degree)
|
||||
dx = terms @ self.coeffs_x
|
||||
dy = terms @ self.coeffs_y
|
||||
return dx.reshape(x.shape), dy.reshape(y.shape)
|
||||
|
||||
|
||||
|
||||
def validate_calibration_dict(data: dict) -> tuple[int, int, int]:
|
||||
required_keys = {
|
||||
"red_channel", "blue_channel", "image_width", "image_height"
|
||||
}
|
||||
missing = required_keys - data.keys()
|
||||
if missing:
|
||||
raise ValueError(f"Missing keys in YAML: {', '.join(missing)}")
|
||||
|
||||
width = int(data["image_width"])
|
||||
height = int(data["image_height"])
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError("Image width and height must be positive integers")
|
||||
|
||||
def _get_coeffs(channel: str, axis: str) -> np.ndarray:
|
||||
try:
|
||||
coeffs = np.asarray(data[channel][f"coeffs_{axis}"], dtype=float)
|
||||
except KeyError as e:
|
||||
raise ValueError(f"Missing {axis} coefficients for {channel}") from e
|
||||
if coeffs.ndim != 1:
|
||||
raise ValueError(f"{channel} {axis} coefficients must be a 1‑D list/array")
|
||||
if not np.all(np.isfinite(coeffs)):
|
||||
raise ValueError(f"{channel} {axis} coefficients contain NaN or Inf")
|
||||
return coeffs
|
||||
|
||||
rx = _get_coeffs("red_channel", "x")
|
||||
ry = _get_coeffs("red_channel", "y")
|
||||
bx = _get_coeffs("blue_channel", "x")
|
||||
by = _get_coeffs("blue_channel", "y")
|
||||
|
||||
for channel in ["red_channel", "blue_channel"]:
|
||||
try:
|
||||
rms = data[channel]["rms"]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"Missing rms for {channel}") from e
|
||||
|
||||
for name, cx, cy in [("red", rx, ry), ("blue", bx, by)]:
|
||||
if cx.size != cy.size:
|
||||
raise ValueError(
|
||||
f"{name} channel: coeffs_x ({cx.size}) and coeffs_y "
|
||||
f"({cy.size}) lengths differ"
|
||||
)
|
||||
|
||||
if rx.size != bx.size:
|
||||
raise ValueError(
|
||||
f"Red and blue channels use different polynomial sizes "
|
||||
f"({rx.size} vs {bx.size})"
|
||||
)
|
||||
|
||||
m = rx.size
|
||||
n_float = (math.sqrt(1 + 8*m) - 3) / 2
|
||||
degree = int(round(n_float))
|
||||
expected_m = (degree + 1) * (degree + 2) // 2
|
||||
if expected_m != m:
|
||||
raise ValueError(
|
||||
f"Coefficient count {m} is not triangular (n != (deg+1)*(deg+2)/2); "
|
||||
f"nearest degree would be {degree} (needs {expected_m})"
|
||||
)
|
||||
|
||||
return degree, height, width
|
||||
|
||||
|
||||
def load_calib_result(path: str | None = None) -> dict[str, Any]:
|
||||
path = pathlib.Path(path)
|
||||
with path.open("r") as fh:
|
||||
if path.suffix.lower() in {".yaml", ".yml"}:
|
||||
data = yaml.safe_load(fh)
|
||||
else:
|
||||
raise ValueError("YAML file expected as input for the calibration result")
|
||||
|
||||
deg, height, width = validate_calibration_dict(data)
|
||||
|
||||
red_data = data["red_channel"]
|
||||
blue_data = data["blue_channel"]
|
||||
|
||||
poly_r = Polynomial2D(
|
||||
np.asarray(red_data["coeffs_x"]),
|
||||
np.asarray(red_data["coeffs_y"]),
|
||||
deg,
|
||||
height,
|
||||
width
|
||||
)
|
||||
poly_b = Polynomial2D(
|
||||
np.asarray(blue_data["coeffs_x"]),
|
||||
np.asarray(blue_data["coeffs_y"]),
|
||||
deg,
|
||||
height,
|
||||
width
|
||||
)
|
||||
|
||||
return {
|
||||
"poly_red": poly_r,
|
||||
"poly_blue": poly_b,
|
||||
"image_height": height,
|
||||
"image_width": width,
|
||||
}
|
||||
|
||||
|
||||
def repr_flow_seq(dumper, data):
|
||||
return dumper.represent_sequence('tag:yaml.org,2002:seq',
|
||||
data,
|
||||
flow_style=True)
|
||||
|
||||
|
||||
yaml.SafeDumper.add_representer(list, repr_flow_seq)
|
||||
|
||||
|
||||
def save_calib_result(calib, path: str | None = None) -> None:
|
||||
d = {
|
||||
"blue_channel": {
|
||||
"coeffs_x": calib["poly_blue"].coeffs_x.tolist(),
|
||||
"coeffs_y": calib["poly_blue"].coeffs_y.tolist(),
|
||||
"rms": calib["rms_red"]
|
||||
},
|
||||
"red_channel": {
|
||||
"coeffs_x": calib["poly_red"].coeffs_x.tolist(),
|
||||
"coeffs_y": calib["poly_red"].coeffs_y.tolist(),
|
||||
"rms": calib["rms_blue"]
|
||||
},
|
||||
"image_width": calib["image_width"],
|
||||
"image_height": calib["image_height"]
|
||||
}
|
||||
if path is not None:
|
||||
with open(path, "w") as fh:
|
||||
yaml.safe_dump(d,
|
||||
fh,
|
||||
version=(1, 2),
|
||||
default_flow_style=False,
|
||||
sort_keys=False)
|
||||
|
||||
|
||||
def monomial_terms(x: np.ndarray, y: np.ndarray, degree: int) -> np.ndarray:
|
||||
x = x.flatten()
|
||||
y = y.flatten()
|
||||
terms = []
|
||||
cnt = 0
|
||||
for total in range(degree + 1):
|
||||
for i in range(total + 1):
|
||||
j = total - i
|
||||
terms.append((x ** i) * (y ** j))
|
||||
cnt += 1
|
||||
return np.vstack(terms).T
|
||||
|
||||
|
||||
def detect_disk_centres(
|
||||
img: np.ndarray,
|
||||
*,
|
||||
min_area: int = 20,
|
||||
max_area: int | None = None,
|
||||
circularity_thresh: float = 0.7,
|
||||
morph_kernel: int = 3,
|
||||
) -> np.ndarray:
|
||||
if img.ndim != 2:
|
||||
raise ValueError("detect_disk_centres expects a grayscale image")
|
||||
blur = cv2.GaussianBlur(img, (5, 5), 0)
|
||||
_, mask = cv2.threshold(
|
||||
blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU
|
||||
)
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (morph_kernel,) * 2)
|
||||
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
|
||||
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
|
||||
|
||||
centres = []
|
||||
|
||||
for c in cnts:
|
||||
if len(c) < 5:
|
||||
continue
|
||||
area = cv2.contourArea(c)
|
||||
if area < min_area:
|
||||
continue
|
||||
if max_area is not None and area > max_area:
|
||||
continue
|
||||
|
||||
peri = cv2.arcLength(c, closed=True)
|
||||
circularity = 4 * np.pi * area / (peri * peri + 1e-12)
|
||||
if circularity < circularity_thresh:
|
||||
continue
|
||||
(cx, cy), (a, b), theta = cv2.fitEllipse(c)
|
||||
|
||||
eps = 1e-6
|
||||
pts = c.reshape(-1, 2).astype(np.float64)
|
||||
ct, st = np.cos(np.radians(theta)), np.sin(np.radians(theta))
|
||||
r = np.array([[ct, st], [-st, ct]])
|
||||
|
||||
# translate points so that they are centered around mean, and rotate them
|
||||
p = (r @ (pts.T - np.array([[cx], [cy]]))).T
|
||||
# ellipse equation
|
||||
f = (p[:, 0] / (a / 2 + eps)) ** 2 + (p[:, 1] / (b / 2 + eps)) ** 2 - 1
|
||||
# gradients of ellipse equation
|
||||
j = np.column_stack(
|
||||
[2 * p[:, 0] / ((a / 2 + eps) ** 2), 2 * p[:, 1] / ((b / 2 + eps) ** 2)]
|
||||
)
|
||||
|
||||
# solve least squares to get delta of centers
|
||||
delta, *_ = np.linalg.lstsq(j, -f, rcond=None)
|
||||
cx -= delta[0]
|
||||
cy -= delta[1]
|
||||
centres.append((cx, cy))
|
||||
|
||||
if len(centres) == 0:
|
||||
raise RuntimeError("No valid disks detected, check function parameters")
|
||||
|
||||
return np.asarray(centres, dtype=np.float32)
|
||||
|
||||
|
||||
def pair_keypoints(
|
||||
ref: np.ndarray,
|
||||
target: np.ndarray,
|
||||
max_error: float = 30.0,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
tree = cKDTree(ref)
|
||||
dists, idx = tree.query(target, distance_upper_bound=max_error)
|
||||
mask = np.isfinite(dists)
|
||||
if not np.any(mask):
|
||||
raise RuntimeError("No valid keypoint matches were created")
|
||||
target_valid = target[mask]
|
||||
ref_valid = ref[idx[mask]]
|
||||
disp = ref_valid - target_valid
|
||||
return target_valid[:, 0], target_valid[:, 1], disp
|
||||
|
||||
|
||||
def fit_channel(
|
||||
x: np.ndarray,
|
||||
y: np.ndarray,
|
||||
disp: np.ndarray,
|
||||
degree: int,
|
||||
height: int,
|
||||
width: int,
|
||||
method: str = "L-BFGS-B",
|
||||
) -> tuple[np.ndarray, np.ndarray, float]:
|
||||
mean_x, mean_y = width * 0.5, height * 0.5
|
||||
inv_std_x, inv_std_y = 1.0 / mean_x, 1.0 / mean_y
|
||||
x = (x - mean_x) * inv_std_x
|
||||
y = (y - mean_y) * inv_std_y
|
||||
|
||||
terms = monomial_terms(x, y, degree)
|
||||
m = terms.shape[1]
|
||||
|
||||
def objective(c: np.ndarray) -> float:
|
||||
cx = c[:m]
|
||||
cy = c[m:]
|
||||
pred_x = terms @ cx
|
||||
pred_y = terms @ cy
|
||||
err = np.hstack([pred_x - disp[:, 0], pred_y - disp[:, 1]])
|
||||
if np.any(np.isnan(err)) or np.any(np.isinf(err)):
|
||||
return 1e12
|
||||
return np.sum(err ** 2)
|
||||
|
||||
cx_ls, *_ = np.linalg.lstsq(terms, disp[:, 0], rcond=None)
|
||||
cy_ls, *_ = np.linalg.lstsq(terms, disp[:, 1], rcond=None)
|
||||
c0 = np.hstack([cx_ls, cy_ls])
|
||||
|
||||
res = minimize(objective, c0, method=method, options={
|
||||
"maxiter": 500,
|
||||
"maxfun": 5000,
|
||||
"maxls": 50,
|
||||
"ftol": 1e-9,
|
||||
})
|
||||
|
||||
coeffs_x = res.x[:m]
|
||||
coeffs_y = res.x[m:]
|
||||
rms = math.sqrt(res.fun / disp.shape[0])
|
||||
return coeffs_x, coeffs_y, rms
|
||||
|
||||
|
||||
def fit_polynomials(
|
||||
x_r: np.ndarray,
|
||||
y_r: np.ndarray,
|
||||
disp_r: np.ndarray,
|
||||
x_b: np.ndarray,
|
||||
y_b: np.ndarray,
|
||||
disp_b: np.ndarray,
|
||||
degree: int,
|
||||
height: int,
|
||||
width: int
|
||||
) -> tuple[Polynomial2D, Polynomial2D, float, float]:
|
||||
crx, cry, rms_r = fit_channel(x_r, y_r, disp_r, degree, height, width)
|
||||
cbx, cby, rms_b = fit_channel(x_b, y_b, disp_b, degree, height, width)
|
||||
poly_r = Polynomial2D(crx, cry, degree, height, width)
|
||||
poly_b = Polynomial2D(cbx, cby, degree, height, width)
|
||||
return poly_r, poly_b, rms_r, rms_b
|
||||
|
||||
def calibrate(
|
||||
imgs: list[np.ndarray],
|
||||
degree: int = 11,
|
||||
):
|
||||
xr_all, yr_all, dr_all = [], [], []
|
||||
xb_all, yb_all, db_all = [], [], []
|
||||
h0, w0 = None, None
|
||||
|
||||
for i, img in enumerate(imgs):
|
||||
if img is None or img.ndim != 3 or img.shape[2] != 3:
|
||||
raise ValueError("Expected a BGR color image")
|
||||
|
||||
h, w = img.shape[:2]
|
||||
b, g, r = cv2.split(img)
|
||||
|
||||
pts_g = detect_disk_centres(g)
|
||||
pts_r = detect_disk_centres(r)
|
||||
pts_b = detect_disk_centres(b)
|
||||
|
||||
xr, yr, disp_r = pair_keypoints(pts_g, pts_r)
|
||||
xb, yb, disp_b = pair_keypoints(pts_g, pts_b)
|
||||
if h0 is None:
|
||||
h0, w0 = h, w
|
||||
else:
|
||||
if (h, w) != (h0, w0):
|
||||
raise ValueError(
|
||||
f"All calibration images must have the same resolution; "
|
||||
f"got {(h,w)} vs {(h0,w0)} at image #{i}"
|
||||
)
|
||||
|
||||
xr_all.append(xr)
|
||||
yr_all.append(yr)
|
||||
dr_all.append(disp_r)
|
||||
xb_all.append(xb)
|
||||
yb_all.append(yb)
|
||||
db_all.append(disp_b)
|
||||
|
||||
xr = np.concatenate(xr_all, axis=0)
|
||||
yr = np.concatenate(yr_all, axis=0)
|
||||
disp_r = np.concatenate(dr_all, axis=0)
|
||||
|
||||
xb = np.concatenate(xb_all, axis=0)
|
||||
yb = np.concatenate(yb_all, axis=0)
|
||||
disp_b = np.concatenate(db_all, axis=0)
|
||||
|
||||
poly_r, poly_b, rms_r, rms_b = fit_polynomials(
|
||||
xr, yr, disp_r,
|
||||
xb, yb, disp_b,
|
||||
degree, h0, w0
|
||||
)
|
||||
|
||||
print(f"Calibrated polynomial with degree {degree} on {len(imgs)} images, "
|
||||
f"RMS red: {rms_r:.3f} px; RMS blue: {rms_b:.3f} px")
|
||||
|
||||
return {
|
||||
"poly_red": poly_r,
|
||||
"poly_blue": poly_b,
|
||||
"image_width": w0,
|
||||
"image_height": h0,
|
||||
"rms_red": rms_r,
|
||||
"rms_blue": rms_b,
|
||||
}
|
||||
|
||||
def calibrate_multi_degree(
|
||||
imgs: list[np.ndarray],
|
||||
k0: int,
|
||||
k1: int,
|
||||
) -> dict[int, tuple[Polynomial2D, Polynomial2D, float, float]]:
|
||||
"""
|
||||
Returns a dict mapping degree → (poly_r, poly_b, rms_r, rms_b).
|
||||
"""
|
||||
xr_all, yr_all, dr_all = [], [], []
|
||||
xb_all, yb_all, db_all = [], [], []
|
||||
h0, w0 = None, None
|
||||
|
||||
for i, img in enumerate(imgs):
|
||||
if img is None or img.ndim != 3 or img.shape[2] != 3:
|
||||
raise ValueError("Expected a BGR color image")
|
||||
|
||||
h, w = img.shape[:2]
|
||||
b, g, r = cv2.split(img)
|
||||
|
||||
pts_g = detect_disk_centres(g)
|
||||
pts_r = detect_disk_centres(r)
|
||||
pts_b = detect_disk_centres(b)
|
||||
|
||||
xr, yr, disp_r = pair_keypoints(pts_g, pts_r)
|
||||
xb, yb, disp_b = pair_keypoints(pts_g, pts_b)
|
||||
if h0 is None:
|
||||
h0, w0 = h, w
|
||||
else:
|
||||
if (h, w) != (h0, w0):
|
||||
raise ValueError(
|
||||
f"All calibration images must have the same resolution; "
|
||||
f"got {(h,w)} vs {(h0,w0)} at image #{i}"
|
||||
)
|
||||
|
||||
xr_all.append(xr)
|
||||
yr_all.append(yr)
|
||||
dr_all.append(disp_r)
|
||||
xb_all.append(xb)
|
||||
yb_all.append(yb)
|
||||
db_all.append(disp_b)
|
||||
|
||||
xr = np.concatenate(xr_all, axis=0)
|
||||
yr = np.concatenate(yr_all, axis=0)
|
||||
disp_r = np.concatenate(dr_all, axis=0)
|
||||
|
||||
xb = np.concatenate(xb_all, axis=0)
|
||||
yb = np.concatenate(yb_all, axis=0)
|
||||
disp_b = np.concatenate(db_all, axis=0)
|
||||
|
||||
results = {}
|
||||
for deg in range(k0, k1+1):
|
||||
print(deg)
|
||||
|
||||
poly_r, poly_b, rms_r, rms_b = fit_polynomials(
|
||||
xr,
|
||||
yr,
|
||||
disp_r,
|
||||
xb,
|
||||
yb,
|
||||
disp_b,
|
||||
deg,
|
||||
h0,
|
||||
w0
|
||||
)
|
||||
print(f"Calibrated polynomial with degree {deg}, RMS red: {rms_r:.3f} px; RMS blue: {rms_b:.3f} px")
|
||||
results[deg] = (poly_r, poly_b, rms_r, rms_b)
|
||||
return results
|
||||
|
||||
|
||||
def build_remap(
|
||||
h: int,
|
||||
w: int,
|
||||
poly: Polynomial2D,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
x, y = np.meshgrid(np.arange(w, dtype=np.float32), np.arange(h, dtype=np.float32))
|
||||
dx, dy = poly.delta(x, y)
|
||||
map_x = (x - dx).astype(np.float32)
|
||||
map_y = (y - dy).astype(np.float32)
|
||||
return map_x, map_y
|
||||
|
||||
|
||||
def correct_image(
|
||||
img: np.ndarray,
|
||||
calib: dict[str, Any],
|
||||
) -> np.ndarray:
|
||||
if img.ndim != 3 or img.shape[2] != 3:
|
||||
raise ValueError("correct_image expects a BGR colour image")
|
||||
|
||||
h, w = img.shape[:2]
|
||||
b, g, r = cv2.split(img)
|
||||
map_x_r, map_y_r = build_remap(h, w, calib["poly_red"])
|
||||
map_x_b, map_y_b = build_remap(h, w, calib["poly_blue"])
|
||||
|
||||
r_corr = cv2.remap(r, map_x_r, map_y_r, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
||||
b_corr = cv2.remap(b, map_x_b, map_y_b, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
||||
|
||||
map_x_g, map_y_g = np.meshgrid(
|
||||
np.arange(w, dtype=np.float32),
|
||||
np.arange(h, dtype=np.float32)
|
||||
)
|
||||
|
||||
g_corr = cv2.remap(g, map_x_g, map_y_g,
|
||||
cv2.INTER_LINEAR,
|
||||
borderMode=cv2.BORDER_REPLICATE)
|
||||
|
||||
corrected = cv2.merge((b_corr, g_corr, r_corr))
|
||||
return corrected
|
||||
|
||||
def detect_disk_contours(
|
||||
img: np.ndarray,
|
||||
*,
|
||||
min_area: int = 20,
|
||||
max_area: int | None = None,
|
||||
circularity_thresh: float = 0.7,
|
||||
morph_kernel: int = 3,
|
||||
) -> list[np.ndarray]:
|
||||
"""
|
||||
Find all external contours of “discs” in a binary mask of `img` and return
|
||||
their raw point coordinates as a list of (N_i,2) float32 arrays.
|
||||
"""
|
||||
if img.ndim != 2:
|
||||
raise ValueError("detect_disk_contours expects a grayscale image")
|
||||
blur = cv2.GaussianBlur(img, (5, 5), 0)
|
||||
_, mask = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (morph_kernel,)*2)
|
||||
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
|
||||
|
||||
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
|
||||
contours = []
|
||||
for c in cnts:
|
||||
if len(c) < 5:
|
||||
continue
|
||||
area = cv2.contourArea(c)
|
||||
if area < min_area or (max_area is not None and area > max_area):
|
||||
continue
|
||||
peri = cv2.arcLength(c, True)
|
||||
circ = 4 * math.pi * area / (peri*peri + 1e-12)
|
||||
if circ < circularity_thresh:
|
||||
continue
|
||||
pts = c.reshape(-1, 2).astype(np.float32)
|
||||
contours.append(pts)
|
||||
if not contours:
|
||||
raise RuntimeError("No valid disk contours found")
|
||||
return contours
|
||||
|
||||
def warp_and_compare(contours_src: list[np.ndarray],
|
||||
poly_src: Polynomial2D,
|
||||
pts_ref: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Warp src-channel contours through poly_src.delta,
|
||||
then compute for each warped point its distance to the nearest
|
||||
green contour point in pts_ref.
|
||||
"""
|
||||
pts = np.vstack(contours_src)
|
||||
xs, ys = pts[:,0], pts[:,1]
|
||||
dx, dy = poly_src.delta(xs, ys)
|
||||
warped = np.column_stack([xs - dx, ys - dy])
|
||||
|
||||
tree = cKDTree(pts_ref)
|
||||
dists, _ = tree.query(warped, k=1)
|
||||
return dists
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Chromatic aberration calibration and correction tool",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
sc = sub.add_parser("calibrate", help="Calibrate from calibration target image")
|
||||
sc.add_argument("image", nargs="+", help="One or more images of black‑disk calibration target")
|
||||
sc.add_argument("--degree", type=int, default=11, help="Polynomial degree")
|
||||
sc.add_argument("--coeffs_file", required=True, help="Save coefficients to YAML file")
|
||||
|
||||
sr = sub.add_parser("correct", help="Correct a photograph using saved coefficients")
|
||||
sr.add_argument("image", help="Input image to be corrected")
|
||||
sr.add_argument("--coeffs_file", required=True,
|
||||
help="Calibration coefficient file (.json/.yaml)")
|
||||
sr.add_argument("-o", "--output", default="corrected.png", help="Output filename")
|
||||
|
||||
sf = sub.add_parser("full",help="Calibrate from calibration target image and \
|
||||
correct the calibration target")
|
||||
sf.add_argument("image", nargs="+", help="One or more images of black‑disk calibration target")
|
||||
sf.add_argument("--degree", type=int, default=11, help="Polynomial degree")
|
||||
sf.add_argument("--coeffs_file", required=True, help="Save coefficients to YAML file")
|
||||
sf.add_argument("-o", "--output", default="corrected.png", help="Output filename")
|
||||
|
||||
ss = sub.add_parser("scan", help="Sweep degree range and report errors")
|
||||
ss.add_argument("image", nargs="+", help="Calibration image path")
|
||||
ss.add_argument("--degree_range", nargs=2, type=int, metavar=("k0","k1"),
|
||||
required=True, help="Inclusive degree range to scan")
|
||||
ss.add_argument("--method", default="POWELL", help="Optimizer method")
|
||||
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def cmd_calibrate(parsed_args: argparse.Namespace) -> None:
|
||||
paths = parsed_args.image if isinstance(parsed_args.image, list) else [parsed_args.image]
|
||||
imgs = []
|
||||
for p in paths:
|
||||
im = cv2.imread(p, cv2.IMREAD_COLOR)
|
||||
if im is None:
|
||||
raise FileNotFoundError(p)
|
||||
imgs.append(im)
|
||||
|
||||
calib = calibrate(imgs, degree=parsed_args.degree)
|
||||
save_calib_result(calib, path=parsed_args.coeffs_file)
|
||||
print("Saved coefficients to", parsed_args.coeffs_file)
|
||||
|
||||
|
||||
def cmd_correct(parsed_args: argparse.Namespace) -> None:
|
||||
path = parsed_args.image
|
||||
|
||||
fs = cv2.FileStorage(parsed_args.coeffs_file, cv2.FileStorage_READ)
|
||||
if not fs.isOpened():
|
||||
print(f"Could not calibration coefficients from {parsed_args.coeffs_file}")
|
||||
return
|
||||
coeff_mat, calib_size, degree = cv2.loadChromaticAberrationParams(fs.root())
|
||||
|
||||
img = cv2.imread(path, cv2.IMREAD_COLOR)
|
||||
if img is None:
|
||||
print(f"Could not read image {path}")
|
||||
return
|
||||
|
||||
fixed = cv2.correctChromaticAberration(img, coeff_mat, calib_size, degree)
|
||||
|
||||
cv2.imwrite(parsed_args.output, fixed)
|
||||
print(f"Corrected image written to {parsed_args.output}")
|
||||
|
||||
|
||||
def cmd_full(parsed_args: argparse.Namespace) -> None:
|
||||
paths = parsed_args.image if isinstance(parsed_args.image, list) else [parsed_args.image]
|
||||
imgs = []
|
||||
for p in paths:
|
||||
im = cv2.imread(p, cv2.IMREAD_COLOR)
|
||||
if im is None:
|
||||
raise FileNotFoundError(p)
|
||||
imgs.append(im)
|
||||
|
||||
calib = calibrate(imgs, degree=parsed_args.degree)
|
||||
img_for_correction = imgs[0]
|
||||
save_calib_result(calib, path=parsed_args.coeffs_file)
|
||||
print("Saved coefficients to", parsed_args.coeffs_file)
|
||||
|
||||
fs = cv2.FileStorage(parsed_args.coeffs_file, cv2.FileStorage_READ)
|
||||
if not fs.isOpened():
|
||||
print(f"Could not calibration coefficients from {parsed_args.coeffs_file}")
|
||||
return
|
||||
coeff_mat, calib_size, degree = cv2.loadChromaticAberrationParams(fs.root())
|
||||
|
||||
fixed = cv2.correctChromaticAberration(img_for_correction, coeff_mat, calib_size, degree)
|
||||
cv2.imwrite(parsed_args.output, fixed)
|
||||
print(f"Corrected image written to {parsed_args.output}")
|
||||
|
||||
|
||||
def cmd_scan(parsed_args: argparse.Namespace) -> None:
|
||||
paths = parsed_args.image if isinstance(parsed_args.image, list) else [parsed_args.image]
|
||||
imgs = []
|
||||
for p in paths:
|
||||
im = cv2.imread(p, cv2.IMREAD_COLOR)
|
||||
if im is None:
|
||||
raise FileNotFoundError(p)
|
||||
imgs.append(im)
|
||||
|
||||
k0, k1 = parsed_args.degree_range
|
||||
results = calibrate_multi_degree(imgs, k0, k1)
|
||||
|
||||
all_contours_b = []
|
||||
all_contours_g = []
|
||||
all_contours_r = []
|
||||
|
||||
for img in imgs:
|
||||
b, g, r = cv2.split(img)
|
||||
all_contours_b.extend(detect_disk_contours(b))
|
||||
all_contours_g.extend(detect_disk_contours(g))
|
||||
all_contours_r.extend(detect_disk_contours(r))
|
||||
|
||||
pts_g = np.vstack(all_contours_g)
|
||||
|
||||
print(f"Reference degree: {k1}\n")
|
||||
header = "deg | max_r mean_r std_r | max_b mean_b std_b"
|
||||
print(header)
|
||||
print("-" * len(header))
|
||||
|
||||
for deg in sorted(results):
|
||||
if deg == k1:
|
||||
continue
|
||||
pr, pb, _, _ = results[deg]
|
||||
|
||||
d_r = warp_and_compare(all_contours_r, pr, pts_g)
|
||||
d_b = warp_and_compare(all_contours_b, pb, pts_g)
|
||||
|
||||
s = {
|
||||
'max_r': d_r.max(), 'mean_r': d_r.mean(), 'std_r': d_r.std(),
|
||||
'max_b': d_b.max(), 'mean_b': d_b.mean(), 'std_b': d_b.std()
|
||||
}
|
||||
|
||||
print(f"{deg:3d} | "
|
||||
f"{s['max_r']:8.3f} {s['mean_r']:8.3f} {s['std_r']:8.3f} | "
|
||||
f"{s['max_b']:8.3f} {s['mean_b']:8.3f} {s['std_b']:8.3f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
if args.cmd == "calibrate":
|
||||
cmd_calibrate(args)
|
||||
elif args.cmd == "correct":
|
||||
cmd_correct(args)
|
||||
elif args.cmd == "full":
|
||||
cmd_full(args)
|
||||
elif args.cmd == "scan":
|
||||
cmd_scan(args)
|
||||
@@ -0,0 +1,3 @@
|
||||
set(DEPS opencv_core opencv_imgproc opencv_features opencv_highgui opencv_3d opencv_calib opencv_videoio opencv_objdetect)
|
||||
file(GLOB SRCS *.cpp)
|
||||
ocv_add_application(opencv_interactive-calibration MODULES ${DEPS} SRCS ${SRCS})
|
||||
@@ -0,0 +1,139 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#ifndef CALIB_COMMON_HPP
|
||||
#define CALIB_COMMON_HPP
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace calib
|
||||
{
|
||||
#define OVERLAY_DELAY 1000
|
||||
#define IMAGE_MAX_WIDTH 1280
|
||||
#define IMAGE_MAX_HEIGHT 960
|
||||
|
||||
bool showOverlayMessage(const std::string& message);
|
||||
|
||||
enum InputType { Video, Pictures };
|
||||
enum InputVideoSource { Camera, File };
|
||||
enum TemplateType { AcirclesGrid, Chessboard, ChArUco, DoubleAcirclesGrid, CirclesGrid };
|
||||
|
||||
static const std::string mainWindowName = "Calibration";
|
||||
static const std::string gridWindowName = "Board locations";
|
||||
static const std::string consoleHelp = "Hot keys:\nesc - exit application\n"
|
||||
"s - save current data to .xml file\n"
|
||||
"r - delete last frame\n"
|
||||
"u - enable/disable applying undistortion\n"
|
||||
"d - delete all frames\n"
|
||||
"v - switch visualization";
|
||||
|
||||
static const double sigmaMult = 1.96;
|
||||
|
||||
struct calibrationData
|
||||
{
|
||||
cv::Mat cameraMatrix;
|
||||
cv::Mat distCoeffs;
|
||||
cv::Mat stdDeviations;
|
||||
cv::Mat perViewErrors;
|
||||
std::vector<cv::Mat> rvecs;
|
||||
std::vector<cv::Mat> tvecs;
|
||||
double totalAvgErr;
|
||||
cv::Size imageSize;
|
||||
|
||||
std::vector<cv::Mat> allFrames;
|
||||
|
||||
std::vector<std::vector<cv::Point2f> > imagePoints;
|
||||
std::vector< std::vector<cv::Point3f> > objectPoints;
|
||||
|
||||
std::vector<cv::Mat> allCharucoCorners;
|
||||
std::vector<cv::Mat> allCharucoIds;
|
||||
|
||||
cv::Mat undistMap1, undistMap2;
|
||||
|
||||
calibrationData()
|
||||
{
|
||||
imageSize = cv::Size(IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT);
|
||||
}
|
||||
};
|
||||
|
||||
struct cameraParameters
|
||||
{
|
||||
cv::Mat cameraMatrix;
|
||||
cv::Mat distCoeffs;
|
||||
cv::Mat stdDeviations;
|
||||
double avgError;
|
||||
|
||||
cameraParameters(){}
|
||||
cameraParameters(cv::Mat& _cameraMatrix, cv::Mat& _distCoeffs, cv::Mat& _stdDeviations, double _avgError = 0) :
|
||||
cameraMatrix(_cameraMatrix), distCoeffs(_distCoeffs), stdDeviations(_stdDeviations), avgError(_avgError)
|
||||
{}
|
||||
};
|
||||
|
||||
struct captureParameters
|
||||
{
|
||||
InputType captureMethod;
|
||||
InputVideoSource source;
|
||||
TemplateType board;
|
||||
cv::Size inputBoardSize;
|
||||
cv::Size boardSizeInnerCorners; // board size in inner corners for chessboard
|
||||
cv::Size boardSizeUnits; // board size in squares, circles, etc.
|
||||
int charucoDictName;
|
||||
std::string charucoDictFile;
|
||||
int calibrationStep;
|
||||
float charucoSquareLength, charucoMarkerSize;
|
||||
float captureDelay;
|
||||
float squareSize;
|
||||
float templDst;
|
||||
std::string videoFileName;
|
||||
bool flipVertical;
|
||||
int camID;
|
||||
int camBackend;
|
||||
int fps;
|
||||
cv::Size cameraResolution;
|
||||
int maxFramesNum;
|
||||
int minFramesNum;
|
||||
bool saveFrames;
|
||||
float zoom;
|
||||
bool forceReopen;
|
||||
|
||||
captureParameters()
|
||||
{
|
||||
calibrationStep = 1;
|
||||
captureDelay = 500.f;
|
||||
maxFramesNum = 30;
|
||||
minFramesNum = 10;
|
||||
fps = 30;
|
||||
cameraResolution = cv::Size(IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT);
|
||||
saveFrames = false;
|
||||
}
|
||||
};
|
||||
|
||||
struct internalParameters
|
||||
{
|
||||
double solverEps;
|
||||
int solverMaxIters;
|
||||
bool fastSolving;
|
||||
bool rationalModel;
|
||||
bool thinPrismModel;
|
||||
bool tiltedModel;
|
||||
double filterAlpha;
|
||||
|
||||
internalParameters()
|
||||
{
|
||||
solverEps = 1e-7;
|
||||
solverMaxIters = 30;
|
||||
fastSolving = false;
|
||||
rationalModel = false;
|
||||
thinPrismModel = false;
|
||||
tiltedModel = false;
|
||||
filterAlpha = 0.1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,351 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#include "calibController.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
|
||||
#include <opencv2/3d.hpp>
|
||||
#include <opencv2/calib.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <opencv2/imgcodecs.hpp>
|
||||
|
||||
using namespace cv;
|
||||
|
||||
double calib::calibController::estimateCoverageQuality()
|
||||
{
|
||||
int gridSize = 10;
|
||||
int xGridStep = mCalibData->imageSize.width / gridSize;
|
||||
int yGridStep = mCalibData->imageSize.height / gridSize;
|
||||
std::vector<int> pointsInCell(gridSize*gridSize);
|
||||
|
||||
std::fill(pointsInCell.begin(), pointsInCell.end(), 0);
|
||||
|
||||
for(std::vector<std::vector<cv::Point2f> >::iterator it = mCalibData->imagePoints.begin(); it != mCalibData->imagePoints.end(); ++it)
|
||||
for(std::vector<cv::Point2f>::iterator pointIt = (*it).begin(); pointIt != (*it).end(); ++pointIt) {
|
||||
int i = (int)((*pointIt).x / xGridStep);
|
||||
int j = (int)((*pointIt).y / yGridStep);
|
||||
pointsInCell[i*gridSize + j]++;
|
||||
}
|
||||
|
||||
for(std::vector<cv::Mat>::iterator it = mCalibData->allCharucoCorners.begin(); it != mCalibData->allCharucoCorners.end(); ++it)
|
||||
for(int l = 0; l < (*it).size[0]; l++) {
|
||||
int i = (int)((*it).at<float>(l, 0) / xGridStep);
|
||||
int j = (int)((*it).at<float>(l, 1) / yGridStep);
|
||||
pointsInCell[i*gridSize + j]++;
|
||||
}
|
||||
|
||||
cv::Mat mean, stdDev;
|
||||
cv::meanStdDev(pointsInCell, mean, stdDev);
|
||||
|
||||
return mean.at<double>(0) / (stdDev.at<double>(0) + 1e-7);
|
||||
}
|
||||
|
||||
calib::calibController::calibController()
|
||||
{
|
||||
mCalibFlags = 0;
|
||||
}
|
||||
|
||||
calib::calibController::calibController(cv::Ptr<calib::calibrationData> data, int initialFlags, bool autoTuning, int minFramesNum) :
|
||||
mCalibData(data)
|
||||
{
|
||||
mCalibFlags = initialFlags;
|
||||
mNeedTuning = autoTuning;
|
||||
mMinFramesNum = minFramesNum;
|
||||
mConfIntervalsState = false;
|
||||
mCoverageQualityState = false;
|
||||
}
|
||||
|
||||
void calib::calibController::updateState()
|
||||
{
|
||||
if(mCalibData->cameraMatrix.total()) {
|
||||
const double relErrEps = 0.05;
|
||||
bool fConfState = false, cConfState = false, dConfState = true;
|
||||
if(sigmaMult*mCalibData->stdDeviations.at<double>(0) / mCalibData->cameraMatrix.at<double>(0,0) < relErrEps &&
|
||||
sigmaMult*mCalibData->stdDeviations.at<double>(1) / mCalibData->cameraMatrix.at<double>(1,1) < relErrEps)
|
||||
fConfState = true;
|
||||
if(sigmaMult*mCalibData->stdDeviations.at<double>(2) / mCalibData->cameraMatrix.at<double>(0,2) < relErrEps &&
|
||||
sigmaMult*mCalibData->stdDeviations.at<double>(3) / mCalibData->cameraMatrix.at<double>(1,2) < relErrEps)
|
||||
cConfState = true;
|
||||
|
||||
for(int i = 0; i < 5; i++)
|
||||
if(mCalibData->stdDeviations.at<double>(4+i) / fabs(mCalibData->distCoeffs.at<double>(i)) > 1)
|
||||
dConfState = false;
|
||||
|
||||
mConfIntervalsState = fConfState && cConfState && dConfState;
|
||||
}
|
||||
|
||||
if(getFramesNumberState())
|
||||
mCoverageQualityState = estimateCoverageQuality() > 1.8 ? true : false;
|
||||
|
||||
if (getFramesNumberState() && mNeedTuning) {
|
||||
if( !(mCalibFlags & cv::CALIB_FIX_ASPECT_RATIO) &&
|
||||
mCalibData->cameraMatrix.total()) {
|
||||
double fDiff = fabs(mCalibData->cameraMatrix.at<double>(0,0) -
|
||||
mCalibData->cameraMatrix.at<double>(1,1));
|
||||
|
||||
if (fDiff < 3*mCalibData->stdDeviations.at<double>(0) &&
|
||||
fDiff < 3*mCalibData->stdDeviations.at<double>(1)) {
|
||||
mCalibFlags |= cv::CALIB_FIX_ASPECT_RATIO;
|
||||
mCalibData->cameraMatrix.at<double>(0,0) =
|
||||
mCalibData->cameraMatrix.at<double>(1,1);
|
||||
}
|
||||
}
|
||||
|
||||
if(!(mCalibFlags & cv::CALIB_ZERO_TANGENT_DIST)) {
|
||||
const double eps = 0.005;
|
||||
if(fabs(mCalibData->distCoeffs.at<double>(2)) < eps &&
|
||||
fabs(mCalibData->distCoeffs.at<double>(3)) < eps)
|
||||
mCalibFlags |= cv::CALIB_ZERO_TANGENT_DIST;
|
||||
}
|
||||
|
||||
if(!(mCalibFlags & cv::CALIB_FIX_K1)) {
|
||||
const double eps = 0.005;
|
||||
if(fabs(mCalibData->distCoeffs.at<double>(0)) < eps)
|
||||
mCalibFlags |= cv::CALIB_FIX_K1;
|
||||
}
|
||||
|
||||
if(!(mCalibFlags & cv::CALIB_FIX_K2)) {
|
||||
const double eps = 0.005;
|
||||
if(fabs(mCalibData->distCoeffs.at<double>(1)) < eps)
|
||||
mCalibFlags |= cv::CALIB_FIX_K2;
|
||||
}
|
||||
|
||||
if(!(mCalibFlags & cv::CALIB_FIX_K3)) {
|
||||
const double eps = 0.005;
|
||||
if(fabs(mCalibData->distCoeffs.at<double>(4)) < eps)
|
||||
mCalibFlags |= cv::CALIB_FIX_K3;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
bool calib::calibController::getCommonCalibrationState() const
|
||||
{
|
||||
int rating = (int)getFramesNumberState() + (int)getConfidenceIntrervalsState() +
|
||||
(int)getRMSState() + (int)mCoverageQualityState;
|
||||
return rating == 4;
|
||||
}
|
||||
|
||||
bool calib::calibController::getFramesNumberState() const
|
||||
{
|
||||
return std::max(mCalibData->imagePoints.size(), mCalibData->allCharucoCorners.size()) > mMinFramesNum;
|
||||
}
|
||||
|
||||
bool calib::calibController::getConfidenceIntrervalsState() const
|
||||
{
|
||||
return mConfIntervalsState;
|
||||
}
|
||||
|
||||
bool calib::calibController::getRMSState() const
|
||||
{
|
||||
return mCalibData->totalAvgErr < 0.5;
|
||||
}
|
||||
|
||||
int calib::calibController::getNewFlags() const
|
||||
{
|
||||
return mCalibFlags;
|
||||
}
|
||||
|
||||
|
||||
//////////////////// calibDataController
|
||||
|
||||
double calib::calibDataController::estimateGridSubsetQuality(size_t excludedIndex)
|
||||
{
|
||||
{
|
||||
int gridSize = 10;
|
||||
int xGridStep = mCalibData->imageSize.width / gridSize;
|
||||
int yGridStep = mCalibData->imageSize.height / gridSize;
|
||||
std::vector<int> pointsInCell(gridSize*gridSize);
|
||||
|
||||
std::fill(pointsInCell.begin(), pointsInCell.end(), 0);
|
||||
|
||||
for(size_t k = 0; k < mCalibData->imagePoints.size(); k++)
|
||||
if(k != excludedIndex)
|
||||
for(std::vector<cv::Point2f>::iterator pointIt = mCalibData->imagePoints[k].begin(); pointIt != mCalibData->imagePoints[k].end(); ++pointIt) {
|
||||
int i = (int)((*pointIt).x / xGridStep);
|
||||
int j = (int)((*pointIt).y / yGridStep);
|
||||
pointsInCell[i*gridSize + j]++;
|
||||
}
|
||||
|
||||
for(size_t k = 0; k < mCalibData->allCharucoCorners.size(); k++)
|
||||
if(k != excludedIndex)
|
||||
for(int l = 0; l < mCalibData->allCharucoCorners[k].size[0]; l++) {
|
||||
int i = (int)(mCalibData->allCharucoCorners[k].at<float>(l, 0) / xGridStep);
|
||||
int j = (int)(mCalibData->allCharucoCorners[k].at<float>(l, 1) / yGridStep);
|
||||
pointsInCell[i*gridSize + j]++;
|
||||
}
|
||||
|
||||
cv::Mat mean, stdDev;
|
||||
cv::meanStdDev(pointsInCell, mean, stdDev);
|
||||
|
||||
return mean.at<double>(0) / (stdDev.at<double>(0) + 1e-7);
|
||||
}
|
||||
}
|
||||
|
||||
calib::calibDataController::calibDataController(cv::Ptr<calib::calibrationData> data, int maxFrames, double convParameter) :
|
||||
mCalibData(data), mParamsFileName("CamParams.xml")
|
||||
{
|
||||
mMaxFramesNum = maxFrames;
|
||||
mAlpha = convParameter;
|
||||
}
|
||||
|
||||
calib::calibDataController::calibDataController()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void calib::calibDataController::filterFrames()
|
||||
{
|
||||
size_t numberOfFrames = std::max(mCalibData->allCharucoIds.size(), mCalibData->imagePoints.size());
|
||||
CV_Assert(numberOfFrames == mCalibData->perViewErrors.total());
|
||||
if(numberOfFrames >= mMaxFramesNum) {
|
||||
|
||||
double worstValue = -HUGE_VAL, maxQuality = estimateGridSubsetQuality(numberOfFrames);
|
||||
size_t worstElemIndex = 0;
|
||||
for(size_t i = 0; i < numberOfFrames; i++) {
|
||||
double gridQDelta = estimateGridSubsetQuality(i) - maxQuality;
|
||||
double currentValue = mCalibData->perViewErrors.at<double>((int)i)*mAlpha + gridQDelta*(1. - mAlpha);
|
||||
if(currentValue > worstValue) {
|
||||
worstValue = currentValue;
|
||||
worstElemIndex = i;
|
||||
}
|
||||
}
|
||||
showOverlayMessage(cv::format("Frame %zu is worst", worstElemIndex + 1));
|
||||
|
||||
if(mCalibData->allFrames.size())
|
||||
mCalibData->allFrames.erase(mCalibData->allFrames.begin() + worstElemIndex);
|
||||
|
||||
if(mCalibData->imagePoints.size()) {
|
||||
mCalibData->imagePoints.erase(mCalibData->imagePoints.begin() + worstElemIndex);
|
||||
mCalibData->objectPoints.erase(mCalibData->objectPoints.begin() + worstElemIndex);
|
||||
if (mCalibData->allCharucoCorners.size()) {
|
||||
mCalibData->allCharucoCorners.erase(mCalibData->allCharucoCorners.begin() + worstElemIndex);
|
||||
mCalibData->allCharucoIds.erase(mCalibData->allCharucoIds.begin() + worstElemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
cv::Mat newErrorsVec = cv::Mat((int)numberOfFrames - 1, 1, CV_64F);
|
||||
std::copy(mCalibData->perViewErrors.ptr<double>(0),
|
||||
mCalibData->perViewErrors.ptr<double>((int)worstElemIndex), newErrorsVec.ptr<double>(0));
|
||||
if((int)worstElemIndex < (int)numberOfFrames-1) {
|
||||
std::copy(mCalibData->perViewErrors.ptr<double>((int)worstElemIndex + 1), mCalibData->perViewErrors.ptr<double>((int)numberOfFrames),
|
||||
newErrorsVec.ptr<double>((int)worstElemIndex));
|
||||
}
|
||||
mCalibData->perViewErrors = newErrorsVec;
|
||||
}
|
||||
}
|
||||
|
||||
void calib::calibDataController::setParametersFileName(const std::string &name)
|
||||
{
|
||||
mParamsFileName = name;
|
||||
}
|
||||
|
||||
void calib::calibDataController::deleteLastFrame()
|
||||
{
|
||||
if(!mCalibData->allFrames.empty())
|
||||
{
|
||||
mCalibData->allFrames.pop_back();
|
||||
}
|
||||
|
||||
if( !mCalibData->imagePoints.empty()) {
|
||||
mCalibData->imagePoints.pop_back();
|
||||
mCalibData->objectPoints.pop_back();
|
||||
}
|
||||
|
||||
if (!mCalibData->allCharucoCorners.empty()) {
|
||||
mCalibData->allCharucoCorners.pop_back();
|
||||
mCalibData->allCharucoIds.pop_back();
|
||||
}
|
||||
|
||||
if(!mParamsStack.empty()) {
|
||||
mCalibData->cameraMatrix = (mParamsStack.top()).cameraMatrix;
|
||||
mCalibData->distCoeffs = (mParamsStack.top()).distCoeffs;
|
||||
mCalibData->stdDeviations = (mParamsStack.top()).stdDeviations;
|
||||
mCalibData->totalAvgErr = (mParamsStack.top()).avgError;
|
||||
mParamsStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void calib::calibDataController::rememberCurrentParameters()
|
||||
{
|
||||
cv::Mat oldCameraMat, oldDistcoeefs, oldStdDevs;
|
||||
mCalibData->cameraMatrix.copyTo(oldCameraMat);
|
||||
mCalibData->distCoeffs.copyTo(oldDistcoeefs);
|
||||
mCalibData->stdDeviations.copyTo(oldStdDevs);
|
||||
mParamsStack.push(cameraParameters(oldCameraMat, oldDistcoeefs, oldStdDevs, mCalibData->totalAvgErr));
|
||||
}
|
||||
|
||||
void calib::calibDataController::deleteAllData()
|
||||
{
|
||||
mCalibData->allFrames.clear();
|
||||
mCalibData->imagePoints.clear();
|
||||
mCalibData->objectPoints.clear();
|
||||
mCalibData->allCharucoCorners.clear();
|
||||
mCalibData->allCharucoIds.clear();
|
||||
mCalibData->cameraMatrix = mCalibData->distCoeffs = cv::Mat();
|
||||
mParamsStack = std::stack<cameraParameters>();
|
||||
rememberCurrentParameters();
|
||||
}
|
||||
|
||||
bool calib::calibDataController::saveCurrentCameraParameters() const
|
||||
{
|
||||
|
||||
for(size_t i = 0; i < mCalibData->allFrames.size(); i++)
|
||||
cv::imwrite(cv::format("calibration_%zu.png", i), mCalibData->allFrames[i]);
|
||||
|
||||
bool success = false;
|
||||
if(mCalibData->cameraMatrix.total()) {
|
||||
cv::FileStorage parametersWriter(mParamsFileName, cv::FileStorage::WRITE);
|
||||
if(parametersWriter.isOpened()) {
|
||||
time_t rawtime;
|
||||
time(&rawtime);
|
||||
char buf[256];
|
||||
strftime(buf, sizeof(buf)-1, "%c", localtime(&rawtime));
|
||||
|
||||
parametersWriter << "calibrationDate" << buf;
|
||||
parametersWriter << "framesCount" << std::max((int)mCalibData->objectPoints.size(), (int)mCalibData->allCharucoCorners.size());
|
||||
parametersWriter << "cameraResolution" << mCalibData->imageSize;
|
||||
parametersWriter << "camera_matrix" << mCalibData->cameraMatrix;
|
||||
parametersWriter << "camera_matrix_std_dev" << mCalibData->stdDeviations.rowRange(cv::Range(0, 4));
|
||||
parametersWriter << "distortion_coefficients" << mCalibData->distCoeffs;
|
||||
parametersWriter << "distortion_coefficients_std_dev" << mCalibData->stdDeviations.rowRange(cv::Range(4, 9));
|
||||
parametersWriter << "avg_reprojection_error" << mCalibData->totalAvgErr;
|
||||
|
||||
parametersWriter.release();
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void calib::calibDataController::printParametersToConsole(std::ostream &output) const
|
||||
{
|
||||
const char* border = "---------------------------------------------------";
|
||||
output << border << std::endl;
|
||||
output << "Frames used for calibration: " << std::max(mCalibData->objectPoints.size(), mCalibData->allCharucoCorners.size())
|
||||
<< " \t RMS = " << mCalibData->totalAvgErr << std::endl;
|
||||
if(mCalibData->cameraMatrix.at<double>(0,0) == mCalibData->cameraMatrix.at<double>(1,1))
|
||||
output << "F = " << mCalibData->cameraMatrix.at<double>(1,1) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(1) << std::endl;
|
||||
else
|
||||
output << "Fx = " << mCalibData->cameraMatrix.at<double>(0,0) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(0) << " \t "
|
||||
<< "Fy = " << mCalibData->cameraMatrix.at<double>(1,1) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(1) << std::endl;
|
||||
output << "Cx = " << mCalibData->cameraMatrix.at<double>(0,2) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(2) << " \t"
|
||||
<< "Cy = " << mCalibData->cameraMatrix.at<double>(1,2) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(3) << std::endl;
|
||||
output << "K1 = " << mCalibData->distCoeffs.at<double>(0) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(4) << std::endl;
|
||||
output << "K2 = " << mCalibData->distCoeffs.at<double>(1) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(5) << std::endl;
|
||||
output << "K3 = " << mCalibData->distCoeffs.at<double>(4) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(8) << std::endl;
|
||||
output << "TD1 = " << mCalibData->distCoeffs.at<double>(2) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(6) << std::endl;
|
||||
output << "TD2 = " << mCalibData->distCoeffs.at<double>(3) << " +- " << sigmaMult*mCalibData->stdDeviations.at<double>(7) << std::endl;
|
||||
}
|
||||
|
||||
void calib::calibDataController::updateUndistortMap()
|
||||
{
|
||||
cv::initUndistortRectifyMap(mCalibData->cameraMatrix, mCalibData->distCoeffs, cv::noArray(),
|
||||
cv::getOptimalNewCameraMatrix(mCalibData->cameraMatrix, mCalibData->distCoeffs, mCalibData->imageSize, 0.0, mCalibData->imageSize),
|
||||
mCalibData->imageSize, CV_16SC2, mCalibData->undistMap1, mCalibData->undistMap2);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#ifndef CALIB_CONTROLLER_HPP
|
||||
#define CALIB_CONTROLLER_HPP
|
||||
|
||||
#include "calibCommon.hpp"
|
||||
|
||||
#include <stack>
|
||||
#include <string>
|
||||
#include <ostream>
|
||||
|
||||
namespace calib {
|
||||
|
||||
class calibController
|
||||
{
|
||||
protected:
|
||||
cv::Ptr<calibrationData> mCalibData;
|
||||
int mCalibFlags;
|
||||
unsigned mMinFramesNum;
|
||||
bool mNeedTuning;
|
||||
bool mConfIntervalsState;
|
||||
bool mCoverageQualityState;
|
||||
|
||||
double estimateCoverageQuality();
|
||||
public:
|
||||
calibController();
|
||||
calibController(cv::Ptr<calibrationData> data, int initialFlags, bool autoTuning,
|
||||
int minFramesNum);
|
||||
|
||||
void updateState();
|
||||
|
||||
bool getCommonCalibrationState() const;
|
||||
|
||||
bool getFramesNumberState() const;
|
||||
bool getConfidenceIntrervalsState() const;
|
||||
bool getRMSState() const;
|
||||
bool getPointsCoverageState() const;
|
||||
int getNewFlags() const;
|
||||
};
|
||||
|
||||
class calibDataController
|
||||
{
|
||||
protected:
|
||||
cv::Ptr<calibrationData> mCalibData;
|
||||
std::stack<cameraParameters> mParamsStack;
|
||||
std::string mParamsFileName;
|
||||
unsigned mMaxFramesNum;
|
||||
double mAlpha;
|
||||
|
||||
double estimateGridSubsetQuality(size_t excludedIndex);
|
||||
public:
|
||||
calibDataController(cv::Ptr<calibrationData> data, int maxFrames, double convParameter);
|
||||
calibDataController();
|
||||
|
||||
void filterFrames();
|
||||
void setParametersFileName(const std::string& name);
|
||||
void deleteLastFrame();
|
||||
void rememberCurrentParameters();
|
||||
void deleteAllData();
|
||||
bool saveCurrentCameraParameters() const;
|
||||
void printParametersToConsole(std::ostream &output) const;
|
||||
void updateUndistortMap();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,150 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#include "calibPipeline.hpp"
|
||||
|
||||
#include <opencv2/highgui.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <opencv2/core/utils/logger.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace calib;
|
||||
|
||||
#define CAP_DELAY 10
|
||||
|
||||
cv::Size CalibPipeline::getCameraResolution()
|
||||
{
|
||||
mCapture.set(cv::CAP_PROP_FRAME_WIDTH, 10000);
|
||||
mCapture.set(cv::CAP_PROP_FRAME_HEIGHT, 10000);
|
||||
int w = (int)mCapture.get(cv::CAP_PROP_FRAME_WIDTH);
|
||||
int h = (int)mCapture.get(cv::CAP_PROP_FRAME_HEIGHT);
|
||||
return cv::Size(w,h);
|
||||
}
|
||||
|
||||
CalibPipeline::CalibPipeline(captureParameters params) :
|
||||
mCaptureParams(params)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
PipelineExitStatus CalibPipeline::start(std::vector<cv::Ptr<FrameProcessor> > processors)
|
||||
{
|
||||
const int allowedEmptyFrames = 5;
|
||||
int emptyFrames = 0;
|
||||
|
||||
auto open_camera = [this] () {
|
||||
if(mCaptureParams.source == Camera)
|
||||
{
|
||||
mCapture.open(mCaptureParams.camID, mCaptureParams.camBackend);
|
||||
cv::Size maxRes = getCameraResolution();
|
||||
cv::Size neededRes = mCaptureParams.cameraResolution;
|
||||
|
||||
if(maxRes.width < neededRes.width) {
|
||||
double aR = (double)maxRes.width / maxRes.height;
|
||||
mCapture.set(cv::CAP_PROP_FRAME_WIDTH, neededRes.width);
|
||||
mCapture.set(cv::CAP_PROP_FRAME_HEIGHT, neededRes.width/aR);
|
||||
}
|
||||
else if(maxRes.height < neededRes.height) {
|
||||
double aR = (double)maxRes.width / maxRes.height;
|
||||
mCapture.set(cv::CAP_PROP_FRAME_HEIGHT, neededRes.height);
|
||||
mCapture.set(cv::CAP_PROP_FRAME_WIDTH, neededRes.height*aR);
|
||||
}
|
||||
else {
|
||||
mCapture.set(cv::CAP_PROP_FRAME_HEIGHT, neededRes.height);
|
||||
mCapture.set(cv::CAP_PROP_FRAME_WIDTH, neededRes.width);
|
||||
}
|
||||
mCapture.set(cv::CAP_PROP_AUTOFOCUS, 0);
|
||||
}
|
||||
else if (mCaptureParams.source == File)
|
||||
mCapture.open(mCaptureParams.videoFileName, mCaptureParams.camBackend);
|
||||
};
|
||||
|
||||
if(!mCapture.isOpened()) {
|
||||
open_camera();
|
||||
}
|
||||
mImageSize = cv::Size((int)mCapture.get(cv::CAP_PROP_FRAME_WIDTH), (int)mCapture.get(cv::CAP_PROP_FRAME_HEIGHT));
|
||||
|
||||
if(!mCapture.isOpened())
|
||||
throw std::runtime_error("Unable to open video source");
|
||||
|
||||
cv::Mat frame, processedFrame, resizedFrame;
|
||||
while (true) {
|
||||
if (!mCapture.grab())
|
||||
{
|
||||
if (!mCaptureParams.forceReopen)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "VideoCapture error: could not grab the frame.");
|
||||
break;
|
||||
}
|
||||
|
||||
CV_LOG_INFO(NULL, "VideoCapture error: trying to reopen...");
|
||||
do
|
||||
{
|
||||
open_camera();
|
||||
} while (!mCapture.isOpened() || !mCapture.grab());
|
||||
|
||||
CV_LOG_INFO(NULL, "VideoCapture error: reopened successfully.");
|
||||
auto newSize = cv::Size((int)mCapture.get(cv::CAP_PROP_FRAME_WIDTH), (int)mCapture.get(cv::CAP_PROP_FRAME_HEIGHT));
|
||||
CV_CheckEQ(mImageSize, newSize, "Camera image size changed after reopening.");
|
||||
}
|
||||
mCapture.retrieve(frame);
|
||||
|
||||
if (frame.empty()) {
|
||||
emptyFrames++;
|
||||
if (emptyFrames >= allowedEmptyFrames) {
|
||||
CV_LOG_ERROR(NULL, "VideoCapture error: grabbed sequence of empty frames. VideoCapture is not ready or broken.");
|
||||
return Finished;
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
emptyFrames = 0;
|
||||
if (mImageSize.width == 0 || mImageSize.height == 0) { // looks like VideoCapture does not support required properties
|
||||
mImageSize = frame.size();
|
||||
}
|
||||
}
|
||||
|
||||
if(mCaptureParams.flipVertical)
|
||||
cv::flip(frame, frame, -1);
|
||||
|
||||
frame.copyTo(processedFrame);
|
||||
for (std::vector<cv::Ptr<FrameProcessor> >::iterator it = processors.begin(); it != processors.end(); ++it)
|
||||
processedFrame = (*it)->processFrame(processedFrame);
|
||||
if (std::fabs(mCaptureParams.zoom - 1.) > 0.001f)
|
||||
{
|
||||
cv::resize(processedFrame, resizedFrame, cv::Size(), mCaptureParams.zoom, mCaptureParams.zoom);
|
||||
}
|
||||
else
|
||||
{
|
||||
resizedFrame = std::move(processedFrame);
|
||||
}
|
||||
cv::imshow(mainWindowName, resizedFrame);
|
||||
char key = (char)cv::waitKey(CAP_DELAY);
|
||||
|
||||
if(key == 27) // esc
|
||||
return Finished;
|
||||
else if (key == 114) // r
|
||||
return DeleteLastFrame;
|
||||
else if (key == 100) // d
|
||||
return DeleteAllFrames;
|
||||
else if (key == 115) // s
|
||||
return SaveCurrentData;
|
||||
else if (key == 117) // u
|
||||
return SwitchUndistort;
|
||||
else if (key == 118) // v
|
||||
return SwitchVisualisation;
|
||||
|
||||
for (std::vector<cv::Ptr<FrameProcessor> >::iterator it = processors.begin(); it != processors.end(); ++it)
|
||||
if((*it)->isProcessed())
|
||||
return Calibrate;
|
||||
}
|
||||
|
||||
return Finished;
|
||||
}
|
||||
|
||||
cv::Size CalibPipeline::getImageSize() const
|
||||
{
|
||||
return mImageSize;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
|
||||
#ifndef CALIB_PIPELINE_HPP
|
||||
#define CALIB_PIPELINE_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <opencv2/highgui.hpp>
|
||||
|
||||
#include "calibCommon.hpp"
|
||||
#include "frameProcessor.hpp"
|
||||
|
||||
namespace calib
|
||||
{
|
||||
|
||||
enum PipelineExitStatus { Finished,
|
||||
DeleteLastFrame,
|
||||
Calibrate,
|
||||
DeleteAllFrames,
|
||||
SaveCurrentData,
|
||||
SwitchUndistort,
|
||||
SwitchVisualisation
|
||||
};
|
||||
|
||||
class CalibPipeline
|
||||
{
|
||||
protected:
|
||||
captureParameters mCaptureParams;
|
||||
cv::Size mImageSize;
|
||||
cv::VideoCapture mCapture;
|
||||
|
||||
cv::Size getCameraResolution();
|
||||
|
||||
public:
|
||||
CalibPipeline(captureParameters params);
|
||||
PipelineExitStatus start(std::vector<cv::Ptr<FrameProcessor> > processors);
|
||||
cv::Size getImageSize() const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0"?>
|
||||
<opencv_storage>
|
||||
<charuco_dict>0</charuco_dict>
|
||||
<charuco_square_length>200</charuco_square_length>
|
||||
<charuco_marker_size>100</charuco_marker_size>
|
||||
<calibration_step>1</calibration_step>
|
||||
<max_frames_num>30</max_frames_num>
|
||||
<min_frames_num>10</min_frames_num>
|
||||
<solver_eps>1e-7</solver_eps>
|
||||
<solver_max_iters>30</solver_max_iters>
|
||||
<fast_solver>0</fast_solver>
|
||||
<frame_filter_conv_param>0.1</frame_filter_conv_param>
|
||||
<camera_resolution>800 600</camera_resolution>
|
||||
</opencv_storage>
|
||||
@@ -0,0 +1,561 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#include "frameProcessor.hpp"
|
||||
#include "rotationConverters.hpp"
|
||||
|
||||
#include <opencv2/3d.hpp>
|
||||
#include <opencv2/calib.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <opencv2/highgui.hpp>
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <limits>
|
||||
|
||||
using namespace calib;
|
||||
|
||||
#define VIDEO_TEXT_SIZE 4
|
||||
#define POINT_SIZE 5
|
||||
|
||||
static cv::SimpleBlobDetector::Params getDetectorParams()
|
||||
{
|
||||
cv::SimpleBlobDetector::Params detectorParams;
|
||||
|
||||
detectorParams.thresholdStep = 40;
|
||||
detectorParams.minThreshold = 20;
|
||||
detectorParams.maxThreshold = 500;
|
||||
detectorParams.minRepeatability = 2;
|
||||
detectorParams.minDistBetweenBlobs = 5;
|
||||
|
||||
detectorParams.filterByColor = true;
|
||||
detectorParams.blobColor = 0;
|
||||
|
||||
detectorParams.filterByArea = true;
|
||||
detectorParams.minArea = 5;
|
||||
detectorParams.maxArea = 5000;
|
||||
|
||||
detectorParams.filterByCircularity = false;
|
||||
detectorParams.minCircularity = 0.8f;
|
||||
detectorParams.maxCircularity = std::numeric_limits<float>::max();
|
||||
|
||||
detectorParams.filterByInertia = true;
|
||||
detectorParams.minInertiaRatio = 0.1f;
|
||||
detectorParams.maxInertiaRatio = std::numeric_limits<float>::max();
|
||||
|
||||
detectorParams.filterByConvexity = true;
|
||||
detectorParams.minConvexity = 0.8f;
|
||||
detectorParams.maxConvexity = std::numeric_limits<float>::max();
|
||||
|
||||
return detectorParams;
|
||||
}
|
||||
|
||||
FrameProcessor::~FrameProcessor()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool CalibProcessor::detectAndParseChessboard(const cv::Mat &frame)
|
||||
{
|
||||
int chessBoardFlags = cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE | cv::CALIB_CB_FAST_CHECK;
|
||||
bool isTemplateFound = cv::findChessboardCorners(frame, mBoardSizeInnerCorners, mCurrentImagePoints, chessBoardFlags);
|
||||
|
||||
if (isTemplateFound) {
|
||||
cv::Mat viewGray;
|
||||
cv::cvtColor(frame, viewGray, cv::COLOR_BGR2GRAY);
|
||||
cv::cornerSubPix(viewGray, mCurrentImagePoints, cv::Size(11,11),
|
||||
cv::Size(-1,-1), cv::TermCriteria( cv::TermCriteria::EPS+cv::TermCriteria::COUNT, 30, 0.1 ));
|
||||
cv::drawChessboardCorners(frame, mBoardSizeInnerCorners, cv::Mat(mCurrentImagePoints), isTemplateFound);
|
||||
mTemplateLocations.insert(mTemplateLocations.begin(), mCurrentImagePoints[0]);
|
||||
}
|
||||
return isTemplateFound;
|
||||
}
|
||||
|
||||
bool CalibProcessor::detectAndParseChAruco(const cv::Mat &frame)
|
||||
{
|
||||
cv::Ptr<cv::aruco::Board> board = mCharucoBoard.staticCast<cv::aruco::Board>();
|
||||
|
||||
std::vector<std::vector<cv::Point2f> > corners;
|
||||
std::vector<int> ids;
|
||||
cv::Mat currentCharucoCorners, currentCharucoIds;
|
||||
detector->detectBoard(frame, currentCharucoCorners, currentCharucoIds, corners, ids);
|
||||
if(ids.size() > 0) cv::aruco::drawDetectedMarkers(frame, corners);
|
||||
|
||||
if(currentCharucoCorners.total() > 3) {
|
||||
float centerX = 0, centerY = 0;
|
||||
for (int i = 0; i < currentCharucoCorners.size[0]; i++) {
|
||||
centerX += currentCharucoCorners.at<float>(i, 0);
|
||||
centerY += currentCharucoCorners.at<float>(i, 1);
|
||||
}
|
||||
centerX /= currentCharucoCorners.size[0];
|
||||
centerY /= currentCharucoCorners.size[0];
|
||||
|
||||
mTemplateLocations.insert(mTemplateLocations.begin(), cv::Point2f(centerX, centerY));
|
||||
cv::aruco::drawDetectedCornersCharuco(frame, currentCharucoCorners, currentCharucoIds);
|
||||
mCurrentCharucoCorners = currentCharucoCorners;
|
||||
mCurrentCharucoIds = currentCharucoIds;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CalibProcessor::detectAndParseCircles(const cv::Mat &frame)
|
||||
{
|
||||
bool isTemplateFound = findCirclesGrid(frame, mBoardSizeUnits, mCurrentImagePoints, cv::CALIB_CB_SYMMETRIC_GRID, mBlobDetectorPtr);
|
||||
if(isTemplateFound) {
|
||||
mTemplateLocations.insert(mTemplateLocations.begin(), mCurrentImagePoints[0]);
|
||||
cv::drawChessboardCorners(frame, mBoardSizeUnits, cv::Mat(mCurrentImagePoints), isTemplateFound);
|
||||
}
|
||||
return isTemplateFound;
|
||||
}
|
||||
|
||||
bool CalibProcessor::detectAndParseACircles(const cv::Mat &frame)
|
||||
{
|
||||
bool isTemplateFound = findCirclesGrid(frame, mBoardSizeUnits, mCurrentImagePoints, cv::CALIB_CB_ASYMMETRIC_GRID, mBlobDetectorPtr);
|
||||
if(isTemplateFound) {
|
||||
mTemplateLocations.insert(mTemplateLocations.begin(), mCurrentImagePoints[0]);
|
||||
cv::drawChessboardCorners(frame, mBoardSizeUnits, cv::Mat(mCurrentImagePoints), isTemplateFound);
|
||||
}
|
||||
return isTemplateFound;
|
||||
}
|
||||
|
||||
bool CalibProcessor::detectAndParseDualACircles(const cv::Mat &frame)
|
||||
{
|
||||
std::vector<cv::Point2f> blackPointbuf;
|
||||
|
||||
cv::Mat invertedView;
|
||||
cv::bitwise_not(frame, invertedView);
|
||||
bool isWhiteGridFound = cv::findCirclesGrid(frame, mBoardSizeUnits, mCurrentImagePoints, cv::CALIB_CB_ASYMMETRIC_GRID, mBlobDetectorPtr);
|
||||
if(!isWhiteGridFound)
|
||||
return false;
|
||||
bool isBlackGridFound = cv::findCirclesGrid(invertedView, mBoardSizeUnits, blackPointbuf, cv::CALIB_CB_ASYMMETRIC_GRID, mBlobDetectorPtr);
|
||||
|
||||
if(!isBlackGridFound)
|
||||
{
|
||||
mCurrentImagePoints.clear();
|
||||
return false;
|
||||
}
|
||||
cv::drawChessboardCorners(frame, mBoardSizeUnits, cv::Mat(mCurrentImagePoints), isWhiteGridFound);
|
||||
cv::drawChessboardCorners(frame, mBoardSizeUnits, cv::Mat(blackPointbuf), isBlackGridFound);
|
||||
mCurrentImagePoints.insert(mCurrentImagePoints.end(), blackPointbuf.begin(), blackPointbuf.end());
|
||||
mTemplateLocations.insert(mTemplateLocations.begin(), mCurrentImagePoints[0]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CalibProcessor::saveFrameData()
|
||||
{
|
||||
std::vector<cv::Point3f> objectPoints;
|
||||
std::vector<cv::Point2f> imagePoints;
|
||||
|
||||
switch(mBoardType)
|
||||
{
|
||||
case Chessboard:
|
||||
objectPoints.reserve(mBoardSizeInnerCorners.height*mBoardSizeInnerCorners.width);
|
||||
for( int i = 0; i < mBoardSizeInnerCorners.height; ++i )
|
||||
for( int j = 0; j < mBoardSizeInnerCorners.width; ++j )
|
||||
objectPoints.push_back(cv::Point3f(j*mSquareSize, i*mSquareSize, 0));
|
||||
mCalibData->imagePoints.push_back(mCurrentImagePoints);
|
||||
mCalibData->objectPoints.push_back(objectPoints);
|
||||
break;
|
||||
case ChArUco:
|
||||
mCalibData->allCharucoCorners.push_back(mCurrentCharucoCorners);
|
||||
mCalibData->allCharucoIds.push_back(mCurrentCharucoIds);
|
||||
|
||||
mCharucoBoard->matchImagePoints(mCurrentCharucoCorners, mCurrentCharucoIds, objectPoints, imagePoints);
|
||||
CV_Assert(mCurrentCharucoIds.total() == imagePoints.size());
|
||||
mCalibData->imagePoints.push_back(imagePoints);
|
||||
mCalibData->objectPoints.push_back(objectPoints);
|
||||
break;
|
||||
case CirclesGrid:
|
||||
objectPoints.reserve(mBoardSizeUnits.height*mBoardSizeUnits.width);
|
||||
for( int i = 0; i < mBoardSizeUnits.height; i++ )
|
||||
for( int j = 0; j < mBoardSizeUnits.width; j++ )
|
||||
objectPoints.push_back(cv::Point3f(j*mSquareSize, i*mSquareSize, 0));
|
||||
mCalibData->imagePoints.push_back(mCurrentImagePoints);
|
||||
mCalibData->objectPoints.push_back(objectPoints);
|
||||
break;
|
||||
case AcirclesGrid:
|
||||
objectPoints.reserve(mBoardSizeUnits.height*mBoardSizeUnits.width);
|
||||
for( int i = 0; i < mBoardSizeUnits.height; i++ )
|
||||
for( int j = 0; j < mBoardSizeUnits.width; j++ )
|
||||
objectPoints.push_back(cv::Point3f((2*j + i % 2)*mSquareSize, i*mSquareSize, 0));
|
||||
mCalibData->imagePoints.push_back(mCurrentImagePoints);
|
||||
mCalibData->objectPoints.push_back(objectPoints);
|
||||
break;
|
||||
case DoubleAcirclesGrid:
|
||||
{
|
||||
float gridCenterX = (2*((float)mBoardSizeUnits.width - 1) + 1)*mSquareSize + mTemplDist / 2;
|
||||
float gridCenterY = (mBoardSizeUnits.height - 1)*mSquareSize / 2;
|
||||
objectPoints.reserve(2*mBoardSizeUnits.height*mBoardSizeUnits.width);
|
||||
|
||||
//white part
|
||||
for( int i = 0; i < mBoardSizeUnits.height; i++ )
|
||||
for( int j = 0; j < mBoardSizeUnits.width; j++ )
|
||||
objectPoints.push_back(
|
||||
cv::Point3f(-float((2*j + i % 2)*mSquareSize + mTemplDist +
|
||||
(2*(mBoardSizeUnits.width - 1) + 1)*mSquareSize - gridCenterX),
|
||||
-float(i*mSquareSize) - gridCenterY,
|
||||
0));
|
||||
//black part
|
||||
for( int i = 0; i < mBoardSizeUnits.height; i++ )
|
||||
for( int j = 0; j < mBoardSizeUnits.width; j++ )
|
||||
objectPoints.push_back(cv::Point3f(-float((2*j + i % 2)*mSquareSize - gridCenterX),
|
||||
-float(i*mSquareSize) - gridCenterY, 0));
|
||||
|
||||
mCalibData->imagePoints.push_back(mCurrentImagePoints);
|
||||
mCalibData->objectPoints.push_back(objectPoints);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CalibProcessor::showCaptureMessage(const cv::Mat& frame, const std::string &message)
|
||||
{
|
||||
cv::Point textOrigin(100, 100);
|
||||
double textSize = VIDEO_TEXT_SIZE * frame.cols / (double) IMAGE_MAX_WIDTH;
|
||||
cv::bitwise_not(frame, frame);
|
||||
cv::putText(frame, message, textOrigin, 1, textSize, cv::Scalar(0,0,255), 2, cv::LINE_AA);
|
||||
cv::Mat resized;
|
||||
if (std::fabs(mZoom - 1.) > 0.001f)
|
||||
{
|
||||
cv::resize(frame, resized, cv::Size(), mZoom, mZoom);
|
||||
}
|
||||
else
|
||||
{
|
||||
resized = frame;
|
||||
}
|
||||
cv::imshow(mainWindowName, resized);
|
||||
cv::waitKey(300);
|
||||
}
|
||||
|
||||
bool CalibProcessor::checkLastFrame()
|
||||
{
|
||||
bool isFrameBad = false;
|
||||
cv::Mat tmpCamMatrix;
|
||||
const double badAngleThresh = 40;
|
||||
|
||||
if(!mCalibData->cameraMatrix.total()) {
|
||||
tmpCamMatrix = cv::Mat::eye(3, 3, CV_64F);
|
||||
tmpCamMatrix.at<double>(0,0) = 20000;
|
||||
tmpCamMatrix.at<double>(1,1) = 20000;
|
||||
tmpCamMatrix.at<double>(0,2) = mCalibData->imageSize.height/2;
|
||||
tmpCamMatrix.at<double>(1,2) = mCalibData->imageSize.width/2;
|
||||
}
|
||||
else
|
||||
mCalibData->cameraMatrix.copyTo(tmpCamMatrix);
|
||||
|
||||
cv::Mat r, t, angles;
|
||||
cv::solvePnP(mCalibData->objectPoints.back(), mCalibData->imagePoints.back(), tmpCamMatrix, mCalibData->distCoeffs, r, t);
|
||||
RodriguesToEuler(r, angles, CALIB_DEGREES);
|
||||
if(fabs(angles.at<double>(0)) > badAngleThresh || fabs(angles.at<double>(1)) > badAngleThresh) {
|
||||
mCalibData->objectPoints.pop_back();
|
||||
mCalibData->imagePoints.pop_back();
|
||||
if (mCalibData->allCharucoCorners.size()) {
|
||||
mCalibData->allCharucoCorners.pop_back();
|
||||
mCalibData->allCharucoIds.pop_back();
|
||||
}
|
||||
isFrameBad = true;
|
||||
}
|
||||
return isFrameBad;
|
||||
}
|
||||
|
||||
CalibProcessor::CalibProcessor(cv::Ptr<calibrationData> data, captureParameters &capParams) :
|
||||
mCalibData(data), mBoardType(capParams.board), mBoardSizeUnits(capParams.boardSizeUnits),
|
||||
mBoardSizeInnerCorners(capParams.boardSizeInnerCorners)
|
||||
{
|
||||
mCapuredFrames = 0;
|
||||
mNeededFramesNum = capParams.calibrationStep;
|
||||
mDelayBetweenCaptures = static_cast<int>(capParams.captureDelay * capParams.fps);
|
||||
mMaxTemplateOffset = std::sqrt(static_cast<float>(mCalibData->imageSize.height * mCalibData->imageSize.height) +
|
||||
static_cast<float>(mCalibData->imageSize.width * mCalibData->imageSize.width)) / 20.0;
|
||||
mSquareSize = capParams.squareSize;
|
||||
mTemplDist = capParams.templDst;
|
||||
mSaveFrames = capParams.saveFrames;
|
||||
mZoom = capParams.zoom;
|
||||
cv::aruco::CharucoParameters charucoParameters;
|
||||
charucoParameters.tryRefineMarkers = true;
|
||||
|
||||
switch(mBoardType)
|
||||
{
|
||||
case ChArUco:
|
||||
if (capParams.charucoDictFile != "None") {
|
||||
std::string filename = capParams.charucoDictFile;
|
||||
cv::FileStorage dict_file(filename, cv::FileStorage::Mode::READ);
|
||||
cv::FileNode fn(dict_file.root());
|
||||
mArucoDictionary.readDictionary(fn);
|
||||
}
|
||||
else {
|
||||
mArucoDictionary = cv::aruco::getPredefinedDictionary(cv::aruco::PredefinedDictionaryType(capParams.charucoDictName));
|
||||
}
|
||||
mCharucoBoard = cv::makePtr<cv::aruco::CharucoBoard>(cv::Size(mBoardSizeUnits.width, mBoardSizeUnits.height), capParams.charucoSquareLength,
|
||||
capParams.charucoMarkerSize, mArucoDictionary);
|
||||
detector = cv::makePtr<cv::aruco::CharucoDetector>(cv::aruco::CharucoDetector(*mCharucoBoard, charucoParameters));
|
||||
break;
|
||||
case CirclesGrid:
|
||||
case AcirclesGrid:
|
||||
mBlobDetectorPtr = cv::SimpleBlobDetector::create();
|
||||
break;
|
||||
case DoubleAcirclesGrid:
|
||||
mBlobDetectorPtr = cv::SimpleBlobDetector::create(getDetectorParams());
|
||||
break;
|
||||
case Chessboard:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cv::Mat CalibProcessor::processFrame(const cv::Mat &frame)
|
||||
{
|
||||
cv::Mat frameCopy;
|
||||
cv::Mat frameCopyToSave;
|
||||
if (frame.channels() == 1)
|
||||
cv::cvtColor(frame, frameCopy, cv::COLOR_GRAY2BGR);
|
||||
else
|
||||
frame.copyTo(frameCopy);
|
||||
bool isTemplateFound = false;
|
||||
mCurrentImagePoints.clear();
|
||||
|
||||
if(mSaveFrames)
|
||||
frame.copyTo(frameCopyToSave);
|
||||
|
||||
switch(mBoardType)
|
||||
{
|
||||
case Chessboard:
|
||||
isTemplateFound = detectAndParseChessboard(frameCopy);
|
||||
break;
|
||||
case ChArUco:
|
||||
isTemplateFound = detectAndParseChAruco(frameCopy);
|
||||
break;
|
||||
case CirclesGrid:
|
||||
isTemplateFound = detectAndParseCircles(frameCopy);
|
||||
break;
|
||||
case AcirclesGrid:
|
||||
isTemplateFound = detectAndParseACircles(frameCopy);
|
||||
break;
|
||||
case DoubleAcirclesGrid:
|
||||
isTemplateFound = detectAndParseDualACircles(frameCopy);
|
||||
break;
|
||||
}
|
||||
|
||||
if(mTemplateLocations.size() > mDelayBetweenCaptures)
|
||||
mTemplateLocations.pop_back();
|
||||
if(mTemplateLocations.size() == mDelayBetweenCaptures && isTemplateFound) {
|
||||
if(cv::norm(mTemplateLocations.front() - mTemplateLocations.back()) < mMaxTemplateOffset) {
|
||||
saveFrameData();
|
||||
bool isFrameBad = checkLastFrame();
|
||||
if (!isFrameBad) {
|
||||
std::string displayMessage = cv::format("Frame # %zu captured", std::max(mCalibData->imagePoints.size(),
|
||||
mCalibData->allCharucoCorners.size()));
|
||||
if(!showOverlayMessage(displayMessage))
|
||||
showCaptureMessage(frame, displayMessage);
|
||||
|
||||
if(mSaveFrames)
|
||||
mCalibData->allFrames.push_back(frameCopyToSave);
|
||||
|
||||
mCapuredFrames++;
|
||||
}
|
||||
else {
|
||||
std::string displayMessage = "Frame rejected";
|
||||
if(!showOverlayMessage(displayMessage))
|
||||
showCaptureMessage(frame, displayMessage);
|
||||
}
|
||||
mTemplateLocations.clear();
|
||||
mTemplateLocations.reserve(mDelayBetweenCaptures);
|
||||
}
|
||||
}
|
||||
|
||||
return frameCopy;
|
||||
}
|
||||
|
||||
bool CalibProcessor::isProcessed() const
|
||||
{
|
||||
if(mCapuredFrames < mNeededFramesNum)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
void CalibProcessor::resetState()
|
||||
{
|
||||
mCapuredFrames = 0;
|
||||
mTemplateLocations.clear();
|
||||
}
|
||||
|
||||
CalibProcessor::~CalibProcessor()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
|
||||
void ShowProcessor::drawBoard(cv::Mat &img, cv::InputArray points)
|
||||
{
|
||||
cv::Mat tmpView = cv::Mat::zeros(img.rows, img.cols, CV_8UC3);
|
||||
std::vector<cv::Point2f> templateHull;
|
||||
std::vector<cv::Point> poly;
|
||||
cv::convexHull(points, templateHull);
|
||||
poly.resize(templateHull.size());
|
||||
for(size_t i=0; i<templateHull.size();i++)
|
||||
poly[i] = cv::Point((int)(templateHull[i].x*mGridViewScale), (int)(templateHull[i].y*mGridViewScale));
|
||||
cv::fillConvexPoly(tmpView, poly, cv::Scalar(0, 255, 0), cv::LINE_AA);
|
||||
cv::addWeighted(tmpView, .2, img, 1, 0, img);
|
||||
}
|
||||
|
||||
void ShowProcessor::drawGridPoints(const cv::Mat &frame)
|
||||
{
|
||||
if(mBoardType != ChArUco)
|
||||
for(std::vector<std::vector<cv::Point2f> >::iterator it = mCalibdata->imagePoints.begin(); it != mCalibdata->imagePoints.end(); ++it)
|
||||
for(std::vector<cv::Point2f>::iterator pointIt = (*it).begin(); pointIt != (*it).end(); ++pointIt)
|
||||
cv::circle(frame, *pointIt, POINT_SIZE, cv::Scalar(0, 255, 0), 1, cv::LINE_AA);
|
||||
else
|
||||
for(std::vector<cv::Mat>::iterator it = mCalibdata->allCharucoCorners.begin(); it != mCalibdata->allCharucoCorners.end(); ++it)
|
||||
for(int i = 0; i < (*it).size[0]; i++)
|
||||
cv::circle(frame, cv::Point((int)(*it).at<float>(i, 0), (int)(*it).at<float>(i, 1)),
|
||||
POINT_SIZE, cv::Scalar(0, 255, 0), 1, cv::LINE_AA);
|
||||
}
|
||||
|
||||
ShowProcessor::ShowProcessor(cv::Ptr<calibrationData> data, cv::Ptr<calibController> controller, TemplateType board) :
|
||||
mCalibdata(data), mController(controller), mBoardType(board)
|
||||
{
|
||||
mNeedUndistort = true;
|
||||
mVisMode = Grid;
|
||||
mGridViewScale = 0.5;
|
||||
mTextSize = VIDEO_TEXT_SIZE;
|
||||
}
|
||||
|
||||
cv::Mat ShowProcessor::processFrame(const cv::Mat &frame)
|
||||
{
|
||||
if (!mCalibdata->cameraMatrix.empty() && !mCalibdata->distCoeffs.empty())
|
||||
{
|
||||
mTextSize = VIDEO_TEXT_SIZE * (double) frame.cols / IMAGE_MAX_WIDTH;
|
||||
cv::Scalar textColor = cv::Scalar(0,0,255);
|
||||
cv::Mat frameCopy;
|
||||
|
||||
if (mNeedUndistort && mController->getFramesNumberState()) {
|
||||
if(mVisMode == Grid)
|
||||
drawGridPoints(frame);
|
||||
cv::remap(frame, frameCopy, mCalibdata->undistMap1, mCalibdata->undistMap2, cv::INTER_LINEAR);
|
||||
int baseLine = 100;
|
||||
cv::Size textSize = cv::getTextSize("Undistorted view", 1, mTextSize, 2, &baseLine);
|
||||
cv::Point textOrigin(baseLine, frame.rows - (int)(2.5*textSize.height));
|
||||
cv::putText(frameCopy, "Undistorted view", textOrigin, 1, mTextSize, textColor, 2, cv::LINE_AA);
|
||||
}
|
||||
else {
|
||||
frame.copyTo(frameCopy);
|
||||
if(mVisMode == Grid)
|
||||
drawGridPoints(frameCopy);
|
||||
}
|
||||
std::string displayMessage;
|
||||
if(mCalibdata->stdDeviations.at<double>(0) == 0)
|
||||
displayMessage = cv::format("F = %d RMS = %.3f", (int)mCalibdata->cameraMatrix.at<double>(0,0), mCalibdata->totalAvgErr);
|
||||
else
|
||||
displayMessage = cv::format("Fx = %d Fy = %d RMS = %.3f", (int)mCalibdata->cameraMatrix.at<double>(0,0),
|
||||
(int)mCalibdata->cameraMatrix.at<double>(1,1), mCalibdata->totalAvgErr);
|
||||
if(mController->getRMSState() && mController->getFramesNumberState())
|
||||
displayMessage.append(" OK");
|
||||
|
||||
int baseLine = 100;
|
||||
cv::Size textSize = cv::getTextSize(displayMessage, 1, mTextSize - 1, 2, &baseLine);
|
||||
cv::Point textOrigin = cv::Point(baseLine, 2*textSize.height);
|
||||
cv::putText(frameCopy, displayMessage, textOrigin, 1, mTextSize - 1, textColor, 2, cv::LINE_AA);
|
||||
|
||||
if(mCalibdata->stdDeviations.at<double>(0) == 0)
|
||||
displayMessage = cv::format("DF = %.2f", mCalibdata->stdDeviations.at<double>(1)*sigmaMult);
|
||||
else
|
||||
displayMessage = cv::format("DFx = %.2f DFy = %.2f", mCalibdata->stdDeviations.at<double>(0)*sigmaMult,
|
||||
mCalibdata->stdDeviations.at<double>(1)*sigmaMult);
|
||||
if(mController->getConfidenceIntrervalsState() && mController->getFramesNumberState())
|
||||
displayMessage.append(" OK");
|
||||
cv::putText(frameCopy, displayMessage, cv::Point(baseLine, 4*textSize.height), 1, mTextSize - 1, textColor, 2, cv::LINE_AA);
|
||||
|
||||
if(mController->getCommonCalibrationState()) {
|
||||
displayMessage = cv::format("Calibration is done");
|
||||
cv::putText(frameCopy, displayMessage, cv::Point(baseLine, 6*textSize.height), 1, mTextSize - 1, textColor, 2, cv::LINE_AA);
|
||||
}
|
||||
int calibFlags = mController->getNewFlags();
|
||||
displayMessage = "";
|
||||
if(!(calibFlags & cv::CALIB_FIX_ASPECT_RATIO))
|
||||
displayMessage.append(cv::format("AR=%.3f ", mCalibdata->cameraMatrix.at<double>(0,0)/mCalibdata->cameraMatrix.at<double>(1,1)));
|
||||
if(calibFlags & cv::CALIB_ZERO_TANGENT_DIST)
|
||||
displayMessage.append("TD=0 ");
|
||||
displayMessage.append(cv::format("K1=%.2f K2=%.2f K3=%.2f", mCalibdata->distCoeffs.at<double>(0), mCalibdata->distCoeffs.at<double>(1),
|
||||
mCalibdata->distCoeffs.at<double>(4)));
|
||||
cv::putText(frameCopy, displayMessage, cv::Point(baseLine, frameCopy.rows - (int)(1.5*textSize.height)),
|
||||
1, mTextSize - 1, textColor, 2, cv::LINE_AA);
|
||||
return frameCopy;
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
bool ShowProcessor::isProcessed() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void ShowProcessor::resetState()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void ShowProcessor::setVisualizationMode(visualisationMode mode)
|
||||
{
|
||||
mVisMode = mode;
|
||||
}
|
||||
|
||||
void ShowProcessor::switchVisualizationMode()
|
||||
{
|
||||
if(mVisMode == Grid) {
|
||||
mVisMode = Window;
|
||||
updateBoardsView();
|
||||
}
|
||||
else {
|
||||
mVisMode = Grid;
|
||||
cv::destroyWindow(gridWindowName);
|
||||
}
|
||||
}
|
||||
|
||||
void ShowProcessor::clearBoardsView()
|
||||
{
|
||||
cv::imshow(gridWindowName, cv::Mat());
|
||||
}
|
||||
|
||||
void ShowProcessor::updateBoardsView()
|
||||
{
|
||||
if(mVisMode == Window) {
|
||||
cv::Size originSize = mCalibdata->imageSize;
|
||||
cv::Mat altGridView = cv::Mat::zeros((int)(originSize.height*mGridViewScale), (int)(originSize.width*mGridViewScale), CV_8UC3);
|
||||
if(mBoardType != ChArUco)
|
||||
for(std::vector<std::vector<cv::Point2f> >::iterator it = mCalibdata->imagePoints.begin(); it != mCalibdata->imagePoints.end(); ++it)
|
||||
if(mBoardType != DoubleAcirclesGrid)
|
||||
drawBoard(altGridView, *it);
|
||||
else {
|
||||
size_t pointsNum = (*it).size()/2;
|
||||
std::vector<cv::Point2f> points(pointsNum);
|
||||
std::copy((*it).begin(), (*it).begin() + pointsNum, points.begin());
|
||||
drawBoard(altGridView, points);
|
||||
std::copy((*it).begin() + pointsNum, (*it).begin() + 2*pointsNum, points.begin());
|
||||
drawBoard(altGridView, points);
|
||||
}
|
||||
else
|
||||
for(std::vector<cv::Mat>::iterator it = mCalibdata->allCharucoCorners.begin(); it != mCalibdata->allCharucoCorners.end(); ++it)
|
||||
drawBoard(altGridView, *it);
|
||||
cv::imshow(gridWindowName, altGridView);
|
||||
}
|
||||
}
|
||||
|
||||
void ShowProcessor::switchUndistort()
|
||||
{
|
||||
mNeedUndistort = !mNeedUndistort;
|
||||
}
|
||||
|
||||
void ShowProcessor::setUndistort(bool isEnabled)
|
||||
{
|
||||
mNeedUndistort = isEnabled;
|
||||
}
|
||||
|
||||
ShowProcessor::~ShowProcessor()
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#ifndef FRAME_PROCESSOR_HPP
|
||||
#define FRAME_PROCESSOR_HPP
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/calib.hpp>
|
||||
#include <opencv2/objdetect.hpp>
|
||||
|
||||
#include "calibCommon.hpp"
|
||||
#include "calibController.hpp"
|
||||
|
||||
namespace calib
|
||||
{
|
||||
class FrameProcessor
|
||||
{
|
||||
protected:
|
||||
|
||||
public:
|
||||
virtual ~FrameProcessor();
|
||||
virtual cv::Mat processFrame(const cv::Mat& frame) = 0;
|
||||
virtual bool isProcessed() const = 0;
|
||||
virtual void resetState() = 0;
|
||||
};
|
||||
|
||||
class CalibProcessor : public FrameProcessor
|
||||
{
|
||||
protected:
|
||||
cv::Ptr<calibrationData> mCalibData;
|
||||
TemplateType mBoardType;
|
||||
cv::Size mBoardSizeUnits;
|
||||
cv::Size mBoardSizeInnerCorners;
|
||||
std::vector<cv::Point2f> mTemplateLocations;
|
||||
std::vector<cv::Point2f> mCurrentImagePoints;
|
||||
cv::Mat mCurrentCharucoCorners;
|
||||
cv::Mat mCurrentCharucoIds;
|
||||
|
||||
cv::Ptr<cv::SimpleBlobDetector> mBlobDetectorPtr;
|
||||
cv::aruco::Dictionary mArucoDictionary;
|
||||
cv::Ptr<cv::aruco::CharucoBoard> mCharucoBoard;
|
||||
cv::Ptr<cv::aruco::CharucoDetector> detector;
|
||||
|
||||
int mNeededFramesNum;
|
||||
unsigned mDelayBetweenCaptures;
|
||||
int mCapuredFrames;
|
||||
double mMaxTemplateOffset;
|
||||
float mSquareSize;
|
||||
float mTemplDist;
|
||||
bool mSaveFrames;
|
||||
float mZoom;
|
||||
|
||||
bool detectAndParseChessboard(const cv::Mat& frame);
|
||||
bool detectAndParseChAruco(const cv::Mat& frame);
|
||||
bool detectAndParseCircles(const cv::Mat& frame);
|
||||
bool detectAndParseACircles(const cv::Mat& frame);
|
||||
bool detectAndParseDualACircles(const cv::Mat& frame);
|
||||
void saveFrameData();
|
||||
void showCaptureMessage(const cv::Mat &frame, const std::string& message);
|
||||
bool checkLastFrame();
|
||||
|
||||
public:
|
||||
CalibProcessor(cv::Ptr<calibrationData> data, captureParameters& capParams);
|
||||
virtual cv::Mat processFrame(const cv::Mat& frame) CV_OVERRIDE;
|
||||
virtual bool isProcessed() const CV_OVERRIDE;
|
||||
virtual void resetState() CV_OVERRIDE;
|
||||
~CalibProcessor() CV_OVERRIDE;
|
||||
};
|
||||
|
||||
enum visualisationMode {Grid, Window};
|
||||
|
||||
class ShowProcessor : public FrameProcessor
|
||||
{
|
||||
protected:
|
||||
cv::Ptr<calibrationData> mCalibdata;
|
||||
cv::Ptr<calibController> mController;
|
||||
TemplateType mBoardType;
|
||||
visualisationMode mVisMode;
|
||||
bool mNeedUndistort;
|
||||
double mGridViewScale;
|
||||
double mTextSize;
|
||||
|
||||
void drawBoard(cv::Mat& img, cv::InputArray points);
|
||||
void drawGridPoints(const cv::Mat& frame);
|
||||
public:
|
||||
ShowProcessor(cv::Ptr<calibrationData> data, cv::Ptr<calibController> controller, TemplateType board);
|
||||
virtual cv::Mat processFrame(const cv::Mat& frame) CV_OVERRIDE;
|
||||
virtual bool isProcessed() const CV_OVERRIDE;
|
||||
virtual void resetState() CV_OVERRIDE;
|
||||
|
||||
void setVisualizationMode(visualisationMode mode);
|
||||
void switchVisualizationMode();
|
||||
void clearBoardsView();
|
||||
void updateBoardsView();
|
||||
|
||||
void switchUndistort();
|
||||
void setUndistort(bool isEnabled);
|
||||
~ShowProcessor() CV_OVERRIDE;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,233 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/3d.hpp>
|
||||
#include <opencv2/calib.hpp>
|
||||
#include <opencv2/cvconfig.h>
|
||||
#include <opencv2/highgui.hpp>
|
||||
#include <opencv2/videoio/registry.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
#include "calibCommon.hpp"
|
||||
#include "calibPipeline.hpp"
|
||||
#include "frameProcessor.hpp"
|
||||
#include "calibController.hpp"
|
||||
#include "parametersController.hpp"
|
||||
#include "rotationConverters.hpp"
|
||||
|
||||
using namespace calib;
|
||||
|
||||
static std::string getVideoIoBackendsString()
|
||||
{
|
||||
std::string result;
|
||||
auto backs = cv::videoio_registry::getBackends();
|
||||
for (const auto& b: backs)
|
||||
{
|
||||
if (!result.empty())
|
||||
result += ", ";
|
||||
|
||||
result += cv::videoio_registry::getBackendName(b);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* keys =
|
||||
"{v | | Input from video file }"
|
||||
"{ci | 0 | Camera id }"
|
||||
"{vb | | Video I/O back-end. One of: %s }"
|
||||
"{flip | false | Vertical flip of input frames }"
|
||||
"{t | circles | Template for calibration (circles, chessboard, dualCircles, charuco, symcircles) }"
|
||||
"{sz | 16.3 | Distance between two nearest centers of circles or squares on calibration board}"
|
||||
"{dst | 295 | Distance between white and black parts of daulCircles template}"
|
||||
"{w | | Width of template (in corners or circles)}"
|
||||
"{h | | Height of template (in corners or circles)}"
|
||||
"{ad | DICT_4X4_50 | Name of predefined ArUco dictionary. Available ArUco dictionaries: "
|
||||
"DICT_4X4_50, DICT_4X4_100, DICT_4X4_250, DICT_4X4_1000, DICT_5X5_50, DICT_5X5_100, DICT_5X5_250, "
|
||||
"DICT_5X5_1000, DICT_6X6_50, DICT_6X6_100, DICT_6X6_250, DICT_6X6_1000, DICT_7X7_50, DICT_7X7_100, "
|
||||
"DICT_7X7_250, DICT_7X7_1000, DICT_ARUCO_ORIGINAL, DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, "
|
||||
"DICT_APRILTAG_36h10, DICT_APRILTAG_36h11, DICT_ARUCO_MIP_36h12 }"
|
||||
"{fad | None | name of file with ArUco dictionary}"
|
||||
"{of | cameraParameters.xml | Output file name}"
|
||||
"{ft | true | Auto tuning of calibration flags}"
|
||||
"{vis | grid | Captured boards visualisation (grid, window)}"
|
||||
"{d | 0.8 | Min delay between captures}"
|
||||
"{pf | defaultConfig.xml| Advanced application parameters}"
|
||||
"{save_frames | false | Save frames that contribute to final calibration}"
|
||||
"{zoom | 1 | Zoom factor applied to the preview image}"
|
||||
"{force_reopen | false | Forcefully reopen camera in case of errors}"
|
||||
"{help | | Print help}";
|
||||
|
||||
bool calib::showOverlayMessage(const std::string& message)
|
||||
{
|
||||
#ifdef HAVE_QT
|
||||
cv::displayOverlay(mainWindowName, message, OVERLAY_DELAY);
|
||||
return true;
|
||||
#else
|
||||
std::cout << message << std::endl;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void deleteButton(int, void* data)
|
||||
{
|
||||
(static_cast<cv::Ptr<calibDataController>*>(data))->get()->deleteLastFrame();
|
||||
calib::showOverlayMessage("Last frame deleted");
|
||||
}
|
||||
|
||||
static void deleteAllButton(int, void* data)
|
||||
{
|
||||
(static_cast<cv::Ptr<calibDataController>*>(data))->get()->deleteAllData();
|
||||
calib::showOverlayMessage("All frames deleted");
|
||||
}
|
||||
|
||||
static void saveCurrentParamsButton(int, void* data)
|
||||
{
|
||||
if((static_cast<cv::Ptr<calibDataController>*>(data))->get()->saveCurrentCameraParameters())
|
||||
calib::showOverlayMessage("Calibration parameters saved");
|
||||
}
|
||||
|
||||
#ifdef HAVE_QT
|
||||
static void switchVisualizationModeButton(int, void* data)
|
||||
{
|
||||
ShowProcessor* processor = static_cast<ShowProcessor*>(((cv::Ptr<FrameProcessor>*)data)->get());
|
||||
processor->switchVisualizationMode();
|
||||
}
|
||||
|
||||
static void undistortButton(int state, void* data)
|
||||
{
|
||||
ShowProcessor* processor = static_cast<ShowProcessor*>(((cv::Ptr<FrameProcessor>*)data)->get());
|
||||
processor->setUndistort(static_cast<bool>(state));
|
||||
calib::showOverlayMessage(std::string("Undistort is ") +
|
||||
(static_cast<bool>(state) ? std::string("on") : std::string("off")));
|
||||
}
|
||||
#endif //HAVE_QT
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
cv::CommandLineParser parser(argc, argv, cv::format(keys, getVideoIoBackendsString().c_str()));
|
||||
|
||||
if(parser.has("help")) {
|
||||
parser.printMessage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cout << consoleHelp << std::endl;
|
||||
parametersController paramsController;
|
||||
|
||||
if(!paramsController.loadFromParser(parser))
|
||||
return 0;
|
||||
|
||||
captureParameters capParams = paramsController.getCaptureParameters();
|
||||
internalParameters intParams = paramsController.getInternalParameters();
|
||||
|
||||
cv::TermCriteria solverTermCrit = cv::TermCriteria(cv::TermCriteria::COUNT+cv::TermCriteria::EPS,
|
||||
intParams.solverMaxIters, intParams.solverEps);
|
||||
cv::Ptr<calibrationData> globalData(new calibrationData);
|
||||
if(!parser.has("v")) globalData->imageSize = capParams.cameraResolution;
|
||||
|
||||
int calibrationFlags = 0;
|
||||
if(intParams.fastSolving) calibrationFlags |= cv::CALIB_USE_QR;
|
||||
if(intParams.rationalModel) calibrationFlags |= cv::CALIB_RATIONAL_MODEL;
|
||||
if(intParams.thinPrismModel) calibrationFlags |= cv::CALIB_THIN_PRISM_MODEL;
|
||||
if(intParams.tiltedModel) calibrationFlags |= cv::CALIB_TILTED_MODEL;
|
||||
|
||||
cv::Ptr<calibController> controller(new calibController(globalData, calibrationFlags,
|
||||
parser.get<bool>("ft"), capParams.minFramesNum));
|
||||
cv::Ptr<calibDataController> dataController(new calibDataController(globalData, capParams.maxFramesNum,
|
||||
intParams.filterAlpha));
|
||||
dataController->setParametersFileName(parser.get<std::string>("of"));
|
||||
|
||||
cv::Ptr<FrameProcessor> capProcessor, showProcessor;
|
||||
|
||||
capProcessor = cv::Ptr<FrameProcessor>(new CalibProcessor(globalData, capParams));
|
||||
showProcessor = cv::Ptr<FrameProcessor>(new ShowProcessor(globalData, controller, capParams.board));
|
||||
|
||||
if(parser.get<std::string>("vis").find("window") == 0) {
|
||||
static_cast<ShowProcessor*>(showProcessor.get())->setVisualizationMode(Window);
|
||||
cv::namedWindow(gridWindowName);
|
||||
cv::moveWindow(gridWindowName, 1280, 500);
|
||||
}
|
||||
|
||||
cv::Ptr<CalibPipeline> pipeline(new CalibPipeline(capParams));
|
||||
std::vector<cv::Ptr<FrameProcessor> > processors;
|
||||
processors.push_back(capProcessor);
|
||||
processors.push_back(showProcessor);
|
||||
|
||||
cv::namedWindow(mainWindowName);
|
||||
cv::moveWindow(mainWindowName, 10, 10);
|
||||
#ifdef HAVE_QT
|
||||
cv::createButton("Delete last frame", deleteButton, &dataController,
|
||||
cv::QT_PUSH_BUTTON | cv::QT_NEW_BUTTONBAR);
|
||||
cv::createButton("Delete all frames", deleteAllButton, &dataController,
|
||||
cv::QT_PUSH_BUTTON | cv::QT_NEW_BUTTONBAR);
|
||||
cv::createButton("Undistort", undistortButton, &showProcessor,
|
||||
cv::QT_CHECKBOX | cv::QT_NEW_BUTTONBAR, false);
|
||||
cv::createButton("Save current parameters", saveCurrentParamsButton, &dataController,
|
||||
cv::QT_PUSH_BUTTON | cv::QT_NEW_BUTTONBAR);
|
||||
cv::createButton("Switch visualisation mode", switchVisualizationModeButton, &showProcessor,
|
||||
cv::QT_PUSH_BUTTON | cv::QT_NEW_BUTTONBAR);
|
||||
#endif //HAVE_QT
|
||||
try {
|
||||
bool pipelineFinished = false;
|
||||
while(!pipelineFinished)
|
||||
{
|
||||
PipelineExitStatus exitStatus = pipeline->start(processors);
|
||||
if (exitStatus == Finished) {
|
||||
if(controller->getCommonCalibrationState())
|
||||
saveCurrentParamsButton(0, &dataController);
|
||||
pipelineFinished = true;
|
||||
continue;
|
||||
}
|
||||
else if (exitStatus == Calibrate) {
|
||||
|
||||
dataController->rememberCurrentParameters();
|
||||
globalData->imageSize = pipeline->getImageSize();
|
||||
calibrationFlags = controller->getNewFlags();
|
||||
|
||||
globalData->totalAvgErr =
|
||||
cv::calibrateCamera(globalData->objectPoints, globalData->imagePoints,
|
||||
globalData->imageSize, globalData->cameraMatrix,
|
||||
globalData->distCoeffs, cv::noArray(), cv::noArray(),
|
||||
globalData->stdDeviations, cv::noArray(), globalData->perViewErrors,
|
||||
calibrationFlags, solverTermCrit);
|
||||
dataController->updateUndistortMap();
|
||||
dataController->printParametersToConsole(std::cout);
|
||||
controller->updateState();
|
||||
for(int j = 0; j < capParams.calibrationStep; j++)
|
||||
dataController->filterFrames();
|
||||
static_cast<ShowProcessor*>(showProcessor.get())->updateBoardsView();
|
||||
}
|
||||
else if (exitStatus == DeleteLastFrame) {
|
||||
deleteButton(0, &dataController);
|
||||
static_cast<ShowProcessor*>(showProcessor.get())->updateBoardsView();
|
||||
}
|
||||
else if (exitStatus == DeleteAllFrames) {
|
||||
deleteAllButton(0, &dataController);
|
||||
static_cast<ShowProcessor*>(showProcessor.get())->updateBoardsView();
|
||||
}
|
||||
else if (exitStatus == SaveCurrentData) {
|
||||
saveCurrentParamsButton(0, &dataController);
|
||||
}
|
||||
else if (exitStatus == SwitchUndistort)
|
||||
static_cast<ShowProcessor*>(showProcessor.get())->switchUndistort();
|
||||
else if (exitStatus == SwitchVisualisation)
|
||||
static_cast<ShowProcessor*>(showProcessor.get())->switchVisualizationMode();
|
||||
|
||||
for (std::vector<cv::Ptr<FrameProcessor> >::iterator it = processors.begin(); it != processors.end(); ++it)
|
||||
(*it)->resetState();
|
||||
}
|
||||
}
|
||||
catch (const std::runtime_error& exp) {
|
||||
std::cout << exp.what() << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#include "parametersController.hpp"
|
||||
#include <opencv2/objdetect/aruco_dictionary.hpp>
|
||||
#include <opencv2/videoio/registry.hpp>
|
||||
#include <iostream>
|
||||
|
||||
template <typename T>
|
||||
static bool readFromNode(cv::FileNode node, T& value)
|
||||
{
|
||||
if(!node.isNone()) {
|
||||
node >> value;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool checkAssertion(bool value, const std::string& msg)
|
||||
{
|
||||
if(!value)
|
||||
std::cerr << "Error: " << msg << std::endl;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
bool calib::parametersController::loadFromFile(const std::string &inputFileName)
|
||||
{
|
||||
cv::FileStorage reader;
|
||||
reader.open(inputFileName, cv::FileStorage::READ);
|
||||
|
||||
if(!reader.isOpened()) {
|
||||
std::cerr << "Warning: Unable to open " << inputFileName <<
|
||||
" Application started with default advanced parameters" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (readFromNode(reader["charuco_square_lenght"], mCapParams.charucoSquareLength)) {
|
||||
std::cout << "DEPRECATION: Parameter 'charuco_square_lenght' has been deprecated (typo). Use 'charuco_square_length' instead." << std::endl;
|
||||
}
|
||||
readFromNode(reader["charuco_square_length"], mCapParams.charucoSquareLength);
|
||||
readFromNode(reader["charuco_marker_size"], mCapParams.charucoMarkerSize);
|
||||
readFromNode(reader["camera_resolution"], mCapParams.cameraResolution);
|
||||
readFromNode(reader["calibration_step"], mCapParams.calibrationStep);
|
||||
readFromNode(reader["max_frames_num"], mCapParams.maxFramesNum);
|
||||
readFromNode(reader["min_frames_num"], mCapParams.minFramesNum);
|
||||
readFromNode(reader["solver_eps"], mInternalParameters.solverEps);
|
||||
readFromNode(reader["solver_max_iters"], mInternalParameters.solverMaxIters);
|
||||
readFromNode(reader["fast_solver"], mInternalParameters.fastSolving);
|
||||
readFromNode(reader["rational_model"], mInternalParameters.rationalModel);
|
||||
readFromNode(reader["thin_prism_model"], mInternalParameters.thinPrismModel);
|
||||
readFromNode(reader["tiltedModel"], mInternalParameters.tiltedModel);
|
||||
readFromNode(reader["frame_filter_conv_param"], mInternalParameters.filterAlpha);
|
||||
|
||||
bool retValue =
|
||||
checkAssertion(mCapParams.charucoMarkerSize > 0, "Marker size must be positive") &&
|
||||
checkAssertion(mCapParams.charucoSquareLength > 0, "Square size must be positive") &&
|
||||
checkAssertion(mCapParams.minFramesNum > 1, "Minimal number of frames for calibration < 1") &&
|
||||
checkAssertion(mCapParams.calibrationStep > 0, "Calibration step must be positive") &&
|
||||
checkAssertion(mCapParams.maxFramesNum > mCapParams.minFramesNum, "maxFramesNum < minFramesNum") &&
|
||||
checkAssertion(mInternalParameters.solverEps > 0, "Solver precision must be positive") &&
|
||||
checkAssertion(mInternalParameters.solverMaxIters > 0, "Max solver iterations number must be positive") &&
|
||||
checkAssertion(mInternalParameters.filterAlpha >=0 && mInternalParameters.filterAlpha <=1 ,
|
||||
"Frame filter convolution parameter must be in [0,1] interval") &&
|
||||
checkAssertion(mCapParams.cameraResolution.width > 0 && mCapParams.cameraResolution.height > 0,
|
||||
"Wrong camera resolution values");
|
||||
|
||||
reader.release();
|
||||
return retValue;
|
||||
}
|
||||
|
||||
calib::parametersController::parametersController()
|
||||
{
|
||||
}
|
||||
|
||||
calib::captureParameters calib::parametersController::getCaptureParameters() const
|
||||
{
|
||||
return mCapParams;
|
||||
}
|
||||
|
||||
calib::internalParameters calib::parametersController::getInternalParameters() const
|
||||
{
|
||||
return mInternalParameters;
|
||||
}
|
||||
|
||||
bool calib::parametersController::loadFromParser(cv::CommandLineParser &parser)
|
||||
{
|
||||
mCapParams.flipVertical = parser.get<bool>("flip");
|
||||
mCapParams.captureDelay = parser.get<float>("d");
|
||||
mCapParams.squareSize = parser.get<float>("sz");
|
||||
mCapParams.templDst = parser.get<float>("dst");
|
||||
mCapParams.saveFrames = parser.get<bool>("save_frames");
|
||||
mCapParams.zoom = parser.get<float>("zoom");
|
||||
mCapParams.forceReopen = parser.get<bool>("force_reopen");
|
||||
|
||||
if(!checkAssertion(mCapParams.squareSize > 0, "Distance between corners or circles must be positive"))
|
||||
return false;
|
||||
if(!checkAssertion(mCapParams.templDst > 0, "Distance between parts of dual template must be positive"))
|
||||
return false;
|
||||
|
||||
if (parser.has("v")) {
|
||||
mCapParams.source = File;
|
||||
mCapParams.videoFileName = parser.get<std::string>("v");
|
||||
}
|
||||
else {
|
||||
mCapParams.source = Camera;
|
||||
mCapParams.camID = parser.get<int>("ci");
|
||||
}
|
||||
|
||||
mCapParams.camBackend = cv::CAP_ANY;
|
||||
if (parser.has("vb"))
|
||||
{
|
||||
std::string backendName = parser.get<std::string>("vb");
|
||||
auto backs = cv::videoio_registry::getBackends();
|
||||
bool backendSet = false;
|
||||
for (const auto& b: backs)
|
||||
{
|
||||
if (backendName == cv::videoio_registry::getBackendName(b))
|
||||
{
|
||||
mCapParams.camBackend = b;
|
||||
backendSet = true;
|
||||
}
|
||||
}
|
||||
if (!backendSet)
|
||||
{
|
||||
std::cout << "Unknown or unsupported backend " << backendName << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string templateType = parser.get<std::string>("t");
|
||||
|
||||
if(templateType.find("symcircles", 0) == 0) {
|
||||
mCapParams.board = CirclesGrid;
|
||||
mCapParams.boardSizeUnits = cv::Size(4, 11);
|
||||
}
|
||||
else if(templateType.find("circles", 0) == 0) {
|
||||
mCapParams.board = AcirclesGrid;
|
||||
mCapParams.boardSizeUnits = cv::Size(4, 11);
|
||||
}
|
||||
else if(templateType.find("chessboard", 0) == 0) {
|
||||
mCapParams.board = Chessboard;
|
||||
mCapParams.boardSizeUnits = cv::Size(7, 7);
|
||||
}
|
||||
else if(templateType.find("dualcircles", 0) == 0) {
|
||||
mCapParams.board = DoubleAcirclesGrid;
|
||||
mCapParams.boardSizeUnits = cv::Size(4, 11);
|
||||
}
|
||||
else if(templateType.find("charuco", 0) == 0) {
|
||||
mCapParams.board = ChArUco;
|
||||
mCapParams.boardSizeUnits = cv::Size(5, 7);
|
||||
mCapParams.charucoDictFile = parser.get<std::string>("fad");
|
||||
std::string arucoDictName = parser.get<std::string>("ad");
|
||||
|
||||
if (arucoDictName == "DICT_4X4_50") { mCapParams.charucoDictName = cv::aruco::DICT_4X4_50; }
|
||||
else if (arucoDictName == "DICT_4X4_100") { mCapParams.charucoDictName = cv::aruco::DICT_4X4_100; }
|
||||
else if (arucoDictName == "DICT_4X4_250") { mCapParams.charucoDictName = cv::aruco::DICT_4X4_250; }
|
||||
else if (arucoDictName == "DICT_4X4_1000") { mCapParams.charucoDictName = cv::aruco::DICT_4X4_1000; }
|
||||
else if (arucoDictName == "DICT_5X5_50") { mCapParams.charucoDictName = cv::aruco::DICT_5X5_50; }
|
||||
else if (arucoDictName == "DICT_5X5_100") { mCapParams.charucoDictName = cv::aruco::DICT_5X5_100; }
|
||||
else if (arucoDictName == "DICT_5X5_250") { mCapParams.charucoDictName = cv::aruco::DICT_5X5_250; }
|
||||
else if (arucoDictName == "DICT_5X5_1000") { mCapParams.charucoDictName = cv::aruco::DICT_5X5_1000; }
|
||||
else if (arucoDictName == "DICT_6X6_50") { mCapParams.charucoDictName = cv::aruco::DICT_6X6_50; }
|
||||
else if (arucoDictName == "DICT_6X6_100") { mCapParams.charucoDictName = cv::aruco::DICT_6X6_100; }
|
||||
else if (arucoDictName == "DICT_6X6_250") { mCapParams.charucoDictName = cv::aruco::DICT_6X6_250; }
|
||||
else if (arucoDictName == "DICT_6X6_1000") { mCapParams.charucoDictName = cv::aruco::DICT_6X6_1000; }
|
||||
else if (arucoDictName == "DICT_7X7_50") { mCapParams.charucoDictName = cv::aruco::DICT_7X7_50; }
|
||||
else if (arucoDictName == "DICT_7X7_100") { mCapParams.charucoDictName = cv::aruco::DICT_7X7_100; }
|
||||
else if (arucoDictName == "DICT_7X7_250") { mCapParams.charucoDictName = cv::aruco::DICT_7X7_250; }
|
||||
else if (arucoDictName == "DICT_7X7_1000") { mCapParams.charucoDictName = cv::aruco::DICT_7X7_1000; }
|
||||
else if (arucoDictName == "DICT_ARUCO_ORIGINAL") { mCapParams.charucoDictName = cv::aruco::DICT_ARUCO_ORIGINAL; }
|
||||
else if (arucoDictName == "DICT_APRILTAG_16h5") { mCapParams.charucoDictName = cv::aruco::DICT_APRILTAG_16h5; }
|
||||
else if (arucoDictName == "DICT_APRILTAG_25h9") { mCapParams.charucoDictName = cv::aruco::DICT_APRILTAG_25h9; }
|
||||
else if (arucoDictName == "DICT_APRILTAG_36h10") { mCapParams.charucoDictName = cv::aruco::DICT_APRILTAG_36h10; }
|
||||
else if (arucoDictName == "DICT_APRILTAG_36h11") { mCapParams.charucoDictName = cv::aruco::DICT_APRILTAG_36h11; }
|
||||
else if (arucoDictName == "DICT_ARUCO_MIP_36h12") { mCapParams.charucoDictName = cv::aruco::DICT_ARUCO_MIP_36h12; }
|
||||
else {
|
||||
std::cout << "incorrect name of aruco dictionary \n";
|
||||
return false;
|
||||
}
|
||||
mCapParams.charucoSquareLength = 200;
|
||||
mCapParams.charucoMarkerSize = 100;
|
||||
}
|
||||
else {
|
||||
std::cerr << "Wrong template name\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(parser.has("w") && parser.has("h")) {
|
||||
mCapParams.inputBoardSize = cv::Size(parser.get<int>("w"), parser.get<int>("h"));
|
||||
//only for chessboard pattern board size given in inner corners
|
||||
if (templateType != "chessboard") {
|
||||
mCapParams.boardSizeUnits = mCapParams.inputBoardSize;
|
||||
}
|
||||
else {
|
||||
mCapParams.boardSizeInnerCorners = mCapParams.inputBoardSize;
|
||||
}
|
||||
if(!checkAssertion(mCapParams.inputBoardSize.width > 0 || mCapParams.inputBoardSize.height > 0,
|
||||
"Board size must be positive"))
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!checkAssertion(parser.get<std::string>("of").find(".xml") > 0,
|
||||
"Wrong output file name: correct format is [name].xml"))
|
||||
return false;
|
||||
|
||||
loadFromFile(parser.get<std::string>("pf"));
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#ifndef PARAMETERS_CONTROLLER_HPP
|
||||
#define PARAMETERS_CONTROLLER_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
#include "calibCommon.hpp"
|
||||
|
||||
namespace calib {
|
||||
|
||||
class parametersController
|
||||
{
|
||||
protected:
|
||||
captureParameters mCapParams;
|
||||
internalParameters mInternalParameters;
|
||||
|
||||
bool loadFromFile(const std::string& inputFileName);
|
||||
public:
|
||||
parametersController();
|
||||
parametersController(cv::Ptr<captureParameters> params);
|
||||
|
||||
captureParameters getCaptureParameters() const;
|
||||
internalParameters getInternalParameters() const;
|
||||
|
||||
bool loadFromParser(cv::CommandLineParser& parser);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,129 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#include "rotationConverters.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <opencv2/3d.hpp>
|
||||
#include <opencv2/calib.hpp>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
#define CALIB_PI 3.14159265358979323846
|
||||
#define CALIB_PI_2 1.57079632679489661923
|
||||
|
||||
using namespace cv;
|
||||
|
||||
void calib::Euler(const cv::Mat& src, cv::Mat& dst, int argType)
|
||||
{
|
||||
if((src.rows == 3) && (src.cols == 3))
|
||||
{
|
||||
//convert rotation matrix to 3 angles (pitch, yaw, roll)
|
||||
dst = cv::Mat(3, 1, CV_64F);
|
||||
double pitch, yaw, roll;
|
||||
|
||||
if(src.at<double>(0,2) < -0.998)
|
||||
{
|
||||
pitch = -atan2(src.at<double>(1,0), src.at<double>(1,1));
|
||||
yaw = -CALIB_PI_2;
|
||||
roll = 0.;
|
||||
}
|
||||
else if(src.at<double>(0,2) > 0.998)
|
||||
{
|
||||
pitch = atan2(src.at<double>(1,0), src.at<double>(1,1));
|
||||
yaw = CALIB_PI_2;
|
||||
roll = 0.;
|
||||
}
|
||||
else
|
||||
{
|
||||
pitch = atan2(-src.at<double>(1,2), src.at<double>(2,2));
|
||||
yaw = asin(src.at<double>(0,2));
|
||||
roll = atan2(-src.at<double>(0,1), src.at<double>(0,0));
|
||||
}
|
||||
|
||||
if(argType == CALIB_DEGREES)
|
||||
{
|
||||
pitch *= 180./CALIB_PI;
|
||||
yaw *= 180./CALIB_PI;
|
||||
roll *= 180./CALIB_PI;
|
||||
}
|
||||
else if(argType != CALIB_RADIANS)
|
||||
CV_Error(cv::Error::StsBadFlag, "Invalid argument type");
|
||||
|
||||
dst.at<double>(0,0) = pitch;
|
||||
dst.at<double>(1,0) = yaw;
|
||||
dst.at<double>(2,0) = roll;
|
||||
}
|
||||
else if( (src.cols == 1 && src.rows == 3) ||
|
||||
(src.cols == 3 && src.rows == 1 ) )
|
||||
{
|
||||
//convert vector which contains 3 angles (pitch, yaw, roll) to rotation matrix
|
||||
double pitch, yaw, roll;
|
||||
if(src.cols == 1 && src.rows == 3)
|
||||
{
|
||||
pitch = src.at<double>(0,0);
|
||||
yaw = src.at<double>(1,0);
|
||||
roll = src.at<double>(2,0);
|
||||
}
|
||||
else{
|
||||
pitch = src.at<double>(0,0);
|
||||
yaw = src.at<double>(0,1);
|
||||
roll = src.at<double>(0,2);
|
||||
}
|
||||
|
||||
if(argType == CALIB_DEGREES)
|
||||
{
|
||||
pitch *= CALIB_PI / 180.;
|
||||
yaw *= CALIB_PI / 180.;
|
||||
roll *= CALIB_PI / 180.;
|
||||
}
|
||||
else if(argType != CALIB_RADIANS)
|
||||
CV_Error(cv::Error::StsBadFlag, "Invalid argument type");
|
||||
|
||||
dst = cv::Mat(3, 3, CV_64F);
|
||||
cv::Mat M(3, 3, CV_64F);
|
||||
cv::Mat i = cv::Mat::eye(3, 3, CV_64F);
|
||||
i.copyTo(dst);
|
||||
i.copyTo(M);
|
||||
|
||||
double* pR = dst.ptr<double>();
|
||||
pR[4] = cos(pitch);
|
||||
pR[7] = sin(pitch);
|
||||
pR[8] = pR[4];
|
||||
pR[5] = -pR[7];
|
||||
|
||||
double* pM = M.ptr<double>();
|
||||
pM[0] = cos(yaw);
|
||||
pM[2] = sin(yaw);
|
||||
pM[8] = pM[0];
|
||||
pM[6] = -pM[2];
|
||||
|
||||
dst *= M;
|
||||
i.copyTo(M);
|
||||
pM[0] = cos(roll);
|
||||
pM[3] = sin(roll);
|
||||
pM[4] = pM[0];
|
||||
pM[1] = -pM[3];
|
||||
|
||||
dst *= M;
|
||||
}
|
||||
else
|
||||
CV_Error(cv::Error::StsBadFlag, "Input matrix must be 1x3, 3x1 or 3x3" );
|
||||
}
|
||||
|
||||
void calib::RodriguesToEuler(const cv::Mat& src, cv::Mat& dst, int argType)
|
||||
{
|
||||
CV_Assert((src.cols == 1 && src.rows == 3) || (src.cols == 3 && src.rows == 1));
|
||||
cv::Mat R;
|
||||
cv::Rodrigues(src, R);
|
||||
Euler(R, dst, argType);
|
||||
}
|
||||
|
||||
void calib::EulerToRodrigues(const cv::Mat& src, cv::Mat& dst, int argType)
|
||||
{
|
||||
CV_Assert((src.cols == 1 && src.rows == 3) || (src.cols == 3 && src.rows == 1));
|
||||
cv::Mat R;
|
||||
Euler(src, R, argType);
|
||||
cv::Rodrigues(R, dst);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#ifndef ROTATION_CONVERTERS_HPP
|
||||
#define ROTATION_CONVERTERS_HPP
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
namespace calib
|
||||
{
|
||||
#define CALIB_RADIANS 0
|
||||
#define CALIB_DEGREES 1
|
||||
|
||||
void Euler(const cv::Mat& src, cv::Mat& dst, int argType = CALIB_RADIANS);
|
||||
void RodriguesToEuler(const cv::Mat& src, cv::Mat& dst, int argType = CALIB_RADIANS);
|
||||
void EulerToRodrigues(const cv::Mat& src, cv::Mat& dst, int argType = CALIB_RADIANS);
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,3 @@
|
||||
ocv_add_application(opencv_model_diagnostics
|
||||
MODULES opencv_core opencv_dnn
|
||||
SRCS model_diagnostics.cpp)
|
||||
@@ -0,0 +1,172 @@
|
||||
/*************************************************
|
||||
USAGE:
|
||||
./model_diagnostics -m <model file location>
|
||||
**************************************************/
|
||||
#include <opencv2/dnn.hpp>
|
||||
#include <opencv2/core/utils/filesystem.hpp>
|
||||
#include <opencv2/dnn/utils/debug_utils.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
|
||||
using namespace cv;
|
||||
using namespace dnn;
|
||||
|
||||
|
||||
static
|
||||
int diagnosticsErrorCallback(int /*status*/, const char* /*func_name*/,
|
||||
const char* /*err_msg*/, const char* /*file_name*/,
|
||||
int /*line*/, void* /*userdata*/)
|
||||
{
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static std::string checkFileExists(const std::string& fileName)
|
||||
{
|
||||
if (fileName.empty() || utils::fs::exists(fileName))
|
||||
return fileName;
|
||||
|
||||
CV_Error(Error::StsObjectNotFound, "File " + fileName + " was not found! "
|
||||
"Please, specify a full path to the file.");
|
||||
}
|
||||
|
||||
static std::vector<int> parseShape(const std::string &shape_str) {
|
||||
std::stringstream ss(shape_str);
|
||||
std::string item;
|
||||
std::vector<std::string> items;
|
||||
|
||||
while (std::getline(ss, item, ',')) {
|
||||
items.push_back(item);
|
||||
}
|
||||
|
||||
std::vector<int> shape;
|
||||
for (size_t i = 0; i < items.size(); i++) {
|
||||
shape.push_back(std::stoi(items[i]));
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
std::string diagnosticKeys =
|
||||
"{ help h | | Print help. }"
|
||||
"{ model m | | Path to the model file. }"
|
||||
"{ config c | | Path to the model configuration file. }"
|
||||
"{ framework f | | [Optional] Name of the model framework. }"
|
||||
"{ engine e | auto | [Optional] Graph negine selector: auto or classic or new}"
|
||||
"{ input0_name | | [Optional] Name of input0. Use with input0_shape}"
|
||||
"{ input0_shape | | [Optional] Shape of input0. Use with input0_name}"
|
||||
"{ input1_name | | [Optional] Name of input1. Use with input1_shape}"
|
||||
"{ input1_shape | | [Optional] Shape of input1. Use with input1_name}"
|
||||
"{ input2_name | | [Optional] Name of input2. Use with input2_shape}"
|
||||
"{ input2_shape | | [Optional] Shape of input2. Use with input2_name}"
|
||||
"{ input3_name | | [Optional] Name of input3. Use with input3_shape}"
|
||||
"{ input3_shape | | [Optional] Shape of input3. Use with input3_name}"
|
||||
"{ input4_name | | [Optional] Name of input4. Use with input4_shape}"
|
||||
"{ input4_shape | | [Optional] Shape of input4. Use with input4_name}";
|
||||
|
||||
int main( int argc, const char** argv )
|
||||
{
|
||||
CommandLineParser argParser(argc, argv, diagnosticKeys);
|
||||
argParser.about("Use this tool to run the diagnostics of provided ONNX/TF model"
|
||||
"to obtain the information about its support (supported layers).");
|
||||
|
||||
if (argc == 1)
|
||||
{
|
||||
argParser.printMessage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(argParser.has("help"))
|
||||
{
|
||||
argParser.printMessage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string model = checkFileExists(argParser.get<std::string>("model"));
|
||||
std::string config = checkFileExists(argParser.get<std::string>("config"));
|
||||
std::string frameworkId = argParser.get<std::string>("framework");
|
||||
|
||||
std::string input0_name = argParser.get<std::string>("input0_name");
|
||||
std::string input0_shape = argParser.get<std::string>("input0_shape");
|
||||
std::string input1_name = argParser.get<std::string>("input1_name");
|
||||
std::string input1_shape = argParser.get<std::string>("input1_shape");
|
||||
std::string input2_name = argParser.get<std::string>("input2_name");
|
||||
std::string input2_shape = argParser.get<std::string>("input2_shape");
|
||||
std::string input3_name = argParser.get<std::string>("input3_name");
|
||||
std::string input3_shape = argParser.get<std::string>("input3_shape");
|
||||
std::string input4_name = argParser.get<std::string>("input4_name");
|
||||
std::string input4_shape = argParser.get<std::string>("input4_shape");
|
||||
|
||||
dnn::EngineType engine = dnn::ENGINE_AUTO;
|
||||
if (argParser.has("engine"))
|
||||
{
|
||||
std::string eng_name = argParser.get<std::string>("engine");
|
||||
if(eng_name == "auto")
|
||||
engine = dnn::ENGINE_AUTO;
|
||||
else if(eng_name == "classic")
|
||||
engine = dnn::ENGINE_CLASSIC;
|
||||
else if(eng_name == "new")
|
||||
engine = dnn::ENGINE_NEW;
|
||||
else
|
||||
{
|
||||
std::cerr << "Unknown DNN graph engine \"" << eng_name << "\"\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
CV_Assert(!model.empty());
|
||||
|
||||
enableModelDiagnostics(true);
|
||||
skipModelImport(true);
|
||||
redirectError(diagnosticsErrorCallback, NULL);
|
||||
|
||||
Net ocvNet = readNet(model, config, frameworkId, engine);
|
||||
|
||||
std::vector<std::string> input_names;
|
||||
std::vector<std::vector<int>> input_shapes;
|
||||
if (!input0_name.empty() || !input0_shape.empty()) {
|
||||
CV_CheckFalse(input0_name.empty(), "input0_name cannot be empty");
|
||||
CV_CheckFalse(input0_shape.empty(), "input0_shape cannot be empty");
|
||||
input_names.push_back(input0_name);
|
||||
input_shapes.push_back(parseShape(input0_shape));
|
||||
}
|
||||
if (!input1_name.empty() || !input1_shape.empty()) {
|
||||
CV_CheckFalse(input1_name.empty(), "input1_name cannot be empty");
|
||||
CV_CheckFalse(input1_shape.empty(), "input1_shape cannot be empty");
|
||||
input_names.push_back(input1_name);
|
||||
input_shapes.push_back(parseShape(input1_shape));
|
||||
}
|
||||
if (!input2_name.empty() || !input2_shape.empty()) {
|
||||
CV_CheckFalse(input2_name.empty(), "input2_name cannot be empty");
|
||||
CV_CheckFalse(input2_shape.empty(), "input2_shape cannot be empty");
|
||||
input_names.push_back(input2_name);
|
||||
input_shapes.push_back(parseShape(input2_shape));
|
||||
}
|
||||
if (!input3_name.empty() || !input3_shape.empty()) {
|
||||
CV_CheckFalse(input3_name.empty(), "input3_name cannot be empty");
|
||||
CV_CheckFalse(input3_shape.empty(), "input3_shape cannot be empty");
|
||||
input_names.push_back(input3_name);
|
||||
input_shapes.push_back(parseShape(input3_shape));
|
||||
}
|
||||
if (!input4_name.empty() || !input4_shape.empty()) {
|
||||
CV_CheckFalse(input4_name.empty(), "input4_name cannot be empty");
|
||||
CV_CheckFalse(input4_shape.empty(), "input4_shape cannot be empty");
|
||||
input_names.push_back(input4_name);
|
||||
input_shapes.push_back(parseShape(input4_shape));
|
||||
}
|
||||
|
||||
if (!input_names.empty() && !input_shapes.empty() && input_names.size() == input_shapes.size()) {
|
||||
ocvNet.setInputsNames(input_names);
|
||||
for (size_t i = 0; i < input_names.size(); i++) {
|
||||
Mat input(input_shapes[i], CV_32F);
|
||||
ocvNet.setInput(input, input_names[i]);
|
||||
}
|
||||
|
||||
size_t dot_index = model.rfind('.');
|
||||
std::string graph_filename = model.substr(0, dot_index) + ".pbtxt";
|
||||
ocvNet.dumpToPbtxt(graph_filename);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
+1101
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
matplotlib
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
use
|
||||
python generate_pattern.py --help
|
||||
|
||||
to generate various calibration svg calibration patterns.
|
||||
Executable
+331
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""generate_pattern.py
|
||||
Usage example:
|
||||
python generate_pattern.py -o out.svg -r 11 -c 8 -T circles -s 20.0 -R 5.0 -u mm -w 216 -h 279
|
||||
-o, --output - output file (default out.svg)
|
||||
-r, --rows - pattern rows (default 11)
|
||||
-c, --columns - pattern columns (default 8)
|
||||
-T, --type - type of pattern: circles, acircles, checkerboard, radon_checkerboard, charuco_board. default circles.
|
||||
-s, --square_size - size of squares in pattern (default 20.0)
|
||||
-R, --radius_rate - circles_radius = square_size/radius_rate (default 5.0)
|
||||
-u, --units - mm, inches, px, m (default mm)
|
||||
-w, --page_width - page width in units (default 216)
|
||||
-h, --page_height - page height in units (default 279)
|
||||
-a, --page_size - page size (default A4), supersedes -h -w arguments
|
||||
-m, --markers - list of cells with markers for the radon checkerboard
|
||||
-p, --aruco_marker_size - aruco markers size for ChAruco pattern (default 10.0)
|
||||
-f, --dict_file - file name of custom aruco dictionary for ChAruco pattern
|
||||
-do, --dict_offset - index of the first ArUco index used
|
||||
-H, --help - show help
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import numpy as np
|
||||
import json
|
||||
import gzip
|
||||
from svgfig import *
|
||||
|
||||
|
||||
class PatternMaker:
|
||||
def __init__(self, cols, rows, output, units, square_size, radius_rate, page_width, page_height, markers, aruco_marker_size, dict_file, dict_offset):
|
||||
self.cols = cols
|
||||
self.rows = rows
|
||||
self.output = output
|
||||
self.units = units
|
||||
self.square_size = square_size
|
||||
self.radius_rate = radius_rate
|
||||
self.width = page_width
|
||||
self.height = page_height
|
||||
self.markers = markers
|
||||
self.aruco_marker_size = aruco_marker_size #for charuco boards only
|
||||
self.dict_file = dict_file
|
||||
self.dict_offset = dict_offset
|
||||
|
||||
self.g = SVG("g") # the svg group container
|
||||
|
||||
def make_circles_pattern(self):
|
||||
spacing = self.square_size
|
||||
r = spacing / self.radius_rate
|
||||
pattern_width = ((self.cols - 1.0) * spacing) + (2.0 * r)
|
||||
pattern_height = ((self.rows - 1.0) * spacing) + (2.0 * r)
|
||||
x_spacing = (self.width - pattern_width) / 2.0
|
||||
y_spacing = (self.height - pattern_height) / 2.0
|
||||
for x in range(0, self.cols):
|
||||
for y in range(0, self.rows):
|
||||
dot = SVG("circle", cx=(x * spacing) + x_spacing + r,
|
||||
cy=(y * spacing) + y_spacing + r, r=r, fill="black", stroke="none")
|
||||
self.g.append(dot)
|
||||
|
||||
def make_acircles_pattern(self):
|
||||
spacing = self.square_size
|
||||
r = spacing / self.radius_rate
|
||||
pattern_width = ((self.cols-1.0) * 2 * spacing) + spacing + (2.0 * r)
|
||||
pattern_height = ((self.rows-1.0) * spacing) + (2.0 * r)
|
||||
x_spacing = (self.width - pattern_width) / 2.0
|
||||
y_spacing = (self.height - pattern_height) / 2.0
|
||||
for x in range(0, self.cols):
|
||||
for y in range(0, self.rows):
|
||||
dot = SVG("circle", cx=(2 * x * spacing) + (y % 2)*spacing + x_spacing + r,
|
||||
cy=(y * spacing) + y_spacing + r, r=r, fill="black", stroke="none")
|
||||
self.g.append(dot)
|
||||
|
||||
def make_checkerboard_pattern(self):
|
||||
spacing = self.square_size
|
||||
xspacing = (self.width - self.cols * self.square_size) / 2.0
|
||||
yspacing = (self.height - self.rows * self.square_size) / 2.0
|
||||
for x in range(0, self.cols):
|
||||
for y in range(0, self.rows):
|
||||
if x % 2 == y % 2:
|
||||
square = SVG("rect", x=x * spacing + xspacing, y=y * spacing + yspacing, width=spacing,
|
||||
height=spacing, fill="black", stroke="none")
|
||||
self.g.append(square)
|
||||
|
||||
@staticmethod
|
||||
def _make_round_rect(x, y, diam, corners=("right", "right", "right", "right")):
|
||||
rad = diam / 2
|
||||
cw_point = ((0, 0), (diam, 0), (diam, diam), (0, diam))
|
||||
mid_cw_point = ((0, rad), (rad, 0), (diam, rad), (rad, diam))
|
||||
res_str = "M{},{} ".format(x + mid_cw_point[0][0], y + mid_cw_point[0][1])
|
||||
n = len(cw_point)
|
||||
for i in range(n):
|
||||
if corners[i] == "right":
|
||||
res_str += "L{},{} L{},{} ".format(x + cw_point[i][0], y + cw_point[i][1],
|
||||
x + mid_cw_point[(i + 1) % n][0], y + mid_cw_point[(i + 1) % n][1])
|
||||
elif corners[i] == "round":
|
||||
res_str += "A{},{} 0,0,1 {},{} ".format(rad, rad, x + mid_cw_point[(i + 1) % n][0],
|
||||
y + mid_cw_point[(i + 1) % n][1])
|
||||
else:
|
||||
raise TypeError("unknown corner type")
|
||||
return res_str
|
||||
|
||||
def _get_type(self, x, y):
|
||||
corners = ["right", "right", "right", "right"]
|
||||
is_inside = True
|
||||
if x == 0:
|
||||
corners[0] = "round"
|
||||
corners[3] = "round"
|
||||
is_inside = False
|
||||
if y == 0:
|
||||
corners[0] = "round"
|
||||
corners[1] = "round"
|
||||
is_inside = False
|
||||
if x == self.cols - 1:
|
||||
corners[1] = "round"
|
||||
corners[2] = "round"
|
||||
is_inside = False
|
||||
if y == self.rows - 1:
|
||||
corners[2] = "round"
|
||||
corners[3] = "round"
|
||||
is_inside = False
|
||||
return corners, is_inside
|
||||
|
||||
def make_radon_checkerboard_pattern(self):
|
||||
spacing = self.square_size
|
||||
xspacing = (self.width - self.cols * self.square_size) / 2.0
|
||||
yspacing = (self.height - self.rows * self.square_size) / 2.0
|
||||
for x in range(0, self.cols):
|
||||
for y in range(0, self.rows):
|
||||
if x % 2 == y % 2:
|
||||
corner_types, is_inside = self._get_type(x, y)
|
||||
if is_inside:
|
||||
square = SVG("rect", x=x * spacing + xspacing, y=y * spacing + yspacing, width=spacing,
|
||||
height=spacing, fill="black", stroke="none")
|
||||
else:
|
||||
square = SVG("path", d=self._make_round_rect(x * spacing + xspacing, y * spacing + yspacing,
|
||||
spacing, corner_types), fill="black", stroke="none")
|
||||
self.g.append(square)
|
||||
if self.markers is not None:
|
||||
r = self.square_size * 0.17
|
||||
pattern_width = ((self.cols - 1.0) * spacing) + (2.0 * r)
|
||||
pattern_height = ((self.rows - 1.0) * spacing) + (2.0 * r)
|
||||
x_spacing = (self.width - pattern_width) / 2.0
|
||||
y_spacing = (self.height - pattern_height) / 2.0
|
||||
for x, y in self.markers:
|
||||
color = "black"
|
||||
if x % 2 == y % 2:
|
||||
color = "white"
|
||||
dot = SVG("circle", cx=(x * spacing) + x_spacing + r,
|
||||
cy=(y * spacing) + y_spacing + r, r=r, fill=color, stroke="none")
|
||||
self.g.append(dot)
|
||||
|
||||
@staticmethod
|
||||
def _create_marker_bits(markerSize_bits, byteList):
|
||||
|
||||
marker = np.zeros((markerSize_bits+2, markerSize_bits+2))
|
||||
bits = marker[1:markerSize_bits+1, 1:markerSize_bits+1]
|
||||
|
||||
for i in range(markerSize_bits):
|
||||
for j in range(markerSize_bits):
|
||||
bits[i][j] = int(byteList[i*markerSize_bits+j])
|
||||
|
||||
return marker
|
||||
|
||||
def make_charuco_board(self):
|
||||
if (self.aruco_marker_size>self.square_size):
|
||||
print("Error: Aruco marker cannot be lager than chessboard square!")
|
||||
return
|
||||
|
||||
if (self.dict_file.split(".")[-1] == "gz"):
|
||||
with gzip.open(self.dict_file, 'r') as fin:
|
||||
json_bytes = fin.read()
|
||||
json_str = json_bytes.decode('utf-8')
|
||||
dictionary = json.loads(json_str)
|
||||
|
||||
else:
|
||||
f = open(self.dict_file)
|
||||
dictionary = json.load(f)
|
||||
|
||||
if (dictionary["nmarkers"] < int(self.cols*self.rows/2)):
|
||||
print("Error: Aruco dictionary contains less markers than it needs for chosen board. Please choose another dictionary or use smaller board than required for chosen board")
|
||||
return
|
||||
|
||||
markerSize_bits = dictionary["markersize"]
|
||||
|
||||
side = self.aruco_marker_size / (markerSize_bits+2)
|
||||
spacing = self.square_size
|
||||
xspacing = (self.width - self.cols * self.square_size) / 2.0
|
||||
yspacing = (self.height - self.rows * self.square_size) / 2.0
|
||||
|
||||
ch_ar_border = (self.square_size - self.aruco_marker_size)/2
|
||||
if ch_ar_border < side*0.7:
|
||||
print("Marker border {} is less than 70% of ArUco pin size {}. Please increase --square_size or decrease --marker_size for stable board detection".format(ch_ar_border, int(side)))
|
||||
marker_id = self.dict_offset
|
||||
for y in range(0, self.rows):
|
||||
for x in range(0, self.cols):
|
||||
|
||||
if x % 2 == y % 2:
|
||||
square = SVG("rect", x=x * spacing + xspacing, y=y * spacing + yspacing, width=spacing,
|
||||
height=spacing, fill="black", stroke="none")
|
||||
self.g.append(square)
|
||||
else:
|
||||
img_mark = self._create_marker_bits(markerSize_bits, dictionary["marker_"+str(marker_id)])
|
||||
marker_id +=1
|
||||
x_pos = x * spacing + xspacing
|
||||
y_pos = y * spacing + yspacing
|
||||
|
||||
square = SVG("rect", x=x_pos+ch_ar_border, y=y_pos+ch_ar_border, width=self.aruco_marker_size,
|
||||
height=self.aruco_marker_size, fill="black", stroke="none")
|
||||
self.g.append(square)
|
||||
|
||||
# BUG: https://github.com/opencv/opencv/issues/27871
|
||||
# The loop bellow merges white squares horizontally and vertically to exclude visible grid on the final pattern
|
||||
for x_ in range(len(img_mark[0])):
|
||||
y_ = 0
|
||||
while y_ < len(img_mark):
|
||||
y_start = y_
|
||||
while y_ < len(img_mark) and img_mark[y_][x_] != 0:
|
||||
y_ += 1
|
||||
|
||||
if y_ > y_start:
|
||||
rect = SVG("rect", x=x_pos+ch_ar_border+(x_)*side, y=y_pos+ch_ar_border+(y_start)*side, width=side,
|
||||
height=(y_ - y_start)*side, fill="white", stroke="none")
|
||||
self.g.append(rect)
|
||||
|
||||
y_ += 1
|
||||
|
||||
for y_ in range(len(img_mark)):
|
||||
x_ = 0
|
||||
while x_ < len(img_mark[0]):
|
||||
x_start = x_
|
||||
while x_ < len(img_mark[0]) and img_mark[y_][x_] != 0:
|
||||
x_ += 1
|
||||
|
||||
if x_ > x_start:
|
||||
rect = SVG("rect", x=x_pos+ch_ar_border+(x_start)*side, y=y_pos+ch_ar_border+(y_)*side, width=(x_-x_start)*side,
|
||||
height=side, fill="white", stroke="none")
|
||||
self.g.append(rect)
|
||||
|
||||
x_ += 1
|
||||
|
||||
def save(self):
|
||||
c = canvas(self.g, width="%d%s" % (self.width, self.units), height="%d%s" % (self.height, self.units),
|
||||
viewBox="0 0 %d %d" % (self.width, self.height))
|
||||
c.save(self.output)
|
||||
|
||||
|
||||
def main():
|
||||
# parse command line options
|
||||
parser = argparse.ArgumentParser(description="generate camera-calibration pattern", add_help=False)
|
||||
parser.add_argument("-H", "--help", help="show help", action="store_true", dest="show_help")
|
||||
parser.add_argument("-o", "--output", help="output file", default="out.svg", action="store", dest="output")
|
||||
parser.add_argument("-c", "--columns", help="pattern columns", default="8", action="store", dest="columns",
|
||||
type=int)
|
||||
parser.add_argument("-r", "--rows", help="pattern rows", default="11", action="store", dest="rows", type=int)
|
||||
parser.add_argument("-T", "--type", help="type of pattern", default="circles", action="store", dest="p_type",
|
||||
choices=["circles", "acircles", "checkerboard", "radon_checkerboard", "charuco_board"])
|
||||
parser.add_argument("-u", "--units", help="length unit", default="mm", action="store", dest="units",
|
||||
choices=["mm", "inches", "px", "m"])
|
||||
parser.add_argument("-s", "--square_size", help="size of squares in pattern", default="20.0", action="store",
|
||||
dest="square_size", type=float)
|
||||
parser.add_argument("-R", "--radius_rate", help="circles_radius = square_size/radius_rate", default="5.0",
|
||||
action="store", dest="radius_rate", type=float)
|
||||
parser.add_argument("-w", "--page_width", help="page width in units", default=argparse.SUPPRESS, action="store",
|
||||
dest="page_width", type=float)
|
||||
parser.add_argument("-h", "--page_height", help="page height in units", default=argparse.SUPPRESS, action="store",
|
||||
dest="page_height", type=float)
|
||||
parser.add_argument("-a", "--page_size", help="page size, superseded if -h and -w are set", default="A4",
|
||||
action="store", dest="page_size", choices=["A0", "A1", "A2", "A3", "A4", "A5"])
|
||||
parser.add_argument("-m", "--markers", help="list of cells with markers for the radon checkerboard. Marker "
|
||||
"coordinates as list of numbers: -m 1 2 3 4 means markers in cells "
|
||||
"[1, 2] and [3, 4]",
|
||||
default=argparse.SUPPRESS, action="store", dest="markers", nargs="+", type=int)
|
||||
parser.add_argument("-p", "--marker_size", help="aruco markers size for ChAruco pattern (default 10.0)", default="10.0",
|
||||
action="store", dest="aruco_marker_size", type=float)
|
||||
parser.add_argument("-f", "--dict_file", help="file name of custom aruco dictionary for ChAruco pattern", default="DICT_ARUCO_ORIGINAL.json",
|
||||
action="store", dest="dict_file", type=str)
|
||||
parser.add_argument("-do", "--dict_offset", help="index of the first ArUco index used", default=0,
|
||||
action="store", dest="dict_offset", type=int)
|
||||
args = parser.parse_args()
|
||||
|
||||
show_help = args.show_help
|
||||
if show_help:
|
||||
parser.print_help()
|
||||
return
|
||||
output = args.output
|
||||
columns = args.columns
|
||||
rows = args.rows
|
||||
p_type = args.p_type
|
||||
units = args.units
|
||||
square_size = args.square_size
|
||||
radius_rate = args.radius_rate
|
||||
aruco_marker_size = args.aruco_marker_size
|
||||
dict_file = args.dict_file
|
||||
dict_offset = args.dict_offset
|
||||
|
||||
if 'page_width' and 'page_height' in args:
|
||||
page_width = args.page_width
|
||||
page_height = args.page_height
|
||||
else:
|
||||
page_size = args.page_size
|
||||
# page size dict (ISO standard, mm) for easy lookup. format - size: [width, height]
|
||||
page_sizes = {"A0": [840, 1188], "A1": [594, 840], "A2": [420, 594], "A3": [297, 420], "A4": [210, 297],
|
||||
"A5": [148, 210]}
|
||||
page_width = page_sizes[page_size][0]
|
||||
page_height = page_sizes[page_size][1]
|
||||
markers = None
|
||||
if p_type == "radon_checkerboard" and "markers" in args:
|
||||
if len(args.markers) % 2 == 1:
|
||||
raise ValueError("The length of the markers array={} must be even".format(len(args.markers)))
|
||||
markers = set()
|
||||
for x, y in zip(args.markers[::2], args.markers[1::2]):
|
||||
if x in range(0, columns) and y in range(0, rows):
|
||||
markers.add((x, y))
|
||||
else:
|
||||
raise ValueError("The marker {},{} is outside the checkerboard".format(x, y))
|
||||
|
||||
if p_type == "charuco_board" and aruco_marker_size >= square_size:
|
||||
raise ValueError("ArUco markers size must be smaller than square size")
|
||||
|
||||
pm = PatternMaker(columns, rows, output, units, square_size, radius_rate, page_width, page_height, markers, aruco_marker_size, dict_file, dict_offset)
|
||||
# dict for easy lookup of pattern type
|
||||
mp = {"circles": pm.make_circles_pattern, "acircles": pm.make_acircles_pattern,
|
||||
"checkerboard": pm.make_checkerboard_pattern, "radon_checkerboard": pm.make_radon_checkerboard_pattern,
|
||||
"charuco_board": pm.make_charuco_board}
|
||||
mp[p_type]()
|
||||
# this should save pattern to output
|
||||
pm.save()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
svglib>=1.5.1
|
||||
reportlab>=4.0.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,124 @@
|
||||
import os, tempfile, numpy as np
|
||||
|
||||
import sys
|
||||
import cv2 as cv
|
||||
from tests_common import NewOpenCVTests
|
||||
import generate_pattern
|
||||
|
||||
class aruco_objdetect_test(NewOpenCVTests):
|
||||
|
||||
def test_aruco_dicts(self):
|
||||
try:
|
||||
import cairosvg
|
||||
except:
|
||||
raise self.skipTest("cairosvg library was not found")
|
||||
else:
|
||||
cols = 3
|
||||
rows = 5
|
||||
square_size = 100
|
||||
aruco_type = [cv.aruco.DICT_4X4_1000, cv.aruco.DICT_5X5_1000, cv.aruco.DICT_6X6_1000,
|
||||
cv.aruco.DICT_7X7_1000, cv.aruco.DICT_ARUCO_ORIGINAL, cv.aruco.DICT_APRILTAG_16h5,
|
||||
cv.aruco.DICT_APRILTAG_25h9, cv.aruco.DICT_APRILTAG_36h10, cv.aruco.DICT_APRILTAG_36h11, cv.aruco.DICT_ARUCO_MIP_36h12]
|
||||
aruco_type_str = ['DICT_4X4_1000','DICT_5X5_1000', 'DICT_6X6_1000',
|
||||
'DICT_7X7_1000', 'DICT_ARUCO_ORIGINAL', 'DICT_APRILTAG_16h5',
|
||||
'DICT_APRILTAG_25h9', 'DICT_APRILTAG_36h10', 'DICT_APRILTAG_36h11', 'DICT_ARUCO_MIP_36h12']
|
||||
marker_size = 0.8*square_size
|
||||
board_width = cols*square_size
|
||||
board_height = rows*square_size
|
||||
|
||||
for aruco_type_i in range(len(aruco_type)):
|
||||
#draw desk using opencv
|
||||
aruco_dict = cv.aruco.getPredefinedDictionary(aruco_type[aruco_type_i])
|
||||
board = cv.aruco.CharucoBoard((cols, rows), square_size, marker_size, aruco_dict)
|
||||
charuco_detector = cv.aruco.CharucoDetector(board)
|
||||
from_cv_img = board.generateImage((cols*square_size, rows*square_size))
|
||||
|
||||
#draw desk using svg
|
||||
fd1, filesvg = tempfile.mkstemp(prefix="out", suffix=".svg")
|
||||
os.close(fd1)
|
||||
fd2, filepng = tempfile.mkstemp(prefix="svg_marker", suffix=".png")
|
||||
os.close(fd2)
|
||||
|
||||
try:
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
pm = generate_pattern.PatternMaker(cols, rows, filesvg, "px", square_size, 0, board_width,
|
||||
board_height, "charuco_checkboard", marker_size,
|
||||
os.path.join(basedir, aruco_type_str[aruco_type_i]+'.json.gz'), 0)
|
||||
pm.make_charuco_board()
|
||||
pm.save()
|
||||
cairosvg.svg2png(url=filesvg, write_to=filepng, background_color="white")
|
||||
from_svg_img = cv.imread(filepng)
|
||||
_charucoCorners, _charuco_ids_svg, marker_corners_svg, marker_ids_svg = charuco_detector.detectBoard(from_svg_img)
|
||||
_charucoCorners, _charuco_ids_cv, marker_corners_cv, marker_ids_cv = charuco_detector.detectBoard(from_cv_img)
|
||||
marker_corners_svg_map, marker_corners_cv_map = {}, {}
|
||||
for i in range(len(marker_ids_svg)):
|
||||
marker_corners_svg_map[int(marker_ids_svg[i])] = marker_corners_svg[i]
|
||||
for i in range(len(marker_ids_cv)):
|
||||
marker_corners_cv_map[int(marker_ids_cv[i])] = marker_corners_cv[i]
|
||||
|
||||
for key_svg in marker_corners_svg_map.keys():
|
||||
marker_svg = marker_corners_svg_map[key_svg]
|
||||
marker_cv = marker_corners_cv_map[key_svg]
|
||||
np.testing.assert_allclose(marker_svg, marker_cv, 0.1, 0.1)
|
||||
finally:
|
||||
if os.path.exists(filesvg):
|
||||
os.remove(filesvg)
|
||||
if os.path.exists(filepng):
|
||||
os.remove(filepng)
|
||||
|
||||
def test_aruco_marker_sizes(self):
|
||||
try:
|
||||
import cairosvg
|
||||
except:
|
||||
raise self.skipTest("cairosvg library was not found")
|
||||
else:
|
||||
cols = 3
|
||||
rows = 5
|
||||
square_size = 100
|
||||
aruco_type = cv.aruco.DICT_5X5_1000
|
||||
aruco_type_str = 'DICT_5X5_1000'
|
||||
marker_sizes_rate = [0.25, 0.5, 0.75, 0.9]
|
||||
board_width = cols*square_size
|
||||
board_height = rows*square_size
|
||||
|
||||
for marker_s_rate in marker_sizes_rate:
|
||||
marker_size = marker_s_rate*square_size
|
||||
#draw desk using opencv
|
||||
aruco_dict = cv.aruco.getPredefinedDictionary(aruco_type)
|
||||
board = cv.aruco.CharucoBoard((cols, rows), square_size, marker_size, aruco_dict)
|
||||
charuco_detector = cv.aruco.CharucoDetector(board)
|
||||
from_cv_img = board.generateImage((cols*square_size, rows*square_size))
|
||||
|
||||
#draw desk using svg
|
||||
fd1, filesvg = tempfile.mkstemp(prefix="out", suffix=".svg")
|
||||
os.close(fd1)
|
||||
fd2, filepng = tempfile.mkstemp(prefix="svg_marker", suffix=".png")
|
||||
os.close(fd2)
|
||||
|
||||
try:
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
pm = generate_pattern.PatternMaker(cols, rows, filesvg, "px", square_size, 0, board_width,
|
||||
board_height, "charuco_checkboard", marker_size, os.path.join(basedir, aruco_type_str+'.json.gz'), 0)
|
||||
pm.make_charuco_board()
|
||||
pm.save()
|
||||
cairosvg.svg2png(url=filesvg, write_to=filepng, background_color="white")
|
||||
from_svg_img = cv.imread(filepng)
|
||||
|
||||
#test
|
||||
_charucoCorners, _charuco_ids_svg, marker_corners_svg, marker_ids_svg = charuco_detector.detectBoard(from_svg_img)
|
||||
_charucoCorners, _charuco_ids_cv, marker_corners_cv, marker_ids_cv = charuco_detector.detectBoard(from_cv_img)
|
||||
marker_corners_svg_map, marker_corners_cv_map = {}, {}
|
||||
for i in range(len(marker_ids_svg)):
|
||||
marker_corners_svg_map[int(marker_ids_svg[i])] = marker_corners_svg[i]
|
||||
for i in range(len(marker_ids_cv)):
|
||||
marker_corners_cv_map[int(marker_ids_cv[i])] = marker_corners_cv[i]
|
||||
|
||||
for key_svg in marker_corners_svg_map.keys():
|
||||
marker_svg = marker_corners_svg_map[key_svg]
|
||||
marker_cv = marker_corners_cv_map[key_svg]
|
||||
np.testing.assert_allclose(marker_svg, marker_cv, 0.1, 0.1)
|
||||
finally:
|
||||
if os.path.exists(filesvg):
|
||||
os.remove(filesvg)
|
||||
if os.path.exists(filepng):
|
||||
os.remove(filepng)
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
sys.dont_write_bytecode = True # Don't generate .pyc files / __pycache__ directories
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
# Python 3 moved urlopen to urllib.requests
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib import urlopen
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
sys.path.append(os.path.join(os.path.split(basedir)[0], "modules", "python", "test"))
|
||||
from tests_common import NewOpenCVTests
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
cwd = os.getcwd()
|
||||
config_file = 'opencv_apps_python_tests.cfg'
|
||||
locations = [cwd, basedir]
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r') as f:
|
||||
locations += [str(s).strip() for s in f.readlines()]
|
||||
else:
|
||||
print('WARNING: OpenCV tests config file ({}) is missing, running subset of tests'.format(config_file))
|
||||
|
||||
tests_pattern = os.environ.get('OPENCV_APPS_TEST_FILTER', 'test_*') + '.py'
|
||||
if tests_pattern != 'test_*.py':
|
||||
print('Tests filter: {}'.format(tests_pattern))
|
||||
|
||||
processed = set()
|
||||
for l in locations:
|
||||
if not os.path.isabs(l):
|
||||
l = os.path.normpath(os.path.join(cwd, l))
|
||||
if l in processed:
|
||||
continue
|
||||
processed.add(l)
|
||||
print('Discovering python tests from: {}'.format(l))
|
||||
sys_path_modify = l not in sys.path
|
||||
if sys_path_modify:
|
||||
sys.path.append(l) # Hack python loader
|
||||
discovered_tests = loader.discover(l, pattern=tests_pattern, top_level_dir=l)
|
||||
print(' found {} tests'.format(discovered_tests.countTestCases()))
|
||||
tests.addTests(loader.discover(l, pattern=tests_pattern))
|
||||
if sys_path_modify:
|
||||
sys.path.remove(l)
|
||||
return tests
|
||||
|
||||
if __name__ == '__main__':
|
||||
NewOpenCVTests.bootstrap()
|
||||
@@ -0,0 +1,5 @@
|
||||
ocv_add_application(opencv_version MODULES opencv_core SRCS opencv_version.cpp)
|
||||
if(WIN32)
|
||||
ocv_add_application(opencv_version_win32 MODULES opencv_core SRCS opencv_version.cpp)
|
||||
target_compile_definitions(opencv_version_win32 PRIVATE "OPENCV_WIN32_API=1")
|
||||
endif()
|
||||
@@ -0,0 +1,107 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/core/utils/trace.hpp>
|
||||
|
||||
#include <opencv2/core/opencl/opencl_info.hpp>
|
||||
|
||||
#ifdef OPENCV_WIN32_API
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
// defined in core/private.hpp
|
||||
namespace cv {
|
||||
CV_EXPORTS const char* currentParallelFramework();
|
||||
}
|
||||
|
||||
static void dumpHWFeatures(bool showAll = false)
|
||||
{
|
||||
std::cout << "OpenCV's HW features list:" << std::endl;
|
||||
int count = 0;
|
||||
for (int i = 0; i < CV_HARDWARE_MAX_FEATURE; i++)
|
||||
{
|
||||
cv::String name = cv::getHardwareFeatureName(i);
|
||||
if (name.empty())
|
||||
continue;
|
||||
bool enabled = cv::checkHardwareSupport(i);
|
||||
if (enabled)
|
||||
count++;
|
||||
if (enabled || showAll)
|
||||
{
|
||||
printf(" ID=%3d (%s) -> %s\n", i, name.c_str(), enabled ? "ON" : "N/A");
|
||||
}
|
||||
}
|
||||
std::cout << "Total available: " << count << std::endl;
|
||||
}
|
||||
|
||||
static void dumpParallelFramework()
|
||||
{
|
||||
const char* parallelFramework = cv::currentParallelFramework();
|
||||
if (parallelFramework)
|
||||
{
|
||||
int threads = cv::getNumThreads();
|
||||
std::cout << "Parallel framework: " << parallelFramework << " (nthreads=" << threads << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, const char** argv)
|
||||
{
|
||||
CV_TRACE_FUNCTION();
|
||||
CV_TRACE_ARG(argc);
|
||||
CV_TRACE_ARG_VALUE(argv0, "argv0", argv[0]);
|
||||
CV_TRACE_ARG_VALUE(argv1, "argv1", argv[1]);
|
||||
|
||||
#ifndef OPENCV_WIN32_API
|
||||
cv::CommandLineParser parser(argc, argv,
|
||||
"{ help h usage ? | | show this help message }"
|
||||
"{ verbose v | | show build configuration log }"
|
||||
"{ opencl | | show information about OpenCL (available platforms/devices, default selected device) }"
|
||||
"{ hw | | show detected HW features (see cv::checkHardwareSupport() function). Use --hw=0 to show available features only }"
|
||||
"{ threads | | show configured parallel framework and number of active threads }"
|
||||
);
|
||||
|
||||
if (parser.has("help"))
|
||||
{
|
||||
parser.printMessage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (parser.has("verbose"))
|
||||
{
|
||||
std::cout << cv::getBuildInformation().c_str() << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << CV_VERSION << std::endl;
|
||||
}
|
||||
|
||||
if (parser.has("opencl"))
|
||||
{
|
||||
cv::dumpOpenCLInformation();
|
||||
}
|
||||
|
||||
if (parser.has("hw"))
|
||||
{
|
||||
dumpHWFeatures(parser.get<bool>("hw"));
|
||||
}
|
||||
|
||||
if (parser.has("threads"))
|
||||
{
|
||||
dumpParallelFramework();
|
||||
}
|
||||
|
||||
#else
|
||||
std::cout << cv::getBuildInformation().c_str() << std::endl;
|
||||
cv::dumpOpenCLInformation();
|
||||
dumpHWFeatures();
|
||||
dumpParallelFramework();
|
||||
MessageBoxA(NULL, "Check console window output", "OpenCV(" CV_VERSION ")", MB_ICONINFORMATION | MB_OK);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user