Esempio n. 1
0
class ImageTest(unittest.TestCase):
    """Check plant identification"""
    def setUp(self):
        self.parameters = Parameters()
        self.db = DB()
        self.image = Image(self.parameters, self.db)

    def test_wrong_parameters(self):
        """Check that incompatible parameters are fixed"""
        self.image.params.parameters['blur'] = 2
        self.image.params.parameters['morph'] = 0
        self.image.params.parameters['iterations'] = 0
        self.image.load('plant_detection/soil_image.jpg')
        self.image.initial_processing()
        self.assertEqual(self.image.params.parameters['blur'], 3)
        self.assertEqual(self.image.params.parameters['morph'], 1)
        self.assertEqual(self.image.params.parameters['iterations'], 1)

    def test_image_size_reduction(self):
        """Reduce large image"""
        large_image = np.zeros([1000, 2000, 3], np.uint8)
        self.image.image_name = None
        self.image.save('large', image=large_image)
        self.image.load('large.jpg')
        new_height = self.image.images['current'].shape[0]
        self.assertEqual(new_height, 600)

    def tearDown(self):
        try:
            os.remove('large.jpg')
        except OSError:
            pass
Esempio n. 2
0
 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 _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']
Esempio n. 4
0
 def setUp(self):
     self.parameters = Parameters()
     self.db = DB()
     self.image = Image(self.parameters, self.db)
Esempio n. 5
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

        if not self.calibration_params['easy_calibration']:
            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',
                            'easy_calibration',
                            '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()
        if self.calibration_params['easy_calibration']:
            from plant_detection.PatternCalibration import PatternCalibration
            pattern_calibration = PatternCalibration(self.calibration_params)
            result_flag = pattern_calibration.move_and_capture()
            if not result_flag:
                fail_flag = True
                return fail_flag
            result_flag = pattern_calibration.calibrate()
            if not result_flag:
                fail_flag = True
                return fail_flag
            self.image.images['marked'] = pattern_calibration.output_img
            self.image.grid(self)
            fail_flag = False
            return fail_flag
        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='warn', 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()
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()
        self.params.add_missing_params('detect')

        # 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']))

        # Send calibration result log toast
        if self.args['app']:
            log(
                'Camera calibration complete; setting pixel coordinate scale'
                ' to {} and camera rotation to {} degrees.'.format(
                    self.p2c.calibration_params['coord_scale'],
                    self.p2c.calibration_params['total_rotation_angle']),
                'success', 'Success', ['toast'], True)

        # 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('detect')
            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(self.params.parameters)
        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']:
            save_detected_plants = self.params.parameters[
                'save_detected_plants']
            # add detected weeds and points to FarmBot Web App
            self.plant_db.upload_plants(save_detected_plants)
        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()
Esempio n. 7
0
 def take_photo(self):
     self.image = Image(self.params, self.plant_db)
     self.image.capture()
     self.image.save('Seedling_photo_' + strftime("%Y-%m-%d_%H:%M:%S", gmtime()))
