Exemplo n.º 1
0
    def __init__(self,
                 h,
                 w,
                 CALIBRATION_IMAGES,
                 NX,
                 NY,
                 nwindows=9,
                 margin=100,
                 tol=50):

        #Variables for the image size
        self.h = h
        self.w = w

        #Initlaise objects to perform the calibration and perspective transforms
        self.calibrator = CameraCalibration(CALIBRATION_IMAGES, NX, NY)
        self.perspective_transform = PerspectiveTransform()

        #Initalise line objects to keep track of the properties of the fitted lines
        #overtime
        self.left_line = Line(self.h, self.w)
        self.right_line = Line(self.h, self.w)

        #Initalise window objects to keep track of the
        self.nwindows = nwindows
        self.window_height = np.int(self.h / nwindows)
        self.left_window = []
        self.right_window = []
        self.margin = margin
        self.tol = tol
        self.initalise_windows()
Exemplo n.º 2
0
    def __init__(self, parent, title):

        wx.Frame.__init__(self,
                          parent,
                          title=title,
                          style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)
        self.captured_image = None
        self.timer = None
        self.bitmap = None
        self.camera_label = None
        self.camera_menu_ids = []
        self.menu_load_calibration = None
        self.menu_save_capture = None
        self.menu_save_calibration = None
        self.button_capture = None
        self.button_calibrate = None
        self.button_cancel = None
        self.chk_undistort = None
        self.undistort = False
        self.camera = Camera()
        self.screen = None
        self.right_panel = None
        self.calibration_panel = None
        self.calibration = CameraCalibration()
        self.calibration.calibrated += self.on_calibrated
        self.calibration.on_progress += self.on_calibration_progress

        self.create_layout()
        self.create_menu()

        self.button_capture.Bind(wx.EVT_BUTTON, self.on_capture)
        self.button_calibrate.Bind(wx.EVT_BUTTON, self.on_calibrate)
        self.button_cancel.Bind(wx.EVT_BUTTON, self.on_cancel_calibrate)
        self.chk_undistort.Bind(wx.EVT_CHECKBOX, self.on_undistort)

        self.Show(True)
