Example #1
0
class PlantDetection(object):
    """Detect plants in image and output an image with plants marked.

    Kwargs:
       image (str): filename of image to process (default = None)
           None -> take photo instead
       coordinates (boolean): use coordinate conversion (default = False)
       calibration_img (filename): calibration image filename used to
           output coordinates instead of pixel locations (default = None)
       calibration_data (dict): calibration data inputs,
           overwrites other calibration data inputs (default = None)
       known_plants (list): {'x': x, 'y': y, 'radius': radius}
                            of known (intentional) plants
                            (default = None)
       debug (boolean): output debug images (default = False)
       blur (int): blur kernel size (must be odd, default = 5)
       morph (int): amount of filtering (default = 5)
       iterations (int): number of morphological iterations (default = 1)
       array (list): list of morphs to run
           [morph kernel size, morph kernel type, morph type, iterations]
           example:
           [{"size": 5, "kernel": 'ellipse', "type": 'erode',  "iters": 2},
            {"size": 3, "kernel": 'ellipse', "type": 'dilate', "iters": 8}]
                  (default = None)
       save (boolean): save images (default = True)
       clump_buster (boolean): attempt to break
                               plant clusters (default = False)
       HSV_min (list): green lower bound Hue(0-179), Saturation(0-255),
                       and Value(0-255) (default = [30, 20, 20])
       HSV_max (list): green upper bound Hue(0-179), Saturation(0-255),
                       and Value(0-255) (default = [90, 255, 255])
       from_file (boolean): load data from file
            plant-detection_inputs.json
            plant-detection_p2c_calibration_parameters.json
            plant-detection_plants.json
            (default = False)
       from_env_var (boolean): load data from environment variable,
            overriding other parameter inputs
            (default = False)
       text_output (boolean): print text to STDOUT (default = True)
       verbose (boolean): print verbose text to STDOUT.
            otherwise, print condensed text output (default = True)
       print_all_json (boolean): print all JSON data used to STDOUT
            (default = False)
       grey_out (boolean): grey out regions in image that have
            not been selected (default = False)
       draw_contours (boolean): draw an outline around the boundary of
            detected plants (default = True)
       circle_plants (boolean): draw an enclosing circle around
            detected plants (default = True)
       GUI (boolean): settings for the local GUI (default = False)
       app (boolean): connect to the FarmBot web app (default = False)
       app_image_id (string): use image from the FarmBot API (default = None)

    Examples:
       PD = PlantDetection()
       PD.detect_plants()

       PD = PlantDetection(
          image='plant_detection/soil_image.jpg', morph=3, iterations=10,
          debug=True)
       PD.detect_plants()

       PD = PlantDetection(
          image='plant_detection/soil_image.jpg',
          blur=9, morph=7, iterations=4,
          calibration_img="plant_detection/p2c_test_calibration.jpg")
       PD.calibrate()
       PD.detect_plants()

       PD = PlantDetection(
         image='plant_detection/soil_image.jpg', blur=15, grey_out=True,
         array=[
            {"size": 5, "kernel": 'ellipse', "type": 'erode',  "iters": 2},
            {"size": 3, "kernel": 'ellipse', "type": 'dilate', "iters": 8}],
         debug=True, clump_buster=False,
         HSV_min=[30, 15, 15], HSV_max=[85, 245, 245])
       PD.detect_plants()
    """
    def __init__(self, **kwargs):
        """Read arguments (and change settings) and initialize modules."""
        # Default Data Inputs
        self.image = None
        self.plant_db = DB()

        # Default Parameter Inputs
        self.params = Parameters()

        # Load keyword argument inputs
        self._data_inputs(kwargs)
        self._parameter_inputs(kwargs)
        self.args = kwargs

        # Set remaining arguments to defaults
        self._set_defaults()

        # Changes based on inputs
        if self.args['calibration_img'] is not None:
            # self.coordinates = True
            self.args['coordinates'] = True
        if self.args['GUI']:
            self.args['save'] = False
            self.args['text_output'] = False
        if self.args['app']:
            self.args['verbose'] = False
            self.args['from_env_var'] = True
            self.plant_db.app = True

        # Remaining initialization
        self.p2c = None
        self.capture = Capture().capture
        self.final_marked_image = None
        self.plant_db.tmp_dir = None

    def _set_defaults(self):
        default_args = {
            # Default Data Inputs
            'image':
            None,
            'calibration_img':
            None,
            'known_plants':
            None,
            'app_image_id':
            None,
            'calibration_data':
            None,
            # Default Program Options
            'coordinates':
            False,
            'from_file':
            False,
            'from_env_var':
            False,
            'clump_buster':
            False,
            'GUI':
            False,
            'app':
            False,
            # Default Output Options
            'debug':
            False,
            'save':
            True,
            'text_output':
            True,
            'verbose':
            True,
            'print_all_json':
            False,
            'output_celeryscript_points':
            False,
            # Default Graphic Options
            'grey_out':
            False,
            'draw_contours':
            True,
            'circle_plants':
            True,
            # Default processing options
            'array':
            None,
            'blur':
            self.params.parameters['blur'],
            'morph':
            self.params.parameters['morph'],
            'iterations':
            self.params.parameters['iterations'],
            'HSV_min': [
                self.params.parameters['H'][0], self.params.parameters['S'][0],
                self.params.parameters['V'][0]
            ],
            'HSV_max': [
                self.params.parameters['H'][1], self.params.parameters['S'][1],
                self.params.parameters['V'][1]
            ],
        }
        for key, value in default_args.items():
            if key not in self.args:
                self.args[key] = value

    def _data_inputs(self, kwargs):
        """Load data inputs from keyword arguments."""
        for key in kwargs:
            if key == 'known_plants':
                self.plant_db.plants['known'] = kwargs[key]

    def _parameter_inputs(self, kwargs):
        """Load parameter inputs from keyword arguments."""
        for key in kwargs:
            if key == 'blur':
                self.params.parameters['blur'] = kwargs[key]
            if key == 'morph':
                self.params.parameters['morph'] = kwargs[key]
            if key == 'iterations':
                self.params.parameters['iterations'] = kwargs[key]
            if key == 'array':
                self.params.array = kwargs[key]
            if key == 'HSV_min':
                hsv_min = kwargs[key]
                self.params.parameters['H'][0] = hsv_min[0]
                self.params.parameters['S'][0] = hsv_min[1]
                self.params.parameters['V'][0] = hsv_min[2]
            if key == 'HSV_max':
                hsv_max = kwargs[key]
                self.params.parameters['H'][1] = hsv_max[0]
                self.params.parameters['S'][1] = hsv_max[1]
                self.params.parameters['V'][1] = hsv_max[2]

    def _calibration_input(self):  # provide inputs to calibration
        if self.args['app_image_id'] is not None:
            self.args['calibration_img'] = int(self.args['app_image_id'])
        if self.args['calibration_img'] is None and self.args['coordinates']:
            # Calibration requested, but no image provided.
            # Take a calibration image.
            self.args['calibration_img'] = self.capture()

        # Set calibration input parameters
        if self.args['from_env_var']:
            calibration_input = 'env_var'
        elif self.args['from_file']:  # try to load from file
            calibration_input = 'file'
        else:  # Use default calibration inputs
            calibration_input = None

        # Call coordinate conversion module
        self.p2c = Pixel2coord(self.plant_db,
                               calibration_image=self.args['calibration_img'],
                               calibration_data=self.args['calibration_data'],
                               load_data_from=calibration_input)
        self.p2c.debug = self.args['debug']

    def calibrate(self):
        """Calibrate the camera for plant detection.

        Initialize the coordinate conversion module using a calibration image,
        perform calibration, and save calibration data.
        """
        self._calibration_input()  # initialize coordinate conversion module
        exit_flag = self.p2c.calibration()  # perform calibration
        if exit_flag:
            sys.exit(0)
        self._calibration_output()  # save calibration data

    def _calibration_output(self):  # save calibration data
        if self.args['save'] or self.args['debug']:
            self.p2c.image.images['current'] = self.p2c.image.images['marked']
            self.p2c.image.save('calibration_result')

        # Print verbose results
        if self.args['verbose'] and self.args['text_output']:
            if self.p2c.calibration_params['total_rotation_angle'] != 0:
                print(" Note: required rotation of "
                      "{:.2f} degrees executed.".format(
                          self.p2c.calibration_params['total_rotation_angle']))
            if self.args['debug']:
                # print number of objects detected
                self.plant_db.print_count(calibration=True)
                # print coordinate locations of calibration objects
                self.p2c.p2c(self.plant_db)
                self.plant_db.print_coordinates()
                print('')

        # Print condensed output if verbose output is not chosen
        if self.args['text_output'] and not self.args['verbose']:
            print("Calibration complete. (rotation:{}, scale:{})".format(
                self.p2c.calibration_params['total_rotation_angle'],
                self.p2c.calibration_params['coord_scale']))

        # Save calibration data
        if self.args['from_env_var']:
            # to environment variable
            self.p2c.save_calibration_data_to_env()
        elif self.args['from_file']:  # to file
            self.p2c.save_calibration_parameters()
        else:  # to Parameters() instance
            self.params.calibration_data = self.p2c.calibration_params

    def _detection_input(self):  # provide input to detect_plants
        # Load input parameters
        if self.args['from_file']:
            # Requested to load detection parameters from file
            try:
                self.params.load()
            except IOError:
                print("Warning: Input parameter file load failed. "
                      "Using defaults.")
            self.plant_db.load_plants_from_file()
        if self.args['app']:
            self.plant_db.load_plants_from_web_app()
        if self.args['from_env_var']:
            # Requested to load detection parameters from json ENV variable
            self.params.load_env_var('detect')

        # Print input parameters and filename of image to process
        if self.args['verbose'] and self.args['text_output']:
            self.params.print_input()
            print("\nProcessing image: {}".format(self.args['image']))

    def _detection_image(self):  # get image to process
        self.image = Image(self.params, self.plant_db)
        # Get image to process
        try:  # check for API image ID
            image_id = self.args['app_image_id']
        except KeyError:
            image_id = None
        if image_id is not None:  # download image
            try:
                self.image.download(image_id)
            except IOError:
                print("Image download failed for image ID {}.".format(
                    str(image_id)))
                sys.exit(0)
        elif self.args['image'] is None:  # No image provided. Capture one.
            self.image.capture()
            if self.args['debug']:
                self.image.save('photo')
        else:  # Image provided. Load it.
            filename = self.args['image']
            self.image.load(filename)
        self.image.debug = self.args['debug']

    def _coordinate_conversion(self):  # determine detected object coordinates
        # Load calibration data
        load_data_from = None
        calibration_data = None
        if self.args['from_env_var']:
            load_data_from = 'env_var'
        elif self.args['from_file']:
            load_data_from = 'file'
        else:  # use data saved in self.params
            calibration_data = self.params.calibration_data
        # Initialize coordinate conversion module
        self.p2c = Pixel2coord(self.plant_db,
                               load_data_from=load_data_from,
                               calibration_data=calibration_data)
        self.p2c.debug = self.args['debug']
        # Check for coordinate conversion calibration results
        present = {
            'coord_scale': False,
            'camera_z': False,
            'center_pixel_location': False,
            'total_rotation_angle': False
        }
        try:
            for key in present:
                present[key] = self.p2c.calibration_params[key]
        except KeyError:
            log(
                "ERROR: Coordinate conversion calibration values "
                "not found. Run calibration first.",
                message_type='error',
                title='plant-detection')
            sys.exit(0)
        # Validate coordinate conversion calibration data for image
        calibration_data_valid = self.p2c.validate_calibration_data(
            self.image.images['current'])
        if not calibration_data_valid:
            log(
                "ERROR: Coordinate conversion calibration values "
                "invalid for provided image.",
                message_type='error',
                title='plant-detection')
            sys.exit(0)
        # Determine object coordinates
        self.image.coordinates(self.p2c,
                               draw_contours=self.args['draw_contours'])
        # Organize objects into plants and weeds
        self.plant_db.identify()
        if self.plant_db.plants['safe_remove']:
            self.image.safe_remove(self.p2c)

    def _coordinate_conversion_output(self):  # output detected object data
        # Print and output results
        if self.args['text_output']:
            self.plant_db.print_count()  # print number of objects detected
        if self.args['verbose'] and self.args['text_output']:
            self.plant_db.print_identified()  # print organized plant data
        if self.args['output_celeryscript_points']:
            self.plant_db.output_celery_script()  # print points JSON to stdout
        if self.args['app']:
            self.plant_db.upload_plants()  # add plants to FarmBot Web App
        if self.args['debug']:
            self.image.save_annotated('contours')
            self.image.images['current'] = self.image.images['marked']
            self.image.save_annotated('coordinates_found')
        if self.args['circle_plants']:
            self.image.label(self.p2c)  # mark objects with colored circles
        self.image.grid(self.p2c)  # add coordinate grid and features

    def detect_plants(self):
        """Detect the green objects in the image."""
        # Gather inputs
        self._detection_input()
        self._detection_image()

        # Process image in preparation for detecting plants (blur, mask, morph)
        self.image.initial_processing()

        # Optionally break up masses by splitting them into quarters
        if self.args['clump_buster']:
            self.image.clump_buster()

        # Optionally grey out regions not detected as objects
        if self.args['grey_out']:
            self.image.grey()

        # Return coordinates if requested
        if self.args['coordinates']:  # Convert pixel locations to coordinates
            self._coordinate_conversion()
            self._coordinate_conversion_output()
        else:  # No coordinate conversion
            # get pixel locations of objects
            self.image.find(draw_contours=self.args['draw_contours'])
            if self.args['circle_plants']:
                self.image.label()  # Mark plants with red circle
            if self.args['debug']:
                self.image.save_annotated('contours')
            if self.args['text_output']:
                self.plant_db.print_count()  # print number of objects detected
            if self.args['verbose'] and self.args['text_output']:
                self.plant_db.print_pixel()  # print object pixel location text
            self.image.images['current'] = self.image.images['marked']

        self._show_detection_output()  # show output data
        self._save_detection_output()  # save output data

    def _show_detection_output(self):  # show detect_plants output
        # Print raw JSON to STDOUT
        if self.args['print_all_json']:
            print("\nJSON:")
            print(self.params.parameters)
            print(self.plant_db.plants)
            if self.p2c is not None:
                print(self.p2c.calibration_params)

        # Print condensed inputs if verbose output is not chosen
        if self.args['text_output'] and not self.args['verbose']:
            print('{}: {}'.format('known plants input',
                                  self.plant_db.plants['known']))
            print('{}: {}'.format('parameters input', self.params.parameters))
            print('{}: {}'.format('coordinates input',
                                  self.plant_db.coordinates))

    def _save_detection_output(self):  # save detect_plants output
        # Final marked image
        if self.args['save'] or self.args['debug']:
            self.image.save('marked')
        elif self.args['GUI']:
            self.final_marked_image = self.image.images['marked']

        # Save input parameters
        if self.args['from_env_var']:
            # to environment variable
            self.params.save_to_env_var('detect')
        elif self.args['save']:
            # to file
            self.params.save()
        elif self.args['GUI']:
            # to file for GUI
            self.params.save()

        # Save plants
        if self.args['save']:
            self.plant_db.save_plants()
