农业图像预处理技术学习综述:原理、实现与应用

摘要

随着计算机视觉技术在农业领域的深入应用,图像预处理技术已成为提升农作物检测、病虫害识别、产量预测等任务精度的关键环节。本文系统综述了农业图像预处理技术的分类体系、实现方法与应用场景,重点阐述了数据增强、噪声去除、特征提取等核心技术的实现原理与代码实现,为农业智能化研究提供全面的技术参考。

一、引言

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
相关推荐
roman_日积跬步-终至千里1 小时前
【计算机视觉(2)】图像几何变换基础篇:从平移旋转到投影变换
人工智能·算法·计算机视觉
小oo呆1 小时前
【自然语言处理与大模型】三种实现多模态技术的路线
人工智能·自然语言处理
亚里随笔1 小时前
SAPO:软自适应策略优化——大语言模型强化学习训练的稳定新范式
人工智能·深度学习·机器学习·语言模型·大语言模型·rlhf
hy15687861 小时前
看好大模型技术,不看好“AI 手机”现状
人工智能·热点乱评
世界宇宙超级无敌究极特级顶级第一非常谱尼1 小时前
RF Power Amplifiers for Wireless Communications 第二章学习笔记
笔记·学习·pa·功率放大器·mmic
cyyt1 小时前
深度学习周报(12.1~12.7)
人工智能·深度学习
优质网络系统领域创作者1 小时前
华为链路聚合原理
人工智能·华为
围炉聊科技1 小时前
Orchestrator-8B:近期HuggingFace上火热的小型模型
人工智能
im_AMBER1 小时前
Leetcode 71 买卖股票的最佳时机 | 增量元素之间的最大差值
笔记·学习·算法·leetcode