Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ test_perf.py
*.jpeg
*.jpg
*.tiff
CLAUDE.md
112 changes: 103 additions & 9 deletions Granny/Analyses/BlushColor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down
119 changes: 113 additions & 6 deletions Granny/Analyses/PeelColor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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)
)
Expand Down
Loading