技术原理与算法设计
流程图
开始
↓
用户上传草莓照片
↓
HSV 三区间 + Lab a* Otsu 分割草莓区域
↓
形态学处理:去果蒂(绿色)、去瑕疵(暗斑)
↓
是否检测到草莓?
否
回到"分割草莓区域",提示用户调整照片
是
逐颗草莓提取 42 维特征
↓
ML 模型是否可用?
否
颜色经验公式估算 Brix(降级通道)
是
随机森林回归模型预测 Brix
↓
按 Brix 排序编号 + 绘制标注框
↓
结束
本流程开始时,用户上传草莓照片后,系统先用 HSV 颜色空间三个阈值区间(深红、紫红、橙红)粗分割出候选草莓区域,再用 CIELab 的 a* 通道(红绿轴)做 Otsu 自动阈值分割作为双保险,两条通道合并得到草莓掩膜。随后通过形态学运算去除顶部的绿色果蒂和表面的暗色瑕疵。若未检测到草莓,则提示用户调整照片;若检测到,则对每颗草莓提取 42 维特征(颜色统计 18 维 + GLCM 纹理 16 维 + 形态 8 维)。特征送入随机森林回归模型预测 Brix 糖度值,模型文件不存在时自动降级为颜色经验公式。全部草莓按 Brix 从高到低排序编号,绘制标注框返回给前端展示。
软件基础设置信息
| 功能名称 | 草莓甜度检测系统(Strawberry Brix Detection System) |
| 适用场景 | 草莓采摘分级、农产品品质快速检测、水果检测教学演示 |
| 开发语言 | Python 3.10+ |
| 核心依赖 | Flask、OpenCV (cv2)、NumPy、scikit-image、scikit-learn、joblib |
| AI 模型 | 随机森林回归模型(42 维特征 → Brix 糖度),无模型时降级为颜色经验公式 |
软件系统设计
- HSV 三区间分割:深红 (H 0-10)、紫红 (H 160-180)、橙红 (H 10-25) 三个阈值区间取并集,配合椭圆核开运算(去噪点)+ 闭运算(填小洞),得到基础草莓掩膜。
- Lab a* Otsu 双保险:CIELab 的 a* 通道(红绿轴)归一化后做 Otsu 自动阈值,与 a* > 135 直接阈值取交集,两条通道都同意的像素才是高置信草莓——对光照变化比固定 HSV 阈值更抗揍。
- 果蒂与瑕疵去除:绿色果蒂(H 35-90)从掩膜中剔除,避免干扰颜色统计;Lab 亮度 L* < 35 的暗色区域标记为瑕疵不计入特征,避免烂斑误导甜度判断。面积小于 800 像素的碎片丢弃,最多保留 9 颗草莓。
- 42 维特征工程:颜色统计 18 维(RGB/HSV/Lab 三空间每通道均值+标准差)+ GLCM 纹理 16 维(对比度/同质性/能量/相关性 × 4 方向)+ 形态 8 维(面积/周长/圆度等)。
- 双通道甜度预测:优先用 joblib 加载的随机森林回归模型(含 StandardScaler 标准化),预测值截断到 0~20 Brix;模型文件缺失时自动降级为颜色经验公式(越红 + 越深 + 越鲜艳 - 绿色 = 越甜)。
- Unicode 中文路径兼容:图片以二进制读入后用 cv2.imdecode 解码,绕过 OpenCV imread 不支持中文文件名的坑。
- 上传安全:16MB 大小限制 + 格式白名单(JPG/PNG/BMP/TIF)+ UUID 随机文件名,防止重名覆盖和路径注入。
参数配置
| 字段 | 说明 |
| HSV 阈值 | 深红 (0,150,80)-(10,255,255)、紫红 (160,130,70)-(180,255,255)、橙红 (10,120,80)-(25,255,255),三区间并集 |
| Lab 分割 | a* 通道 Otsu 自动阈值 + a* > 135 直接阈值,取交集 |
| 果蒂去除 | HSV_GREEN (30,40,40)-(90,255,255),绿色区域从掩膜剔除 |
| 瑕疵去除 | LAB_BLEMISH_L_THRESHOLD = 35,L* 低于此值的暗斑不计入特征 |
| 最小草莓面积 | MIN_STRAWBERRY_AREA = 800 像素,过滤碎片区域 |
| GLCM 参数 | DISTANCES = [1,2],ANGLES = [0°,45°,90°,135°],256 灰度级 |
| 最大草莓数 | MAX_BERRIES = 9,超过时按面积取前 9 颗 |
| Brix 分级 | 极甜 ≥12 / 甜 10-12 / 酸甜 7-10 / 微甜 5-7 / 不太甜 <5 |
| 上传限制 | MAX_CONTENT_LENGTH = 16MB,支持 JPG/PNG/BMP/TIF |
| 模型路径 | models/sweetness_model.pkl + scaler.pkl,缺失时自动降级颜色公式 |
HSV 颜色阈值配置
# config.py — HSV 阈值(三区间并集,紧区间避免匹配到背景)
# Red range 1: pure deep red, high saturation
HSV_RED_LOWER1 = (0, 150, 80)
HSV_RED_UPPER1 = (10, 255, 255)
# Red range 2: purple-red
HSV_RED_LOWER2 = (160, 130, 70)
HSV_RED_UPPER2 = (180, 255, 255)
# Orange range: transitional strawberry
HSV_ORANGE_LOWER = (10, 120, 80)
HSV_ORANGE_UPPER = (25, 255, 255)
# Green threshold for sepal removal (H: 35-85)
HSV_GREEN_LOWER = (30, 40, 40)
HSV_GREEN_UPPER = (90, 255, 255)
# Dark threshold for blemish removal (Lab L*)
LAB_BLEMISH_L_THRESHOLD = 35
# Minimum area for valid strawberry region (pixels)
MIN_STRAWBERRY_AREA = 800
# GLCM parameters
GLCM_DISTANCES = [1, 2]
GLCM_ANGLES = [0, 0.785, 1.571, 2.356] # 0, 45, 90, 135 degrees
草莓分割模块
# src/preprocessing.py — HSV 分割 + Lab a* Otsu 双保险
def segment_strawberry_red(img_hsv):
"""Segment strawberry red/orange regions using HSV thresholds."""
mask1 = cv2.inRange(img_hsv, HSV_RED_LOWER1, HSV_RED_UPPER1)
mask2 = cv2.inRange(img_hsv, HSV_RED_LOWER2, HSV_RED_UPPER2)
mask3 = cv2.inRange(img_hsv, HSV_ORANGE_LOWER, HSV_ORANGE_UPPER)
red_mask = mask1 | mask2 | mask3
k3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
k5 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
red_mask = cv2.morphologyEx(red_mask, cv2.MORPH_OPEN, k3) # 去噪点
red_mask = cv2.morphologyEx(red_mask, cv2.MORPH_CLOSE, k5) # 填小洞
return red_mask
def segment_strawberry_lab(img_lab):
"""CIELab a* channel — more robust to lighting than HSV."""
a_channel = img_lab[:, :, 1].astype(np.float32)
# Otsu auto threshold on a* to separate red from background
a_norm = cv2.normalize(a_channel, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
_, otsu_mask = cv2.threshold(a_norm, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Direct threshold: a* > 135 (reddish) in typical 0-255 range
direct_mask = (a_channel > 135).astype(np.uint8) * 255
# Combine: both agree = high confidence strawberry
lab_mask = otsu_mask & direct_mask
return lab_mask
特征提取模块
# src/features.py — 颜色统计 18 维(RGB/HSV/Lab 每通道均值+标准差)
def extract_color_features(roi_img, mask):
valid = mask > 0
if valid.sum() < 50:
return np.zeros(18, dtype=np.float64)
pixels_rgb = roi_img[valid]
pixels_hsv = cv2.cvtColor(roi_img, cv2.COLOR_RGB2HSV)[valid]
pixels_lab = cv2.cvtColor(roi_img, cv2.COLOR_RGB2Lab)[valid]
features = []
for pixels in [pixels_rgb, pixels_hsv, pixels_lab]:
for c in range(3):
features.append(np.mean(pixels[:, c])) # 均值
features.append(np.std(pixels[:, c])) # 标准差
return np.array(features, dtype=np.float64)
# 另有 GLCM 纹理 16 维(4 角度 x 对比度/同质性/能量/相关性)
# 与形态 8 维(面积/周长/圆度等),合计 42 维
甜度预测模块
# src/predictor.py — 随机森林回归 + 颜色公式降级
class SweetnessPredictor:
def predict_brix(self, features):
"""Predict Brix value from a feature vector (1D or 2D array)."""
if not self.loaded:
return None
feats = np.array(features, dtype=np.float64)
if feats.ndim == 1:
feats = feats.reshape(1, -1)
if self.scaler is not None:
feats = self.scaler.transform(feats)
try:
pred_raw = self.model.predict(feats)
except Exception:
return None
pred_arr = np.asarray(pred_raw).ravel()
if len(pred_arr) > 1:
pred = float(pred_arr.mean())
else:
pred = float(pred_arr[0])
return round(max(0, min(20, pred)), 1) # 截断到 0~20
def estimate_brix_from_color(roi_img, mask):
"""Fallback: 越红(a*高) + 越深(L*低) + 越鲜艳(S高) - 绿色 = 越甜.
Typical strawberry Brix range: 4 (green/unripe) to 14 (deep red/very ripe)."""
valid = mask > 0
if valid.sum() < 100:
return 0.0
lab = cv2.cvtColor(roi_img, cv2.COLOR_RGB2Lab)
# ... 基于 Lab 通道统计的经验公式 ...