Example #2
0
class Pixel2coord(object):
    """Image pixel to machine coordinate conversion.

    Calibrates the conversion of pixel locations to machine coordinates
    in images. Finds object coordinates in image.
    """
    def __init__(self,
                 plant_db,
                 calibration_image=None,
                 calibration_data=None,
                 load_data_from=None):
        """Set initial attributes.

        Arguments:
            Database() instance

        Optional Keyword Arguments:
            calibration_image (str): filename (default: None)
            calibration_data: P2C().calibration_params JSON,
                              or 'file' or 'env_var' string
                              (default: None)
        """
        self.dir = os.path.dirname(os.path.realpath(__file__)) + os.sep
        self.parameters_file = "plant-detection_calibration_parameters.json"

        self.calibration_params = {}
        self.debug = False
        self.env_var_name = 'PLANT_DETECTION_calibration'
        self.plant_db = plant_db
        self.defaults = Parameters().cdefaults

        # Data and parameter preparation
        self.cparams = Parameters()
        self._calibration_data_preparation(calibration_data, load_data_from)
        # Image preparation
        self.image = None
        self._calibration_image_preparation(calibration_image)

        self.rotationangle = 0
        self.test_rotation = 5  # for testing, add some image rotation
        self.viewoutputimage = False  # overridden as True if running script
        self.json_calibration_data = None

    def _calibration_data_preparation(self,
                                      calibration_data=None,
                                      load_data_from=None):
        for key, value in self.defaults.items():
            if key in self.cparams.defaults:
                self.cparams.defaults[key] = value
        if calibration_data is not None:
            self.calibration_params = calibration_data
        elif load_data_from == 'file':
            # self._load_parameters(self.load_calibration_parameters,
            #                       IOError)
            self._load_inputs(self.cparams.load, IOError)
            self._additional_calibration_inputs(
                self.load_calibration_parameters, IOError)
            self.initialize_data_keys()
        elif load_data_from == 'env_var':
            # Method 1
            # self._load_parameters(self.load_calibration_data_from_env,
            #                       ValueError)
            # Method 2
            # self._load_inputs(
            #     self.cparams.load_env_var, ValueError)
            # self._additional_calibration_inputs(
            #     self.load_calibration_data_from_env, ValueError)
            # self.initialize_data_keys()
            # Method 3
            self.cparams.load_env_var('calibration')
            self.calibration_params = self.cparams.parameters.copy()
        else:  # load defaults
            self.calibration_params = self.defaults

        self.set_calibration_input_params()

    def _load_inputs(self, get_inputs, error):
        # load only image processing input parameters
        try:
            message = get_inputs()  # Parameters object
            if message != "":
                raise error("Load Failed.")
        except error:
            print("Warning: Input parameter load failed. " "Using Defaults.")
            self.calibration_params = self.cparams.defaults.copy()
        else:
            self.calibration_params = self.cparams.parameters.copy()

    def _load_parameters(self, get_parameters, error):
        # load all parameters necessary for calibration / coordinate conversion
        try:
            get_parameters()
        except error:
            print("Warning: Calibration data load failed. " "Using defaults.")
            self.calibration_params = self.defaults

    def _additional_calibration_inputs(self, get_additional, error):
        # load extra inputs needed (when using _load_inputs only)
        temp_inputs = self.calibration_params
        self.calibration_params = {}
        try:
            get_additional()
        except error:  # no additional calibration inputs to add
            self.calibration_params = temp_inputs
        else:  # add additional calibration inputs
            for key, value in self.calibration_params.items():
                if key not in temp_inputs:
                    temp_inputs[key] = value
            self.calibration_params = temp_inputs

    @staticmethod
    def get_image_center(image):
        """Return the pixel location (X, Y) of the image center."""
        return [int(a / 2) for a in image.shape[:2][::-1]]

    def _calibration_image_preparation(self, calibration_image):
        if calibration_image is not None:
            self.image = Image(self.cparams, self.plant_db)
            if isinstance(calibration_image, int):
                try:
                    self.image.download(calibration_image)
                except IOError:
                    print("Image download failed for image ID {}.".format(
                        str(calibration_image)))
                    sys.exit(0)
            else:
                self.image.load(calibration_image)
            self.calibration_params[
                'center_pixel_location'] = self.get_image_center(
                    self.image.images['current'])
            self.image.calibration_debug = self.debug

    def save_calibration_parameters(self):
        """Save calibration parameters to file."""
        if self.plant_db.tmp_dir is None:
            directory = self.dir
        else:
            directory = self.plant_db.tmp_dir
        with open(directory + self.parameters_file, 'w') as oututfile:
            json.dump(self.calibration_params, oututfile)

    def save_calibration_data_to_env(self):
        """Save calibration parameters to environment variable."""
        self.json_calibration_data = self.calibration_params
        self.cparams.parameters = self.calibration_params
        self.cparams.save_to_env_var('calibration')

    def load_calibration_data_from_env(self):
        """Load calibration parameters from environment variable."""
        self.calibration_params = ENV.load(self.env_var_name)
        if self.calibration_params is None:
            raise ValueError("ENV load failed.")

    def initialize_data_keys(self):
        """If using JSON with inputs only, create calibration data keys."""
        def _check_for_key(key):
            try:
                self.calibration_params[key]
            except KeyError:
                self.calibration_params[key] = self.defaults[key]

        calibration_keys = [
            'calibration_circles_xaxis', 'image_bot_origin_location',
            'calibration_circle_separation', 'camera_offset_coordinates',
            'calibration_iters'
        ]
        for key in calibration_keys:
            _check_for_key(key)

    def set_calibration_input_params(self):
        """Set input parameters from calibration parameters."""
        self.cparams.parameters['blur'] = self.calibration_params['blur']
        self.cparams.parameters['morph'] = self.calibration_params['morph']
        self.cparams.parameters['H'] = self.calibration_params['H']
        self.cparams.parameters['S'] = self.calibration_params['S']
        self.cparams.parameters['V'] = self.calibration_params['V']

    def load_calibration_parameters(self):
        """Load calibration parameters from file or use defaults."""
        def _load(directory):  # Load calibration parameters from file
            with open(directory + self.parameters_file, 'r') as inputfile:
                self.calibration_params = json.load(inputfile)

        try:
            _load(self.dir)
        except IOError:
            self.plant_db.tmp_dir = "/tmp/"
            _load(self.plant_db.tmp_dir)

    def validate_calibration_data(self, check_image):
        """Check that calibration parameters can be applied to the image."""
        # Prepare data
        image_center = self.get_image_center(check_image)
        image_center = self._block_rotations(
            self.calibration_params['total_rotation_angle'], cpl=image_center)
        image_location = self.plant_db.coordinates
        camera_dz = abs(self.calibration_params['camera_z'] -
                        image_location[2])
        center_deltas = [
            abs(calibration - current) for calibration, current in zip(
                self.calibration_params['center_pixel_location'], image_center)
        ]
        # Check data
        check_status = True
        if camera_dz > 5:
            check_status = False  # set True to try camera height compensation
        for center_delta in center_deltas:
            if center_delta > 5:
                check_status = False
        return check_status

    def _block_rotations(self, angle, cpl=None):
        def _determine_turns(angle):  # number of 90 degree rotations
            turns = -int(angle / 90.)
            remain = abs(angle) % 90
            if angle < 0:
                remain = -remain
            if remain > 45:
                turns -= 1
            if remain < -45:
                turns += 1
            return turns

        def _origin_rot(horiz, vert):  # rotate image origin with image
            if cpl is None:
                # get image origin
                origin = self.calibration_params['image_bot_origin_location']
                # rotate image origin
                if origin[0] == origin[1]:
                    origin[vert] = int(not origin[vert])
                else:
                    origin[horiz] = int(not origin[horiz])
                # set image origin
                self.calibration_params['image_bot_origin_location'] = origin
            # swap image center pixel horiz/vert
            if cpl is None:
                center = self.calibration_params['center_pixel_location']
            else:
                center = cpl
            center = center[::-1]
            self.calibration_params['center_pixel_location'] = center
            return center

        turns = _determine_turns(angle)
        if turns > 0:
            cpl = _origin_rot(0, 1)
        if turns < 0:
            cpl = _origin_rot(1, 0)
        return cpl

    def rotationdetermination(self):
        """Determine angle of rotation if necessary."""
        threshold = 0
        along_x = self.calibration_params['calibration_circles_xaxis']
        [[cdx,
          cdy]] = np.diff(self.plant_db.calibration_pixel_locations[:2, :2],
                          axis=0)
        if cdx == 0:
            trig = None
        else:
            trig = cdy / cdx
        difference = abs(cdy)
        if not along_x:
            if cdy == 0:
                trig = None
            else:
                trig = cdx / cdy
            difference = abs(cdx)
        if difference > threshold:
            if trig is None:
                self.rotationangle = 90
            else:
                rotation_angle_radians = np.arctan(trig)
                self.rotationangle = 180. / np.pi * rotation_angle_radians
                if abs(cdy) > abs(cdx) and along_x:
                    self.rotationangle = -self.rotationangle
                if abs(cdx) > abs(cdy) and not along_x:
                    self.rotationangle = -self.rotationangle
            self._block_rotations(self.rotationangle)
        else:
            self.rotationangle = 0

    def determine_scale(self):
        """Determine coordinate conversion scale."""
        if len(self.plant_db.calibration_pixel_locations) > 1:
            calibration_circle_sep = float(
                self.calibration_params['calibration_circle_separation'])
            object_sep = max(
                abs(
                    np.diff(self.plant_db.calibration_pixel_locations[:2, :2],
                            axis=0)[0]))
            self.calibration_params['coord_scale'] = _round(
                calibration_circle_sep / object_sep, 4)

    def c2p(self, plant_db):
        """Convert coordinates to pixel locations using image center."""
        plant_db.pixel_locations = self.convert(plant_db.coordinate_locations,
                                                to_='pixels')

    def p2c(self, plant_db):
        """Convert pixel locations to machine coordinates from image center."""
        plant_db.coordinate_locations = self.convert(plant_db.pixel_locations,
                                                     to_='coordinates')

    def plant_dict_to_pixel_array(self, plant_dict, extend_radius=0):
        """Convert a plant coordinate dictionary to a pixel array."""
        pixel_array = np.array(
            self.convert([
                plant_dict['x'], plant_dict['y'],
                plant_dict['radius'] + extend_radius
            ],
                         to_='pixels'))[0]
        return pixel_array

    def convert(self, input_, to_=None):
        """Convert between image pixels and bot coordinates."""
        # Check and manage input
        if to_ is None:
            raise TypeError("Conversion direction not provided.")
        input_ = np.array(input_)
        if len(input_) == 0:
            output_ = []
            return output_
        try:
            input_.shape[1]
        except IndexError:
            input_ = np.vstack([input_])
        # Get conversion parameters
        bot_location = np.array(self.plant_db.coordinates[:2], dtype=float)
        current_z = self.plant_db.coordinates[2]
        camera_offset = np.array(
            self.calibration_params['camera_offset_coordinates'], dtype=float)
        camera_coordinates = bot_location + camera_offset  # img center coord
        center_pixel_location = self.calibration_params[
            'center_pixel_location'][:2]
        sign = [
            1 if s == 1 else -1
            for s in self.calibration_params['image_bot_origin_location']
        ]
        coord_scale = np.repeat(self.calibration_params['coord_scale'], 2)
        # Adjust scale factor for camera height
        calibration_z = self.calibration_params['camera_z']
        camera_dz = current_z - calibration_z
        coord_scale = coord_scale + camera_dz / 157.3
        # Convert
        output_ = []
        for obj_num, obj_loc in enumerate(input_[:, :2]):
            if to_ == 'pixels':
                radius = input_[obj_num][2]
                result = (center_pixel_location -
                          ((obj_loc - camera_coordinates) /
                           (sign * coord_scale)))
                output_.append([result[0], result[1], radius / coord_scale[0]])
            if to_ == 'coordinates':
                radius = input_[:][obj_num][2]
                result = (camera_coordinates + sign * coord_scale *
                          (center_pixel_location - obj_loc))
                output_.append([result[0], result[1], coord_scale[0] * radius])
        return output_

    def calibration(self):
        """Determine pixel to coordinate scale and image rotation angle."""
        total_rotation_angle = 0
        warning_issued = False
        if self.debug:
            self.cparams.print_input()
        for i in range(0, self.calibration_params['calibration_iters']):
            self.image.initial_processing()
            self.image.find(calibration=True)  # find objects
            # If not the last iteration, determine camera rotation angle
            if i != (self.calibration_params['calibration_iters'] - 1):
                # Check number of objects detected and notify user if needed.
                if len(self.plant_db.calibration_pixel_locations) == 0:
                    log("ERROR: Calibration failed. No objects detected.",
                        message_type='error',
                        title='camera-calibration')
                    return True
                if self.plant_db.object_count > 2:
                    if not warning_issued:
                        log(" Warning: {} objects detected. "
                            "Exactly 2 recommended. "
                            "Incorrect results possible. Check output.".format(
                                self.plant_db.object_count),
                            message_type='warning',
                            title='camera-calibration')
                        warning_issued = True
                if self.plant_db.object_count < 2:
                    log(" ERROR: {} objects detected. "
                        "At least 2 required. Exactly 2 recommended.".format(
                            self.plant_db.object_count),
                        message_type='error',
                        title='camera-calibration')
                    return True
                # Use detected objects to determine required rotation angle
                self.rotationdetermination()
                if abs(self.rotationangle) > 120:
                    log(
                        " ERROR: Excessive rotation required. "
                        "Check that the calibration objects are "
                        "parallel with the desired axis and that "
                        "they are the only two objects detected.",
                        message_type='error',
                        title='camera-calibration')
                    return True
                self.image.rotate_main_images(self.rotationangle)
                total_rotation_angle += self.rotationangle
        self.determine_scale()
        fail_flag = self._calibration_output(total_rotation_angle)
        return fail_flag

    def _calibration_output(self, total_rotation_angle):
        if self.viewoutputimage:
            self.image.images['current'] = self.image.images['marked']
            self.image.show()
        while abs(total_rotation_angle) > 360:
            if total_rotation_angle < 0:
                total_rotation_angle += 360
            else:
                total_rotation_angle -= 360
        self.calibration_params['total_rotation_angle'] = _round(
            total_rotation_angle, 2)
        self.calibration_params['camera_z'] = self.plant_db.coordinates[2]
        try:
            self.calibration_params['coord_scale']  # pylint:disable=W0104
            failure_flag = False
        except KeyError:
            log("ERROR: Calibration failed.",
                message_type='error',
                title='camera-calibration')
            failure_flag = True
        return failure_flag

    def determine_coordinates(self):
        """Use calibration parameters to determine locations of objects."""
        self.image.rotate_main_images(
            self.calibration_params['total_rotation_angle'])
        if self.debug:
            self.cparams.print_input()
        self.image.initial_processing()
        self.image.find(calibration=True)
        self.plant_db.print_count(calibration=True)  # print detected obj count
        self.p2c(self.plant_db)
        self.plant_db.print_coordinates()
        if self.viewoutputimage:
            self.image.grid(self)
            self.image.images['current'] = self.image.images['marked']
            self.image.show()
        return self.plant_db.get_json_coordinates()