Computer Vision - Using the MV1 to Automatically Measure Plants

After having finished the capstone project for this semester, I developed lambda code that would be applicable to be usable to apply to all of the s3 objects. I replaced the plantcv functions I used with skimage for speed, but I haven’t tested them yet to ensure they work. The session credentials probably wouldn’t be needed in lambda, so adjust that as needed.

Here is the code:

#!/usr/bin/env python3
import boto3
from botocore.client import Config as BotoConfig
from botocore.exceptions import ClientError, ResponseStreamingError
import numpy as np
import cv2
from copy import deepcopy
import configparser
from PIL import Image, UnidentifiedImageError, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
from io import BytesIO
from skimage import morphology
from skimage.measure import label

#doctor, are you sure this work?
#haha, I have no idea!
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Create a ConfigParser object
config = configparser.ConfigParser()

# Read in access key and secret key
config.read('/mnt/stor/ceph/csb/marsfarm/projects/aws_key/aws_key.cfg')

# Accessing the values
# Access a specific section
settings = config['Secrets']

#DO NOT PRINT secret access key or key id
session = boto3.Session(
    aws_access_key_id=settings['aws_access_key_id'],
    aws_secret_access_key=settings['aws_secret_access_key'],
)
config = BotoConfig(connect_timeout=120, read_timeout=300, retries={"max_attempts": 5, "mode": "standard"})
s3 = session.client('s3', config=config, verify=False)

def lambda_return(plant_pixels, MayHavePlant, number_of_plants):
    return {
        'statusCode': 200,
        'body': {
            'estimated_plant_area': plant_pixels,
            'MayHavePlant': MayHavePlant,
            'estimated_number_of_plants': number_of_plants
        }
    }  

def lambda_handler(event, context):
    bucket_name = event['bucket']
    key = event['key']

    # Retrieve image from S3
    try:
        response = s3.get_object(Bucket=bucket_name, Key=key)
        image_content = response['Body'].read()

        # Open the image using PIL
        #not using chunks, assuming it doesn't fail
        image = Image.open(BytesIO(image_content))
    except UnidentifiedImageError as e:
        print(f"{key} won't load")
        return lambda_return(0, 0, 0)
    except urllib3.exceptions.SSLError:
        print(f"SSL failed for {key}")
        return lambda_return(0, 0, 0)
    except ResponseStreamingError as e:
        print(f"ResponseStreamingError for {key}")
        return lambda_return(0, 0, 0)
    
    #convert to 3d numpy array
    image_np = np.array(image)
    
    # Convert to HSV and LAB color spaces
    hsv_image = cv2.cvtColor(image_np, cv2.COLOR_BGR2HSV)
    lab_image = cv2.cvtColor(image_np, cv2.COLOR_BGR2LAB)

    # Define HSV range for filtering using OpenCV
    # OpenCV uses 0-180 for Hue, so the values are halved
    hsv_min = np.array([int(28/2), int(20/100*255), int(20/100*255)])
    hsv_max = np.array([int(144/2), 255, 255])
    hsv_mask = cv2.inRange(hsv_image, hsv_min, hsv_max)

    # Define LAB range for filtering using OpenCV
    # OpenCV uses 0-255 for L, a*, and b*
    # Note: 'a' and 'b' ranges need to be shifted from [-128, 127] to [0, 255]
    # L is scaled from [0, 100] in LAB to [0, 255] in OpenCV
    lab_lower = np.array([int(10/100*255), 0, 132])
    lab_upper = np.array([int(90/100*255), 124, 255])
    lab_mask = cv2.inRange(lab_image, lab_lower, lab_upper)

    # Combine the masks (logical AND) and apply to the original image
    combined_mask = cv2.bitwise_and(hsv_mask, lab_mask)
    
    #remove small objects in the image so object detection won't be as bad
    denoised_mask = morphology.remove_small_objects(combined_mask, 500)

    #output mask to file
    #denoised_filename = image_folder / ("denoised_mask_" + Path(key).name)
    #if not denoised_filename.is_file(): cv2.imwrite(str(denoised_filename), denoised_mask)

    #count the number of nonzero pixels, determine if > 110k
    plant_pixels = cv2.countNonZero(denoised_mask)
    MayHavePlant = bool(plant_pixels > 110000)

    #create labels for masks that may have plants, count number of objects
    number_of_plants = 0
    if MayHavePlant:
        # Label connected regions
        labeled_mask = label(denoised_mask, connectivity=1)  # You can adjust connectivity (1 or 2)

        # Find the number of objects by ignoring the background (label 0)
        number_of_plants = len(np.unique(labeled_mask)) - 1  # Subtract one for the background label

    # Here, you might want to save or further process the result_image
    # For demonstration, let's just return the number of white pixels in the mask
    lambda_return(plant_pixels, MayHavePlant, number_of_plants)
    return plant_pixels, MayHavePlant, number_of_plants