File size: 9,759 Bytes
b4123b8
 
 
dd1d7f5
b4123b8
 
 
 
 
dd1d7f5
b4123b8
 
 
 
 
dd1d7f5
b4123b8
 
 
dd1d7f5
 
b4123b8
 
 
dd1d7f5
b4123b8
dd1d7f5
 
b4123b8
dd1d7f5
b4123b8
dd1d7f5
 
b4123b8
 
 
 
 
dd1d7f5
 
b4123b8
 
dd1d7f5
 
 
 
 
 
 
b4123b8
dd1d7f5
b4123b8
dd1d7f5
 
b4123b8
 
 
 
 
 
dd1d7f5
b4123b8
dd1d7f5
b4123b8
 
dd1d7f5
b4123b8
 
dd1d7f5
b4123b8
dd1d7f5
b4123b8
 
 
dd1d7f5
 
b4123b8
 
dd1d7f5
 
 
 
 
 
 
 
b4123b8
dd1d7f5
b4123b8
dd1d7f5
b4123b8
 
dd1d7f5
 
b4123b8
dd1d7f5
 
b4123b8
dd1d7f5
 
 
 
 
b4123b8
 
dd1d7f5
 
b4123b8
 
 
 
 
 
 
 
 
 
 
dd1d7f5
 
b4123b8
 
 
dd1d7f5
 
 
 
b4123b8
 
 
 
 
 
dd1d7f5
 
b4123b8
 
 
 
 
dd1d7f5
b4123b8
 
 
 
 
 
 
 
dd1d7f5
b4123b8
dd1d7f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b4123b8
 
 
 
dd1d7f5
b4123b8
 
dd1d7f5
b4123b8
 
 
dd1d7f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b4123b8
 
 
 
 
dd1d7f5
b4123b8
 
dd1d7f5
b4123b8
 
dd1d7f5
b4123b8
dd1d7f5
b4123b8
 
 
dd1d7f5
b4123b8
 
 
 
 
 
 
 
dd1d7f5
 
b4123b8
dd1d7f5
b4123b8
dd1d7f5
 
 
b4123b8
dd1d7f5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
"""
Main pipeline class for the Sorghum Plant Phenotyping Pipeline.

Minimal single-image version for Hugging Face demo.
"""

import os
import logging
from pathlib import Path
from typing import Dict, Any, Optional
import numpy as np
import cv2
from sklearn.decomposition import PCA

from .config import Config
from .data import ImagePreprocessor, MaskHandler
from .features import TextureExtractor, VegetationIndexExtractor, MorphologyExtractor
from .output import OutputManager
from .segmentation import SegmentationManager

logger = logging.getLogger(__name__)


