diff --git a/.gitignore b/.gitignore index 02c7566..8b5b402 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ test_perf.py *.jpeg *.jpg *.tiff +CLAUDE.md diff --git a/Granny/Analyses/BlushColor.py b/Granny/Analyses/BlushColor.py index aeb50d3..d6231fd 100644 --- a/Granny/Analyses/BlushColor.py +++ b/Granny/Analyses/BlushColor.py @@ -103,8 +103,102 @@ def __init__(self): self.threshold.setValue(148) self.threshold.setIsRequired(False) - # adds threshold to the parameter input list - self.addInParam(self.threshold) + # Fruit detection threshold parameter (B channel in LAB space) + self.fruit_threshold = IntValue( + "fruit_threshold", + "fruit_threshold", + "Threshold for fruit pixel detection using the B channel in LAB color space. " + + "Pixels with B channel values > this threshold are considered fruit. " + + "Range is 0 to 255, default is 140.", + ) + self.fruit_threshold.setMin(0) + self.fruit_threshold.setMax(255) + self.fruit_threshold.setValue(140) + self.fruit_threshold.setIsRequired(False) + + # Visualization parameters + self.blush_color_r = IntValue( + "blush_color_r", + "blush_color_r", + "Red component of blush mask color (BGR format). Range is 0 to 255, default is 150.", + ) + self.blush_color_r.setMin(0) + self.blush_color_r.setMax(255) + self.blush_color_r.setValue(150) + self.blush_color_r.setIsRequired(False) + + self.blush_color_g = IntValue( + "blush_color_g", + "blush_color_g", + "Green component of blush mask color (BGR format). Range is 0 to 255, default is 55.", + ) + self.blush_color_g.setMin(0) + self.blush_color_g.setMax(255) + self.blush_color_g.setValue(55) + self.blush_color_g.setIsRequired(False) + + self.blush_color_b = IntValue( + "blush_color_b", + "blush_color_b", + "Blue component of blush mask color (BGR format). Range is 0 to 255, default is 50.", + ) + self.blush_color_b.setMin(0) + self.blush_color_b.setMax(255) + self.blush_color_b.setValue(50) + self.blush_color_b.setIsRequired(False) + + self.text_x = IntValue( + "text_x", + "text_x", + "X coordinate for text position on output image. Default is 20.", + ) + self.text_x.setMin(0) + self.text_x.setMax(5000) + self.text_x.setValue(20) + self.text_x.setIsRequired(False) + + self.text_y = IntValue( + "text_y", + "text_y", + "Y coordinate for text position on output image. Default is 50.", + ) + self.text_y.setMin(0) + self.text_y.setMax(5000) + self.text_y.setValue(50) + self.text_y.setIsRequired(False) + + self.font_scale = FloatValue( + "font_scale", + "font_scale", + "Font scale for text labels on output images. Default is 1.0.", + ) + self.font_scale.setMin(0.1) + self.font_scale.setMax(10.0) + self.font_scale.setValue(1.0) + self.font_scale.setIsRequired(False) + + self.text_thickness = IntValue( + "text_thickness", + "text_thickness", + "Thickness of text labels in pixels. Default is 3.", + ) + self.text_thickness.setMin(1) + self.text_thickness.setMax(50) + self.text_thickness.setValue(3) + self.text_thickness.setIsRequired(False) + + # adds thresholds to the parameter input list + self.addInParam( + self.threshold, + self.fruit_threshold, + self.blush_color_r, + self.blush_color_g, + self.blush_color_b, + self.text_x, + self.text_y, + self.font_scale, + self.text_thickness, + ) def _calculateBlush( self, img: NDArray[np.uint8] @@ -126,21 +220,21 @@ def _calculateBlush( # create thresholded matrices blush_threshold = self.threshold.getValue() - fruit_px = lab_img[:, :, 2] > 140 + fruit_px = lab_img[:, :, 2] > self.fruit_threshold.getValue() blush_px = lab_img[:, :, 1] > blush_threshold - new_img[:, :, 0][blush_px] = 150 - new_img[:, :, 1][blush_px] = 55 - new_img[:, :, 2][blush_px] = 50 + new_img[:, :, 0][blush_px] = self.blush_color_r.getValue() + new_img[:, :, 1][blush_px] = self.blush_color_g.getValue() + new_img[:, :, 2][blush_px] = self.blush_color_b.getValue() blush_pct = 100 * blush_px.sum() / fruit_px.sum() cv2.putText( new_img, "Blush: " + str(blush_pct.round(1)) + "%", - (20, 50), + (self.text_x.getValue(), self.text_y.getValue()), fontFace=cv2.FONT_HERSHEY_SIMPLEX, - fontScale=1, + fontScale=self.font_scale.getValue(), color=(0, 0, 255), - thickness=3, + thickness=self.text_thickness.getValue(), ) return blush_px.sum() / fruit_px.sum(), new_img diff --git a/Granny/Analyses/PeelColor.py b/Granny/Analyses/PeelColor.py index 3e14a86..2d5d737 100644 --- a/Granny/Analyses/PeelColor.py +++ b/Granny/Analyses/PeelColor.py @@ -55,6 +55,103 @@ def __init__(self): "input", "input", "The directory where input images are located." ) self.input_images.setIsRequired(True) + + # Purple removal threshold parameter + self.purple_threshold = IntValue( + "purple_threshold", + "purple_threshold", + "Threshold for removing purple background/tray pixels using YCrCb color space. " + + "Pixels with Cb channel <= this value are kept. Range is 0 to 255, default is 126.", + ) + self.purple_threshold.setMin(0) + self.purple_threshold.setMax(255) + self.purple_threshold.setValue(126) + self.purple_threshold.setIsRequired(False) + + # Lightness minimum parameter + self.lightness_min = IntValue( + "lightness_min", + "lightness_min", + "Minimum lightness value for peel color detection in LAB color space. " + + "Range is 0 to 255, default is 0.", + ) + self.lightness_min.setMin(0) + self.lightness_min.setMax(255) + self.lightness_min.setValue(0) + self.lightness_min.setIsRequired(False) + + # Lightness maximum parameter + self.lightness_max = IntValue( + "lightness_max", + "lightness_max", + "Maximum lightness value for peel color detection in LAB color space. " + + "Range is 0 to 255, default is 255.", + ) + self.lightness_max.setMin(0) + self.lightness_max.setMax(255) + self.lightness_max.setValue(255) + self.lightness_max.setIsRequired(False) + + # Green channel minimum parameter + self.green_min = IntValue( + "green_min", + "green_min", + "Minimum green channel value for peel color detection in LAB color space. " + + "Range is 0 to 255, default is 0.", + ) + self.green_min.setMin(0) + self.green_min.setMax(255) + self.green_min.setValue(0) + self.green_min.setIsRequired(False) + + # Green channel maximum parameter + self.green_max = IntValue( + "green_max", + "green_max", + "Maximum green channel value for peel color detection in LAB color space. " + + "Range is 0 to 255, default is 128.", + ) + self.green_max.setMin(0) + self.green_max.setMax(255) + self.green_max.setValue(128) + self.green_max.setIsRequired(False) + + # Yellow channel minimum parameter + self.yellow_min = IntValue( + "yellow_min", + "yellow_min", + "Minimum yellow channel value for peel color detection in LAB color space. " + + "Range is 0 to 255, default is 128.", + ) + self.yellow_min.setMin(0) + self.yellow_min.setMax(255) + self.yellow_min.setValue(128) + self.yellow_min.setIsRequired(False) + + # Yellow channel maximum parameter + self.yellow_max = IntValue( + "yellow_max", + "yellow_max", + "Maximum yellow channel value for peel color detection in LAB color space. " + + "Range is 0 to 255, default is 255.", + ) + self.yellow_max.setMin(0) + self.yellow_max.setMax(255) + self.yellow_max.setValue(255) + self.yellow_max.setIsRequired(False) + + # Normalization lightness parameter + self.normalize_lightness = IntValue( + "normalize_lightness", + "normalize_lightness", + "Target lightness value for color normalization in LAB space. " + + "Range is 0 to 100, default is 50.", + ) + self.normalize_lightness.setMin(0) + self.normalize_lightness.setMax(100) + self.normalize_lightness.setValue(50) + self.normalize_lightness.setIsRequired(False) + self.output_images = ImageListValue( "output", "output", @@ -67,7 +164,17 @@ def __init__(self): datetime.now().strftime("%Y-%m-%d-%H-%M"), ) self.output_images.setValue(result_dir) - self.addInParam(self.input_images) + self.addInParam( + self.input_images, + self.purple_threshold, + self.lightness_min, + self.lightness_max, + self.green_min, + self.green_max, + self.yellow_min, + self.yellow_max, + self.normalize_lightness, + ) # sets up output result directory self.output_results = MetaDataValue( @@ -138,7 +245,7 @@ def remove_purple(self, img: NDArray[np.uint8]) -> NDArray[np.uint8]: # create binary matrices threshold_1 = np.logical_and((ycc_img[:, :, 0] >= 0), (ycc_img[:, :, 0] <= 255)) threshold_2 = np.logical_and((ycc_img[:, :, 1] >= 0), (ycc_img[:, :, 1] <= 255)) - threshold_3 = np.logical_and((ycc_img[:, :, 2] >= 0), (ycc_img[:, :, 2] <= 126)) + threshold_3 = np.logical_and((ycc_img[:, :, 2] >= 0), (ycc_img[:, :, 2] <= self.purple_threshold.getValue())) # combine to one matrix th123 = np.logical_and( @@ -166,9 +273,9 @@ def get_green_yellow_values( lab_img = cast(NDArray[np.uint8], cv2.cvtColor(img, cv2.COLOR_BGR2LAB)) # create binary matrices - threshold_1 = np.logical_and((lab_img[:, :, 0] > 0), (lab_img[:, :, 0] < 255)) - threshold_2 = np.logical_and((lab_img[:, :, 1] > 0), (lab_img[:, :, 1] < 128)) - threshold_3 = np.logical_and((lab_img[:, :, 2] > 128), (lab_img[:, :, 2] < 255)) + threshold_1 = np.logical_and((lab_img[:, :, 0] > self.lightness_min.getValue()), (lab_img[:, :, 0] < self.lightness_max.getValue())) + threshold_2 = np.logical_and((lab_img[:, :, 1] > self.green_min.getValue()), (lab_img[:, :, 1] < self.green_max.getValue())) + threshold_3 = np.logical_and((lab_img[:, :, 2] > self.yellow_min.getValue()), (lab_img[:, :, 2] < self.yellow_max.getValue())) # combine to one matrix th123 = np.logical_and( @@ -188,7 +295,7 @@ def get_green_yellow_values( # normalize by shifting point in the spherical coordinates radius = np.sqrt(mean_l**2 + mean_a**2 + mean_b**2) - scaled_l = 50 + scaled_l = self.normalize_lightness.getValue() scaled_a = np.sign(mean_a) * np.sqrt( np.abs(radius**2 - scaled_l**2) / (1 + (mean_b / mean_a) ** 2) ) diff --git a/Granny/Analyses/Segmentation.py b/Granny/Analyses/Segmentation.py index e442c31..a4abfe6 100644 --- a/Granny/Analyses/Segmentation.py +++ b/Granny/Analyses/Segmentation.py @@ -34,6 +34,7 @@ from Granny.Models.Values.FileNameValue import FileNameValue from Granny.Models.Values.FloatValue import FloatValue from Granny.Models.Values.ImageListValue import ImageListValue +from Granny.Models.Values.IntValue import IntValue from numpy.typing import NDArray @@ -130,6 +131,99 @@ def __init__(self): "input", "input", "The directory where input images are located." ) self.input_images.setIsRequired(True) + + # YOLO confidence threshold parameter + self.conf_threshold = FloatValue( + "conf", + "confidence", + "Confidence threshold for YOLO detections. Only detections with confidence " + + "scores above this threshold will be kept. Range is 0.0 to 1.0, " + + "default is 0.25.", + ) + self.conf_threshold.setMin(0.0) + self.conf_threshold.setMax(1.0) + self.conf_threshold.setValue(0.25) + self.conf_threshold.setIsRequired(False) + + # YOLO IOU threshold parameter + self.iou_threshold = FloatValue( + "iou", + "iou_threshold", + "Intersection over Union (IOU) threshold for non-maximum suppression. " + + "Used to filter overlapping detections. Range is 0.0 to 1.0, " + + "default is 0.45.", + ) + self.iou_threshold.setMin(0.0) + self.iou_threshold.setMax(1.0) + self.iou_threshold.setValue(0.45) + self.iou_threshold.setIsRequired(False) + + # Visualization parameters + self.mask_alpha = FloatValue( + "mask_alpha", + "mask_alpha", + "Alpha transparency value for mask overlay on output images. " + + "Range is 0.0 (transparent) to 1.0 (opaque), default is 0.5.", + ) + self.mask_alpha.setMin(0.0) + self.mask_alpha.setMax(1.0) + self.mask_alpha.setValue(0.5) + self.mask_alpha.setIsRequired(False) + + self.color_brightness = FloatValue( + "color_brightness", + "color_brightness", + "Brightness value for mask colors in HSV color space. " + + "Range is 0.0 (dark) to 1.0 (bright), default is 1.0.", + ) + self.color_brightness.setMin(0.0) + self.color_brightness.setMax(1.0) + self.color_brightness.setValue(1.0) + self.color_brightness.setIsRequired(False) + + self.bbox_thickness = IntValue( + "bbox_thickness", + "bbox_thickness", + "Thickness of bounding box lines in pixels. Default is 5.", + ) + self.bbox_thickness.setMin(1) + self.bbox_thickness.setMax(50) + self.bbox_thickness.setValue(5) + self.bbox_thickness.setIsRequired(False) + + self.font_scale = FloatValue( + "font_scale", + "font_scale", + "Font scale for text labels on output images. Default is 2.0.", + ) + self.font_scale.setMin(0.1) + self.font_scale.setMax(10.0) + self.font_scale.setValue(2.0) + self.font_scale.setIsRequired(False) + + self.text_thickness = IntValue( + "text_thickness", + "text_thickness", + "Thickness of text labels in pixels. Default is 3.", + ) + self.text_thickness.setMin(1) + self.text_thickness.setMax(50) + self.text_thickness.setValue(3) + self.text_thickness.setIsRequired(False) + + # Sorting/grouping parameter + self.row_tolerance = IntValue( + "row_tolerance", + "row_tolerance", + "Row grouping tolerance factor. Fruits are grouped into rows when their y-centers " + + "differ by more than height/row_tolerance pixels. Smaller values = looser grouping, " + + "larger values = tighter grouping. Default is 20.", + ) + self.row_tolerance.setMin(1) + self.row_tolerance.setMax(100) + self.row_tolerance.setValue(20) + self.row_tolerance.setIsRequired(False) + self.seg_images = ImageListValue( "seg_img", "segmented_images", @@ -173,18 +267,18 @@ def __init__(self): ) ) - # Add confidence threshold parameter for YOLO predictions - self.confidence_threshold = FloatValue( - "conf_threshold", - "confidence_threshold", - "Minimum confidence threshold for YOLO detections. Objects with confidence below this value will be filtered out." + self.addInParam( + self.model, + self.input_images, + self.conf_threshold, + self.iou_threshold, + self.mask_alpha, + self.color_brightness, + self.bbox_thickness, + self.font_scale, + self.text_thickness, + self.row_tolerance, ) - self.confidence_threshold.setValue(0.25) # YOLO default - self.confidence_threshold.setMin(0.0) - self.confidence_threshold.setMax(1.0) - self.confidence_threshold.setIsRequired(False) - - self.addInParam(self.model, self.input_images, self.confidence_threshold) def _getModelUrl(self, model_name: str): """ @@ -231,9 +325,13 @@ def _segmentInstances(self, image: NDArray[np.uint8]) -> List[Any]: including: masks, boxes, xyxy's, classes, confident scores """ # detects instances on the image - # Get confidence threshold from parameters - conf_threshold = self.in_params.get(self.confidence_threshold.getName()).getValue() - results = self.segmentation_model.predict(image, retina_masks=True, conf=conf_threshold) # type: ignore + + results = self.segmentation_model.predict( + image, + retina_masks=True, + conf=self.conf_threshold.getValue(), + iou=self.iou_threshold.getValue() + ) # type: ignore return results @@ -265,9 +363,9 @@ def _extractMaskedImage(self, tray_image: Image) -> Image: img = tray_image.getImage() result = img.copy() - alpha = 0.5 + alpha = self.mask_alpha.getValue() num_instances = masks.shape[0] - brightness = 1.0 + brightness = self.color_brightness.getValue() hsv = [(i / num_instances, 1, brightness) for i in range(num_instances)] colors = list(map(lambda c: colorsys.hsv_to_rgb(*c), hsv)) random.shuffle(colors) @@ -283,15 +381,15 @@ def _extractMaskedImage(self, tray_image: Image) -> Image: x1, y1, x2, y2, _, _ = sorted_boxes[i] x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) - cv2.rectangle(result, (x1, y1), (x2, y2), (r * 255, g * 255, b * 255), 5) + cv2.rectangle(result, (x1, y1), (x2, y2), (r * 255, g * 255, b * 255), self.bbox_thickness.getValue()) cv2.putText( result, "{:2.0f}-{:.3f}".format(i, confs[i]), (x1, y1), fontFace=cv2.FONT_HERSHEY_SIMPLEX, - fontScale=2, + fontScale=self.font_scale.getValue(), color=(255, 255, 255), - thickness=3, + thickness=self.text_thickness.getValue(), ) image_instance: Image = RGBImage( pathlib.Path(tray_image.getImageName()).stem + f"_masked_image" + ".png" @@ -327,7 +425,7 @@ def _sortInstances(self, boxes: NDArray[np.float32], img_shape: Tuple[int, int]) df["apple_id"] = 0 df["nums"] = df.index df = df.sort_values("ycenter", ascending=True).reset_index(drop=True) - df["rows"] = (df["ycenter"].diff().abs().gt(h // 20).cumsum() + 1).fillna(1).astype(int) + df["rows"] = (df["ycenter"].diff().abs().gt(h // self.row_tolerance.getValue()).cumsum() + 1).fillna(1).astype(int) df_list: List[pd.DataFrame] = [] apple_id = 1 diff --git a/Granny/Analyses/StarchArea.py b/Granny/Analyses/StarchArea.py index 5a531ef..87e7cea 100644 --- a/Granny/Analyses/StarchArea.py +++ b/Granny/Analyses/StarchArea.py @@ -26,6 +26,7 @@ from Granny.Models.IO.RGBImageFile import RGBImageFile from Granny.Models.Values.FloatValue import FloatValue from Granny.Models.Values.ImageListValue import ImageListValue +from Granny.Models.Values.IntValue import IntValue from Granny.Models.Values.MetaDataValue import MetaDataValue from numpy.typing import NDArray @@ -188,6 +189,45 @@ def __init__(self): "input", "input", "The directory where input images are located." ) self.input_images.setIsRequired(True) + + # Starch threshold parameter + self.starch_threshold = IntValue( + "starch_threshold", + "starch_threshold", + "Threshold value for starch detection. Pixels with gray values <= this threshold " + + "are considered starch. Lower values detect only darker starch regions, higher " + + "values include lighter regions. Range is 0 to 255, default is 172.", + ) + self.starch_threshold.setMin(0) + self.starch_threshold.setMax(255) + self.starch_threshold.setValue(172) + self.starch_threshold.setIsRequired(False) + + # Gaussian blur kernel size parameter + self.blur_kernel = IntValue( + "blur_kernel", + "blur_kernel", + "Size of the Gaussian blur kernel for noise reduction preprocessing. " + + "Must be an odd positive integer. Larger values produce more smoothing. " + + "Default is 7 (creates a 7x7 kernel).", + ) + self.blur_kernel.setMin(1) + self.blur_kernel.setMax(99) + self.blur_kernel.setValue(7) + self.blur_kernel.setIsRequired(False) + + # Visualization parameter + self.mask_alpha = FloatValue( + "mask_alpha", + "mask_alpha", + "Alpha transparency value for starch mask overlay on output images. " + + "Range is 0.0 (transparent) to 1.0 (opaque), default is 0.6.", + ) + self.mask_alpha.setMin(0.0) + self.mask_alpha.setMax(1.0) + self.mask_alpha.setValue(0.6) + self.mask_alpha.setIsRequired(False) + self.output_images = ImageListValue( "output", "output", @@ -200,7 +240,7 @@ def __init__(self): datetime.now().strftime("%Y-%m-%d-%H-%M"), ) self.output_images.setValue(result_dir) - self.addInParam(self.input_images) + self.addInParam(self.input_images, self.starch_threshold, self.blur_kernel, self.mask_alpha) # sets up output result directory self.output_results = MetaDataValue( @@ -222,7 +262,7 @@ def _drawMask( """ result = img.copy() color = (0, 0, 0) - alpha = 0.6 + alpha = self.mask_alpha.getValue() for c in range(3): result[:, :, c] = np.where( mask == 0, @@ -282,7 +322,8 @@ def adjustImage( new_img = img.copy() # blurs the image to remove sharp noises, then converts it to gray scale - img = cast(NDArray[np.uint8], cv2.GaussianBlur(img, (7, 7), 0)) + kernel_size = self.blur_kernel.getValue() + img = cast(NDArray[np.uint8], cv2.GaussianBlur(img, (kernel_size, kernel_size), 0)) gray = cast(NDArray[np.uint8], cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) # re-adjusts the image to [0 255] @@ -290,7 +331,7 @@ def adjustImage( gray = adjustImage(gray, low, high) # create thresholded matrices - image_threshold = 172 + image_threshold = self.starch_threshold.getValue() mask = np.logical_and((gray > 0), (gray <= image_threshold)).astype(np.uint8) # creates new image using threshold matrices diff --git a/Granny/Analyses/SuperficialScald.py b/Granny/Analyses/SuperficialScald.py index 49d8a9c..1b08c5e 100644 --- a/Granny/Analyses/SuperficialScald.py +++ b/Granny/Analyses/SuperficialScald.py @@ -53,6 +53,79 @@ def __init__(self): "input", "input", "The directory where input images are located." ) self.input_images.setIsRequired(True) + + # Morphological kernel size parameter + self.morph_kernel = IntValue( + "morph_kernel", + "morph_kernel", + "Size of the morphological (ellipse) kernel for mask smoothing operations. " + + "Larger values produce more smoothing. Default is 10 (creates a 10x10 kernel).", + ) + self.morph_kernel.setMin(1) + self.morph_kernel.setMax(99) + self.morph_kernel.setValue(10) + self.morph_kernel.setIsRequired(False) + + # Minimum threshold parameter + self.min_threshold = IntValue( + "min_threshold", + "min_threshold", + "Minimum threshold value for scald detection. Pixels below this threshold " + + "are considered potential scald regions. Range is 0 to 255, default is 100.", + ) + self.min_threshold.setMin(0) + self.min_threshold.setMax(255) + self.min_threshold.setValue(100) + self.min_threshold.setIsRequired(False) + + # Purple threshold parameter + self.purple_threshold = IntValue( + "purple_threshold", + "purple_threshold", + "Threshold for removing purple background/tray pixels using YCrCb color space. " + + "Pixels with Cb channel <= this value are kept. Range is 0 to 255, default is 126.", + ) + self.purple_threshold.setMin(0) + self.purple_threshold.setMax(255) + self.purple_threshold.setValue(126) + self.purple_threshold.setIsRequired(False) + + # Gaussian blur kernel parameter + self.blur_kernel = IntValue( + "blur_kernel", + "blur_kernel", + "Size of the Gaussian blur kernel for image smoothing. Must be an odd positive " + + "integer. Default is 3 (creates a 3x3 kernel).", + ) + self.blur_kernel.setMin(1) + self.blur_kernel.setMax(99) + self.blur_kernel.setValue(3) + self.blur_kernel.setIsRequired(False) + + # Histogram range factor parameter + self.hist_factor = FloatValue( + "hist_factor", + "hist_factor", + "Fraction of histogram range to subtract from threshold calculation. " + + "Range is 0.0 to 1.0, default is 0.333 (1/3).", + ) + self.hist_factor.setMin(0.0) + self.hist_factor.setMax(1.0) + self.hist_factor.setValue(0.333) + self.hist_factor.setIsRequired(False) + + # Histogram analysis parameter + self.hist_top_n = IntValue( + "hist_top_n", + "hist_top_n", + "Number of top histogram values to consider for threshold calculation. " + + "Default is 10.", + ) + self.hist_top_n.setMin(1) + self.hist_top_n.setMax(100) + self.hist_top_n.setValue(10) + self.hist_top_n.setIsRequired(False) + self.output_images = ImageListValue( "output", "output", @@ -65,7 +138,15 @@ def __init__(self): datetime.now().strftime("%Y-%m-%d-%H-%M"), ) self.output_images.setValue(result_dir) - self.addInParam(self.input_images) + self.addInParam( + self.input_images, + self.morph_kernel, + self.min_threshold, + self.purple_threshold, + self.blur_kernel, + self.hist_factor, + self.hist_top_n, + ) # sets up output result directory self.output_results = MetaDataValue( @@ -87,8 +168,9 @@ def _smoothMask(self, bin_mask: NDArray[np.uint8]) -> NDArray[np.uint8]: """ bin_mask = bin_mask - # create a circular structuring element of size 10 - ksize = (10, 10) + # create a circular structuring element + kernel_size = self.morph_kernel.getValue() + ksize = (kernel_size, kernel_size) strel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, ksize=ksize) # using to structuring element to perform one close and one open operation on the binary mask @@ -125,9 +207,11 @@ def _removeScald( def _calculate_threshold_from_hist(hist: NDArray[np.int8]) -> int: hist_range = 255 - (hist[::-1] != 0).argmax() - (hist != 0).argmax() - threshold = np.max(np.argsort(hist)[-10:]) - threshold = int(threshold - 1 / 3 * hist_range) - threshold = 100 if threshold < 100 else int(threshold) + top_n = self.hist_top_n.getValue() + threshold = np.max(np.argsort(hist)[-top_n:]) + threshold = int(threshold - self.hist_factor.getValue() * hist_range) + min_thresh = self.min_threshold.getValue() + threshold = min_thresh if threshold < min_thresh else int(threshold) return threshold # create binary matrices @@ -169,7 +253,7 @@ def _removeTrayResidue(self, img: NDArray[np.uint8]) -> NDArray[np.uint8]: # create binary matrices threshold_1 = np.logical_and((ycc_img[:, :, 0] >= 0), (ycc_img[:, :, 0] <= 255)) threshold_2 = np.logical_and((ycc_img[:, :, 1] >= 0), (ycc_img[:, :, 1] <= 255)) - threshold_3 = np.logical_and((ycc_img[:, :, 2] >= 0), (ycc_img[:, :, 2] <= 126)) + threshold_3 = np.logical_and((ycc_img[:, :, 2] >= 0), (ycc_img[:, :, 2] <= self.purple_threshold.getValue())) # combine to one matrix th123 = np.logical_and( @@ -201,7 +285,8 @@ def _score_image( nopurple_img = img.copy() # Image smoothing - img = cast(NDArray[np.uint8], cv2.GaussianBlur(img, (3, 3), sigmaX=0, sigmaY=0)) + blur_size = self.blur_kernel.getValue() + img = cast(NDArray[np.uint8], cv2.GaussianBlur(img, (blur_size, blur_size), sigmaX=0, sigmaY=0)) # Removal of scald regions bw, img = self._removeScald(img) diff --git a/docs/users_guide/parameter_adjustments.md b/docs/users_guide/parameter_adjustments.md index 63bfbc6..498ab27 100644 --- a/docs/users_guide/parameter_adjustments.md +++ b/docs/users_guide/parameter_adjustments.md @@ -4,59 +4,185 @@ This guide covers the configurable parameters available in Granny for fine-tunin ## Segmentation Parameters -### Confidence Threshold +### Analysis Parameters -The confidence threshold parameter allows you to control the quality and quantity of fruit detections during segmentation analysis. +#### Confidence Threshold (`--conf`) +- **Default:** 0.25 | **Range:** 0.0 - 1.0 | **Type:** Float +- Controls YOLO detection confidence. Higher values = fewer, more accurate detections. -**Parameter:** `--confidence_threshold` +#### IOU Threshold (`--iou`) +- **Default:** 0.45 | **Range:** 0.0 - 1.0 | **Type:** Float +- Intersection over Union threshold for non-maximum suppression. Controls how overlapping detections are filtered. -**Usage:** +#### Row Grouping Tolerance (`--row_tolerance`) +- **Default:** 20 | **Range:** 1 - 100 | **Type:** Integer +- Controls fruit row detection sensitivity. Fruits grouped into rows when y-centers differ by more than height/row_tolerance pixels. -```bash -granny -i cli --analysis segmentation --confidence_threshold 0.8 --input /path/to/images -``` +### Visualization Parameters -**Details:** +#### Mask Alpha (`--mask_alpha`) +- **Default:** 0.5 | **Range:** 0.0 - 1.0 | **Type:** Float +- Transparency of mask overlay on output images (0.0 = transparent, 1.0 = opaque). -- **Default Value:** 0.25 -- **Range:** 0.0 - 1.0 -- **Type:** Float -- **Required:** No (uses default if not specified) +#### Color Brightness (`--color_brightness`) +- **Default:** 1.0 | **Range:** 0.0 - 1.0 | **Type:** Float +- Brightness value for mask colors in HSV color space. -**How it works:** +#### Bounding Box Thickness (`--bbox_thickness`) +- **Default:** 5 | **Range:** 1 - 50 | **Type:** Integer +- Thickness of bounding box lines in pixels. -The confidence threshold filters YOLO model predictions based on how confident the model is about each detection. Only detections with confidence scores above the threshold are kept for processing. +#### Font Scale (`--font_scale`) +- **Default:** 2.0 | **Range:** 0.1 - 10.0 | **Type:** Float +- Font scale for text labels on output images. +#### Text Thickness (`--text_thickness`) +- **Default:** 3 | **Range:** 1 - 50 | **Type:** Integer +- Thickness of text labels in pixels. -**Example Scenarios:** +--- -**High-Quality Analysis (Fewer, More Accurate Results):** +## Starch Analysis Parameters -```bash -granny -i cli --analysis segmentation --confidence_threshold 0.85 --input ./fruit_images/ -``` +### Analysis Parameters + +#### Starch Threshold (`--starch_threshold`) +- **Default:** 172 | **Range:** 0 - 255 | **Type:** Integer +- Pixels with gray values ≤ this threshold are considered starch. Lower = only darkest regions, higher = includes lighter regions. + +#### Blur Kernel (`--blur_kernel`) +- **Default:** 7 | **Range:** 1 - 99 (odd values recommended) | **Type:** Integer +- Gaussian blur kernel size for noise reduction. Larger values = more smoothing. + +### Visualization Parameters + +#### Mask Alpha (`--mask_alpha`) +- **Default:** 0.6 | **Range:** 0.0 - 1.0 | **Type:** Float +- Transparency of starch mask overlay on output images. + +--- + +## Blush Color Analysis Parameters + +### Analysis Parameters + +#### Blush Threshold (`--th`) +- **Default:** 148 | **Range:** 0 - 255 | **Type:** Integer +- A channel threshold in LAB color space for blush detection. + +#### Fruit Threshold (`--fruit_threshold`) +- **Default:** 140 | **Range:** 0 - 255 | **Type:** Integer +- B channel threshold in LAB color space for fruit pixel detection. + +### Visualization Parameters + +#### Blush Color RGB (`--blush_color_r`, `--blush_color_g`, `--blush_color_b`) +- **Defaults:** R=150, G=55, B=50 | **Range:** 0 - 255 each | **Type:** Integer +- RGB color values for blush mask overlay (BGR format). + +#### Text Position (`--text_x`, `--text_y`) +- **Defaults:** X=20, Y=50 | **Range:** 0 - 5000 | **Type:** Integer +- X and Y coordinates for text position on output images. + +#### Font Scale (`--font_scale`) +- **Default:** 1.0 | **Range:** 0.1 - 10.0 | **Type:** Float +- Font scale for text labels. + +#### Text Thickness (`--text_thickness`) +- **Default:** 3 | **Range:** 1 - 50 | **Type:** Integer +- Thickness of text labels in pixels. + +--- + +## Superficial Scald Analysis Parameters + +### Analysis Parameters + +#### Morphological Kernel (`--morph_kernel`) +- **Default:** 10 | **Range:** 1 - 99 | **Type:** Integer +- Size of ellipse kernel for morphological operations. Larger = more smoothing. -**Maximum Coverage (More Detections, Some False Positives):** +#### Minimum Threshold (`--min_threshold`) +- **Default:** 100 | **Range:** 0 - 255 | **Type:** Integer +- Minimum threshold for scald detection. Pixels below this are potential scald regions. +#### Purple Threshold (`--purple_threshold`) +- **Default:** 126 | **Range:** 0 - 255 | **Type:** Integer +- Threshold for removing purple background/tray pixels using YCrCb color space. + +#### Blur Kernel (`--blur_kernel`) +- **Default:** 3 | **Range:** 1 - 99 (odd values recommended) | **Type:** Integer +- Gaussian blur kernel size for image smoothing. + +#### Histogram Factor (`--hist_factor`) +- **Default:** 0.333 | **Range:** 0.0 - 1.0 | **Type:** Float +- Fraction of histogram range to subtract from threshold calculation. + +#### Histogram Top N (`--hist_top_n`) +- **Default:** 10 | **Range:** 1 - 100 | **Type:** Integer +- Number of top histogram values to consider for threshold calculation. + +--- + +## Peel Color Analysis Parameters + +### Analysis Parameters + +#### Purple Threshold (`--purple_threshold`) +- **Default:** 126 | **Range:** 0 - 255 | **Type:** Integer +- Threshold for removing purple background/tray pixels using YCrCb color space. + +#### Lightness Range (`--lightness_min`, `--lightness_max`) +- **Defaults:** Min=0, Max=255 | **Range:** 0 - 255 | **Type:** Integer +- Lightness channel range in LAB color space for peel color detection. + +#### Green Range (`--green_min`, `--green_max`) +- **Defaults:** Min=0, Max=128 | **Range:** 0 - 255 | **Type:** Integer +- Green channel range in LAB color space for green detection. + +#### Yellow Range (`--yellow_min`, `--yellow_max`) +- **Defaults:** Min=128, Max=255 | **Range:** 0 - 255 | **Type:** Integer +- Yellow channel range in LAB color space for yellow detection. + +#### Normalize Lightness (`--normalize_lightness`) +- **Default:** 50 | **Range:** 0 - 100 | **Type:** Integer +- Target lightness value for color normalization in LAB space. + +--- + +## Usage Examples + +### Segmentation with Custom Parameters ```bash -granny -i cli --analysis segmentation --confidence_threshold 0.15 --input ./fruit_images/ +granny -i cli --analysis segmentation --input ./images/ --conf 0.35 --iou 0.6 --row_tolerance 15 ``` -**Testing Different Thresholds:** - -You can run the same image with different confidence thresholds to find the optimal setting for your specific images: +### Starch Analysis with Custom Threshold +```bash +granny -i cli --analysis starch --input ./segmented/ --starch_threshold 180 --blur_kernel 9 +``` +### Blush with Custom Visualization ```bash -# Conservative approach -granny -i cli --analysis segmentation --confidence_threshold 0.8 --input ./test_images/ +granny -i cli --analysis blush --input ./pears/ --th 150 --blush_color_r 200 --font_scale 1.5 +``` -# Default approach -granny -i cli --analysis segmentation --confidence_threshold 0.25 --input ./test_images/ +### Scald with Fine-Tuned Detection +```bash +granny -i cli --analysis scald --input ./apples/ --min_threshold 105 --hist_factor 0.4 --hist_top_n 15 +``` -# Liberal approach -granny -i cli --analysis segmentation --confidence_threshold 0.1 --input ./test_images/ +### Peel Color with Custom Ranges +```bash +granny -i cli --analysis color --input ./pears/ --green_max 125 --yellow_min 130 --normalize_lightness 55 ``` -Compare the number of detected fruits and manually verify the results to determine the best threshold for your use case. +--- + +## Tips -**Note:** This parameter only affects the segmentation analysis. Subsequent analysis steps (starch, color, etc.) will use whatever fruits were detected during segmentation. \ No newline at end of file +- **Start with defaults** and adjust incrementally based on your specific images +- **Visualization parameters** only affect output images, not analysis results +- **Analysis parameters** directly impact ratings and measurements +- Test different values on a small subset before processing large batches +- Document which parameters work best for your specific fruit varieties and imaging conditions