Exemplo n.º 3
0
class LaneDetection(object):
    """Class to dectect and draw the lanes on a serise of images/frames"""
    def __init__(self,
                 h,
                 w,
                 CALIBRATION_IMAGES,
                 NX,
                 NY,
                 nwindows=9,
                 margin=100,
                 tol=50):

        #Variables for the image size
        self.h = h
        self.w = w

        #Initlaise objects to perform the calibration and perspective transforms
        self.calibrator = CameraCalibration(CALIBRATION_IMAGES, NX, NY)
        self.perspective_transform = PerspectiveTransform()

        #Initalise line objects to keep track of the properties of the fitted lines
        #overtime
        self.left_line = Line(self.h, self.w)
        self.right_line = Line(self.h, self.w)

        #Initalise window objects to keep track of the
        self.nwindows = nwindows
        self.window_height = np.int(self.h / nwindows)
        self.left_window = []
        self.right_window = []
        self.margin = margin
        self.tol = tol
        self.initalise_windows()

    def process_frame(self, image, debug=True):
        # Step 1: Correct for the camera image distortion
        corrected_image = self.calibrator.undistort(image)
        # Step 2: Binary threshold image to detect the lane lines
        threshold_image = detect_edges(corrected_image)
        # Step 3: Apply a perspective transform to the image for birds eye view
        perspective_image = self.perspective_transform.warp(threshold_image)

        # Step 4: Finding and fitting a polynomial line to the lane lines

        if not (self.left_line.detected) or not (self.right_line.detected):
            self.fit_windows(perspective_image)
            if debug:
                edges_debug = detect_edges(corrected_image, True)
                perspective_debug = self.perspective_transform.warp(
                    edges_debug)
                debug_image = self.debug_visualisation_windows(
                    perspective_debug)
        else:
            self.fit_region(perspective_image)
            if debug:
                edges_debug = detect_edges(corrected_image, True)
                perspective_debug = self.perspective_transform.warp(
                    edges_debug)
                debug_image = self.debug_visualisation_region(
                    perspective_debug)

        if debug:
            overhead_lane = self.draw_lanes(
                self.perspective_transform.warp(corrected_image), True)
            debug_viewer = cv2.resize(debug_image, (0, 0), fx=0.3, fy=0.3)
            overhead_viewer = cv2.resize(overhead_lane, (0, 0), fx=0.3, fy=0.3)

            #Make top region of image darker
            corrected_image[:250, :, :] = corrected_image[:250, :, :] * 0.4
            debug_h = debug_viewer.shape[0]
            debug_w = debug_viewer.shape[1]
            #Overlay the debug view to the top of the image
            corrected_image[20:20 + debug_h, 20:20 + debug_w, :] = debug_viewer
            #Overlay the overhead view to the top of the image
            corrected_image[20:20 + debug_h, 40 + 3 * 20 + 2 * debug_w:3 * 20 +
                            3 * debug_w + 40, :] = overhead_viewer

            #Print the lane distance and radius of curv to the screen
            x_location = 3 * 20 + debug_w
            self.text_to_image(
                corrected_image, 'Radius of curvature: {:.1f} m'.format(
                    self.avg_radius_of_curvature()), x_location, 40)
            self.text_to_image(
                corrected_image, 'Left distance:  {:.3f} m'.format(
                    self.left_line.camera_distance()), x_location, 100)
            self.text_to_image(
                corrected_image, 'Right distance:  {:.3f} m'.format(
                    self.right_line.camera_distance()), x_location, 160)
            self.text_to_image(
                corrected_image, 'Center distance:  {:.3f} m'.format(
                    self.left_line.camera_distance() -
                    self.right_line.camera_distance()), x_location, 220)

        identified_lane = self.draw_lanes(corrected_image)

        return identified_lane

    def text_to_image(self, image, text, coordinate_x, coordinate_y):
        cv2.putText(image, text, (coordinate_x, coordinate_y),
                    cv2.FONT_HERSHEY_SIMPLEX, .8, (255, 255, 255))

    def fit_region(self, warped_image):

        left_fit = self.left_line.average_fits()
        right_fit = self.right_line.average_fits()

        if not (self.left_line.detected) or not (self.right_line.detected):
            print("No current coefficents")
            return

        nonzero = warped_image.nonzero()
        nonzeroy = np.array(nonzero[0])
        nonzerox = np.array(nonzero[1])
        margin = self.margin

        left_lane_inds = (
            (nonzerox >
             (left_fit[0] *
              (nonzeroy**2) + left_fit[1] * nonzeroy + left_fit[2] - margin)) &
            (nonzerox <
             (left_fit[0] *
              (nonzeroy**2) + left_fit[1] * nonzeroy + left_fit[2] + margin)))

        right_lane_inds = (
            (nonzerox >
             (right_fit[0] *
              (nonzeroy**2) + right_fit[1] * nonzeroy + right_fit[2] - margin))
            &
            (nonzerox <
             (right_fit[0] *
              (nonzeroy**2) + right_fit[1] * nonzeroy + right_fit[2] + margin))
        )

        # Again, extract left and right line pixel positions
        leftx = nonzerox[left_lane_inds]
        lefty = nonzeroy[left_lane_inds]
        rightx = nonzerox[right_lane_inds]
        righty = nonzeroy[right_lane_inds]

        self.left_line.compute_line(lefty, leftx)
        self.right_line.compute_line(righty, rightx)

    def initalise_windows(self):

        leftx = np.int(self.w / 4)
        rightx = np.int(3 * (self.w / 4))

        for i in range(self.nwindows):

            left_window = Window(self.h - (i + 1) * self.window_height,
                                 self.h - i * self.window_height, leftx,
                                 self.margin, self.tol)
            right_window = Window(self.h - (i + 1) * self.window_height,
                                  self.h - i * self.window_height, rightx,
                                  self.margin, self.tol)

            self.left_window.append(left_window)
            self.right_window.append(right_window)

    def fit_windows(self, image_edges):

        #Create a histrogram of the bottom half of the image and find two peaks
        histogram = np.sum(image_edges[int(image_edges.shape[0] / 2):, :],
                           axis=0)
        midpoint = np.int(histogram.shape[0] / 2)
        leftx_current = np.argmax(histogram[:midpoint])
        rightx_current = np.argmax(histogram[midpoint:]) + midpoint

        # Find the locations of the non-zero pixels
        nonzero = image_edges.nonzero()
        nonzeroy = np.array(nonzero[0])
        nonzerox = np.array(nonzero[1])

        # Create empty lists for the left and right lane pixel indices (might not be required)
        left_inds = []
        right_inds = []

        for i in range(self.nwindows):
            if i == 0:
                self.left_window[i].x_center = leftx_current
                self.right_window[i].x_center = rightx_current
            else:
                self.left_window[i].x_center = self.left_window[
                    i - 1].x_next_window
                self.right_window[i].x_center = self.right_window[
                    i - 1].x_next_window

            left_inds.append(self.left_window[i].find_pixels(nonzero))
            right_inds.append(self.right_window[i].find_pixels(nonzero))

        # Concatenate the arrays of indices
        left_inds = np.concatenate(left_inds)
        right_inds = np.concatenate(right_inds)

        # Extract left and right line pixel positions
        leftx = nonzerox[left_inds]
        lefty = nonzeroy[left_inds]
        rightx = nonzerox[right_inds]
        righty = nonzeroy[right_inds]

        # Find lines to the extracted pixels
        self.left_line.compute_line(lefty, leftx)
        self.right_line.compute_line(righty, rightx)

    def debug_visualisation_windows(self, image, lines=True, windows=True):

        out_img = image

        if windows:
            for window in self.left_window:
                coordinate = window.get_coordinate()
                cv2.rectangle(out_img, coordinate[0], coordinate[1],
                              (0, 255, 0), 2)
            for window in self.right_window:
                coordinate = window.get_coordinate()
                cv2.rectangle(out_img, coordinate[0], coordinate[1],
                              (0, 255, 0), 2)
        if lines:

            #Print the average fitted lines
            y, left_fitx = self.left_line.generate_line()
            if left_fitx is not None:
                cv2.polylines(out_img,
                              [np.stack((left_fitx, y)).astype(np.int).T],
                              False, (255, 0, 255), 2)

            y, right_fitx = self.right_line.generate_line()
            if right_fitx is not None:
                cv2.polylines(out_img,
                              [np.stack((right_fitx, y)).astype(np.int).T],
                              False, (255, 0, 255), 2)

            #Print the current fitted lines
            y, left_fit_currentx = self.left_line.generate_line_current()
            if left_fit_currentx is not None:
                cv2.polylines(
                    out_img,
                    [np.stack((left_fit_currentx, y)).astype(np.int).T], False,
                    (255, 128, 0), 2)

            y, right_fit_currentx = self.right_line.generate_line_current()
            if right_fit_currentx is not None:
                cv2.polylines(
                    out_img,
                    [np.stack((right_fit_currentx, y)).astype(np.int).T],
                    False, (255, 128, 0), 2)

        return out_img

    def debug_visualisation_region(self, image, lines=True, region=True):

        out_img = image  #*255
        window_img = np.zeros_like(out_img)

        if region:
            y, left_fitx = self.left_line.generate_line_current()
            y, right_fitx = self.right_line.generate_line_current()
            margin = self.margin
            # Generate a polygon to illustrate the search window area
            # And recast the x and y points into usable format for cv2.fillPoly()
            left_line_window1 = np.array(
                [np.transpose(np.vstack([left_fitx - margin, y]))])
            left_line_window2 = np.array(
                [np.flipud(np.transpose(np.vstack([left_fitx + margin, y])))])
            left_line_pts = np.hstack((left_line_window1, left_line_window2))
            right_line_window1 = np.array(
                [np.transpose(np.vstack([right_fitx - margin, y]))])
            right_line_window2 = np.array(
                [np.flipud(np.transpose(np.vstack([right_fitx + margin, y])))])
            right_line_pts = np.hstack(
                (right_line_window1, right_line_window2))

            # Draw the lane onto the warped blank image
            cv2.fillPoly(window_img, np.int_([left_line_pts]), (0, 255, 0))
            cv2.fillPoly(window_img, np.int_([right_line_pts]), (0, 255, 0))
            out_img = cv2.addWeighted(out_img, 1.0, window_img, 0.3, 0)

        if lines:
            #Print the average fitted lines
            y, left_fitx = self.left_line.generate_line()
            if left_fitx is not None:
                cv2.polylines(out_img,
                              [np.stack((left_fitx, y)).astype(np.int).T],
                              False, (255, 0, 255), 2)

            y, right_fitx = self.right_line.generate_line()
            if right_fitx is not None:
                cv2.polylines(out_img,
                              [np.stack((right_fitx, y)).astype(np.int).T],
                              False, (255, 0, 255), 2)

            #Print the current fitted lines
            y, left_fit_currentx = self.left_line.generate_line_current()
            if left_fit_currentx is not None:
                cv2.polylines(
                    out_img,
                    [np.stack((left_fit_currentx, y)).astype(np.int).T], False,
                    (255, 128, 0), 2)

            y, right_fit_currentx = self.right_line.generate_line_current()
            if right_fit_currentx is not None:
                cv2.polylines(
                    out_img,
                    [np.stack((right_fit_currentx, y)).astype(np.int).T],
                    False, (255, 128, 0), 2)

        return out_img

    def draw_lanes(self, undist, birdseye=False):

        y, left_fitx = self.left_line.generate_line()
        y, right_fitx = self.right_line.generate_line()

        # Create an image to draw the lines on
        warp_zero = np.zeros_like(undist[:, :, 0]).astype(np.uint8)
        color_warp = np.dstack((warp_zero, warp_zero, warp_zero))

        if left_fitx is None or right_fitx is None:
            return undist

        # Recast the x and y points into usable format for cv2.fillPoly()
        pts_left = np.array([np.transpose(np.vstack([left_fitx, y]))])
        pts_right = np.array(
            [np.flipud(np.transpose(np.vstack([right_fitx, y])))])
        pts = np.hstack((pts_left, pts_right))
        # Draw the lane onto the warped blank image
        cv2.fillPoly(color_warp, np.int_([pts]), (0, 255, 0))

        if not (birdseye):
            lane = self.perspective_transform.inverse_warp(color_warp)
        else:
            lane = color_warp

        # Warp the blank back to original image space using inverse perspective matrix (Minv)
        #newwarp = cv2.warpPerspective(color_warp, Minv, (image.shape[1], image.shape[0]))
        # Combine the result with the original image
        result = cv2.addWeighted(undist, 1, lane, 0.3, 0)

        return result

    def avg_radius_of_curvature(self):

        return np.average([
            self.left_line.radius_of_curvature(),
            self.right_line.radius_of_curvature()
        ])
