def test_load_calib_imgs_paths(): """ Test load_calib_imgs path creation/checking. All `calib_imgs_paths` tests are basically the same """ global p, o if not os_exists(testdir+'/raw'): raise RuntimeError('test \'raw\' directory could not be found') # Setup calib = CalibratePSEye() calib.init_chessboard(p, o) try: calib.load_calib_imgs(testdir+'/raw') # Make sure everything was created properly for p in ('/raw', '/corners'): if not isdir(calib.calibpath + p): raise RuntimeError('path \'%s\' wasn\'t created') # Make sure raw images were copied correctly for fn in listdir(testdir + '/raw'): f1 = calib.calibpath + '/raw/' + fn f2 = testdir + '/raw/' + fn g1 = cv_imread(f1) i2 = cv_imread(f2) g2 = cvtColor(cvtColor(i2, COLOR_RGB2GRAY), COLOR_GRAY2RGB) if not array_equal(g1, g2): raise RuntimeError('frame \'%s\' did not match' % fn) debug('\'%s\' matched' % fn) finally: calib.removepath()
def test_record_calib_imgs_member_data(): """ Test member data assignment inside of `record_calib_imgs` `record_calib_imgs` is called with a negative countdown to force immediate chessboard logging. The test calibration images provided should all have valid chessboards for the provided params. """ # Setup h, w, _ = cv_imread(testdir+'/raw/f00001.jpg').shape nf = len([ f for f in listdir(testdir+'/raw') if f[-4:].lower() == '.jpg' ]) # Tests calib = CalibratePSEye() calib.init_chessboard(p, o) try: calib.record_calib_imgs( cam=testdir+'/raw/f%05d.jpg', nframes=nf, countdown=-1 ) if calib.w != w: raise RuntimeError('\'w\' wasn\'t set properly') if calib.h != h: raise RuntimeError('\'h\' wasn\'t set properly') if calib.img_arr.shape != (h,w,1,nf): raise RuntimeError('\'img_arr.shape\' wasn\'t set properly') if calib.img_arr.dtype != uint8: raise RuntimeError('\'img_arr.dtype\' wasn\'t set properly') finally: calib.removepath()
def get_next_frame(self): self.current_frame += 1 frame_path = self.frame_list.get_next_path() if frame_path is None and (self.end_frame is None or self.current_frame < self.end_frame): r = self.buffer self.buffer = cv_imread(frame_path) return r else: return None
def correct_and_save(self, imgpath): """ Correct images and then saves them with the calibration timestamp. INPUTS imgpath -- str -- path to images to correct OUTPUTS undistorted_imgs -- uint8 N x H x W x BYTES NOTE Only corrects filenames ending in '.jpg' ALGORITHM Saves corrected images at same level as the 'imgpath' directory, but adds a timestamp which references the calibration dataset/params used. `undistorted_YYMMDD-HHmmss/` and `remapped_YYMMDD-HHmmss/` EXCEPTIONS Raises RuntimeError If calibration path isn't set. Likely results from failing to load/compute calibration matrices. """ if type(imgpath) != str: raise TypeError('imgpath must be str') if self.calibpath is None: raise RuntimeError( 'Be sure to set self.calibpath (did you compute/load calibrations?)' ) # Find images to correct potential_imgs = listdir(imgpath) fn_imgs = [f for f in potential_imgs if f[-4:].lower() == '.jpg'] # select jpegs img_list = [cv_imread(imgpath + '/' + fn) for fn in fn_imgs] # read all jpegs # Make Save Directories savedir = realpath(imgpath + '/..') timestamp = self.get_timestamp() copyfile(self.calibpath + '/camera_params.csv', savedir + '/' + timestamp + '_camera_params.csv') ud_path = savedir + '/' + timestamp + '_undistorted' if not isdir(ud_path): if os_exists(ud_path): os_remove(ud_path) mkdir(ud_path) # Correct & Save Frames ud = self.correct(img_list) for i in range(len(img_list)): fnud = ud_path + ('/f%s' % str(i + 1).zfill(5)) + '.jpg' cv_imwrite(fnud, ud[i, ...], (IMWRITE_JPEG_QUALITY, 100)) # Return in case we want to use them later return ud
def read_image(img_name, grey=False, use_opencv=False, uint8=False): """ Read an image file (.png) into a numpy array in which each entry is a row of pixels (i.e. ``len(img)`` is the image height in px. If grey is True (default is False), returns a grayscale image (dtype uint8 if RGBA, and dtype float32 if greyscale). use_opencv uses the `cv2.imread` function rather than `imageio.imread`, which always returns a dtype of uint8. uint8 will enforce dtype of uint8 (i.e. for greyscale from `imageio.imread`) if set to True, but defaults to False. """ data_dir = Path('..') / 'img' if use_opencv: if grey: img = cv_imread(data_dir / img_name, 0) else: img = cv_imread(data_dir / img_name) else: img = imread(data_dir / img_name, as_gray=grey) if uint8 and img.dtype != 'uint8': img = np.uint8(img) return img
def inspect(self): """ Run through calibration images, showing original and undistorted. Intended to be run after saving calibration images. """ # access calibration images if self.calibpath is None: raise RuntimeError('calibpath is unset') imgpath = self.calibpath + '/corners2' fns = [ imgpath + '/' + f for f in listdir(imgpath) if f[-4:].lower() == '.jpg' ] fns.sort() imgs = [cv_imread(f) for f in fns] shape = list(imgs[0].shape) shape.append(len(imgs)) # undistort # go through 1-by-1 b/c asarray(imgs) gets shaped wrong & reshape messes with things ud = zeros(shape, dtype=uint8) for i in range(len(imgs)): ud[..., i] = self.correct(imgs[i].squeeze()) # display side-by-side for i in range(len(imgs)): img = imgs[i].copy() u = ud[..., i].copy() cv_putText(img, fns[i][-10:], (5, 15), 1, 1, (0, 0, 255), thickness=2) cv_putText(img, 'Press space for next image', (5, 30), 1, 1, (0, 0, 255), thickness=2) cv_putText(u, 'Undistort', (5, 15), 1, 1, (0, 0, 255), thickness=2) frame = concatenate((img, u), axis=0) cv_imshow('frame', frame) press = waitKey(-1) if press == ord('q'): break destroyAllWindows()
def test_internal_correct_and_save(): """ Test internal correction saving method. """ calib = CalibratePSEye() fn_c = calibsdir + '/camera_params.csv' # Asserts for t in (int, float, complex, list, tuple, range, dict, set, frozenset, bool, bytes, bytearray, memoryview): try: calib.correct_and_save(t) except TypeError: pass else: raise RuntimeError('Failed to catch %s imgpath' % t.__name__) calib.load_calibrations(fn_c) cp = calib.calibpath calib.calibpath = None try: calib.correct_and_save('file-that-does-not-exist') except RuntimeError: pass else: raise RuntimeError('Failed to catch _calib_path is None') # Saving calib.calibpath = cp imgpath = testdir + '/raw' storeddir = testdir + '/00000000-000000_undistorted' storedcp = testdir + '/00000000-000000_camera_params.csv' if os_exists(storeddir): rmtree(storeddir) if os_exists(storedcp): os_remove(storedcp) ud1 = calib.correct_and_save(imgpath) try: # Proper saving if not os_exists(storeddir) or not os_exists(storedcp): raise RuntimeError('Error creating corrected directories') imgcount1 = len([f for f in listdir(imgpath) if f[-4:].lower() == '.jpg']) imgcount2 = len([f for f in listdir(storeddir) if f[-4:].lower() == '.jpg']) if imgcount1 != imgcount2: raise RuntimeError('Not all images were saved') # Correct calibration # Check pre-save equality imglist = [f for f in listdir(imgpath) if f[-4:].lower() == '.jpg'] rawimg = [cv_imread(imgpath + '/' + f) for f in imglist] ud2 = calib.correct(rawimg) # will know if `correct` works if not array_equal(ud1, ud2): raise RuntimeError('Failed pre-save equality check') # Check post-save equality for i in range(len(imglist)): fnud = storeddir + ('/_f%s' % str(i+1).zfill(5)) + '.jpg' cv_imwrite(fnud, ud2[i,...], (IMWRITE_JPEG_QUALITY, 100)) ud1list = [cv_imread(storeddir + '/' + f) for f in imglist] ud2list = [cv_imread(storeddir + '/_' + f) for f in imglist] ud1reload = asarray(ud1list, dtype='uint8') ud2reload = asarray(ud2list, dtype='uint8') if not array_equal(ud1reload, ud2reload): raise RuntimeError('Failed reload equality check') finally: os_remove(storedcp) rmtree(storeddir) try: if os_exists(storedcp): raise RuntimeError('failed to deleted cameraParams csv') if os_exists(storeddir): raise RuntimeError('failed to remove undistored img dir') except AssertionError: raise RuntimeError('Exception during test cleanup')
def test_internal_correct(): """ Test internal correction method. Discovered this song while debugging this test case on 2020-06-19 https://open.spotify.com/track/5XgYWQKEqSqA5vXJmwZa6n?si=HvcZD32-T2KRspTPcb4uGQ """ calib = CalibratePSEye() calib.load_calibrations(calibsdir + '/camera_params.csv') # test datatype check try: for t in ('uint16', 'uint32', 'uint64', 'int16', 'int32', 'int64', 'float32', 'float64', 'object'): frames = zeros((240, 320, 3), dtype=t) calib.correct(frames) except TypeError: pass else: raise RuntimeError('Failed to catch not-uint8 dtype') # test size check try: frames = zeros((240,320), dtype='uint8') calib.correct(frames) except TypeError: pass else: raise RuntimeError('Failed to catch frames too few dimensions') try: frames = zeros((240,320,1,1,1), dtype='uint8') calib.correct(frames) except TypeError: pass else: raise RuntimeError('Failed to catch frames too many dimensions') # setup test frames = [] for f in listdir(testdir+'/raw'): if f[-4:].lower() == '.jpg': frames.append(cv_imread(testdir + '/raw/' + f)) fshape = [len(frames)] + list(frames[0].shape) u1 = zeros(fshape, dtype='uint8') for i in range(len(frames)): f = frames[i].copy() u1[i,...] = undistort(f, calib.cameraMatrix, calib.distCoeffs, None) # test single frame u2 = calib.correct(frames[0]) if not array_equal(u1[0,...], u2): raise RuntimeError('Single-frame undistort/remap is incorrect') # test several frames u2 = calib.correct(asarray(frames, dtype='uint8')) if not array_equal(u1, u2): raise RuntimeError('Multi-frame ndarray undistort/remap is incorrect') u2 = calib.correct(frames) if not array_equal(u1, u2): raise RuntimeError('Multi-frame list undistort/remap is incorrect')
def clean_calib_imgs(self, basepath=None, rawpath=None, cpath=None): """ Provides interface to sanitize calibration images using a tutorial-like imshow() interface. Deletes frames, and removes elements from self.corners_arr, if they are not None. INPUTS (optional) basepath -- str -- Base path for calib frames; None rawpath -- str -- Path to frames for calibration; None cpath -- str -- Path to frames with chessboard; None NOTE The frames in each path must have the same name, e.g. 'f00001.jpg' USAGE SPECIFIED ONLY BASEPATH When all other paths the frame paths will be defined as: rawpath = basepath + '/raw' cpath = basepath + '/corners' PATHS TO ALL FRAMES If rawpath and cpath are all explicitly defined, they will be used as is. basepath will be ignored. """ # Parse paths if basepath is not None: basepath = realpath(basepath) if basepath[basepath.rfind('/') + 1:] not in ('raw', 'corners', 'corners2'): logging.warning( 'Assuming \'%s\' is base dir for all unspecified frames' % basepath) basepath = basepath else: basepath = dirname(basepath) if rawpath is None: rawpath = basepath + '/raw' if cpath is None: cpath = basepath + '/corners' # Select images to save fn_imgs = [f for f in listdir(cpath) if f[-4:].lower() == '.jpg'] fn_imgs.sort() i = 0 while i < len(fn_imgs): f = fn_imgs[i] img = cv_imread(cpath + '/' + f) if img is None: # catch deleted image fn_imgs.pop(i) if self.corners_arr is not None: self.corners_arr.pop(i) i += 1 continue cv_putText(img, f, (5, 15), 1, 1, (0, 0, 255), thickness=2) cv_putText(img, '\'r\' to remove', (5, 30), 1, 1, (0, 0, 255), thickness=2) cv_putText(img, 'left bracket to go back', (5, 45), 1, 1, (0, 0, 255), thickness=2) cv_imshow('image', img) # interface press = waitKey(-1) if press == ord('r'): fn = fn_imgs[i][fn_imgs[i].rfind('f'):] os_remove(rawpath + '/' + fn) os_remove(cpath + '/' + fn) elif press in (27, 81, 113): # esc, q, Q break elif press == 91: # left bracket i -= 2 i += 1 destroyAllWindows()
def load_calib_imgs(self, img_path, clean=False): """ Load calibration JPGs from directory & get chessboard. Be sure to call init_chessboard() first. INPUTS img_path -- str -- path to calibration jpegs (optional) clean -- bool -- Whether or not to go through sanitization; False EXCEPTIONS raises RuntimeError: when chessboard hasn't been properly initialized by constructor or `init_chessboard`. ALGORITHM Finds all files ending in '.jpg' and loads them. Objp should have been handled in init_chessboard() """ if type(img_path) != str: raise TypeError('img_path must be str') if not (type(clean) == bool or clean in (0, 1)): raise TypeError('clean must be bool') if self.img_arr is not None or self.calibpath is None: raise RuntimeError('Did you call init_chessboard() first?') # Find calibration images and process potential_files = listdir(img_path) fn_imgs = [ img_path + '/' + f for f in potential_files if f[-4:].lower() == '.jpg' ] imageshape = cv_imread(fn_imgs[0]).shape self.h = imageshape[0] self.w = imageshape[1] # Save images in calib path rawpath = self.calibpath + '/raw' # copy to current calibration dir if rawpath != img_path: if not isdir(rawpath): if os_exists(rawpath): os_remove(rawpath) mkdir(rawpath) for f in fn_imgs: copyfile(f, rawpath + '/' + basename(f)) # corners frames for debugging cpath = self.calibpath + '/corners' # save drawn corners for debug mkdir(cpath) logging.info('saving raw frames to \'%s\'' % rawpath) logging.info('saving corners frames to \'%s\'' % cpath) # Load images self.img_arr = zeros((self.h, self.w, 1, len(fn_imgs)), uint8) for i in range(len(fn_imgs)): f = fn_imgs[i] if imageshape[-1] == 3: self.img_arr[..., i] = cvtColor(cv_imread(f), COLOR_RGB2GRAY)[..., newaxis] else: self.img_arr[..., i] = cv_imread(f) # Chessboard computations logging.debug('finding chessboards...') for i in range(self.img_arr.shape[-1]): gray = self.img_arr[..., i].copy() corners = self._find_chessboard(gray) if corners is None: logging.error('Failed to find chessboard at frame \'%s\'' % str(i + 1).zfill(5)) continue self.corners_arr.append(corners) self.objpoints.append(self.objp) # 3d position (same for all?) # save chessboard images for debugging # cvt to rgb for color chessboard fn_c = cpath + ('/f%s' % str(i + 1).zfill(5)) + '.jpg' gray_color = cvtColor(gray, COLOR_GRAY2RGB) img_corners = drawChessboardCorners(gray_color, self.boardsize, corners, 1) cv_imwrite(fn_c, img_corners, (IMWRITE_JPEG_QUALITY, 100)) # Go through chessboards to make sure okay if clean: basepath = dirname(cpath) self.clean_calib_imgs(basepath=basepath) logging.debug('load_calib_imgs() done!')