Esempio n. 8
0
class MyFarmware():
    def __init__(self, farmwarename):
        self.farmwarename = farmwarename
        prefix = self.farmwarename.lower().replace('-', '_')
        self.input_default_speed = int(os.environ.get(prefix + "_default_speed", 800))
        self.x_photo_pos = 400
        self.y_photo_pos = 235
        self.z_photo_pos = 0
        self.image = None
        self.plant_db = DB()
        self.params = Parameters()
        self.plant_detection = None
        self.dir = os.path.dirname(os.path.realpath(__file__)) + os.sep

        """"self.api = API(self)
        self.points = []"""
    def mov_robot_origin(self):
        log('Execute move: ', message_type='debug', title=str(self.farmwarename))
        move_absolute(
            location=[0, 0, 0],
            offset=[0, 0, 0],
            speed=800)
    def mov_robot_photo(self):
        log('Execute move: ', message_type='debug', title=str(self.farmwarename))
        move_absolute(
            location=[self.x_photo_pos, self.y_photo_pos, self.z_photo_pos],
            offset=[0, 0, 0],
            speed=800)
    def take_photo(self):
        self.image = Image(self.params, self.plant_db)
        self.image.capture()
        self.image.save('Seedling_photo_' + strftime("%Y-%m-%d_%H:%M:%S", gmtime()))

    def process_photo(self):
        self.plant_detection = PlantDetection(coordinates=True, app=True)
        self.plant_detection.detect_plan()
    """def graph_plant_centroid(self):

    def execute_sequence_init(self):"""

    def save_data(self, idata):
        HEADERS = {
            'Authorization': 'bearer {}'.format(os.environ['FARMWARE_TOKEN']),
            'content-type': 'application/json'}

        def timestamp(value):
            """Add a timestamp to the pin value."""
            return {'time': time(), 'value': value}

        def append(data):
            """Add new data to existing data."""
            try:
                existing_data = json.loads(os.environ[LOCAL_STORE])
            except KeyError:
                existing_data = []
            existing_data.append(data)
            return existing_data

        def wrap(data):
            """Wrap the data in a `set_user_env` Celery Script command to save it."""
            return {
                'kind': 'set_user_env',
                'args': {},
                'body': [{
                    'kind': 'pair',
                    'args': {
                        'label': LOCAL_STORE,
                        'value': json.dumps(data)
                    }}]}

        def post(wrapped_data):
            """Send the Celery Script command."""
            payload = json.dumps(wrapped_data)
            requests.post(os.environ['FARMWARE_URL'] + 'api/v1/celery_script',
                          data=payload, headers=HEADERS)
            """log('Data is supposed to be saved')"""

        def save_data_csv(value):
            """To save data into a csv file"""
            with open(self.dir + 'db_plant_radius_test.csv', mode='a') as dbprt:
                db_writer = csv.writer(dbprt, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
                db_writer.writerow([strftime("%Y-%m-%d_%H:%M:%S", gmtime()), value])
            log('Data is supposed to have been saved into db_plant_radius_test.csv')

        LOCAL_STORE = 'test_data'
        post(wrap(append(timestamp(save_data_csv(idata)))))
        """post(wrap(append(timestamp(idata))))"""

    def plot_data(self):
        TIME_SCALE_FACTOR = 60 * 2
        DATA_SCALE_FACTOR = 2
        RECENT = {'time': None}

        def post(wrapped_data):
            """Send the Celery Script command."""
            headers = {
                'Authorization': 'bearer {}'.format(os.environ['FARMWARE_TOKEN']),
                'content-type': 'application/json'}
            payload = json.dumps(wrapped_data)
            requests.post(os.environ['FARMWARE_URL'] + 'api/v1/celery_script',
                          data=payload, headers=headers)

        def no_data_error():
            """Send an error to the log if there's no data."""
            message = 'No data available'
            wrapped_message = {
                'kind': 'send_message',
                'args': {
                    'message_type': 'error',
                    'message': message}}
            post(wrapped_message)

        def get_data():
            """Get existing historical pin data."""
            data = json.loads(os.getenv('test_data', '[]'))
            if len(data) < 1:
                no_data_error()
                sys.exit(0)
            else:
                return data

        def reduce_data(data):
            """Reduce the loaded data for plotting."""
            times, values = [], []
            for record in data:
                times.append(round(float(record['time']) / TIME_SCALE_FACTOR))
                values.append(round(float(record['value']) / DATA_SCALE_FACTOR))
            RECENT['time'] = max(times) * TIME_SCALE_FACTOR
            times = abs(np.array(times) - max(times))
            all_data = np.column_stack((times, values))
            filtered_data = all_data[all_data[:, 0] < 720]
            return filtered_data

        def plot(data):
            """Plot the reduced data."""
            # Create blank plot
            p = np.full([512, 24 * 60 / 2], 255, np.uint8)
            # Add shaded plot areas
            for i in range(512):
                if i < 100:  # N/A
                    p[i, :] = 220
                elif i > 425:  # off
                    p[i, :] = 220
                else:  # sensor range (gradient)
                    p[i, :] = 255 - 175 * ((i - 100) / float(425 - 100))
            # Add horizontal gridlines
            for i in range(0, 512, 32):
                p[i, :] = 100
                if i == 384:
                    p[i, :] = 125
            # Add minor vertical gridlines
            for i in range(0, 720, 30):
                p[:, i] = 100
            # Add major vertical gridlines
            for i in range(0, 720, 90):
                p[:, i - 1:i + 1] = 100
            # Add plot border
            cv2.rectangle(p, (0, 0), (719, 511), 50, 4)
            # Add data
            for record in data:
                reading_time = int(record[0])
                reading_value = int(record[1])
                cv2.circle(p, (reading_time, reading_value), 5, 0, 3)
            # Flip plot to display oldest to newest, low to high
            p = cv2.flip(p, -1)
            # Create plot border label area
            border = np.full([800, 600], 255, np.uint8)

            def _add_labels(image_area, labels):
                for label in labels:
                    cv2.putText(image_area, label['text'].upper(),
                                label['position'], 0, 0.5, 0, 1)

            # Add sensor range text
            """range_labels = [{'text': 'off', 'position': (500, 25)},
                            {'text': 'wet', 'position': (425, 25)},
                            {'text': 'dry', 'position': (160, 25)},
                            {'text': 'n/a', 'position': (75, 25)}]

            _add_labels(border, range_labels)"""
            # Flip labels to display vertically
            full = cv2.flip(cv2.transpose(border), 0)
            # Add sensor value labels
            value_labels = [{'text': '0', 'position': (760, 560)},
                            {'text': '512', 'position': (760, 305)},
                            {'text': '1023', 'position': (760, 50)}]
            _add_labels(full, value_labels)
            # Add most recent time
            time_string = strftime('%b %d %H:%M UTC', gmtime(RECENT['time']))
            _add_labels(full, [{'text': time_string, 'position': (650, 580)}])
            # Add time offset labels
            for i, column in enumerate(range(10, 600, 90)[::-1]):
                _add_labels(full, [{'text': '-{} hr'.format(6 + i * 3),
                                    'position': (column, 580)}])
            # Add label area to plot area
            full[44:556, 40:760] = p
            # Add plot title
            title = 'Database tests'
            cv2.putText(full, title.upper(), (325, 25), 0, 0.75, 0, 2)
            return full

        def save(image):
            """Save the plot image."""
            filename = '/test_data_plot_{}.png'.format(int(time()))
            log('Image to be saved in: ' + self.dir + filename)
            cv2.imwrite(self.dir + filename, image)

        """PIN = get_env('pin')
        IS_SOIL_SENSOR = PIN == 59"""
        save(plot(reduce_data(get_data())))

    def run(self):
        self.mov_robot_origin()
        self.mov_robot_photo()
        #self.take_photo()
        #self.process_photo()
        for i in range(0, 500, 21):
            self.save_data(i)
        self.plot_data()
        sys.exit(0)