Exemplo n.º 4
0
    def rectify_images(self,
                       metadata,
                       image_files,
                       intrinsic_cal_list,
                       extrinsic_cal_list,
                       local_origin,
                       progress_callback=None):
        """Given list of image paths from N cameras, return a georectified image (ndarray)

        Arguments:
            image_files (list): List of image files.
            camera_calibration_files (list): List of calibrations for cameras used to get image_files.
            progress_callback (callable): Optional; callable taking one parameter, an integer of progress 0-100

        Returns:
            M (np.ndarray): Georectified images merged from supplied images.
        """
        M = np.tile(np.zeros_like(self.target_grid.X[:, :, np.newaxis]),
                    (self.ncolors, ))
        totalW = M.copy()

        total_pairs = len(image_files)
        steps_per_pair = 3
        total_steps = total_pairs * steps_per_pair
        # if progress_callback is None:
        #     progress_callback = lambda x: x

        for cur_idx, (image_file, intrinsic_cal, extrinsic_cal) in enumerate(
                zip(image_files, intrinsic_cal_list, extrinsic_cal_list)):
            pct_base = cur_idx * steps_per_pair
            print("loop", cur_idx, "calibrations:")
            print(intrinsic_cal, extrinsic_cal)
            # load camera calibration file and find pixel locations
            camera_calibration = CameraCalibration(metadata, intrinsic_cal,
                                                   extrinsic_cal, local_origin)
            # print("back from CameraCalibration")
            U, V, flag = self._find_distort_UV(camera_calibration)
            # print("back from _find_distort_UV")
            #            progress_callback(int((pct_base + 1) / total_steps * 100))

            # load image and apply weights to pixels
            impath = 'cmgp-coastcam/cameras/caco-01/products/' + image_file
            fs = fsspec.filesystem('s3')
            with fs.open(impath) as f:
                image = imageio.imread(f)
            print('Image shape: ', image.shape)
            K = self.get_pixels(U, V, image)
            print("back from get_pixels")
            W = self.assemble_image_weights(K)
            print("back from assemble_image_weights")
            K_weighted = self.apply_weights_to_pixels(K, W)
            print("back from apply_weights_to_pixles")
            # progress_callback(int((pct_base + 2) / total_steps * 100))

            # add up weights and pixel itensities
            W[np.isnan(W)] = 0
            totalW = totalW + W[:, :, np.newaxis]
            K_weighted[np.isnan(K_weighted)] = 0
            M = M + K_weighted

            # progress_callback(int((pct_base + 3) / total_steps * 100))

        # stop divide by 0 warnings
        with np.errstate(invalid='ignore'):
            M = M / totalW

        return M
