AI Cross-Disciplinary Innovation

草莓甜度智能检测

基于可见光图像与机器学习  ·  Brix 糖度快速无损预测  ·  模型状态: loaded

🍓

上传草莓照片,即刻检测甜度

拖拽或点击上传 · JPG / PNG / BMP · 建议 800×600 以上

预览

AI 正在分析草莓甜度...

提取 42 维特征 · 随机森林预测

🍓
0
检测草莓数
📊
0°Bx
平均甜度
🏆
0°Bx
最高甜度
📉
0°Bx
最低甜度
0ms
检测耗时

检测结果可视化

检测结果

逐颗草莓甜度详情

技术原理与算法设计

流程图

开始
用户上传草莓照片
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 糖度),无模型时降级为颜色经验公式

软件系统设计

  1. HSV 三区间分割:深红 (H 0-10)、紫红 (H 160-180)、橙红 (H 10-25) 三个阈值区间取并集,配合椭圆核开运算(去噪点)+ 闭运算(填小洞),得到基础草莓掩膜。
  2. Lab a* Otsu 双保险:CIELab 的 a* 通道(红绿轴)归一化后做 Otsu 自动阈值,与 a* > 135 直接阈值取交集,两条通道都同意的像素才是高置信草莓——对光照变化比固定 HSV 阈值更抗揍。
  3. 果蒂与瑕疵去除:绿色果蒂(H 35-90)从掩膜中剔除,避免干扰颜色统计;Lab 亮度 L* < 35 的暗色区域标记为瑕疵不计入特征,避免烂斑误导甜度判断。面积小于 800 像素的碎片丢弃,最多保留 9 颗草莓。
  4. 42 维特征工程:颜色统计 18 维(RGB/HSV/Lab 三空间每通道均值+标准差)+ GLCM 纹理 16 维(对比度/同质性/能量/相关性 × 4 方向)+ 形态 8 维(面积/周长/圆度等)。
  5. 双通道甜度预测:优先用 joblib 加载的随机森林回归模型(含 StandardScaler 标准化),预测值截断到 0~20 Brix;模型文件缺失时自动降级为颜色经验公式(越红 + 越深 + 越鲜艳 - 绿色 = 越甜)。
  6. Unicode 中文路径兼容:图片以二进制读入后用 cv2.imdecode 解码,绕过 OpenCV imread 不支持中文文件名的坑。
  7. 上传安全: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 通道统计的经验公式 ...