def stage3_extract_plant_parts(**args):
    ''' 
    Extract out possible plant parts to be clustered in next stage.
    args should match the names and descriptions of command line parameters,
    but unlike command line, all arguments must be present.
    '''
    # Copy args so we can archive them to a file when function is finished.
    args_copy = args.copy()
    
    # Convert arguments to local variables of the correct type.
    input_filepath = args.pop('input_filepath')
    out_directory = args.pop('output_directory')
    pad = float(args.pop('pad'))
    special_pad = float(args.pop('special_pad'))
    min_leaf_size = float(args.pop('min_leaf_size'))
    max_leaf_size = float(args.pop('max_leaf_size'))
    min_stick_part_size = float(args.pop('min_stick_part_size'))
    max_stick_part_size = float(args.pop('max_stick_part_size'))
    min_tag_size = float(args.pop('min_tag_size'))
    max_tag_size = float(args.pop('max_tag_size'))
    disable_sticks = args.pop('disable_sticks').lower() == 'true'
    disable_tags = args.pop('disable_tags').lower() == 'true'
    use_marked_image = args.pop('marked_image').lower() == 'true'
    debug_start = args.pop('debug_start')
    debug_stop = args.pop('debug_stop')
    
    if len(args) > 0:
        print "Unexpected arguments provided: {}".format(args)
        return ExitReason.bad_arguments

    rows, geo_images = unpickle_stage2_output(input_filepath)
    
    if len(rows) == 0 or len(geo_images) == 0:
        print "No rows or no geo images could be loaded from {}".format(input_filepath)
        return ExitReason.no_rows
    
    ImageWriter.level = ImageWriter.NORMAL
    
    # Write images out to subdirectory to keep separated from pickled results.
    image_out_directory = os.path.join(out_directory, 'images/')
    if not os.path.exists(image_out_directory):
        os.makedirs(image_out_directory)
    
    rows = sorted(rows, key=lambda r: r.number)
    
    # Sort geo images so they're processed by time.
    geo_images = sorted(geo_images, key=lambda img: img.image_time)
    
    # Look for start/stop filenames so user doesn't have to process all images.
    start_geo_index, stop_geo_index = get_subset_of_geo_images(geo_images, debug_start, debug_stop)
        
    print "Processing geo images {} through {}".format(start_geo_index, stop_geo_index)
    geo_images = geo_images[start_geo_index : stop_geo_index+1]

    num_images_not_in_segment = 0
    num_images_without_path = 0

    leaf_finder = LeafFinder(min_leaf_size, max_leaf_size)
    
    if disable_sticks:
        stick_finder = None
    else:
        stick_finder = BlueStickFinder(min_stick_part_size, max_stick_part_size)
        
    if disable_tags:
        tag_finder = None
    else:
        tag_finder = TagFinder(min_tag_size, max_tag_size)

    all_segments = all_segments_from_rows(rows)
    
    for segment in all_segments:
        if segment.is_special:
            segment.lrud = calculate_special_segment_lrud(segment, special_pad)
        else:
            segment.lrud = calculate_segment_lrud(segment, pad)
    
    num_matched = [] # keep track of how many segments each image maps to.
    num_leaves = [] # how many leaves are in each processed image
    num_sticks = [] # how many sticks are in each processed images
    num_tags = [] # how many tags are in each processed images
    
    for k, geo_image in enumerate(geo_images):
        
        if not geo_image.file_path:
            num_images_without_path += 1
            continue
        
        # Check if image east/west/north/south (lrud) overlaps with any segments.
        image_lrud = calculate_image_lrud(geo_image)
        overlapping_segments = [seg for seg in all_segments if is_overlapping_segment(image_lrud, seg)]
        
        if len(overlapping_segments) == 0:
            num_images_not_in_segment += 1
            continue
        
        print "{} [{} / {}]".format(geo_image.file_name, k, len(geo_images))
            
        leaves, sticks, tags = process_geo_image_to_find_plant_parts(geo_image, leaf_finder, stick_finder, tag_finder, image_out_directory, use_marked_image)
        
        # Remove any false positive items that came from codes.
        geo_codes = geo_image.items['codes'] 
        leaves = dont_overlap_with_items(geo_codes, leaves)
        sticks = dont_overlap_with_items(geo_codes, sticks)
        tags = dont_overlap_with_items(geo_codes, tags)
        
        geo_image.items['leaves'] = leaves
        geo_image.items['stick_parts'] = sticks
        geo_image.items['tags'] = tags
        
        print "Found {} leaves, {} stick parts and {} tags".format(len(leaves), len(sticks), len(tags))

        for segment in overlapping_segments:
            segment.geo_images.append(geo_image)
         
        num_matched.append(len(overlapping_segments))
        num_leaves.append(len(leaves))
        num_sticks.append(len(sticks))
        num_tags.append(len(tags))

    print "\nProcessed {}".format(len(num_matched))
    print "Not in segment {}".format(num_images_not_in_segment)
    print "Invalid path {}".format(num_images_without_path)

    print "Matched images were in average of {} segments".format(np.mean(num_matched))
    print "Average of {} leaves, {} stick parts and {} tags per image".format(np.mean(num_leaves), np.mean(num_sticks), np.mean(num_tags))

    if not os.path.exists(out_directory):
        os.makedirs(out_directory)

    # Pickle
    dump_filename = "stage3_output.s3"
    print "\nSerializing {} rows to {}".format(len(rows), dump_filename)
    pickle_results(dump_filename, out_directory, rows)
    
    # Write arguments out to file for archiving purposes.
    write_args_to_file("stage3_args.csv", out_directory, args_copy)