Exemplo n.º 5
0
import matplotlib.image as mpimg

from os import listdir
from os.path import join

from utils import *

from calibration import CameraCalibration

# directory paths
CAMERA_CALIBRATION_DIR = './camera_cal'
TEST_IMAGES_DIR = './test_images'
EXAMPLES_DIR = './examples'
OUTPUT_IMAGES_DIR = './output_images'

camera_cal = CameraCalibration(nx=9, ny=6)


# Define a class to receive the characteristics of each line detection
class LineHistory():
    def __init__(self):
        # was the line detected in the last iteration?
        self.detected = False
        # x values of the last n fits of the line
        self.recent_xfitted = []
        #average x values of the fitted line over the last n iterations
        self.bestx = None
        #polynomial coefficients averaged over the last n iterations
        self.best_fit = None
        #polynomial coefficients for the most recent fit
        self.current_fit = [np.array([False])]
def main():
    logging.basicConfig(level=logging.DEBUG)

    cap = cv2.VideoCapture('footage/7_edit.avi')
    '''
    cap = cv2.VideoCapture(1)
    cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)
    cap.set(cv2.CAP_PROP_EXPOSURE, 0.01)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH,1280)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT,720)
    '''

    DIM = (640, 480)
    K = np.array([[359.0717640266508, 0.0, 315.08914578097387],
                  [0.0, 358.06497428501837, 240.75242680088732],
                  [0.0, 0.0, 1.0]])
    D = np.array([[-0.041705903204711826], [0.3677107787593379],
                  [-1.4047363783373128], [1.578157237454529]])
    profile = (DIM, K, D)
    CC = CameraCalibration(profile)

    yellow_HSV_th_min = np.array([0, 70, 70])
    yellow_HSV_th_max = np.array([50, 255, 255])

    line_lt = Line(buffer_len=time_window)  # line on the left of the lane
    line_rt = Line(buffer_len=time_window)  # line on the right of the lane
    processed_frames = 0

    ### traffic light detector setup ###
    '''
    red_bound = ([0,0,0], [255,255,360])
    green_bound = ([0,0,0], [255,255,360])
    color_bounds = {'red':red_bound, 'green':green_bound}

    reference_images = []
    reference_paths = glob('./reference/*.jpg')
    for path in reference_paths:
        reference_images.append(cv2.imread(path))

    TLD = TrafficLightDetector(reference_images, color_bounds)
    '''

    while cap.isOpened():
        ret, frame = cap.read()

        ### traffic light detection
        # TODO: replace placeholder values
        '''
        while True:
            state = TLD.get_state(frame)

            logging.debug('traffic light state:', str(state))

            if state == 'green':
                comm.write_serial_message('s30')
                break
        '''

        frame = CC.undistort(frame)

        ### calibration ###

        ### crop to ROI ###
        ### perspective transform ###

        img, warped = debug_roi(frame, None, None)

        ### binarize frame ###

        ### get steering angle v1 ###
        '''
        _, lines = pathfinder.get_line_segments(warped)
        grey = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)

        turn_angle = pathfinder.compute_turn_angle(grey)
        print('turn angle:', turn_angle)

        cv2.imshow('lines', lines)
        '''

        ### get steering angle v2 ###

        #mask, res = filter_hsv_colour(img, yellow_HSV_th_max, yellow_HSV_th_min)

        img_binary = binarization_utils.binarize(warped, verbose=False)
        cv2.imshow('img binary', img_binary)

        ### contours ###
        _, contours, hierarchy = cv2.findContours(img_binary,
                                                  cv2.RETR_EXTERNAL,
                                                  cv2.CHAIN_APPROX_NONE)

        img_binary_colour = cv2.cvtColor(img_binary, cv2.COLOR_GRAY2BGR)

        if len(contours) > 0:
            for cnt in contours:
                if cnt.size >= 5:
                    epsilon = 0.1 * cv2.arcLength(cnt, True)
                    approx = cv2.approxPolyDP(cnt, epsilon, True)

                    ellipse = cv2.fitEllipse(cnt)
                    cv2.ellipse(img_binary_colour, ellipse, (255, 0, 0), 5)

                    rect = cv2.minAreaRect(cnt)
                    box = cv2.boxPoints(rect)
                    box = np.int0(box)
                    cv2.drawContours(img_binary_colour, [box], 0, (0, 0, 255),
                                     5)

        ### basic pathfinder ###
        #img_binary_colour = img_binary_colour[:][100:]
        _, lines = pathfinder.get_line_segments(img_binary_colour)
        turn_angle = pathfinder.compute_turn_angle(img_binary_colour)
        print('turn angle:', turn_angle)

        turn_angle += 90

        turn_angle_message = str('a%d' % int(turn_angle))
        comm.write_serial_message(turn_angle_message)
        #comm.write_serial_message('s50')

        cv2.putText(lines, str(int(turn_angle)), (200, 450),
                    cv2.FONT_HERSHEY_SIMPLEX, 4, (0, 255, 0), 4, cv2.LINE_AA)
        cv2.imshow('lines', lines)
        '''
        keep_state = False


        if processed_frames > 0 and keep_state and line_lt.detected and line_rt.detected:
            line_lt, line_rt, img_fit = line_utils.get_fits_by_previous_fits(img_binary, line_lt, line_rt, verbose=False)
        else:
            line_lt, line_rt, img_fit = line_utils.get_fits_by_sliding_windows(img_binary, line_lt, line_rt, n_windows=9, verbose=False)

        offset_meter = compute_offset_from_center(line_lt, line_rt, frame_width=frame.shape[1])
        '''
        #print(offset_meter)

        #Minv = np.zeros(shape=(3, 3))

        # draw the surface enclosed by lane lines back onto the original frame
        #blend_on_road = line_utils.draw_back_onto_the_road(img, Minv, line_lt, line_rt, keep_state)

        #cv2.imshow('asfdfd', blend_on_road)

        # stitch on the top of final output images from different steps of the pipeline
        #blend_output = prepare_out_blend_frame(blend_on_road, img_binary, warped, img_fit, line_lt, line_rt, offset_meter)

        #processed_frames += 1

        #cv2.imshow('mask', mask)
        #cv2.imshow('res', res)

        cv2.imshow('frame', frame)
        cv2.imshow('warped', warped)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()