class SorghumPipeline:
    """Minimal pipeline for single-image plant phenotyping."""
    
    def __init__(self, config: Config):
        """Initialize the minimal pipeline."""
        self._setup_logging()
        self.config = config
        self.config.validate()
        self._initialize_components()
        logger.info("Sorghum Pipeline initialized")
    
    def _setup_logging(self):
        """Setup logging configuration."""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[logging.StreamHandler()]
        )
    
    def _initialize_components(self):
        """Initialize pipeline components."""
        self.preprocessor = ImagePreprocessor(target_size=None)
        self.mask_handler = MaskHandler(min_area=1000, kernel_size=7)
        self.texture_extractor = TextureExtractor()
        self.vegetation_extractor = VegetationIndexExtractor()
        self.morphology_extractor = MorphologyExtractor()
        self.segmentation_manager = SegmentationManager(
            model_name="briaai/RMBG-2.0",
            device=self.config.get_device(),
            threshold=0.5,
            trust_remote_code=True
        )
        self.output_manager = OutputManager(
            output_folder=self.config.paths.output_folder,
            settings=self.config.output
        )

    def run(self, single_image_path: str) -> Dict[str, Any]:
        """
        Run minimal pipeline on single image.
        
        Args:
            single_image_path: Path to input image
            
        Returns:
            Dictionary containing results
        """
        logger.info("Starting minimal single-image pipeline...")
        
        try:
            import time
            from PIL import Image as _Image
            
            total_start = time.perf_counter()
            
            # Load single image
            _p = Path(single_image_path)
            _img = _Image.open(str(_p))
            plants = {
                "demo_demo_frame1": {
                    "raw_image": (_img, _p.name),
                    "plant_name": "demo",
                    "file_path": str(_p)
                }
            }
            
            # Create composite
            plants = self.preprocessor.create_composites(plants)
            
            # Segment
            plants = self._segment_plants(plants)
            
            # Extract features
            plants = self._extract_features(plants)
            
            # Generate outputs
            self._generate_outputs(plants)
            
            # Summary
            summary = self._create_summary(plants)
            
            total_time = time.perf_counter() - total_start
            logger.info(f"Pipeline completed in {total_time:.2f}s")
            
            return {
                "plants": plants,
                "summary": summary,
                "config": self.config,
                "timing_seconds": total_time
            }
            
        except Exception as e:
            logger.error(f"Pipeline failed: {e}")
            raise

    def _segment_plants(self, plants: Dict[str, Any]) -> Dict[str, Any]:
        """Segment plants using BRIA model (full image)."""
        for key, pdata in plants.items():
            try:
                composite = pdata['composite']
                soft_mask = self.segmentation_manager.segment_image_soft(composite)
                pdata['soft_mask'] = soft_mask
                pdata['mask'] = (soft_mask * 255.0).astype(np.uint8)
                logger.info(f"Segmented {key}")
            except Exception as e:
                logger.error(f"Segmentation failed for {key}: {e}")
                pdata['soft_mask'] = np.zeros(composite.shape[:2], dtype=np.float32)
                pdata['mask'] = np.zeros(composite.shape[:2], dtype=np.uint8)
        return plants
    
    def _extract_features(self, plants: Dict[str, Any]) -> Dict[str, Any]:
        """Extract features from plants."""
        for key, pdata in plants.items():
            try:
                pdata['texture_features'] = self._extract_texture_features(pdata)
                pdata['vegetation_indices'] = self._extract_vegetation_indices(pdata)
                pdata['morphology_features'] = self._extract_morphology_features(pdata)
                logger.info(f"Features extracted for {key}")
            except Exception as e:
                logger.error(f"Feature extraction failed for {key}: {e}")
                pdata['texture_features'] = {}
                pdata['vegetation_indices'] = {}
                pdata['morphology_features'] = {}
        return plants
    
    def _extract_texture_features(self, pdata: Dict[str, Any]) -> Dict[str, Any]:
        """Extract texture features from pseudo-color image only."""
        features = {}
        try:
            # Only process pseudo-color composite
            composite = pdata['composite']
            mask = pdata.get('mask')
            if mask is not None:
                masked = self.mask_handler.apply_mask_to_image(composite, mask)
                gray_image = cv2.cvtColor(masked, cv2.COLOR_BGR2GRAY)
            else:
                gray_image = cv2.cvtColor(composite, cv2.COLOR_BGR2GRAY)
            
            band_features = self.texture_extractor.extract_all_texture_features(gray_image)
            stats = self.texture_extractor.compute_texture_statistics(band_features, mask)
            
            features['color'] = {
                'features': band_features,
                'statistics': stats
            }
        except Exception as e:
            logger.error(f"Texture extraction failed: {e}")
            features['color'] = {'features': {}, 'statistics': {}}
        
        return features
    
    def _extract_vegetation_indices(self, pdata: Dict[str, Any]) -> Dict[str, Any]:
        """Extract vegetation indices (NDVI, ARI, GNDVI only)."""
        try:
            spectral_stack = pdata.get('spectral_stack', {})
            mask = pdata.get('mask')
            if not spectral_stack or mask is None:
                return {}
            
            out: Dict[str, Any] = {}
            for name in ("NDVI", "ARI", "GNDVI"):
                bands = self.vegetation_extractor.index_bands.get(name, [])
                if not all(b in spectral_stack for b in bands):
                    continue
                arrays = []
                for b in bands:
                    arr = spectral_stack[b]
                    if isinstance(arr, np.ndarray):
                        arr = arr.squeeze(-1)
                    arrays.append(np.asarray(arr, dtype=np.float64))
                
                values = self.vegetation_extractor.index_formulas[name](*arrays).astype(np.float64)
                binary_mask = (np.asarray(mask).astype(np.int32) > 0)
                masked_values = np.where(binary_mask, values, np.nan)
                valid = masked_values[~np.isnan(masked_values)]
                
                stats = {
                    'mean': float(np.mean(valid)) if valid.size else 0.0,
                    'std': float(np.std(valid)) if valid.size else 0.0,
                    'min': float(np.min(valid)) if valid.size else 0.0,
                    'max': float(np.max(valid)) if valid.size else 0.0,
                    'median': float(np.median(valid)) if valid.size else 0.0,
                }
                out[name] = {'values': masked_values, 'statistics': stats}
            return out
        except Exception as e:
            logger.error(f"Vegetation index extraction failed: {e}")
            return {}
    
    def _extract_morphology_features(self, pdata: Dict[str, Any]) -> Dict[str, Any]:
        """Extract morphological features."""
        try:
            composite = pdata.get('composite')
            mask = pdata.get('mask')
            if composite is None or mask is None:
                return {}
            return self.morphology_extractor.extract_morphology_features(composite, mask)
        except Exception as e:
            logger.error(f"Morphology extraction failed: {e}")
            return {}
    
    def _generate_outputs(self, plants: Dict[str, Any]) -> None:
        """Generate output files."""
        self.output_manager.create_output_directories()
        for key, pdata in plants.items():
            try:
                self.output_manager.save_plant_results(key, pdata)
            except Exception as e:
                logger.error(f"Output generation failed for {key}: {e}")
    
    def _create_summary(self, plants: Dict[str, Any]) -> Dict[str, Any]:
        """Create summary of results."""
        return {
            "total_plants": len(plants),
            "successful_plants": sum(1 for p in plants.values() if p.get('texture_features')),
            "features_extracted": {
                "texture": sum(1 for p in plants.values() if p.get('texture_features')),
                "vegetation": sum(1 for p in plants.values() if p.get('vegetation_indices')),
                "morphology": sum(1 for p in plants.values() if p.get('morphology_features'))
            }
        }