Ejemplo n.º 2
0
def stage2_group_codes(**args):
    ''' 
    Group codes into rows.
    args should match the names and descriptions of command line parameters,
    but unlike command line, all arguments must be present.
    '''
    # Copy args so we can archive them to a file when function is finished.
    args_copy = args.copy()
    
    # Convert arguments to local variables of the correct type.
    input_directory = args.pop('input_directory')
    field_direction = float(args.pop('field_direction'))
    output_directory = args.pop('output_directory')
    num_rows_per_pass = int(args.pop('num_rows_per_pass'))
    code_list_filepath = args.pop('code_list_filepath')
    code_modifications_filepath = args.pop('code_modifications_filepath')
    
    if len(args) > 0:
        print "Unexpected arguments provided: {}".format(args)
        return ExitReason.bad_arguments
    
    geo_images, all_codes = unpickle_stage1_output(input_directory)

    print 'Found {} codes in {} geo images.'.format(len(all_codes), len(geo_images))

    if len(geo_images) == 0 or len(all_codes) == 0:
        print "Couldn't load any geo images or codes from input directory {}".format(input_directory)
        return ExitReason.no_geo_images

    if code_modifications_filepath != 'none':
        if not os.path.exists(code_modifications_filepath):
            print "Provided code modification file {} doesn't exist".format(code_modifications_filepath)
            return ExitReason.no_geo_images
        modifications_out_directory = os.path.join(output_directory, 'modifications')
        code_modifications = parse_code_modifications_file(code_modifications_filepath)
        geo_images, all_codes = apply_code_modifications(code_modifications, geo_images, all_codes, modifications_out_directory)

    # Merge items so they're unique.  One code references other instances of that same code.
    merged_codes = merge_items(all_codes, max_distance=500)

    print '{} unique codes.'.format(len(merged_codes))
                
    row_codes = [code for code in merged_codes if code.type.lower() == 'rowcode']
    group_codes = [code for code in merged_codes if code.type.lower() == 'groupcode']
    single_codes = [code for code in merged_codes if code.type.lower() == 'singlecode']
        
    '''
    if row_labeling_scheme == 0:
        
        grouped_row_codes = group_row_codes(row_codes)
    
        if len(grouped_row_codes) == 0:
            print "No row codes found. Exiting"
            return ExitReason.no_rows
    
        display_row_info(grouped_row_codes)
            
        rows = create_rows(grouped_row_codes, field_direction)
        
        up_row_nums, back_row_nums = associate_row_numbers_with_up_back_rows()
        
        assign_rows_a_direction(rows, up_row_nums, back_row_nums)
        
        field_passes = [rows[x:x+2] for x in xrange(0, len(rows), 2)]
        
    elif row_labeling_scheme == 1:
        
        grouped_row_codes = group_row_codes_by_pass_name(row_codes)
        
        rows, field_passes = create_rows_and_field_passes_by_pass_codes(grouped_row_codes, field_direction)
        
    elif row_labeling_scheme == 2:
    '''
    
    grouped_row_codes = group_row_codes_by_row_name(row_codes)

    if len(grouped_row_codes) == 0:
        print "No row codes found. Exiting"
        return ExitReason.no_rows

    display_row_info(grouped_row_codes)
    
    rows, field_passes = create_rows_and_field_passes_by_row_codes(grouped_row_codes, field_direction, num_rows_per_pass)

    if len(rows) == 0:
        print "No complete rows found.  Exiting."
        return ExitReason.no_rows
    
    print sorted([r.number for r in rows], key=lambda r: r)
    
    print "Calculating field positions."
    calculate_field_positions_and_range(rows, merged_codes, all_codes, geo_images)
    for code in merged_codes:
        code.refresh_fields()
    
    print "Calculating projections to nearest row"
    codes_with_projections = calculate_projection_to_nearest_row(group_codes + single_codes, rows)
            
    print "Creating segments"
    group_segments, special_segments = create_segments(codes_with_projections, rows)
        
    print "Organizing segments"
    start_segments, middle_segments, end_segments, single_segments = organize_group_segments(group_segments)
    
    if len(middle_segments) > 0:
        print "Middle segments that span entire row aren't supported right now. Exiting"
        return ExitReason.operation_not_supported
    
    print "Forming groups"
    groups = complete_groups(end_segments, single_segments, field_passes, num_rows_per_pass)
        
    handle_single_segments(single_segments, groups)
    
    # Add in information about max number of plants and optional alternate ids.
    if code_list_filepath != 'none':
        if not os.path.exists(code_list_filepath):
            print "Code list file doesn't exist {}".format(code_list_filepath)
            return ExitReason.bad_arguments
        else:
            code_listings, alternate_ids_included = parse_code_listing_file(code_list_filepath)
            print "Applying code listings"
            apply_code_listings(code_listings, groups, alternate_ids_included)
        
    display_segment_info(group_segments, special_segments, groups)
    
    if not os.path.exists(output_directory):
        os.makedirs(output_directory)
 
    dump_filename = "stage2_output_{}_{}.s2".format(int(geo_images[0].image_time), int(geo_images[-1].image_time))
    print "Serializing {} rows and {} geo images to {}.".format(len(rows), len(geo_images), dump_filename)
    pickle_results(dump_filename, output_directory, rows, geo_images)
    
    # Write arguments out to file for archiving purposes.
    args_filename = "stage2_args_{}_{}.csv".format(int(geo_images[0].image_time), int(geo_images[-1].image_time))
    write_args_to_file(args_filename, output_directory, args_copy)