Exemplo n.º 7
0
class MainWindow(wx.Frame):
    """Base class for the UI layout."""
    def __init__(self, parent, title):

        wx.Frame.__init__(self,
                          parent,
                          title=title,
                          style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)
        self.captured_image = None
        self.timer = None
        self.bitmap = None
        self.camera_label = None
        self.camera_menu_ids = []
        self.menu_load_calibration = None
        self.menu_save_capture = None
        self.menu_save_calibration = None
        self.button_capture = None
        self.button_calibrate = None
        self.button_cancel = None
        self.chk_undistort = None
        self.undistort = False
        self.camera = Camera()
        self.screen = None
        self.right_panel = None
        self.calibration_panel = None
        self.calibration = CameraCalibration()
        self.calibration.calibrated += self.on_calibrated
        self.calibration.on_progress += self.on_calibration_progress

        self.create_layout()
        self.create_menu()

        self.button_capture.Bind(wx.EVT_BUTTON, self.on_capture)
        self.button_calibrate.Bind(wx.EVT_BUTTON, self.on_calibrate)
        self.button_cancel.Bind(wx.EVT_BUTTON, self.on_cancel_calibrate)
        self.chk_undistort.Bind(wx.EVT_CHECKBOX, self.on_undistort)

        self.Show(True)

    def disable_event(*pargs, **kwargs):
        """Empty event handler."""
        # pylint: disable=E0211
        pass

    def create_layout(self):
        """Create UI layout elements."""
        self.screen = wx.Panel(self, size=(SCREEN_WIDTH, SCREEN_HEIGHT))
        self.screen.Bind(wx.EVT_PAINT, self.on_paint)
        self.screen.Bind(wx.EVT_ERASE_BACKGROUND, self.disable_event)

        self.right_panel = wx.Panel(self)

        sizer1 = wx.BoxSizer(wx.VERTICAL)
        panel1 = wx.Panel(self.right_panel)
        panel1.SetMinSize((200, 400))
        sizer2 = wx.BoxSizer(wx.VERTICAL)

        self.camera_label = wx.StaticText(panel1, label="Camera 0")
        sizer2.Add(self.camera_label, flag=wx.TOP)

        self.preview = wx.StaticBitmap(panel1,
                                       size=(SCREEN_WIDTH // 4,
                                             SCREEN_HEIGHT // 4))
        self.preview.SetBackgroundColour(wx.LIGHT_GREY)
        sizer2.Add(self.preview, flag=wx.TOP, border=10)

        self.button_capture = wx.Button(panel1, label='Capture')
        sizer2.Add(self.button_capture, flag=wx.TOP, border=6)

        self.calibration_panel = CalibrationPanel(panel1)
        self.calibration_panel.status = "no"
        sizer2.Add(self.calibration_panel, flag=wx.TOP | wx.EXPAND, border=16)

        self.button_calibrate = wx.Button(panel1, label='Calibrate')
        self.button_cancel = wx.Button(panel1, label='Cancel')
        self.button_cancel.Disable()

        sizer3 = wx.BoxSizer(wx.HORIZONTAL)
        sizer3.Add(self.button_calibrate, flag=wx.TOP, border=6)
        sizer3.Add(self.button_cancel, flag=wx.TOP, border=6)

        sizer2.Add(sizer3, flag=wx.LEFT | wx.RIGHT | wx.EXPAND)

        self.chk_undistort = wx.CheckBox(panel1, label="Undistort")
        self.chk_undistort.SetValue(False)
        self.chk_undistort.Disable()
        sizer2.Add(self.chk_undistort, flag=wx.TOP, border=16)

        sizer1.Add(panel1, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM, border=16)
        panel1.SetSizer(sizer2)
        self.right_panel.SetSizer(sizer1)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.screen, 0, wx.ALIGN_CENTER)
        sizer.Add(self.right_panel, flag=wx.EXPAND)

        self.SetSizerAndFit(sizer)

    def create_menu(self):
        """Create main menu."""
        menu1 = wx.Menu()
        self.menu_load_calibration = menu1.Append(wx.ID_ANY,
                                                  "Load calibration file")
        self.menu_save_calibration = menu1.Append(wx.ID_ANY,
                                                  "Save calibration file")
        self.menu_save_capture = menu1.Append(wx.ID_ANY, "Save captured image")
        menu1.AppendSeparator()
        menu_exit = menu1.Append(wx.ID_EXIT, "E&xit")

        self.menu_save_capture.Enable(False)
        self.menu_save_calibration.Enable(False)

        menu2 = wx.Menu()
        for i in range(0, 5):
            menu_id = wx.NewId()
            self.camera_menu_ids.append(menu_id)
            menu_item = menu2.Append(menu_id,
                                     f"Camera {i}",
                                     kind=wx.ITEM_RADIO)
            self.Bind(wx.EVT_MENU, self.on_camera, menu_item)

        menu2.Check(self.camera_menu_ids[0], True)

        menu_bar = wx.MenuBar()
        menu_bar.Append(menu1, "&File")
        menu_bar.Append(menu2, "&Camera")
        self.SetMenuBar(menu_bar)

        # Set events
        self.Bind(wx.EVT_MENU, self.on_load_calibration,
                  self.menu_load_calibration)
        self.Bind(wx.EVT_MENU, self.on_save_calibration,
                  self.menu_save_calibration)
        self.Bind(wx.EVT_MENU, self.on_save_capture, self.menu_save_capture)
        self.Bind(wx.EVT_MENU, self.on_exit, menu_exit)

    def capture_video(self, device=0, fps=30, size=(640, 480)):
        """Sets periodic screen capture.

        Args:
            device (int): Device number. Default is 0.
            fps (int): Frames per second. Default is 30 frames.
            size ((width, height)): Frame width and height in pixels.
        """
        # open webcam
        try:
            self.camera.capture_video(device, fps, size)
        except TypeError:
            dialog = wx.MessageDialog(
                self, f"Could not open camera {self.camera.device}", "Error")
            dialog.SetOKLabel("Close")
            dialog.ShowModal()
            dialog.Destroy()
            return

        # set up periodic screen capture
        self.timer = wx.Timer(self)
        self.timer.Start(1000. / self.camera.fps)
        self.Bind(wx.EVT_TIMER, self.on_next_frame)

    def on_camera(self, event):
        """Connect to a camera with selected device number."""
        # pylint: disable=W0613
        menu_id = event.GetId()
        device_number = self.camera_menu_ids.index(menu_id)
        self.camera_label.SetLabel(f"Camera {device_number}")
        self.capture_video(device_number)

    def on_calibrate(self, event):
        """Start calibration process."""
        # pylint: disable=W0613
        self.undistort = False
        self.chk_undistort.SetValue(False)
        self.chk_undistort.Disable()
        self.button_calibrate.Disable()
        self.button_cancel.Enable()
        self.calibration.start_calibration()
        self.menu_load_calibration.Enable(False)
        self.menu_save_calibration.Enable(False)
        self.calibration_panel.filename = ""

    def on_cancel_calibrate(self, event):
        """Cancel calibration process."""
        # pylint: disable=W0613
        self.button_calibrate.Enable()
        self.button_cancel.Disable()
        self.calibration.cancel_calibration()
        self.calibration_panel.status = "no"
        self.calibration_panel.error = ""
        self.menu_load_calibration.Enable(True)

    def on_undistort(self, event):
        """Update undistort flag."""
        # pylint: disable=W0613
        self.undistort = self.chk_undistort.GetValue()

    def on_calibrated(self):
        """Update controls upon calibration completion."""
        calibrated = self.calibration.is_calibrated
        self.button_calibrate.Enable()
        self.button_cancel.Disable()
        self.calibration_panel.status = "yes" if calibrated else "no"
        self.calibration_panel.error = "{:.3f}".format(self.calibration.mean_error) \
            if calibrated else ""
        self.chk_undistort.Enabled = calibrated
        self.menu_load_calibration.Enable(True)
        self.menu_save_calibration.Enable(calibrated)

    def on_calibration_progress(self, progress):
        """Update calibration progress."""
        self.calibration_panel.status = progress

    def on_next_frame(self, event):
        """Captures a new frame from the camera and copies it
           to an image buffer to be displayed.
        """
        # pylint: disable=W0613
        success, frame = self.camera.read_frame()
        if success:
            if self.calibration.is_calibrating:
                self.calibration.calibrate(frame)

            if self.calibration.can_undistort and self.undistort:
                frame = self.calibration.undistort(frame)

            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

            # update buffer and paint
            if self.bitmap is None:
                self.bitmap = wx.Bitmap.FromBuffer(frame.shape[1],
                                                   frame.shape[0], frame)
            else:
                self.bitmap.CopyFromBuffer(frame)

            self.Refresh(eraseBackground=False)

    def on_paint(self, event):
        """Draw an image captured by the camera on the self.screen panel."""
        # pylint: disable=W0613
        # read and draw buffered bitmap
        if self.bitmap is not None:
            _device_context = wx.BufferedPaintDC(self.screen)
            _device_context.DrawBitmap(self.bitmap, 0, 0)

    def on_capture(self, event):
        """Draw captured image on the self.preview control."""
        # pylint: disable=W0613
        if self.bitmap is not None:
            size = self.preview.GetSize()
            self.captured_image = self.bitmap.ConvertToImage()
            image = self.captured_image.Scale(size.Width, size.Height,
                                              wx.IMAGE_QUALITY_HIGH)
            self.preview.SetBitmap(wx.Bitmap(image))
            self.menu_save_capture.Enable(True)

    def on_save_capture(self, event):
        """Save captured image in the file."""
        # pylint: disable=W0613
        with wx.FileDialog(self,
                           "Save captured image",
                           wildcard="PNG files (*.png)|*.png",
                           style=wx.FD_SAVE
                           | wx.FD_OVERWRITE_PROMPT) as file_dialog:

            if file_dialog.ShowModal() == wx.ID_CANCEL:
                return

            pathname = file_dialog.GetPath()
            try:
                self.captured_image.SaveFile(pathname, wx.BITMAP_TYPE_BMP)
            except IOError:
                wx.LogError(
                    f"Cannot save captured image in file '{pathname}'.")

    def on_load_calibration(self, event):
        """Load calibration parameters from a file."""
        # pylint: disable=W0613
        with wx.FileDialog(self,
                           "Load calibration file",
                           wildcard="JSON files (*.json)|*.json",
                           style=wx.FD_OPEN
                           | wx.FD_FILE_MUST_EXIST) as file_dialog:

            if file_dialog.ShowModal() == wx.ID_CANCEL:
                return

            pathname = file_dialog.GetPath()
            try:
                self.calibration.load_calibration(pathname)
                self.calibration_panel.status = "yes"
                self.calibration_panel.filename = pathname
                self.calibration_panel.error = "{:.3f}".format(
                    self.calibration.mean_error)
                self.menu_save_calibration.Enable(True)
                self.chk_undistort.Enabled = True
            except IOError:
                wx.LogError(
                    f"Cannot save calibration data in file '{pathname}'.")

    def on_save_calibration(self, event):
        """Save calibration parameters to the file."""
        # pylint: disable=W0613
        with wx.FileDialog(self,
                           "Save calibration file",
                           wildcard="JSON files (*.json)|*.json",
                           style=wx.FD_SAVE
                           | wx.FD_OVERWRITE_PROMPT) as file_dialog:

            if file_dialog.ShowModal() == wx.ID_CANCEL:
                return

            pathname = file_dialog.GetPath()
            try:
                self.calibration.save_calibration(pathname)
                self.calibration_panel.filename = pathname
            except IOError:
                wx.LogError(
                    f"Cannot save calibration data in file '{pathname}'.")

    def on_exit(self, event):
        """Release resources and close the application."""
        # pylint: disable=W0613
        self.timer.Stop()
        self.camera.release()
        self.Close(True)