├── .idea └── vcs.xml ├── LICENSE ├── README.md ├── UndistortFishEye ├── .idea │ ├── UndistortFishEye.iml │ ├── encodings.xml │ ├── misc.xml │ ├── modules.xml │ └── workspace.xml ├── UndistortFishEye.py ├── UndistortFishEye_process.py ├── UndistortFishEye_widget.py ├── __init__.py └── fisheye_module.py ├── calibrate.py ├── evaluate.py └── undistort.py /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ikomia-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FishEyeModel 2 | Python project based on OpenCV module to model FishEye camera and undistort acquired images 3 | 4 | ## Dependencies 5 | Python 3.7 or higher 6 | OpenCV 4.0 or higher 7 | 8 | ## Calibration 9 | OpenCV calibration algorithm for FishEye camera enables to model intrinsic and/or extrinsic parameters from a set of images acquired by the camera. 10 | To use the function, you have to acquire images of known pattern from which it is possible to extract distortions (chessboard for example). 11 | A chessboard pattern can be downloaded following this link: 12 | [Chessboard pattern](https://drive.google.com/file/d/0B3EXsqKhWPfdVk9NRmIzNE1RQms/view?usp=sharing). 13 | 14 | Then you have to acquire several images at different positions to integrate as much distorsion information as possible into the model. 15 | In our tests, a set of 25-30 images can be sufficient to have good undistort results. 16 | 17 | We give a Python script to launch the calibration 18 | 19 | ```console 20 | python calibrate.py "ImageFolder" [-o "OutputFile"] [-s "True/False"] 21 | ``` 22 | 23 | **ImageFolder (mandatory)**: folder where acquired images for calibration are stored. 24 | **OutputFile (optional)**: path of the saved model file. By default, file *calibration.txt* is saved into the current folder. 25 | **True/False (optional)**: boolean value to indicate if chessboard detection images have to be saved. 26 | 27 | The main steps of the algorithm are: 28 | 1. Detection of all inner corners of the chessboard for each image. 29 | 2. Improvement of corner positions thanks to super-resolution algorithm. 30 | 3. Valid detections storage. 31 | 4. FishEye modelization on valid detections. 32 | 5. Save calibration parameters to file (extrinsic or intrinsic). 33 | 34 | **Note:** calibration can failed even if chessboard detection is valid. 35 | Error can occured during the model computation (because of *cv2.fisheye.CALIB_CHECK_COND* flag). 36 | You have to remove the corresponding chessboard image to ensure good calibration. 37 | 38 | ## Undistort 39 | We give a Python script to launch the correction: 40 | 41 | ```console 42 | python undistort.py "SourceImage" [-c "CalibrationFile"] [-o "OutputImage"] 43 | ``` 44 | 45 | **SourceImage (mandatory)**: path of the image to undistort. 46 | **CalibrationFile (optional)**: path of the model file generated by the calibration. 47 | If not set, the script will search the default file *calibratio.txt" into the current folder and fail if not found. 48 | **OutputImage (optional)**: path of the saved undistorted image. 49 | By default, undistorted image is saved in the file *undistort.png* into the current folder. 50 | -------------------------------------------------------------------------------- /UndistortFishEye/.idea/UndistortFishEye.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /UndistortFishEye/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /UndistortFishEye/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /UndistortFishEye/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /UndistortFishEye/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /UndistortFishEye/UndistortFishEye.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import numpy 3 | import PyCore 4 | import PyDataProcess 5 | import UndistortFishEye_process as processMod 6 | import UndistortFishEye_widget as widgetMod 7 | 8 | 9 | # -------------------- 10 | # - Interface class to integrate the process with Ikomia application 11 | # - Inherits PyDataProcess.CPluginProcessInterface from Ikomia API 12 | # -------------------- 13 | class UndistortFishEye(PyDataProcess.CPluginProcessInterface): 14 | 15 | def __init__(self): 16 | PyDataProcess.CPluginProcessInterface.__init__(self) 17 | 18 | def getProcessFactory(self): 19 | # Instantiate process object 20 | return processMod.UndistortFishEyeProcessFactory() 21 | 22 | def getWidgetFactory(self): 23 | # Instantiate associated widget object 24 | return widgetMod.UndistortFishEyeWidgetFactory() 25 | -------------------------------------------------------------------------------- /UndistortFishEye/UndistortFishEye_process.py: -------------------------------------------------------------------------------- 1 | import PyCore 2 | import PyDataProcess 3 | import copy 4 | import fisheye_module 5 | 6 | 7 | # -------------------- 8 | # - Class to handle the process parameters 9 | # - Inherits PyCore.CProtocolTaskParam from Ikomia API 10 | # -------------------- 11 | class UndistortFishEyeProcessParam(PyCore.CProtocolTaskParam): 12 | 13 | def __init__(self): 14 | PyCore.CProtocolTaskParam.__init__(self) 15 | # Place default value initialization here 16 | self.calib_file_path = str() 17 | 18 | def setParamMap(self, paramMap): 19 | # Set parameters values from Ikomia application 20 | # Parameters values are stored as string and accessible like a python dict 21 | self.calib_file_path = paramMap['calibrationFile'] 22 | 23 | def getParamMap(self): 24 | # Send parameters values to Ikomia application 25 | # Create the specific dict structure (string container) 26 | paramMap = PyCore.ParamMap() 27 | paramMap['calibrationFile'] = self.calib_file_path 28 | return paramMap 29 | 30 | 31 | # -------------------- 32 | # - Class which implements the process 33 | # - Inherits PyCore.CProtocolTask or derived from Ikomia API 34 | # -------------------- 35 | class UndistortFishEyeProcess(PyDataProcess.CImageProcess2d): 36 | 37 | def __init__(self, name, param): 38 | PyDataProcess.CImageProcess2d.__init__(self, name) 39 | 40 | #Create parameters class 41 | if param is None: 42 | self.setParam(UndistortFishEyeProcessParam()) 43 | else: 44 | self.setParam(copy.deepcopy(param)) 45 | 46 | def getProgressSteps(self, eltCount=1): 47 | # Function returning the number of progress steps for this process 48 | # This is handled by the main progress bar of Ikomia application 49 | return 3 50 | 51 | def run(self): 52 | # Core function of your process 53 | # Call beginTaskRun for initialization 54 | self.beginTaskRun() 55 | 56 | param = self.getParam() 57 | k, d, dims = fisheye_module.load_calibration(param.calib_file_path) 58 | 59 | # Step progress bar: 60 | self.emitStepProgress() 61 | 62 | img_input = self.getInput(0) 63 | src_img = img_input.getImage() 64 | dst_img = fisheye_module.undistort(src_img, k, d, dims) 65 | 66 | # Step progress bar: 67 | self.emitStepProgress() 68 | 69 | output = self.getOutput(0) 70 | output.setImage(dst_img) 71 | 72 | # Step progress bar: 73 | self.emitStepProgress() 74 | 75 | # Call endTaskRun to finalize process 76 | self.endTaskRun() 77 | 78 | 79 | # -------------------- 80 | # - Factory class to build process object 81 | # - Inherits PyDataProcess.CProcessFactory from Ikomia API 82 | # -------------------- 83 | class UndistortFishEyeProcessFactory(PyDataProcess.CProcessFactory): 84 | 85 | def __init__(self): 86 | PyDataProcess.CProcessFactory.__init__(self) 87 | # Set process information as string here 88 | self.info.name = "UndistortFishEye" 89 | self.info.description = "This process undistorts images acquired by FishEye camera. " \ 90 | "It requires a calibration file that models extrinsic parameters of the camera. " \ 91 | "This file can be generated with the given Python script calibrate.py" 92 | self.info.authors = "Ikomia" 93 | # relative path -> as displayed in Ikomia application process tree 94 | self.info.path = "Plugins/Python/FishEye" 95 | # self.info.iconPath = "your path to a specific icon" 96 | self.info.keywords = "distortion,fisheye,camera,calibration" 97 | 98 | def create(self, param=None): 99 | # Create process object 100 | return UndistortFishEyeProcess(self.info.name, param) 101 | -------------------------------------------------------------------------------- /UndistortFishEye/UndistortFishEye_widget.py: -------------------------------------------------------------------------------- 1 | import PyCore 2 | import PyDataProcess 3 | import QtConversion 4 | import UndistortFishEye_process as processMod 5 | 6 | #PyQt GUI framework 7 | from PyQt5.QtWidgets import * 8 | 9 | 10 | # -------------------- 11 | # - Class which implements widget associated with the process 12 | # - Inherits PyCore.CProtocolTaskWidget from Ikomia API 13 | # -------------------- 14 | class UndistortFishEyeWidget(PyCore.CProtocolTaskWidget): 15 | 16 | def __init__(self, param, parent): 17 | PyCore.CProtocolTaskWidget.__init__(self, parent) 18 | 19 | if param is None: 20 | self.parameters = processMod.UndistortFishEyeProcessParam() 21 | else: 22 | self.parameters = param 23 | 24 | label_calib_file = QLabel("Calibration file") 25 | self.edit_calib_file = QLineEdit(self.parameters.calib_file_path) 26 | browse_btn = QPushButton("...") 27 | browse_btn.setToolTip("Select calibration file") 28 | 29 | # Mandatory : apply button to launch the process 30 | applyButton = QPushButton("Apply") 31 | 32 | # Create layout : QGridLayout by default 33 | self.gridLayout = QGridLayout() 34 | self.gridLayout.addWidget(label_calib_file, 0, 0) 35 | self.gridLayout.addWidget(self.edit_calib_file, 0, 1) 36 | self.gridLayout.addWidget(browse_btn, 0, 2) 37 | self.gridLayout.addWidget(applyButton, 1, 0, 1, 3) 38 | 39 | # Set clicked signal connection 40 | browse_btn.clicked.connect(self.on_browse) 41 | applyButton.clicked.connect(self.on_apply) 42 | 43 | # PyQt -> Qt wrapping 44 | layoutPtr = QtConversion.PyQtToQt(self.gridLayout) 45 | 46 | # Set widget layout 47 | self.setLayout(layoutPtr) 48 | 49 | def on_browse(self): 50 | filter = "Calibration file(*.txt)" 51 | calibration_file = self.get_selected_file(filter) 52 | self.edit_calib_file.setText(calibration_file); 53 | 54 | def on_apply(self): 55 | # Apply button clicked slot 56 | 57 | # Get parameters from widget 58 | self.parameters.calib_file_path = self.edit_calib_file.text() 59 | 60 | # Send signal to launch the process 61 | self.emitApply(self.parameters) 62 | 63 | def get_selected_file(self, filter): 64 | file_path = str() 65 | file_dlg = QFileDialog() 66 | file_dlg.setFileMode(QFileDialog.ExistingFile) 67 | file_dlg.setViewMode(QFileDialog.Detail) 68 | file_dlg.setNameFilter(filter) 69 | 70 | if file_dlg.exec(): 71 | path_list = file_dlg.selectedFiles() 72 | file_path = path_list[0] 73 | 74 | return file_path 75 | 76 | 77 | #-------------------- 78 | #- Factory class to build process widget object 79 | #- Inherits PyDataProcess.CWidgetFactory from Ikomia API 80 | #-------------------- 81 | class UndistortFishEyeWidgetFactory(PyDataProcess.CWidgetFactory): 82 | 83 | def __init__(self): 84 | PyDataProcess.CWidgetFactory.__init__(self) 85 | # Set the name of the process -> it must be the same as the one declared in the process factory class 86 | self.name = "UndistortFishEye" 87 | 88 | def create(self, param): 89 | # Create widget object 90 | return UndistortFishEyeWidget(param, None) 91 | -------------------------------------------------------------------------------- /UndistortFishEye/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ikomia-dev/FishEyeModel/eabf544163841c9162afc10b9e5fc7a0e7304d80/UndistortFishEye/__init__.py -------------------------------------------------------------------------------- /UndistortFishEye/fisheye_module.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import glob 4 | import pickle 5 | import numpy as np 6 | 7 | # Size of the chessboard used for calibration 8 | CHESSBOARD_SIZE = (6, 9) 9 | 10 | # Calculate curvature for each horizontal and vertical curves from the chessboard 11 | # Return: curvature integral 12 | def chessboard_measure(corners): 13 | cornersMat = np.reshape(corners, (-1, CHESSBOARD_SIZE[0], 2)) 14 | curvature_integral = 0 15 | # Vertical curves 16 | for k in range(CHESSBOARD_SIZE[1]): 17 | dx_dt = np.gradient(cornersMat[k, :, 0]) 18 | dy_dt = np.gradient(cornersMat[k, :, 1]) 19 | d2x_dt2 = np.gradient(dx_dt) 20 | d2y_dt2 = np.gradient(dy_dt) 21 | curvature = np.abs(d2x_dt2 * dy_dt - dx_dt * d2y_dt2) / (dx_dt * dx_dt + dy_dt * dy_dt) ** 1.5 22 | curvature_integral += np.sum(curvature) 23 | # Horizontal curves 24 | for k in range(CHESSBOARD_SIZE[0]): 25 | dx_dt = np.gradient(cornersMat[:, k, 0]) 26 | dy_dt = np.gradient(cornersMat[:, k, 1]) 27 | d2x_dt2 = np.gradient(dx_dt) 28 | d2y_dt2 = np.gradient(dy_dt) 29 | curvature = np.abs(d2x_dt2 * dy_dt - dx_dt * d2y_dt2) / (dx_dt * dx_dt + dy_dt * dy_dt) ** 1.5 30 | curvature_integral += np.sum(curvature) 31 | return curvature_integral 32 | 33 | # Chessboard detection on the image loaded from the given file 34 | # If saveDetection==True an image with the result of the detection is saved to the disk 35 | # Return: success status (True or False), corners detected in image coordinates, size of the image 36 | def detect_chessboard(img_path, save_detection): 37 | img = cv2.imread(img_path) 38 | ret, corners, img_shape = detect_chessboard_img(img, save_detection) 39 | if ret == True: 40 | if save_detection: 41 | chess_path = img_path.replace('.png', '_chess.jpg') 42 | cv2.imwrite(chess_path, img) 43 | 44 | return ret, corners, img_shape 45 | 46 | # Chessboard detection on input opencv image 47 | # If showChessboard==True(default) we draw corners and lines on input image 48 | # Return: success status (True or False), corners detected in image coordinates, size of the image 49 | def detect_chessboard_img(img, showChessboard=True): 50 | chessboard_flags = cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_FAST_CHECK + cv2.CALIB_CB_NORMALIZE_IMAGE 51 | subpix_criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.01) 52 | img_shape = img.shape[:2] 53 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 54 | ret, corners = cv2.findChessboardCorners(gray, CHESSBOARD_SIZE, chessboard_flags) 55 | if ret == True: 56 | # Refining corners position with sub-pixels based algorithm 57 | cv2.cornerSubPix(gray, corners, (3, 3), (-1, -1), subpix_criteria) 58 | if showChessboard == True: 59 | cv2.drawChessboardCorners(img, CHESSBOARD_SIZE, corners, ret) 60 | else: 61 | print('Chessboard not detected in image ') 62 | 63 | return ret, corners, img_shape 64 | 65 | # Show deformation measure on source image and undistorted image 66 | # The less the better (no deformation means perfect straight lines which means 0 curvature) 67 | def deformation_measure(src_image, undistorted_img): 68 | ret, corners1, _ = detect_chessboard_img(src_image) 69 | if ret == False: 70 | return 71 | ret, corners2, _ = detect_chessboard_img(undistorted_img) 72 | if ret == False: 73 | return 74 | m1 = chessboard_measure(corners1) 75 | m2 = chessboard_measure(corners2) 76 | r = (1-m2/m1)*100 77 | print('Deformation measure on source image: ' + str(m1)) 78 | print('Deformation measure on undistorted image: ' + str(m2)) 79 | print('Correction rate in percent: ' + str(r)) 80 | 81 | def evaluate(img_path, calibration_path): 82 | src_image = cv2.imread(img_path) 83 | k, d, dims = load_calibration(calibration_path) 84 | undistorted_img = undistort(src_image, k, d, dims) 85 | deformation_measure(src_image, undistorted_img) 86 | 87 | # Launch calibration process from all png images in the given folder 88 | # Return: calibration parameters K, D and image dimensions 89 | def calibrate(img_folder, save_detection=False): 90 | # Calibration paramenters 91 | # NB: when CALIB_CHECK_COND is set, the algorithm checks if the detected corners of each images are valid. 92 | # If not, an exception is thrown which indicates the zero-based index of the invalid image. 93 | # Such image should be replaced or removed from the calibration dataset to ensure a good calibration. 94 | calibration_flags = cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC + cv2.fisheye.CALIB_CHECK_COND + cv2.fisheye.CALIB_FIX_SKEW 95 | term_criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) 96 | # Logical coordinates of chessboard corners 97 | obj_p = np.zeros((1, CHESSBOARD_SIZE[0]*CHESSBOARD_SIZE[1], 3), np.float32) 98 | obj_p[0, :, :2] = np.mgrid[0:CHESSBOARD_SIZE[0], 0:CHESSBOARD_SIZE[1]].T.reshape(-1, 2) 99 | 100 | img_ref_shape = None 101 | obj_points = [] # 3d point in real world space 102 | img_points = [] # 2d points in image plane. 103 | 104 | # Iterate through all images in the folder 105 | images = glob.glob(img_folder + '/*.png') 106 | for filename in images: 107 | # Chessboard detection 108 | ret, corners, img_shape = detect_chessboard(filename, save_detection) 109 | 110 | if img_ref_shape == None: 111 | img_ref_shape = img_shape 112 | else: 113 | assert img_ref_shape == img_shape, "All images must share the same size." 114 | 115 | # If found, add object points, image points (after refining them) 116 | if ret == True: 117 | obj_points.append(obj_p) 118 | img_points.append(corners) 119 | print('Image ' + filename + ' is valid for calibration') 120 | 121 | k = np.zeros((3, 3)) 122 | d = np.zeros((4, 1)) 123 | dims = img_shape[::-1] 124 | valid_img_count = len(obj_points) 125 | 126 | if valid_img_count > 0: 127 | rvecs = [np.zeros((1, 1, 3), dtype=np.float64) for i in range(valid_img_count)] 128 | tvecs = [np.zeros((1, 1, 3), dtype=np.float64) for i in range(valid_img_count)] 129 | 130 | print('Beginning calibration process...') 131 | rms, _, _, _, _ = cv2.fisheye.calibrate( 132 | obj_points, 133 | img_points, 134 | img_shape, 135 | k, 136 | d, 137 | rvecs, 138 | tvecs, 139 | calibration_flags, 140 | term_criteria 141 | ) 142 | 143 | print("Calibration done!") 144 | print("Found " + str(valid_img_count) + " valid images for calibration") 145 | return k, d, dims 146 | 147 | 148 | # Save calibration to file -> use of pickle serialization 149 | def save_calibration(path, dims, k, d ): 150 | with open(path, 'wb') as f: 151 | pickle.dump(dims, f) 152 | pickle.dump(k, f) 153 | pickle.dump(d, f) 154 | 155 | 156 | # Load calibration from file -> use of pickle serialization 157 | def load_calibration(path): 158 | k = np.zeros((3, 3)) 159 | d = np.zeros((4, 1)) 160 | dims = np.zeros(2) 161 | 162 | with open(path, 'rb') as f: 163 | k = pickle.load(f) 164 | d = pickle.load(f) 165 | dims = pickle.load(f) 166 | 167 | return k, d, dims 168 | 169 | 170 | # Undistort FishEye image with given calibration parameters 171 | def undistort(src_image, k, d, dims): 172 | dim1 = src_image.shape[:2][::-1] # dim1 is the dimension of input image to un-distort 173 | assert dim1[0] / dim1[1] == dims[0] / dims[1], "Image to undistort needs to have same aspect ratio as the ones used in calibration" 174 | map1, map2 = cv2.fisheye.initUndistortRectifyMap(k, d, np.eye(3), k, dims, cv2.CV_16SC2) 175 | undistorted_img = cv2.remap(src_image, map1, map2, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT) 176 | return undistorted_img 177 | 178 | 179 | # Undistort FishEye image (from a file) with given calibration parameters 180 | def undistort_from_file(img_path, k, d, dims): 181 | img = cv2.imread(img_path) 182 | return undistort(img, k, d, dims) 183 | 184 | 185 | # Undistort FishEye image (from a file) with given calibration parameters 186 | # This function has few others parameters to set the field of view of the undistorted image 187 | # Not suitable for large FOV FishEye camera (near 180°) 188 | def undistort_beta(img_path, k, d, dims, balance=0.0, dim2=None, dim3=None): 189 | img = cv2.imread(img_path) 190 | dim1 = img.shape[:2][::-1] # dim1 is the dimension of input image to un-distort 191 | assert dim1[0] / dim1[1] == dims[0] / dims[1], "Image to undistort needs to have same aspect ratio as the ones used in calibration" 192 | 193 | if not dim2: 194 | dim2 = dim1 195 | 196 | if not dim3: 197 | dim3 = dim1 198 | 199 | scaled_k = k * dim1[0] / dims[0] # The values of K is to scale with image dimension. 200 | scaled_k[2][2] = 1.0 # Except that K[2][2] is always 1.0 201 | 202 | # This is how scaled_K, dim2 and balance are used to determine the final K used to un-distort image 203 | new_k = cv2.fisheye.estimateNewCameraMatrixForUndistortRectify(scaled_k, d, dim2, np.eye(3), None, balance, None, 1.0) 204 | map1, map2 = cv2.fisheye.initUndistortRectifyMap(scaled_k, d, np.eye(3), new_k, dim3, cv2.CV_16SC2) 205 | undistorted_img = cv2.remap(img, map1, map2, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT) 206 | return undistorted_img 207 | 208 | 209 | # Tool function to save image to disk at the given path 210 | def save_image(image, image_path): 211 | cv2.imwrite(image_path, image) 212 | 213 | 214 | # Tool function to save image to disk with automatic path definition (from the original image path) 215 | # -> use for test only 216 | def save_undistort_image(undistorted_img, img_path, suffix): 217 | folder = os.path.dirname(img_path) 218 | save_folder = folder + '/Undistort/' 219 | 220 | if not os.path.exists(save_folder): 221 | os.makedirs(save_folder) 222 | 223 | filename = os.path.basename(img_path) 224 | filename, file_extension = os.path.splitext(filename) 225 | cv2.imwrite(save_folder + filename + suffix + file_extension, undistorted_img) 226 | 227 | # Tool function to test chessboard detection on all images in subfolder "CalibImages" 228 | def test_chessboard_detection(): 229 | images = glob.glob('CalibImages/*.png') 230 | for filename in images: 231 | ret, _, _ = detect_chessboard(filename, True) 232 | if ret: 233 | print('Chessboard detected correctly in image ' + filename) 234 | else: 235 | print('Chessboard not detected in image ' + filename) 236 | 237 | # Tool function to test calibration function on all images in subfolder "CalibImages" 238 | def test_calibration(): 239 | k, d, dims = calibrate('CalibImages') 240 | print('----- Calibration results -----') 241 | print("Dimensions =" + str(dims)) 242 | print("K = np.array(" + str(k.tolist()) + ")") 243 | print("D = np.array(" + str(d.tolist()) + ")") 244 | 245 | save_calibration('calibration.txt', k, d, dims) 246 | print("Calibration file saved successfully") 247 | 248 | k, d, dims = load_calibration('calibration.txt') 249 | print("Calibration file loaded successfully") 250 | print("Dimensions =" + str(dims)) 251 | print("K = np.array(" + str(k.tolist()) + ")") 252 | print("D = np.array(" + str(d.tolist()) + ")") 253 | 254 | # Tool function to test undistortion 255 | # Calibration file "calibration.txt" must be in the current folder 256 | def test_undistort(): 257 | k, d, dims = load_calibration('calibration.txt') 258 | 259 | # filename = 'SrcImages/Natuition/pos1.jpg' 260 | # undistorted_img = undistort_beta(filename, k, d, dims) 261 | # save_undistort_image(undistorted_img, filename, '_simple') 262 | # undistorted_img = undistort_beta(filename, k, d, dims, balance=0.0, dim2=(2000, 1500)) 263 | # save_undistort_image(undistorted_img, filename, '_b0.0') 264 | # undistorted_img = undistort_beta(filename, k, d, dims, balance=0.5, dim2=(2000, 1500)) 265 | # save_undistort_image(undistorted_img, filename, '_b0.5') 266 | # undistorted_img = undistort_beta(filename, k, d, dims, balance=1.0, dim2=(2000, 1500)) 267 | # save_undistort_image(undistorted_img, filename, '_b1.0') 268 | 269 | images = glob.glob('SrcImages/Natuition/*.jpg') 270 | for filename in images: 271 | undistorted_img = undistort_from_file(filename, k, d, dims) 272 | save_undistort_image(undistorted_img, filename, '') 273 | 274 | images = glob.glob('SrcImages/Ikomia/*.png') 275 | for filename in images: 276 | undistorted_img = undistort_from_file(filename, k, d, dims) 277 | save_undistort_image(undistorted_img, filename, '') 278 | 279 | print("Undistort process finished successfully") 280 | 281 | # Tool function to test deformation measure on a specific image 282 | # Calibration file "calibration.txt" must be in the current folder 283 | def test_measure(img_path): 284 | src_image = cv2.imread(img_path) 285 | k, d, dims = load_calibration('calibration.txt') 286 | undistorted_img = undistort(src_image, k, d, dims) 287 | deformation_measure(src_image, undistorted_img) 288 | 289 | if __name__ == '__main__': 290 | #test_measure('CalibImages/img14.png') 291 | test_chessboard_detection() 292 | #test_calibration() 293 | #test_undistort() 294 | 295 | 296 | -------------------------------------------------------------------------------- /calibrate.py: -------------------------------------------------------------------------------- 1 | from UndistortFishEye import fisheye_module 2 | import argparse 3 | 4 | 5 | # Python script to launch calibration process from terminal 6 | # Usage: calibrate.py "folder_of_calibration_images" "output_path" 7 | if __name__ == '__main__': 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument( 10 | "image_folder", 11 | help="Folder where the calibration images are stored" 12 | ) 13 | 14 | parser.add_argument( 15 | "-o", 16 | dest="output_file_path", 17 | default="calibration.txt", 18 | help="Path where the calibration file will be saved" 19 | ) 20 | 21 | parser.add_argument( 22 | "-s", 23 | dest="save_detection", 24 | default=False, 25 | help="Path where the calibration file will be saved" 26 | ) 27 | 28 | args = parser.parse_args() 29 | if not args.image_folder: 30 | raise RuntimeError("Folder of calibration images is required") 31 | 32 | k, d, dims = fisheye_module.calibrate(args.image_folder, args.save_detection) 33 | print('----- Calibration results -----') 34 | print("Dimensions =" + str(dims)) 35 | print("K = np.array(" + str(k.tolist()) + ")") 36 | print("D = np.array(" + str(d.tolist()) + ")") 37 | 38 | fisheye_module.save_calibration(args.output_file_path, k, d, dims) 39 | print("Calibration file saved successfully") 40 | 41 | -------------------------------------------------------------------------------- /evaluate.py: -------------------------------------------------------------------------------- 1 | from UndistortFishEye import fisheye_module 2 | import argparse 3 | 4 | # Python script to launch evaluate process from terminal 5 | # Usage: evaluate.py "source_image_path" -c "calibration_file_path" 6 | if __name__ == '__main__': 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument( 9 | "source_image_path", 10 | help="Path of the source image to undistort" 11 | ) 12 | 13 | parser.add_argument( 14 | "-c", 15 | dest="calibration_file_path", 16 | default="calibration.txt", 17 | help="Path of the calibration file" 18 | ) 19 | 20 | args = parser.parse_args() 21 | 22 | if not args.source_image_path: 23 | raise RuntimeError("Source image path could not be empty") 24 | 25 | if not args.calibration_file_path: 26 | raise RuntimeError("Calibration file path could not be empty") 27 | 28 | fisheye_module.evaluate(args.source_image_path, args.calibration_file_path) 29 | print("Evaluate process finished successfully") 30 | -------------------------------------------------------------------------------- /undistort.py: -------------------------------------------------------------------------------- 1 | from UndistortFishEye import fisheye_module 2 | import argparse 3 | 4 | # Python script to launch undistort process from terminal 5 | # Usage: undistort.py "source_image_path" "calibration_file_path" "output_image_path" 6 | if __name__ == '__main__': 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument( 9 | "source_image_path", 10 | help="Path of the source image to undistort" 11 | ) 12 | 13 | parser.add_argument( 14 | "-c", 15 | dest="calibration_file_path", 16 | default="calibration.txt", 17 | help="Path of the calibration file" 18 | ) 19 | 20 | parser.add_argument( 21 | "-o", 22 | dest="output_image_path", 23 | default="undistort.png", 24 | help="Path where the result undistorted image will be saved" 25 | ) 26 | 27 | args = parser.parse_args() 28 | 29 | if not args.source_image_path: 30 | raise RuntimeError("Source image path could not be empty") 31 | 32 | if not args.calibration_file_path: 33 | raise RuntimeError("Calibration file path could not be empty") 34 | 35 | k, d, dims = fisheye_module.load_calibration(args.calibration_file_path) 36 | undistorted_img = fisheye_module.undistort_from_file(args.source_image_path, k, d, dims) 37 | fisheye_module.save_image(undistorted_img, args.output_image_path) 38 | print("Undistort process finished successfully") 39 | --------------------------------------------------------------------------------