本项目项目主要功能:通过植物照片判断当前植物的健康状况。可以在电脑或手机上通过局域网访问布署在树莓派上的网页,上传拍好的照片或者直接拍摄照片,树莓派后端收到图片后进行本地分析,记录分析结果并给出相应养护建议。
项目背景:在工作之余会养殖一些绿植盆栽,但由于不清楚作物习性,不知道什么时候该浇水、什么时候该施肥,已经养死了好几盆了,现在遇到这个活动了就想着能否用树莓派来根据植物叶片来判断是否缺水、缺肥,甚至判断是否产生病变。这样就可以及时干预,降低盆栽养死的概率。
在上一篇过程贴中已经说明了特征提取方法,这里主要展示具体如何实现。
首先是叶片提取,在一副照片中我们感兴趣的区域只在叶片上,其他区域我们并不关心,通常叶片是绿色的,或者说是在绿色周围(比如嫩叶会浅,老叶则会发棕色黄色),这样我就可以根据这个范围提取相应区域从而得到叶片区域,具体实现如下
def detect_plant_region(self, img: np.ndarray) -> np.ndarray: """ 检测植物区域 :param img: 输入图像 :return: 植物掩码 """ # 转换到HSV颜色空间 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 定义绿色范围(针对植物叶片) lower_green = np.array([35, 40, 40]) upper_green = np.array([85, 255, 255]) plant_mask = cv2.inRange(hsv, lower_green, upper_green) # 应用形态学操作去除噪声 kernel = np.ones((5, 5), np.uint8) plant_mask = cv2.morphologyEx(plant_mask, cv2.MORPH_CLOSE, kernel) plant_mask = cv2.morphologyEx(plant_mask, cv2.MORPH_OPEN, kernel) return plant_mask
获取到叶片区域后就可以对其进行颜色比例分析,用于后续健康状况进行分析,方法跟上述叶片提取类似,计算相应颜色像素占叶片总像素的比例,具体实现如下
def analyze_color_ratios(self, img: np.ndarray, plant_mask: np.ndarray) -> Dict[str, float]:
"""
分析颜色比例
:param img: 输入图像
:param plant_mask: 植物掩码
:return: 颜色比例字典
"""
# 将图像转换为HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 应用掩码
masked_hsv = cv2.bitwise_and(hsv, hsv, mask=plant_mask)
# 定义颜色范围
h, s, v = cv2.split(masked_hsv)
# 计算绿色像素数量(健康叶片)
green_lower = np.array([35, 40, 40])
green_upper = np.array([85, 255, 255])
green_mask = cv2.inRange(hsv, green_lower, green_upper)
green_pixels = cv2.countNonZero(cv2.bitwise_and(green_mask, plant_mask))
# 计算黄色像素数量(老化或应激)
yellow_lower = np.array([15, 40, 40])
yellow_upper = np.array([35, 255, 255])
yellow_mask = cv2.inRange(hsv, yellow_lower, yellow_upper)
yellow_pixels = cv2.countNonZero(cv2.bitwise_and(yellow_mask, plant_mask))
# 计算棕色像素数量(枯萎或病变)
brown_lower = np.array([0, 40, 40])
brown_upper = np.array([15, 255, 255])
brown_mask = cv2.inRange(hsv, brown_lower, brown_upper)
brown_pixels = cv2.countNonZero(cv2.bitwise_and(brown_mask, plant_mask))
# 计算总植物像素数
total_plant_pixels = cv2.countNonZero(plant_mask)
# 计算比例
if total_plant_pixels > 0:
green_ratio = green_pixels / total_plant_pixels
yellow_ratio = yellow_pixels / total_plant_pixels
brown_ratio = brown_pixels / total_plant_pixels
else:
green_ratio = yellow_ratio = brown_ratio = 0.0
return {
'green_ratio': green_ratio,
'yellow_ratio': yellow_ratio,
'brown_ratio': brown_ratio
}获取到叶片区域后也可以直接对这个区域进行数学计算,提取叶片的形态特征,具体实现方法如下
def analyze_leaf_features(self, img: np.ndarray, plant_mask: np.ndarray) -> Dict:
"""
分析叶片特征
:param img: 输入图像
:param plant_mask: 植物掩码
:return: 叶片特征字典
"""
# 查找轮廓
contours, hierarchy = cv2.findContours(plant_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 过滤小轮廓
min_area = 500
filtered_contours = [cnt for cnt in contours if cv2.contourArea(cnt) > min_area]
if not filtered_contours:
return {
'leaf_count': 0,
'avg_leaf_size': 0,
'leaf_sizes': []
}
# 计算每个轮廓的面积
leaf_areas = [cv2.contourArea(cnt) for cnt in filtered_contours]
avg_leaf_size = np.mean(leaf_areas) if leaf_areas else 0
return {
'leaf_count': len(filtered_contours),
'avg_leaf_size': avg_leaf_size,
'leaf_sizes': leaf_areas
}有了以上特征后可以对植物进行一个简单的健康分析,可以给出一个简单的缺水缺肥状态,具体实现如下
def assess_plant_health(self, features: Dict, color_ratios: Dict, plant_type: str) -> Dict:
"""
评估植物健康状况
:param features: 特征数据
:param color_ratios: 颜色比例
:param plant_type: 植物类型
:return: 健康评估结果
"""
thresholds = self.plant_thresholds.get(plant_type, self.plant_thresholds['gardenia'])
# 计算健康评分
health_score = 0.0
# 基于颜色的健康评分
green_health = min(color_ratios['green_ratio'] / thresholds['green_ratio_threshold'], 1.0)
yellow_health = max(0, 1.0 - (color_ratios['yellow_ratio'] / thresholds['yellow_ratio_threshold']))
brown_health = max(0, 1.0 - (color_ratios['brown_ratio'] / thresholds['brown_ratio_threshold']))
# 叶片数量健康评分
leaf_count = features['leaf_count']
if leaf_count >= 3:
leaf_health = min(leaf_count / 10.0, 1.0)
else:
leaf_health = leaf_count / 3.0 if leaf_count > 0 else 0.0
# 综合健康评分
health_score = (green_health * 0.4 + yellow_health * 0.3 + brown_health * 0.2 + leaf_health * 0.1)
# 评估水分和营养需求
water_needed = color_ratios['yellow_ratio'] > thresholds['water_needed_threshold']
fertilizer_needed = color_ratios['brown_ratio'] > thresholds['fertilizer_needed_threshold']
# 评估叶片老化程度
aging_level = color_ratios['yellow_ratio'] + color_ratios['brown_ratio']
return {
'health_score': min(health_score, 1.0),
'water_needed': water_needed,
'fertilizer_needed': fertilizer_needed,
'aging_level': aging_level,
'health_issues': self.identify_health_issues(color_ratios, features, thresholds)
}当然,除了以上特征以外还可以扩展更多表型特征,比如以下实现
def analyze_advanced_features(self, img: np.ndarray, plant_mask: np.ndarray) -> Dict:
"""
分析更多表型特征
:param img: 输入图像
:param plant_mask: 植物掩码
:return: 扩展特征字典
"""
# 查找轮廓
contours, hierarchy = cv2.findContours(plant_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 过滤小轮廓
min_area = 500
filtered_contours = [cnt for cnt in contours if cv2.contourArea(cnt) > min_area]
if not filtered_contours:
return {
'leaf_count': 0,
'avg_leaf_size': 0,
'total_plant_area': 0,
'avg_compactness': 0,
'avg_symmetry': 0,
'avg_elongation': 0,
'texture_variance': 0,
'edge_density': 0
}
# 计算各种形状特征
compactness_values = []
symmetry_values = []
elongation_values = []
for cnt in filtered_contours:
area = cv2.contourArea(cnt)
if area < min_area:
continue
# 计算周长
perimeter = cv2.arcLength(cnt, True)
# 计算紧凑度 (圆形度): 4π*area/perimeter²
compactness = (4 * math.pi * area) / (perimeter * perimeter) if perimeter > 0 else 0
compactness_values.append(compactness)
# 计算细长度 (elongation)
rect = cv2.minAreaRect(cnt)
width, height = rect[1]
if width != 0 and height != 0:
elongation = max(width, height) / min(width, height)
elongation_values.append(elongation)
# 计算对称性 (通过比较轮廓与其镜像的相似度)
hull = cv2.convexHull(cnt)
hull_area = cv2.contourArea(hull)
solidity = float(area) / hull_area if hull_area > 0 else 0
# 使用凸性缺陷来估算对称性
defects = cv2.convexityDefects(cnt, cv2.convexHull(cnt, returnPoints=False))
if defects is not None:
symmetry = 1 - (len(defects) / 100.0)
symmetry = max(0, symmetry)
symmetry_values.append(symmetry)
# 计算纹理方差(使用Laplacian算子)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
# 计算边缘密度
edges = cv2.Canny(gray, 50, 150)
edge_density = cv2.countNonZero(edges) / (img.shape[0] * img.shape[1])
return {
'leaf_count': len(filtered_contours),
'avg_leaf_size': np.mean([cv2.contourArea(cnt) for cnt in filtered_contours]) if filtered_contours else 0,
'total_plant_area': sum([cv2.contourArea(cnt) for cnt in filtered_contours]),
'avg_compactness': np.mean(compactness_values) if compactness_values else 0,
'avg_symmetry': np.mean(symmetry_values) if symmetry_values else 0,
'avg_elongation': np.mean(elongation_values) if elongation_values else 0,
'texture_variance': laplacian_var,
'edge_density': edge_density
}
def analyze_color_distribution(self, img: np.ndarray, plant_mask: np.ndarray) -> Dict:
"""
分析颜色分布特征
:param img: 输入图像
:param plant_mask: 植物掩码
:return: 颜色分布特征字典
"""
# 应用掩码到原图
masked_img = cv2.bitwise_and(img, img, mask=plant_mask)
# 转换到不同的色彩空间进行分析
hsv = cv2.cvtColor(masked_img, cv2.COLOR_BGR2HSV)
lab = cv2.cvtColor(masked_img, cv2.COLOR_BGR2LAB)
# 获取非零像素(即植物区域的像素)
hsv_nonzero = hsv[plant_mask > 0]
lab_nonzero = lab[plant_mask > 0]
if hsv_nonzero.size == 0:
return {
'mean_hsv': [0, 0, 0],
'std_hsv': [0, 0, 0],
'mean_lab': [0, 0, 0],
'std_lab': [0, 0, 0],
'color_uniformity': 0
}
# 计算HSV和LAB颜色空间的统计特征
mean_hsv = np.mean(hsv_nonzero, axis=0)
std_hsv = np.std(hsv_nonzero, axis=0)
mean_lab = np.mean(lab_nonzero, axis=0)
std_lab = np.std(lab_nonzero, axis=0)
# 计算颜色均匀性(越低表示颜色变化越大)
color_uniformity = np.mean(std_hsv) / (np.mean(mean_hsv) + 1e-6)
return {
'mean_hsv': mean_hsv.tolist(),
'std_hsv': std_hsv.tolist(),
'mean_lab': mean_lab.tolist(),
'std_lab': std_lab.tolist(),
'color_uniformity': float(color_uniformity)
}现在最重要的表型特征分析已经完成了,接下来就需要考虑如何打通整个链路了,总不能每次拍完照都先拷贝到U盘上再拷贝到树莓派上吧。我首先想到的是通过scp指令或者FTP将图片传到树莓派上进行分析,但这样实际操作起来也很麻烦,每次都需要拷贝过去手动执行指令。有没有直接上传过去自动分析并给出护理建议的方案呢,于是我就想到可以在树莓派上布署一个在局域网内的网页,直接在这个网页上进行照片上传,树莓派分析完后自动在网页上显示结果和建议,这样操作起来就比较方便和合理了。方案有了,但我对网页、前后端这些都不熟悉怎么办呢,想到现在各种生成式AI大行其道,于是我就准备使用AI生成一个可以直接布署的网页。以我的使用体验来看,至少AI直接生成代码这块还有很大的进步空间。我的需求很简单,点按钮上传图片,点按钮自动执行分析代码并显示结果,就这么简单的功能AI前前后后不断修修改改了无数次,在把前后端报错日志喂给AI好多次之后总算是有个勉强能用的版本了,期间还有些错别词得自己修改。实际成果展示见顶部视频。
我要赚赏金
