Fahimeh Orvati Nia commited on
Commit
7c31b44
·
1 Parent(s): 88a828f
Files changed (2) hide show
  1. app.py +8 -9
  2. sorghum_pipeline/pipeline.py +57 -36
app.py CHANGED
@@ -2,16 +2,14 @@ import gradio as gr
2
  import tempfile
3
  from pathlib import Path
4
  from wrapper import run_pipeline_on_image
5
- import numpy as np
6
  from PIL import Image
7
- from itertools import product
8
 
9
- def process(image_path):
10
- if not image_path:
11
  return None, None, None, None, [], ""
12
  with tempfile.TemporaryDirectory() as tmpdir:
13
- # Copy uploaded file as-is to preserve format/bit-depth
14
- src = Path(image_path)
15
  ext = src.suffix.lstrip('.') or 'tif'
16
  img_path = Path(tmpdir) / f"input.{ext}"
17
  try:
@@ -20,6 +18,7 @@ def process(image_path):
20
  except Exception:
21
  # Fallback: save via PIL if direct copy fails
22
  Image.open(src).save(img_path)
 
23
  outputs = run_pipeline_on_image(str(img_path), tmpdir, save_artifacts=True)
24
 
25
  def load_pil(path_str):
@@ -27,7 +26,6 @@ def process(image_path):
27
  if not path_str:
28
  return None
29
  im = Image.open(path_str)
30
- # im = im.convert('RGB')
31
  copied = im.copy()
32
  im.close()
33
  return copied
@@ -52,7 +50,8 @@ with gr.Blocks() as demo:
52
 
53
  with gr.Row():
54
  with gr.Column():
55
- inp = gr.Image(type="filepath", label="Upload Image")
 
56
  run = gr.Button("Run Pipeline", variant="primary")
57
 
58
  with gr.Row():
@@ -70,4 +69,4 @@ with gr.Blocks() as demo:
70
  run.click(process, inputs=inp, outputs=[size_img, composite_img, mask_img, overlay_img, texture_img, gallery, stats])
71
 
72
  if __name__ == "__main__":
73
- demo.launch()
 
2
  import tempfile
3
  from pathlib import Path
4
  from wrapper import run_pipeline_on_image
 
5
  from PIL import Image
 
6
 
7
+ def process(file_obj):
8
+ if not file_obj:
9
  return None, None, None, None, [], ""
10
  with tempfile.TemporaryDirectory() as tmpdir:
11
+ # file_obj is a dict when using gr.File(type="file")
12
+ src = Path(file_obj.name)
13
  ext = src.suffix.lstrip('.') or 'tif'
14
  img_path = Path(tmpdir) / f"input.{ext}"
15
  try:
 
18
  except Exception:
19
  # Fallback: save via PIL if direct copy fails
20
  Image.open(src).save(img_path)
21
+
22
  outputs = run_pipeline_on_image(str(img_path), tmpdir, save_artifacts=True)
23
 
24
  def load_pil(path_str):
 
26
  if not path_str:
27
  return None
28
  im = Image.open(path_str)
 
29
  copied = im.copy()
30
  im.close()
31
  return copied
 
50
 
51
  with gr.Row():
52
  with gr.Column():
53
+ # Use gr.File instead of gr.Image so TIFF is preserved
54
+ inp = gr.File(type="file", file_types=[".tif", ".tiff", ".png", ".jpg"], label="Upload Image")
55
  run = gr.Button("Run Pipeline", variant="primary")
56
 
57
  with gr.Row():
 
69
  run.click(process, inputs=inp, outputs=[size_img, composite_img, mask_img, overlay_img, texture_img, gallery, stats])
70
 
71
  if __name__ == "__main__":
72
+ demo.launch()
sorghum_pipeline/pipeline.py CHANGED
@@ -46,46 +46,61 @@ class SorghumPipeline:
46
  def run(self, single_image_path: str) -> Dict[str, Any]:
47
  """Run pipeline on single image."""
48
  logger.info("Processing single image...")
49
-
50
- import time
51
-
52
- start = time.perf_counter()
53
- import imghdr
54
- import tifffile
55
- import cv2
56
  from PIL import Image
57
 
 
 
 
58
  kind = imghdr.what(single_image_path)
 
59
 
60
- if kind == "tiff":
61
- arr = tifffile.imread(single_image_path)
62
- print("DEBUG loaded TIFF:", arr.shape, arr.dtype)
63
- img = Image.fromarray(arr) # keep pipeline compatibility
 
 
 
 
 
64
  else:
65
  arr = cv2.imread(single_image_path, cv2.IMREAD_UNCHANGED)
66
- print("DEBUG loaded non-TIFF:", arr.shape, arr.dtype)
67
- img = Image.fromarray(arr)
68
-
69
- print("Debug: Uploaded image shape: ", arr.shape)
 
 
 
 
 
 
 
 
 
 
 