Ejemplo n.º 3
0
def stage4_locate_plants(**args):
    """ 
    Cluster and filter plant parts into actual plants.
    args should match the names and descriptions of command line parameters,
    but unlike command line, all arguments must be present.
    """
    # Copy args so we can archive them to a file when function is finished.
    args_copy = args.copy()

    # convert command line arguments
    input_filepath = args.pop("input_filepath")
    out_directory = args.pop("output_directory")
    max_plant_size = float(args.pop("max_plant_size")) / 100.0  # convert to meters
    max_plant_part_distance = float(args.pop("max_plant_part_distance")) / 100.0  # convert to meters
    plant_spacing = float(args.pop("plant_spacing")) / 100.0  # convert to meters
    start_code_spacing = float(args.pop("start_code_spacing")) / 100.0  # convert to meters
    end_code_spacing = float(args.pop("end_code_spacing")) / 100.0  # convert to meters
    single_max_dist = float(args.pop("single_max_dist")) / 100.0  # convert to meters
    stick_multiplier = float(args.pop("stick_multiplier"))
    leaf_multiplier = float(args.pop("leaf_multiplier"))
    tag_multiplier = float(args.pop("tag_multiplier"))
    lateral_penalty = float(args.pop("lateral_penalty"))
    projection_penalty = float(args.pop("projection_penalty"))
    closeness_penalty = float(args.pop("closeness_penalty"))
    spacing_filter_thresh = float(args.pop("spacing_filter_thresh"))
    extract_images = args.pop("extract_images").lower() == "true"
    debug_marked_image = args.pop("marked_image").lower() == "true"

    if len(args) > 0:
        print "Unexpected arguments provided: {}".format(args)
        return ExitReason.bad_arguments

    rows = unpickle_stage3_output(input_filepath)

    if len(rows) == 0:
        print "No rows could be loaded from {}".format(input_filepath)
        sys.exit(ExitReason.no_rows)

    if not os.path.exists(out_directory):
        os.makedirs(out_directory)

    all_segments = all_segments_from_rows(rows)

    # Use different filters for normal vs. single segments
    normal_plant_filter = RecursiveSplitPlantFilter(
        start_code_spacing,
        end_code_spacing,
        plant_spacing,
        lateral_penalty,
        projection_penalty,
        closeness_penalty,
        stick_multiplier,
        leaf_multiplier,
        tag_multiplier,
    )
    closest_plant_filter = ClosestSinglePlantFilter(single_max_dist)

    # Use a spacing filter for detecting and fixing any mis-chosen plants.
    plant_spacing_filter = PlantSpacingFilter(spacing_filter_thresh)

    if extract_images:
        ImageWriter.level = ImageWriter.NORMAL
        image_out_directory = os.path.join(out_directory, "images/")
    else:
        image_out_directory = None

    for seg_num, segment in enumerate(all_segments):

        # if segment.start_code.name != 'TBJ':
        #    continue

        print "Processing segment {} [{}/{}] with {} images".format(
            segment.start_code.name, seg_num + 1, len(all_segments), len(segment.geo_images)
        )

        if segment.row_number > 6:
            for geo_image in segment.geo_images:
                try:
                    del geo_image.items["stick_parts"]
                    del geo_image.items["leaves"]
                except KeyError:
                    pass

        try:
            if segment.start_code.is_gap_item:
                print "Skipping segment since its start code is listed as a gap."
                continue
        except AttributeError:
            pass  # This used to not be supported so it's not a big deal if segment is missing property

        # Cluster together leaves, stick parts and tags into possible plants
        possible_plants = []
        for geo_image in segment.geo_images:
            if "possible_plants" in geo_image.items:
                # Already clustered this image.
                possible_plants += geo_image.items["possible_plants"]
            else:
                possible_plants += cluster_geo_image_items(geo_image, segment, max_plant_size, max_plant_part_distance)

        if len(possible_plants) == 0:
            print "Warning: segment {} has no possible plants.".format(segment.start_code.name)
            continue

        # Remove small parts that didn't get clustered.
        possible_plants = filter_out_noise(possible_plants)

        print "{} possible plants found between all images".format(len(possible_plants))

        print "clustered down to {} possible plants".format(len(possible_plants))

        # Find UTM positions of possible plants so that they can be easily compared between different images.
        last_plant = None
        for plant in possible_plants:
            stick_parts = [part for part in plant["items"] if part["item_type"] == "stick_part"]
            plant_tags = [tag for tag in plant["items"] if part["item_type"] == "tag"]
            if len(plant_tags) > 0:
                # Use position of tag for plant position.
                positioning_rect = merge_corner_rectangles([tag["rect"] for tag in plant_tags])
            elif len(stick_parts) > 0:
                # Use blue stick parts for position
                positioning_rect = merge_corner_rectangles([part["rect"] for part in stick_parts])
            else:
                # No tags or blue sticks so just use entire plant
                positioning_rect = plant["rect"]
            if "image_altitude" in plant:
                altitude = plant["image_altitude"]
            elif last_plant is not None:
                altitude = last_plant["position"][2]
            else:
                altitude = segment.start_code.position[2]
            px, py = corner_rect_center(positioning_rect)
            plant["position"] = (px, py, altitude)

            last_plant = plant

        if segment.start_code.type == "RowCode" and segment.end_code.type == "SingleCode":
            # Special case... don't want to process this segment since there shouldn't be a plant associated with it.
            continue

        if segment.is_special:
            selected_plant = closest_plant_filter.find_actual_plant(possible_plants, segment)
            actual_plants = [selected_plant]
        else:
            actual_plants = normal_plant_filter.locate_actual_plants_in_segment(possible_plants, segment)
            plant_spacing_filter.filter(actual_plants)
            print "{} actual plants found".format(len(actual_plants))

        # Now that plant filter has run make sure all created plants have a bounding rectangle so they show up in output images.
        for plant in actual_plants:
            if plant.type == "CreatedPlant":
                px, py, pz = plant.position
                po = 0.12  # plant offset in meters
                plant.bounding_rect = [(px - po, py - po), (px - po, py + po), (px + po, py - po), (px + po, py + po)]

        extract_global_plants_from_images(actual_plants, segment.geo_images, image_out_directory)

        for plant in actual_plants:
            plant.row = segment.row_number
            segment.add_item(plant)

        if debug_marked_image:
            if len(actual_plants) > 0:
                debug_draw_plants_in_images(segment.geo_images, possible_plants, actual_plants, out_directory)

    print "\n---------Normal Groups----------"
    print "Successfully found {} total plants".format(normal_plant_filter.num_successfully_found_plants)
    print "Created {} plants".format(normal_plant_filter.num_created_plants)

    print "\n---------Single Groups----------"
    print "Successfully found {} total plants".format(closest_plant_filter.num_successfully_found_plants)
    print "Created {} plants due to no valid plants".format(closest_plant_filter.num_created_because_no_plants)

    print "\n-----Spacing Filter Results-----"
    print "Relocated {} plants due to bad spacing.".format(plant_spacing_filter.num_plants_moved)

    # Pickle
    dump_filename = "stage4_output.s4"
    print "\nSerializing {} rows to {}".format(len(rows), dump_filename)
    pickle_results(dump_filename, out_directory, rows)

    # Write arguments out to file for archiving purposes.
    write_args_to_file("stage4_args.csv", out_directory, args_copy)
