摘要
随着计算机视觉技术在农业领域的深入应用,图像预处理技术已成为提升农作物检测、病虫害识别、产量预测等任务精度的关键环节。本文系统综述了农业图像预处理技术的分类体系、实现方法与应用场景,重点阐述了数据增强、噪声去除、特征提取等核心技术的实现原理与代码实现,为农业智能化研究提供全面的技术参考。
一、引言
1.1 研究背景
-
农业现代化对自动化监测技术的迫切需求
-
图像预处理是计算机视觉农业应用的瓶颈环节
-
自然环境下的图像质量受光照、天气、遮挡等因素影响显著
1.2 研究意义
-
提升后续算法精度与鲁棒性
-
降低模型训练复杂度与计算成本
-
构建标准化农业图像处理流程
二、农业图像预处理技术体系
2.1 技术分类框架
农业图像预处理技术体系
├── 数据采集与标注
│ ├── 多源数据采集
│ └── 专业化标注
├── 数据清洗与增强
│ ├── 质量过滤
│ └── 多样性增强
├── 图像预处理核心
│ ├── 噪声处理
│ ├── 光照校正
│ └── 几何校正
├── 特征工程
│ ├── 传统特征提取
│ └── 深度特征学习
└── 标准化处理
├── 像素归一化
└── 格式标准化
三、关键技术实现详解
3.1 数据采集与标注实现
3.1.1 多源数据采集代码实现
python
import cv2
import numpy as np
from pathlib import Path
from datetime import datetime
class AgriculturalImageCollector:
"""农业图像采集系统"""
def __init__(self, save_dir="./data/raw"):
self.save_dir = Path(save_dir)
self.save_dir.mkdir(parents=True, exist_ok=True)
def capture_rgb_image(self, camera_index=0):
"""采集RGB图像"""
cap = cv2.VideoCapture(camera_index)
ret, frame = cap.read()
if ret:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = self.save_dir / f"rgb_{timestamp}.jpg"
cv2.imwrite(str(filename), frame)
print(f"✅ RGB图像已保存: {filename}")
cap.release()
return frame if ret else None
def capture_multispectral_data(self, spectral_bands=['R', 'G', 'B', 'NIR']):
"""模拟多光谱数据采集"""
data = {}
for band in spectral_bands:
# 模拟不同波段的图像
img = np.random.randint(0, 255, (512, 512), dtype=np.uint8)
data[band] = img
cv2.imwrite(str(self.save_dir / f"ms_{band}.png"), img)
return data
def add_metadata(self, image, metadata):
"""添加元数据"""
metadata.update({
'capture_time': datetime.now().isoformat(),
'resolution': f"{image.shape[1]}x{image.shape[0]}",
'channels': image.shape[2] if len(image.shape) == 3 else 1
})
return metadata
# 使用示例
collector = AgriculturalImageCollector()
rgb_img = collector.capture_rgb_image()
metadata = collector.add_metadata(rgb_img, {
'location': 'Farm_A',
'crop_type': 'Tomato',
'camera_model': 'Canon_EOS_5D'
})
3.1.2 自动化标注工具
python
import json
import cv2
from pathlib import Path
class AgriculturalAnnotationTool:
"""农业图像标注工具"""
def __init__(self, classes):
self.classes = classes
self.annotations = []
def create_voc_annotation(self, image_path, bboxes, labels):
"""创建VOC格式标注"""
annotation = {
'filename': Path(image_path).name,
'size': {
'width': 0,
'height': 0,
'depth': 3
},
'objects': []
}
img = cv2.imread(str(image_path))
if img is not None:
h, w = img.shape[:2]
annotation['size']['width'] = w
annotation['size']['height'] = h
for bbox, label in zip(bboxes, labels):
xmin, ymin, xmax, ymax = bbox
obj = {
'name': label,
'bndbox': {
'xmin': xmin,
'ymin': ymin,
'xmax': xmax,
'ymax': ymax
}
}
annotation['objects'].append(obj)
self.annotations.append(annotation)
return annotation
def save_annotations(self, output_path):
"""保存为JSON格式"""
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(self.annotations, f, ensure_ascii=False, indent=2)
def convert_to_yolo_format(self, annotation, output_dir):
"""转换为YOLO格式"""
yolo_anns = []
img_w = annotation['size']['width']
img_h = annotation['size']['height']
for obj in annotation['objects']:
# 获取类别索引
class_idx = self.classes.index(obj['name'])
# 计算归一化坐标
bbox = obj['bndbox']
x_center = (bbox['xmin'] + bbox['xmax']) / 2 / img_w
y_center = (bbox['ymin'] + bbox['ymax']) / 2 / img_h
width = (bbox['xmax'] - bbox['xmin']) / img_w
height = (bbox['ymax'] - bbox['ymin']) / img_h
yolo_line = f"{class_idx} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}"
yolo_anns.append(yolo_line)
return yolo_anns
# 使用示例
classes = ['Tomato_healthy', 'Tomato_Bacterial_spot', 'Tomato_Early_blight']
annotator = AgriculturalAnnotationTool(classes)
# 示例标注
annotation = annotator.create_voc_annotation(
'tomato_001.jpg',
bboxes=[(50, 60, 200, 300), (300, 400, 450, 550)],
labels=['Tomato_Early_blight', 'Tomato_healthy']
)
yolo_lines = annotator.convert_to_yolo_format(annotation, './labels/')
annotator.save_annotations('annotations.json')
3.2 数据增强技术实现
3.2.1 基础数据增强
python
import cv2
import numpy as np
import albumentations as A
from albumentations.pytorch import ToTensorV2
class AgriculturalDataAugmentation:
"""农业数据增强工具箱"""
def __init__(self, img_size=(256, 256)):
self.img_size = img_size
def get_basic_augmentation(self):
"""基础增强管道"""
return A.Compose([
A.RandomResizedCrop(height=self.img_size[0], width=self.img_size[1], scale=(0.8, 1.0)),
A.HorizontalFlip(p=0.5),
A.VerticalFlip(p=0.3),
A.RandomRotate90(p=0.5),
A.Rotate(limit=15, p=0.5),
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),
A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=0.3),
A.GaussianBlur(blur_limit=(3, 7), p=0.3),
A.GaussNoise(var_limit=(10.0, 50.0), p=0.3),
], bbox_params=A.BboxParams(format='pascal_voc', label_fields=['category_ids']))
def get_advanced_augmentation(self):
"""高级增强管道"""
return A.Compose([
A.OneOf([
A.RandomGamma(gamma_limit=(80, 120), p=1),
A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=1),
A.CLAHE(clip_limit=4.0, tile_grid_size=(8, 8), p=1),
], p=0.7),
A.OneOf([
A.GaussianBlur(blur_limit=(3, 5), p=1),
A.MedianBlur(blur_limit=3, p=1),
A.MotionBlur(blur_limit=5, p=1),
], p=0.3),
A.OneOf([
A.RandomFog(fog_coef_lower=0.1, fog_coef_upper=0.5, p=1),
A.RandomRain(slant_lower=-10, slant_upper=10, p=1),
A.RandomShadow(shadow_roi=(0, 0.5, 1, 1), p=1),
], p=0.2),
A.CoarseDropout(max_holes=8, max_height=32, max_width=32, p=0.5),
])
def apply_mixup(self, image1, image2, label1, label2, alpha=0.5):
"""MixUp增强"""
lam = np.random.beta(alpha, alpha)
mixed_image = lam * image1 + (1 - lam) * image2
mixed_label = lam * label1 + (1 - lam) * label2
return mixed_image, mixed_label
def apply_mosaic(self, images, labels, img_size=640):
"""Mosaic增强"""
mosaic_img = np.zeros((img_size, img_size, 3), dtype=np.float32)
mosaic_labels = []
# 随机选择拼接位置
xc = int(np.random.uniform(img_size // 2, 3 * img_size // 2))
yc = int(np.random.uniform(img_size // 2, 3 * img_size // 2))
indices = np.random.permutation(len(images))
for i, idx in enumerate(indices[:4]):
img = images[idx]
h, w = img.shape[:2]
if i == 0: # 左上
x1a, y1a, x2a, y2a = 0, 0, xc, yc
x1b, y1b, x2b, y2b = w - xc, h - yc, w, h
elif i == 1: # 右上
x1a, y1a, x2a, y2a = xc, 0, img_size, yc
x1b, y1b, x2b, y2b = 0, h - yc, w - xc, h
elif i == 2: # 左下
x1a, y1a, x2a, y2a = 0, yc, xc, img_size
x1b, y1b, x2b, y2b = w - xc, 0, w, h - yc
else: # 右下
x1a, y1a, x2a, y2a = xc, yc, img_size, img_size
x1b, y1b, x2b, y2b = 0, 0, w - xc, h - yc
# 裁剪和放置图像
mosaic_img[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b]
# 调整标签坐标
for label in labels[idx]:
class_id, x_center, y_center, width, height = label
x_center = (x_center * w + x1b - x1a) / img_size
y_center = (y_center * h + y1b - y1a) / img_size
mosaic_labels.append([class_id, x_center, y_center, width, height])
return mosaic_img, mosaic_labels
def visualize_augmentations(self, image, augmentations, n_samples=5):
"""可视化增强效果"""
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, n_samples + 1, figsize=(20, 4))
axes[0].imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
axes[0].set_title("Original")
axes[0].axis('off')
for i in range(n_samples):
augmented = augmentations(image=image)['image']
axes[i+1].imshow(cv2.cvtColor(augmented, cv2.COLOR_BGR2RGB))
axes[i+1].set_title(f"Augmented {i+1}")
axes[i+1].axis('off')
plt.tight_layout()
plt.show()
# 使用示例
augmentor = AgriculturalDataAugmentation(img_size=(512, 512))
basic_aug = augmentor.get_basic_augmentation()
advanced_aug = augmentor.get_advanced_augmentation()
# 加载图像
img = cv2.imread('tomato.jpg')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 应用增强
augmented = basic_aug(image=img_rgb)['image']
augmentor.visualize_augmentations(img_rgb, basic_aug)
3.3 图像预处理核心实现
3.3.1 综合预处理管道
python
import cv2
import numpy as np
from skimage import exposure, filters, restoration
from scipy import ndimage
class AgriculturalPreprocessingPipeline:
"""农业图像综合预处理管道"""
def __init__(self):
self.preprocess_steps = []
def add_step(self, step_name, step_function):
"""添加预处理步骤"""
self.preprocess_steps.append((step_name, step_function))
def apply_pipeline(self, image):
"""应用预处理管道"""
results = {'original': image.copy()}
current_image = image.copy()
for step_name, step_func in self.preprocess_steps:
current_image = step_func(current_image)
results[step_name] = current_image.copy()
return results, current_image
# ========== 噪声去除方法 ==========
@staticmethod
def remove_gaussian_noise(image, kernel_size=(5, 5)):
"""高斯滤波去噪"""
return cv2.GaussianBlur(image, kernel_size, 0)
@staticmethod
def remove_salt_pepper_noise(image, kernel_size=3):
"""中值滤波去除椒盐噪声"""
return cv2.medianBlur(image, kernel_size)
@staticmethod
def bilateral_filter(image, d=9, sigma_color=75, sigma_space=75):
"""双边滤波(保边去噪)"""
return cv2.bilateralFilter(image, d, sigma_color, sigma_space)
@staticmethod
def wavelet_denoising(image):
"""小波去噪"""
import pywt
# 转换为灰度图
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
# 小波变换
coeffs2 = pywt.dwt2(gray, 'haar')
LL, (LH, HL, HH) = coeffs2
# 阈值处理
threshold = np.std(HH) * 2
HH = pywt.threshold(HH, threshold, mode='soft')
# 逆变换
coeffs2 = LL, (LH, HL, HH)
denoised = pywt.idwt2(coeffs2, 'haar')
return np.uint8(denoised)
# ========== 光照校正方法 ==========
@staticmethod
def clahe_enhancement(image, clip_limit=2.0, grid_size=(8, 8)):
"""CLAHE对比度增强"""
if len(image.shape) == 3:
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=grid_size)
l = clahe.apply(l)
enhanced_lab = cv2.merge([l, a, b])
return cv2.cvtColor(enhanced_lab, cv2.COLOR_LAB2BGR)
else:
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=grid_size)
return clahe.apply(image)
@staticmethod
def homomorphic_filter(image, gamma_h=1.5, gamma_l=0.5, c=1, d0=30):
"""同态滤波"""
# 转换为浮点数
img_float = np.float32(image) / 255.0
if len(img_float.shape) == 3:
result = np.zeros_like(img_float)
for i in range(3):
channel = img_float[:, :, i]
# 对数变换
log_img = np.log(channel + 1e-6)
# 傅里叶变换
f = np.fft.fft2(log_img)
fshift = np.fft.fftshift(f)
# 创建同态滤波器
rows, cols = channel.shape
crow, ccol = rows // 2, cols // 2
# 高斯高通滤波器
x = np.arange(cols) - ccol
y = np.arange(rows) - crow
X, Y = np.meshgrid(x, y)
D = np.sqrt(X**2 + Y**2)
H = (gamma_h - gamma_l) * (1 - np.exp(-c * (D**2 / d0**2))) + gamma_l
# 应用滤波器
fshift_filtered = fshift * H
# 逆傅里叶变换
f_ishift = np.fft.ifftshift(fshift_filtered)
img_back = np.fft.ifft2(f_ishift)
img_back = np.real(img_back)
# 指数变换
result[:, :, i] = np.exp(img_back)
else:
# 单通道处理
log_img = np.log(img_float + 1e-6)
f = np.fft.fft2(log_img)
fshift = np.fft.fftshift(f)
rows, cols = img_float.shape
crow, ccol = rows // 2, cols // 2
x = np.arange(cols) - ccol
y = np.arange(rows) - crow
X, Y = np.meshgrid(x, y)
D = np.sqrt(X**2 + Y**2)
H = (gamma_h - gamma_l) * (1 - np.exp(-c * (D**2 / d0**2))) + gamma_l
fshift_filtered = fshift * H
f_ishift = np.fft.ifftshift(fshift_filtered)
img_back = np.fft.ifft2(f_ishift)
img_back = np.real(img_back)
result = np.exp(img_back)
# 归一化到[0, 255]
result = np.clip(result * 255, 0, 255).astype(np.uint8)
return result
@staticmethod
def adaptive_illumination_correction(image, window_size=32):
"""自适应光照校正"""
if len(image.shape) == 3:
# 转换为HSV空间
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
# 估计光照分量
v_float = np.float32(v)
v_lowpass = cv2.GaussianBlur(v_float, (window_size, window_size), 0)
# 校正V通道
v_corrected = v_float / (v_lowpass + 1e-6) * 128
v_corrected = np.clip(v_corrected, 0, 255).astype(np.uint8)
# 合并通道
corrected_hsv = cv2.merge([h, s, v_corrected])
return cv2.cvtColor(corrected_hsv, cv2.COLOR_HSV2BGR)
else:
# 灰度图处理
gray_float = np.float32(image)
lowpass = cv2.GaussianBlur(gray_float, (window_size, window_size), 0)
corrected = gray_float / (lowpass + 1e-6) * 128
return np.clip(corrected, 0, 255).astype(np.uint8)
# ========== 几何校正方法 ==========
@staticmethod
def camera_calibration(image, camera_matrix, dist_coeffs):
"""相机畸变校正"""
h, w = image.shape[:2]
new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(
camera_matrix, dist_coeffs, (w, h), 1, (w, h)
)
undistorted = cv2.undistort(image, camera_matrix, dist_coeffs, None, new_camera_matrix)
# 裁剪有效区域
x, y, w, h = roi
return undistorted[y:y+h, x:x+w]
@staticmethod
def perspective_transform(image, src_points, dst_points):
"""透视变换"""
M = cv2.getPerspectiveTransform(src_points, dst_points)
h, w = image.shape[:2]
return cv2.warpPerspective(image, M, (w, h))
# ========== 颜色空间转换 ==========
@staticmethod
def rgb_to_vegetation_index(image, index_type='exg'):
"""计算植被指数"""
if len(image.shape) != 3:
raise ValueError("需要RGB图像")
r, g, b = cv2.split(image.astype(np.float32))
if index_type.lower() == 'exg':
# 超绿指数
return 2 * g - r - b
elif index_type.lower() == 'ndi':
# 归一化差异指数
return (g - r) / (g + r + 1e-6)
elif index_type.lower() == 'gli':
# 绿叶指数
return (2 * g - r - b) / (2 * g + r + b + 1e-6)
else:
raise ValueError(f"未知的指数类型: {index_type}")
@staticmethod
def color_balance(image, percent=1):
"""自动颜色平衡"""
# 转换为LAB空间
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
# 计算直方图百分比
def get_percentile(channel, percent):
total_pixels = channel.size
target_pixels = int(total_pixels * percent / 100)
hist = cv2.calcHist([channel], [0], None, [256], [0, 256])
cumsum = np.cumsum(hist)
low = np.where(cumsum >= target_pixels)[0][0]
high = np.where(cumsum >= total_pixels - target_pixels)[0][0]
return low, high
# 获取L通道的百分比范围
low, high = get_percentile(l, percent)
# 拉伸L通道
l_stretched = cv2.normalize(l, None, low, high, cv2.NORM_MINMAX)
# 合并通道
balanced_lab = cv2.merge([l_stretched, a, b])
return cv2.cvtColor(balanced_lab, cv2.COLOR_LAB2BGR)
# ========== 背景移除 ==========
@staticmethod
def remove_background_grabcut(image, rect=None):
"""使用GrabCut移除背景"""
if rect is None:
h, w = image.shape[:2]
rect = (10, 10, w-20, h-20)
mask = np.zeros(image.shape[:2], np.uint8)
bgd_model = np.zeros((1, 65), np.float64)
fgd_model = np.zeros((1, 65), np.float64)
cv2.grabCut(image, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT)
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
result = image * mask2[:, :, np.newaxis]
return result, mask2
@staticmethod
def remove_background_color(image, lower_hsv, upper_hsv):
"""基于HSV颜色范围移除背景"""
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, lower_hsv, upper_hsv)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))
result = cv2.bitwise_and(image, image, mask=mask)
return result, mask
# ========== 使用示例 ==========
def create_agricultural_preprocessing_pipeline():
"""创建农业图像预处理管道"""
pipeline = AgriculturalPreprocessingPipeline()
# 添加预处理步骤
pipeline.add_step('denoise_bilateral',
lambda img: AgriculturalPreprocessingPipeline.bilateral_filter(img))
pipeline.add_step('illumination_correction',
lambda img: AgriculturalPreprocessingPipeline.clahe_enhancement(img))
pipeline.add_step('color_balance',
lambda img: AgriculturalPreprocessingPipeline.color_balance(img, percent=2))
pipeline.add_step('vegetation_extraction',
lambda img: AgriculturalPreprocessingPipeline.rgb_to_vegetation_index(img, 'exg'))
return pipeline
# 运行示例
if __name__ == "__main__":
# 加载图像
img = cv2.imread('agricultural_image.jpg')
# 创建并运行预处理管道
pipeline = create_agricultural_preprocessing_pipeline()
results, final_image = pipeline.apply_pipeline(img)
# 可视化结果
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.flatten()
steps = list(results.keys())
for i, step in enumerate(steps[:6]):
if len(results[step].shape) == 2:
axes[i].imshow(results[step], cmap='gray')
else:
axes[i].imshow(cv2.cvtColor(results[step], cv2.COLOR_BGR2RGB))
axes[i].set_title(step)
axes[i].axis('off')
plt.tight_layout()
plt.show()
print(f"✅ 预处理完成,共{len(results)}个步骤")
print(f" 最终图像尺寸: {final_image.shape}")
3.4 特征工程实现
3.4.1 综合特征提取
python
import numpy as np
import cv2
from skimage.feature import greycomatrix, greycoprops, local_binary_pattern
from scipy.stats import skew, kurtosis
class AgriculturalFeatureExtractor:
"""农业图像特征提取器"""
def __init__(self):
self.feature_names = []
def extract_all_features(self, image):
"""提取所有特征"""
features = {}
# 颜色特征
color_features = self.extract_color_features(image)
features.update(color_features)
# 纹理特征
texture_features = self.extract_texture_features(image)
features.update(texture_features)
# 形态特征(需要二值化图像)
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
morphology_features = self.extract_morphology_features(gray)
features.update(morphology_features)
# 植被指数
vegetation_features = self.extract_vegetation_features(image)
features.update(vegetation_features)
return features
def extract_color_features(self, image):
"""提取颜色特征"""
features = {}
if len(image.shape) == 3:
# RGB空间
b, g, r = cv2.split(image)
for channel, name in zip([r, g, b], ['r', 'g', 'b']):
features.update(self._extract_channel_stats(channel, f'rgb_{name}'))
# HSV空间
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
for channel, name in zip([h, s, v], ['h', 's', 'v']):
features.update(self._extract_channel_stats(channel, f'hsv_{name}'))
# LAB空间
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b_lab = cv2.split(lab)
for channel, name in zip([l, a, b_lab], ['l', 'a', 'b']):
features.update(self._extract_channel_stats(channel, f'lab_{name}'))
else:
# 灰度图
features.update(self._extract_channel_stats(image, 'gray'))
# 颜色矩(HSV)
if len(image.shape) == 3:
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
for i, channel in enumerate(['h', 's', 'v']):
hist = cv2.calcHist([hsv], [i], None, [256], [0, 256])
hist = hist / hist.sum()
# 一阶矩(均值)
mean = np.sum(np.arange(256) * hist.flatten())
features[f'color_moment_{channel}_mean'] = mean
# 二阶矩(方差)
variance = np.sum((np.arange(256) - mean) ** 2 * hist.flatten())
features[f'color_moment_{channel}_variance'] = variance
# 三阶矩(偏度)
std = np.sqrt(variance)
if std > 0:
skewness = np.sum((np.arange(256) - mean) ** 3 * hist.flatten()) / (std ** 3)
else:
skewness = 0
features[f'color_moment_{channel}_skewness'] = skewness
return features
def _extract_channel_stats(self, channel, prefix):
"""提取通道统计特征"""
flat = channel.flatten().astype(np.float32)
return {
f'{prefix}_mean': np.mean(flat),
f'{prefix}_std': np.std(flat),
f'{prefix}_median': np.median(flat),
f'{prefix}_skewness': skew(flat),
f'{prefix}_kurtosis': kurtosis(flat),
f'{prefix}_min': np.min(flat),
f'{prefix}_max': np.max(flat),
f'{prefix}_range': np.ptp(flat),
f'{prefix}_energy': np.sum(flat ** 2),
f'{prefix}_entropy': -np.sum(flat * np.log2(flat + 1e-10))
}
def extract_texture_features(self, image):
"""提取纹理特征"""
features = {}
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
# LBP特征
radius = 3
n_points = 8 * radius
lbp = local_binary_pattern(gray, n_points, radius, method='uniform')
lbp_hist, _ = np.histogram(lbp, bins=n_points+2, range=(0, n_points+2), density=True)
features['lbp_uniformity'] = np.max(lbp_hist)
features['lbp_entropy'] = -np.sum(lbp_hist * np.log2(lbp_hist + 1e-10))
# GLCM特征
glcm = greycomatrix(gray.astype(np.uint8),
distances=[1, 3, 5],
angles=[0, np.pi/4, np.pi/2, 3*np.pi/4],
levels=256, symmetric=True, normed=True)
for prop in ['contrast', 'dissimilarity', 'homogeneity', 'energy', 'correlation', 'ASM']:
glcm_prop = greycoprops(glcm, prop)
features[f'glcm_{prop}_mean'] = np.mean(glcm_prop)
features[f'glcm_{prop}_std'] = np.std(glcm_prop)
# Gabor特征
gabor_features = self._extract_gabor_features(gray)
features.update(gabor_features)
# 局部二值模式方差
lbp_var = np.var(lbp)
features['lbp_variance'] = lbp_var
return features
def _extract_gabor_features(self, gray_img):
"""提取Gabor特征"""
features = {}
kernels = []
# 创建Gabor滤波器组
for theta in np.arange(0, np.pi, np.pi/4): # 4个方向
for sigma in [3, 5]: # 2个尺度
for frequency in [0.1, 0.3]: # 2个频率
gabor_kernel = cv2.getGaborKernel((21, 21), sigma, theta, frequency, 0.5, 0, ktype=cv2.CV_32F)
kernels.append(gabor_kernel)
# 应用滤波器并提取特征
for i, kernel in enumerate(kernels[:4]): # 取前4个
filtered = cv2.filter2D(gray_img, cv2.CV_32F, kernel)
features[f'gabor_{i}_mean'] = np.mean(filtered)
features[f'gabor_{i}_std'] = np.std(filtered)
features[f'gabor_{i}_energy'] = np.sum(filtered ** 2)
return features
def extract_morphology_features(self, gray_img):
"""提取形态学特征"""
features = {}
# 二值化
_, binary = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 形态学操作
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
# 查找轮廓
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
# 最大轮廓
largest_contour = max(contours, key=cv2.contourArea)
# 面积特征
area = cv2.contourArea(largest_contour)
features['contour_area'] = area
features['contour_area_ratio'] = area / (gray_img.shape[0] * gray_img.shape[1])
# 周长特征
perimeter = cv2.arcLength(largest_contour, True)
features['contour_perimeter'] = perimeter
# 圆形度
if perimeter > 0:
circularity = 4 * np.pi * area / (perimeter ** 2)
features['circularity'] = circularity
else:
features['circularity'] = 0
# 边界矩形
x, y, w, h = cv2.boundingRect(largest_contour)
features['aspect_ratio'] = w / h if h > 0 else 0
features['extent'] = area / (w * h) if w * h > 0 else 0
# 凸包特征
hull = cv2.convexHull(largest_contour)
hull_area = cv2.contourArea(hull)
features['solidity'] = area / hull_area if hull_area > 0 else 0
# 矩特征
M = cv2.moments(largest_contour)
if M['m00'] != 0:
cx = M['m10'] / M['m00']
cy = M['m01'] / M['m00']
features['centroid_x'] = cx
features['centroid_y'] = cy
# Hu矩(形状描述子)
hu_moments = cv2.HuMoments(M).flatten()
for i, hu in enumerate(hu_moments):
features[f'hu_moment_{i+1}'] = -np.sign(hu) * np.log10(np.abs(hu))
return features
def extract_vegetation_features(self, image):
"""提取植被特征"""
features = {}
if len(image.shape) != 3:
return features
# 转换为浮点型
b, g, r = cv2.split(image.astype(np.float32))
# 避免除零
eps = 1e-6
# 各种植被指数
# 超绿指数
exg = 2 * g - r - b
features['exg_mean'] = np.mean(exg)
features['exg_std'] = np.std(exg)
# 归一化差异植被指数(近似)
ndvi = (g - r) / (g + r + eps)
features['ndvi_mean'] = np.mean(ndvi)
features['ndvi_std'] = np.std(ndvi)
# 绿叶指数
gli = (2 * g - r - b) / (2 * g + r + b + eps)
features['gli_mean'] = np.mean(gli)
features['gli_std'] = np.std(gli)
# 可见光大气阻抗指数
vari = (g - r) / (g + r - b + eps)
features['vari_mean'] = np.mean(vari)
features['vari_std'] = np.std(vari)
# 红绿比值
rg_ratio = r / (g + eps)
features['rg_ratio_mean'] = np.mean(rg_ratio)
features['rg_ratio_std'] = np.std(rg_ratio)
return features
def extract_haralick_features(self, image):
"""提取Haralick纹理特征"""
features = {}
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
# 计算灰度共生矩阵
glcm = greycomatrix(gray.astype(np.uint8),
distances=[1],
angles=[0, np.pi/4, np.pi/2, 3*np.pi/4],
levels=256, symmetric=True, normed=True)
# 14个Haralick特征
properties = ['contrast', 'dissimilarity', 'homogeneity', 'energy',
'correlation', 'ASM', 'max_prob']
for prop in properties:
try:
glcm_prop = greycoprops(glcm, prop)
for i, angle in enumerate([0, 45, 90, 135]):
features[f'haralick_{prop}_angle{angle}'] = glcm_prop[0, i]
except:
pass
return features
# ========== 使用示例 ==========
def test_feature_extraction():
"""测试特征提取"""
# 加载图像
img = cv2.imread('tomato_leaf.jpg')
# 创建特征提取器
extractor = AgriculturalFeatureExtractor()
# 提取所有特征
features = extractor.extract_all_features(img)
print(f"✅ 共提取 {len(features)} 个特征")
print("\n📊 特征示例:")
for i, (name, value) in enumerate(features.items()):
if i < 10: # 显示前10个特征
print(f" {name:30}: {value:.4f}")
# 转换为DataFrame
import pandas as pd
df = pd.DataFrame([features])
print(f"\n📈 特征DataFrame形状: {df.shape}")
return df
if __name__ == "__main__":
test_feature_extraction()
四、实际应用案例
4.1 番茄病害检测预处理流程
python
class TomatoDiseasePreprocessor:
"""番茄病害图像预处理专用类"""
def __init__(self):
self.pipeline = AgriculturalPreprocessingPipeline()
def create_tomato_pipeline(self):
"""创建番茄病害专用预处理管道"""
# 1. 去除光照不均
self.pipeline.add_step('illumination_correction',
lambda img: AgriculturalPreprocessingPipeline.homomorphic_filter(img))
# 2. 颜色增强(突出病斑)
self.pipeline.add_step('color_enhancement',
lambda img: AgriculturalPreprocessingPipeline.clahe_enhancement(img, clip_limit=3.0))
# 3. 去除背景
self.pipeline.add_step('background_removal',
lambda img: AgriculturalPreprocessingPipeline.remove_background_color(
img,
lower_hsv=np.array([25, 40, 40]), # 绿色范围
upper_hsv=np.array([90, 255, 255])
)[0])
# 4. 病斑增强
self.pipeline.add_step('lesion_enhancement', self._enhance_lesions)
return self.pipeline
def _enhance_lesions(self, image):
"""增强病斑特征"""
# 转换为LAB空间
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
# 增强a通道(红绿对比)
a = cv2.normalize(a, None, 0, 255, cv2.NORM_MINMAX)
a = cv2.equalizeHist(a.astype(np.uint8))
# 合并通道
enhanced_lab = cv2.merge([l, a, b])
return cv2.cvtColor(enhanced_lab, cv2.COLOR_LAB2BGR)
4.2 高光谱数据处理
python
class HyperspectralPreprocessor:
"""高光谱数据预处理"""
def __init__(self):
self.bands = []
def sg_filter(self, spectrum, window_size=11, poly_order=3):
"""Savitzky-Golay滤波"""
from scipy.signal import savgol_filter
return savgol_filter(spectrum, window_size, poly_order)
def snv_correction(self, spectrum):
"""标准正态变量变换"""
mean = np.mean(spectrum)
std = np.std(spectrum)
return (spectrum - mean) / std if std > 0 else spectrum
def detrend(self, spectrum):
"""去趋势变换"""
from scipy import signal
return signal.detrend(spectrum)
def continuum_removal(self, spectrum, wavelengths):
"""连续统去除"""
from scipy.interpolate import interp1d
# 寻找凸包点
hull_indices = []
n = len(spectrum)
for i in range(n):
# 简单的凸包近似
if i == 0 or i == n-1:
hull_indices.append(i)
elif (spectrum[i] >= spectrum[i-1] and spectrum[i] >= spectrum[i+1]):
hull_indices.append(i)
if len(hull_indices) < 2:
return np.ones_like(spectrum)
# 插值得到连续统
continuum = interp1d(wavelengths[hull_indices], spectrum[hull_indices],
kind='linear', fill_value='extrapolate')(wavelengths)
# 去除连续统
return spectrum / continuum
def process_hyperspectral_cube(self, cube, wavelengths):
"""处理高光谱立方体"""
processed_cube = np.zeros_like(cube)
n_bands = cube.shape[2]
for i in range(n_bands):
band = cube[:, :, i]
# 应用预处理
if i > 0 and i < n_bands - 1:
# SG滤波
spectrum = cube[:, :, i-1:i+2].mean(axis=2) # 简化处理
filtered = self.sg_filter(spectrum.flatten(), window_size=5, poly_order=2)
band = filtered.reshape(band.shape)
processed_cube[:, :, i] = band
return processed_cube