Skip to content

Commit 9ecab1f

Browse files
Nic822rursprung
authored andcommitted
identify banana ripeness
this gives a rough estimation of the ripeness of the banana, with 100% being considered "ripe", anything below it unripe and anything above it over-ripe. a very simple heuristic is used for this: if the banana is 100% yellow it is ripe, any green colour part is considered unripe and any brown colour part is considered over-ripe. thus the formula is simply: $1 - g + b$, with $g$ and $b$ being the green and brown parts of the whole banana. solves #8
1 parent d7701c6 commit 9ecab1f

File tree

2 files changed

+86
-10
lines changed

2 files changed

+86
-10
lines changed

include/banana-lib/lib.hpp

+48-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ namespace banana {
8585

8686
/// The length of the banana (in px).
8787
double length;
88+
89+
/**
90+
* Ripeness as a percentage (100% = 1.0):
91+
* * < 100%: not yet ripe
92+
* * = 100%: ripe
93+
* * > 100%: over-ripe
94+
*/
95+
float ripeness;
8896
};
8997

9098
/**
@@ -117,6 +125,22 @@ namespace banana {
117125
cv::Scalar const contour_annotation_color{0, 255, 0};
118126
/// Color used to annotate debug information on the analyzed image.
119127
cv::Scalar const helper_annotation_color{0, 0, 255};
128+
129+
/// Green color range used to filter the ripeness on the analyzed image.
130+
cv::Scalar const green_lower_threshold_color{35, 50, 50};
131+
cv::Scalar const green_upper_threshold_color{85, 255, 255};
132+
133+
/// Yellow color range used to filter the ripeness on the analyzed image.
134+
cv::Scalar const yellow_lower_threshold_color{20, 100, 100};
135+
cv::Scalar const yellow_upper_threshold_color{30, 255, 255};
136+
137+
/// Brown color range used to filter the ripeness on the analyzed image.
138+
cv::Scalar const brown_lower_threshold_color{10, 100, 20};
139+
cv::Scalar const brown_upper_threshold_color{20, 200, 100};
140+
141+
/// Color threshold used to filter the incoming colors on the analyzed image.
142+
cv::Scalar const filter_lower_threshold_color{0, 41, 0};
143+
cv::Scalar const filter_upper_threshold_color{177, 255, 255};
120144
};
121145

122146
explicit Analyzer(Settings settings = {});
@@ -156,11 +180,14 @@ namespace banana {
156180

157181
/**
158182
* filters the image for banana-related colors and returns a corresponding binary image.
183+
*
159184
* @param image the image to be filtered
185+
* @param low the lower bound which should be passed through - value must be in HSV!
186+
* @param high the upper bound which should be passed through - value must be in HSV!
160187
* @return binary image, which colours the matching pixels white, otherwise black
161188
*/
162189
[[nodiscard]]
163-
auto ColorFilter(cv::Mat const& image) const -> cv::Mat;
190+
auto ColorFilter(cv::Mat const& image, cv::Scalar low, cv::Scalar up) const -> cv::Mat;
164191