Ejemplo n.º 4
0
def stage1_extract_codes(**args):
    ''' 
    Extract codes from set of images and write out results to file.
    args should match the names and descriptions of command line parameters,
    but unlike command line, all arguments must be present.
    '''
    # Copy args so we can archive them to a file when function is finished.
    args_copy = args.copy()
    
    # Convert arguments to local variables of the correct type.
    image_directory = args.pop('image_directory')
    image_geo_file = args.pop('image_geo_file')
    out_directory = args.pop('output_directory')
    postfix_id = args.pop('postfix_id')
    code_min_size = float(args.pop('code_min_size'))
    code_max_size = float(args.pop('code_max_size'))
    provided_resolution = float(args.pop('resolution'))
    camera_height = float(args.pop('camera_height'))
    use_marked_image = args.pop('marked_image').lower() == 'true'
    debug_start = args.pop('debug_start')
    debug_stop = args.pop('debug_stop')

    if len(args) > 0:
        print "Unexpected arguments provided: {}".format(args)
        return ExitReason.bad_arguments
    
    if code_max_size <= 0 or code_min_size <= 0:
        print "\nError: code sizes must be greater than zero.\n"
        return ExitReason.bad_arguments
        
    if code_max_size <= code_min_size:
        print "\nError: Max code size must be greater than min.\n"
        return ExitReason.bad_arguments
    
    if provided_resolution <= 0:
        print "\nError: Resolution must be greater than zero."
        return ExitReason.bad_arguments
    
    if camera_height <= 0:
        print "\nError: Specified camera height must be greater than zero."
        return ExitReason.bad_arguments
        
    image_filenames = list_images(image_directory, ['tiff', 'tif', 'jpg', 'jpeg', 'png'])
                        
    if len(image_filenames) == 0:
        print "No images found in directory: {}".format(image_directory)
        return ExitReason.no_images
    
    print "\nFound {} images to process".format(len(image_filenames))
    
    geo_images = parse_geo_file(image_geo_file, provided_resolution, camera_height)
            
    print "Parsed {} geo images".format(len(geo_images))
    
    if len(geo_images) == 0:
        print "No geo images. Exiting."
        return ExitReason.no_geo_images
    
    # Look for start/stop filenames so user doesn't have to process all images.
    start_geo_index, stop_geo_index = get_subset_of_geo_images(geo_images, debug_start, debug_stop)
        
    print "Processing geo images {} through {}".format(start_geo_index, stop_geo_index)
    geo_images = geo_images[start_geo_index : stop_geo_index+1]
        
    print "Sorting images by timestamp."
    geo_images = sorted(geo_images, key=lambda image: image.image_time)
    
    geo_images, missing_image_count = verify_geo_images(geo_images, image_filenames)
           
    if missing_image_count > 0:
        print "Warning {} geo images do not exist and will be skipped.".format(missing_image_count)

    if len(geo_images) == 0:
        print "No images match up with any geo images. Exiting."
        return ExitReason.no_geo_images

    missed_code_finder = MissedCodeFinder()
    code_finder = CodeFinder(code_min_size, code_max_size, missed_code_finder)
    
    ImageWriter.level = ImageWriter.NORMAL
    
    # Write images out to subdirectory to keep separated from pickled results.
    image_out_directory = os.path.join(out_directory, 'images/')
    if not os.path.exists(image_out_directory):
        os.makedirs(image_out_directory)

    # Find and extract all codes from images.
    codes = []
    try:
        for i, geo_image in enumerate(geo_images):
            print "Analyzing image {} [{}/{}]".format(geo_image.file_name, i+1, len(geo_images))
            newly_found_codes = process_geo_image(geo_image, [code_finder], image_directory, image_out_directory, use_marked_image)
            geo_image.items["codes"] = newly_found_codes
            for code in newly_found_codes:
                print "Found {}: {}".format(code.type, code.name)
            codes += newly_found_codes
    except KeyboardInterrupt:
        print "\nKeyboard interrupt detected."
        answer = raw_input("\nType y to save results or anything else to quit: ").strip()
        if answer.lower() != 'y':
            return ExitReason.user_interrupt
        
    # Write possibly missed codes out to separate directory
    missed_codes_out_directory = os.path.join(out_directory, 'missed_codes_{}/'.format(postfix_id))
    if not os.path.exists(missed_codes_out_directory):
        os.makedirs(missed_codes_out_directory)
        
    print "Writing out missed codes"
    missed_code_filename = "missed_codes_{}.txt".format(postfix_id)
    missed_code_finder.write_out_missed_codes(codes, missed_code_filename, missed_codes_out_directory)
  
    dump_filename = "stage1_output_{}_{}_{}.s1".format(postfix_id, int(geo_images[0].image_time), int(geo_image.image_time))
    print "Serializing {} geo images and {} codes to {}.".format(len(geo_images), len(codes), dump_filename)
    pickle_results(dump_filename, out_directory, geo_images, codes)
    
    # Display code stats for user.
    merged_codes = merge_items(codes, max_distance=500)
    if len(merged_codes) == 0:
        print "No codes found."
    else:
        print "There were {} codes found and {} were unique.  Average code is in {} images.".format(len(codes), len(merged_codes), float(len(codes)) / len(merged_codes))
        print "Merged codes not being saved.  Just for user information."

    # Write arguments out to file for archiving purposes.
    args_filename = "stage1_args_{}_{}_{}.csv".format(postfix_id, int(geo_images[0].image_time), int(geo_image.image_time))
    write_args_to_file(args_filename, out_directory, args_copy)
        
    return ExitReason.success