70
  plants = {
71
  "demo": {
72
  "raw_image": (img, Path(single_image_path).name),
73
  "plant_name": "demo",
74
  }
75
  }
76
-
77
  # Process: composite → segment → features → save
78
  plants = self.preprocessor.create_composites(plants)
79
  plants = self._segment(plants)
80
  plants = self._extract_features(plants)
81
  self.output_manager.create_output_directories()
82
-
83
  for key, pdata in plants.items():
84
  self.output_manager.save_plant_results(key, pdata)
85
-
86
  elapsed = time.perf_counter() - start
87
  logger.info(f"Completed in {elapsed:.2f}s")
88
-
89
  return {"plants": plants, "timing": elapsed}
90
 
91
  def _segment(self, plants: Dict[str, Any]) -> Dict[str, Any]:
@@ -101,60 +116,66 @@ class SorghumPipeline:
101
  return plants
102
 
103
  def _extract_features(self, plants: Dict[str, Any]) -> Dict[str, Any]:
104
- """Extract features (NDVI only for now)."""
105
  for key, pdata in plants.items():
106
  composite = pdata['composite']
107
  mask = pdata.get('mask')
108
 
109
- # Texture: ONLY LBP on green band within mask
110
  pdata['texture_features'] = {}
111
- green_band = None
112
  spectral = pdata.get('spectral_stack', {})
113
  if 'green' in spectral:
114
- green_band = spectral['green'].squeeze(-1).astype(np.float64)
 
 
 
115
  if mask is not None:
116
  valid = np.where(mask > 0, green_band, np.nan)
117
  else:
118
  valid = green_band
119
- # normalize to uint8 for LBP
120
- v = valid.copy()
121
- v = np.nan_to_num(v, nan=np.nanmin(v))
122
  m, M = np.min(v), np.max(v)
123
  denom = (M - m) if (M - m) > 1e-6 else 1.0
124
  gray8 = ((v - m) / denom * 255.0).astype(np.uint8)
 
125
  lbp_map = self.texture_extractor.extract_lbp(gray8)
126
  pdata['texture_features'] = {'green': {'features': {'lbp': lbp_map}}}
127
 
128
- # Vegetation: NDVI, GNDVI, SAVI
129
- spectral = pdata.get('spectral_stack', {})
130
  if spectral and mask is not None:
131
  pdata['vegetation_indices'] = self._compute_vegetation(spectral, mask)
132
  else:
133
  pdata['vegetation_indices'] = {}
134
-
135
- # # Morphology: PlantCV size analysis (COMMENTED OUT)
136
- # pdata['morphology_features'] = self.morphology_extractor.extract_morphology_features(composite, mask)
137
  pdata['morphology_features'] = {}
138
 
139
  return plants
140
 
141
  def _compute_vegetation(self, spectral: Dict[str, np.ndarray], mask: np.ndarray) -> Dict[str, Any]:
142
- """Compute NDVI, ARI, GNDVI only."""
143
  out = {}
144
  for name in ("NDVI", "GNDVI", "SAVI"):
145
  bands = self.vegetation_extractor.index_bands.get(name, [])
146
  if not all(b in spectral for b in bands):
147
  continue
148
-
149
- arrays = [np.asarray(spectral[b].squeeze(-1), dtype=np.float64) for b in bands]
 
 
 
 
 
 
150
  values = self.vegetation_extractor.index_formulas[name](*arrays).astype(np.float64)
151
  binary_mask = (mask > 0)
152
  masked_values = np.where(binary_mask, values, np.nan)
153
  valid = masked_values[~np.isnan(masked_values)]
154
-
155
  stats = {
156
  'mean': float(np.mean(valid)) if valid.size else 0.0,
157
  'std': float(np.std(valid)) if valid.size else 0.0,
158
  }
159
  out[name] = {'values': masked_values, 'statistics': stats}
160
- return out
 
46
  def run(self, single_image_path: str) -> Dict[str, Any]:
47
  """Run pipeline on single image."""
48
  logger.info("Processing single image...")
49
+
50
+ import time, imghdr, tifffile
 
 
 
 
 
51
  from PIL import Image
52
 
53
+ start = time.perf_counter()
54
+
55
+ # --- Load image with TIFF preference ---
56
  kind = imghdr.what(single_image_path)
57
+ suffix = Path(single_image_path).suffix.lower()
58
 
59
+ arr = None
60
+ if kind == "tiff" or suffix in [".tif", ".tiff"]:
61
+ try:
62
+ arr = tifffile.imread(single_image_path)
63
+ logger.info(f"Loaded TIFF: shape={arr.shape}, dtype={arr.dtype}")
64
+ except Exception as e:
65
+ logger.warning(f"tifffile failed ({e}), falling back to cv2")
66
+ arr = cv2.imread(single_image_path, cv2.IMREAD_UNCHANGED)
67
+ logger.info(f"Fallback read: shape={arr.shape}, dtype={arr.dtype}")
68
  else:
69
  arr = cv2.imread(single_image_path, cv2.IMREAD_UNCHANGED)
70
+ logger.info(f"Loaded non-TIFF: shape={arr.shape}, dtype={arr.dtype}")
71
+
72
+ # --- Normalize array shape ---
73
+ if arr is None:
74
+ raise ValueError(f"Could not read image: {single_image_path}")
75
+ if arr.ndim > 3:
76
+ arr = arr[..., 0] # drop extra dimension
77
+ if arr.ndim == 3 and arr.shape[-1] == 1:
78
+ arr = arr[..., 0] # squeeze singleton
79
+
80
+ logger.info(f"DEBUG normalized input: shape={arr.shape}, dtype={arr.dtype}")
81
+
82
+ # Wrap into PIL image for downstream pipeline
83
+ img = Image.fromarray(arr)
84
+
85
  plants = {
86
  "demo": {
87
  "raw_image": (img, Path(single_image_path).name),
88
  "plant_name": "demo",
89
  }
90
  }
91
+
92
  # Process: composite → segment → features → save
93
  plants = self.preprocessor.create_composites(plants)
94
  plants = self._segment(plants)
95
  plants = self._extract_features(plants)
96
  self.output_manager.create_output_directories()
97
+
98
  for key, pdata in plants.items():
99
  self.output_manager.save_plant_results(key, pdata)
100
+
101
  elapsed = time.perf_counter() - start
102
  logger.info(f"Completed in {elapsed:.2f}s")
103
+
104
  return {"plants": plants, "timing": elapsed}
105
 
106
  def _segment(self, plants: Dict[str, Any]) -> Dict[str, Any]:
 
116
  return plants
117
 
118
  def _extract_features(self, plants: Dict[str, Any]) -> Dict[str, Any]:
119
+ """Extract features: texture + vegetation indices."""
120
  for key, pdata in plants.items():
121
  composite = pdata['composite']
122
  mask = pdata.get('mask')
123
 
124
+ # --- Texture: LBP on green band ---
125
  pdata['texture_features'] = {}
 
126
  spectral = pdata.get('spectral_stack', {})
127
  if 'green' in spectral:
128
+ green_band = np.asarray(spectral['green'], dtype=np.float64)
129
+ if green_band.ndim == 3 and green_band.shape[-1] == 1:
130
+ green_band = green_band[..., 0]
131
+
132
  if mask is not None:
133
  valid = np.where(mask > 0, green_band, np.nan)
134
  else:
135
  valid = green_band
136
+
137
+ v = np.nan_to_num(valid, nan=np.nanmin(valid))
 
138
  m, M = np.min(v), np.max(v)
139
  denom = (M - m) if (M - m) > 1e-6 else 1.0
140
  gray8 = ((v - m) / denom * 255.0).astype(np.uint8)
141
+
142
  lbp_map = self.texture_extractor.extract_lbp(gray8)
143
  pdata['texture_features'] = {'green': {'features': {'lbp': lbp_map}}}
144
 
145
+ # --- Vegetation indices ---
 
146
  if spectral and mask is not None:
147
  pdata['vegetation_indices'] = self._compute_vegetation(spectral, mask)
148
  else:
149
  pdata['vegetation_indices'] = {}
150
+
151
+ # --- Morphology (currently empty) ---
 
152
  pdata['morphology_features'] = {}
153
 
154
  return plants
155
 
156
  def _compute_vegetation(self, spectral: Dict[str, np.ndarray], mask: np.ndarray) -> Dict[str, Any]:
157
+ """Compute NDVI, GNDVI, SAVI."""
158
  out = {}
159
  for name in ("NDVI", "GNDVI", "SAVI"):
160
  bands = self.vegetation_extractor.index_bands.get(name, [])
161
  if not all(b in spectral for b in bands):
162
  continue
163
+
164
+ arrays = []
165
+ for b in bands:
166
+ arr = np.asarray(spectral[b], dtype=np.float64)
167
+ if arr.ndim == 3 and arr.shape[-1] == 1:
168
+ arr = arr[..., 0]
169
+ arrays.append(arr)
170
+
171
  values = self.vegetation_extractor.index_formulas[name](*arrays).astype(np.float64)
172
  binary_mask = (mask > 0)
173
  masked_values = np.where(binary_mask, values, np.nan)
174
  valid = masked_values[~np.isnan(masked_values)]
175
+
176
  stats = {
177
  'mean': float(np.mean(valid)) if valid.size else 0.0,
178
  'std': float(np.std(valid)) if valid.size else 0.0,
179
  }
180
  out[name] = {'values': masked_values, 'statistics': stats}
181
+ return out