165192
/**
166193
* Checks whether the passed contour is - with a good likelihood - a banana.
@@ -240,6 +267,26 @@ namespace banana {
240267
[[nodiscard]]
241268
auto CalculateBananaLength(AnalysisResult::CenterLine const& center_line) const -> double;
242269

270+
/**
271+
* Extract the masked part of an image for the defined contour.
272+
* @param image the image from which the masked part should be extracted.
273+
* @param contour the contour defining the mask.
274+
* @return the masked image. all parts outside of the mask will be black.
275+
*/
276+
[[nodiscard]]
277+
auto GetMaskedImage(cv::Mat const& image, Contour const& contour) const -> cv::Mat;
278+
279+
/**
280+
* Identify the ripeness of the banana.
281+
* @param banana_image the image containing exactly the banana to be analysed (extracted using mask from original).
282+
* @return Ripeness as a percentage (100% = 1.0):
283+
* * < 100%: not yet ripe
284+
* * = 100%: ripe
285+
* * > 100%: over-ripe
286+
*/
287+
[[nodiscard]]
288+
auto IdentifyBananaRipeness(cv::Mat const& banana_image) const -> float;
289+
243290
/**
244291
* Analyse the banana.
245292
*

src/lib.cpp

+38-9
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ namespace banana {
5050
o << " Mean curvature = " << std::format("{:.2f}", banana.mean_curvature / 100) << " 1/cm"
5151
<< " (corresponds to a circle with radius = " << std::format("{:.2f}", 1/banana.mean_curvature * 100) << " cm)" << std::endl;
5252
o << " Length along center line = " << std::format("{:.2f}", banana.length * 100) << " cm" << std::endl;
53+
o << " ripeness= " << std::format("{:.0f}", banana.ripeness * 100) << " %" << std::endl;
5354
o << std::endl;
5455
}
5556

@@ -94,16 +95,12 @@ namespace banana {
9495
});
9596
}
9697

97-
auto Analyzer::ColorFilter(cv::Mat const& image) const -> cv::Mat {
98+
auto Analyzer::ColorFilter(cv::Mat const& image, cv::Scalar low, cv::Scalar up) const -> cv::Mat {
9899
cv::Mat hsvImage;
99100
cv::cvtColor(image, hsvImage, cv::COLOR_BGR2HSV);
100101

101-
// Define the range for colors in the HSV color space
102-
auto const lowerThreshold = cv::Scalar(0, 41, 0);
103-
auto const upperThreshold = cv::Scalar(177, 255, 255);
104-
105102
cv::Mat mask;
106-
cv::inRange(hsvImage, lowerThreshold, upperThreshold, mask);
103+
cv::inRange(hsvImage, low, up, mask);
107104

108105
return mask;
109106
}
@@ -113,7 +110,7 @@ namespace banana {
113110
}
114111

115112
auto Analyzer::FindBananaContours(cv::Mat const& image) const -> Contours {
116-
auto filtered_image = ColorFilter(image);
113+
auto filtered_image = ColorFilter(image, settings_.filter_lower_threshold_color, settings_.filter_upper_threshold_color);
117114
SHOW_DEBUG_IMAGE(filtered_image, "color filtered image");
118115

119116
// Removing noise
@@ -122,7 +119,7 @@ namespace banana {
122119
SHOW_DEBUG_IMAGE(filtered_image, "morph");
123120

124121
// Smooth the image
125-
cv::medianBlur(filtered_image, filtered_image, 37); // TODO: test again with 41
122+
cv::medianBlur(filtered_image, filtered_image, 37);
126123
SHOW_DEBUG_IMAGE(filtered_image, "blur");
127124

128125
Contours contours;
@@ -206,7 +203,6 @@ namespace banana {
206203

207204
auto const x = center_line.points_in_banana_coordsys
208205
| std::views::transform(&cv::Point2d::x);
209-
//| std::views::transform(px_to_m);
210206

211207
auto const calc_first_deriv = [coeff_1, coeff_2](auto const& x) -> auto {
212208
return 2 * coeff_2 * x + coeff_1;
@@ -240,6 +236,36 @@ namespace banana {
240236
return length_in_px / this->settings_.pixels_per_meter;
241237
}
242238

239+
auto Analyzer::GetMaskedImage(const cv::Mat& image, const Contour& contour) const -> cv::Mat {
240+
auto mask = cv::Mat{image.size(), CV_8UC3, cv::Scalar{255,255,255}};
241+
cv::drawContours(mask, std::vector{{contour}}, -1, {0,0,0}, cv::FILLED);
242+
SHOW_DEBUG_IMAGE(mask, "mask");
243+
cv::Mat masked;
244+
cv::bitwise_or(image, mask, masked);
245+
SHOW_DEBUG_IMAGE(masked, "filtered image (masked area only)");
246+
return masked;
247+
}
248+
249+
auto Analyzer::IdentifyBananaRipeness(const cv::Mat& banana_image) const -> float {
250+
/// mask for green, yellow and brown colors
251+
auto const green_mask = ColorFilter(banana_image, settings_.green_lower_threshold_color, settings_.green_upper_threshold_color);
252+
auto const yellow_mask = ColorFilter(banana_image, settings_.yellow_lower_threshold_color, settings_.yellow_upper_threshold_color);
253+
auto const brown_mask = ColorFilter(banana_image, settings_.brown_lower_threshold_color, settings_.brown_upper_threshold_color);
254+
255+
/// count pixels in the three color spaces
256+
auto const green_pixel_count = cv::countNonZero(green_mask);
257+
auto const yellow_pixel_count = cv::countNonZero(yellow_mask);
258+
auto const brown_pixel_count = cv::countNonZero(brown_mask);
259+
260+
auto const total_pixel_count = green_pixel_count + yellow_pixel_count + brown_pixel_count;
261+
float green_share = static_cast<float>(green_pixel_count) / (static_cast<float>(total_pixel_count)+1e-3f);
262+
float brown_share = static_cast<float>(brown_pixel_count) / (static_cast<float>(total_pixel_count)+1e-3f);
263+
264+
// assumption: if 100% is yellow we consider it ripe.
265+
// the more brown there is the riper it is, the more green there is the more unripe it is
266+
return 1 - green_share + brown_share;
267+
}
268+
243269
auto Analyzer::AnalyzeBanana(cv::Mat const& image, Contour const& banana_contour) const -> std::expected<AnalysisResult, AnalysisError> {
244270
auto const pca = this->GetPCA(banana_contour);
245271

@@ -256,13 +282,16 @@ namespace banana {
256282
.points_in_banana_coordsys = this->GetBananaCenterLine(rotated_contour, *coeffs),
257283
};
258284

285+
auto const banana_only = this->GetMaskedImage(image, banana_contour);
286+
259287
return AnalysisResult{
260288
.contour = banana_contour,
261289
.center_line = center_line,
262290
.rotation_angle = pca.angle,
263291
.estimated_center = pca.center,
264292
.mean_curvature = this->CalculateMeanCurvature(center_line),
265293
.length = this->CalculateBananaLength(center_line),
294+
.ripeness = this->IdentifyBananaRipeness(banana_only),
266295
};
267296
}
268297

0 commit comments

Comments
 (0)