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
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']
def setUp(self): self.parameters = Parameters() self.db = DB() self.image = Image(self.parameters, self.db)
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()
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()))
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)