0
# Postprocessing
1
2
SAHI provides advanced postprocessing methods for combining overlapping predictions from sliced inference. These algorithms intelligently merge predictions to eliminate duplicates and improve detection accuracy across slice boundaries.
3
4
## Capabilities
5
6
### Postprocessing Base Class
7
8
Base class for all prediction postprocessing algorithms with configurable matching criteria.
9
10
```python { .api }
11
class PostprocessPredictions:
12
def __init__(
13
self,
14
match_threshold: float = 0.5,
15
match_metric: str = "IOS",
16
class_agnostic: bool = False,
17
):
18
"""
19
Initialize postprocessing configuration.
20
21
Parameters:
22
- match_threshold (float): Overlap threshold for matching predictions (0-1)
23
- match_metric (str): Overlap calculation method ("IOU" or "IOS")
24
- "IOU": Intersection over Union
25
- "IOS": Intersection over Smaller area
26
- class_agnostic (bool): Whether to ignore class when matching predictions
27
"""
28
29
def __call__(
30
self,
31
object_predictions: List[ObjectPrediction],
32
) -> List[ObjectPrediction]:
33
"""
34
Apply postprocessing to prediction list.
35
36
Parameters:
37
- object_predictions: List of ObjectPrediction instances
38
39
Returns:
40
List[ObjectPrediction]: Processed predictions with duplicates removed
41
"""
42
```
43
44
### Non-Maximum Suppression (NMS)
45
46
Classic NMS algorithm that removes predictions with high overlap, keeping only the highest confidence detection.
47
48
```python { .api }
49
class NMSPostprocess(PostprocessPredictions):
50
"""
51
Non-Maximum Suppression postprocessing.
52
Removes overlapping predictions, keeping only the highest confidence detection.
53
"""
54
55
def nms(
56
predictions: np.ndarray,
57
match_threshold: float = 0.5,
58
class_agnostic: bool = False,
59
) -> List[int]:
60
"""
61
Non-Maximum Suppression algorithm implementation.
62
63
Parameters:
64
- predictions (np.ndarray): Predictions array with bbox and scores
65
- match_threshold (float): IoU threshold for suppression
66
- class_agnostic (bool): Whether to apply NMS across all classes
67
68
Returns:
69
List[int]: Indices of predictions to keep
70
"""
71
72
def batched_nms(
73
predictions: np.ndarray,
74
match_threshold: float = 0.5,
75
class_agnostic: bool = False,
76
) -> List[int]:
77
"""
78
Batched Non-Maximum Suppression for efficient processing.
79
80
Parameters:
81
- predictions (np.ndarray): Predictions with bbox, scores, and classes
82
- match_threshold (float): IoU threshold for suppression
83
- class_agnostic (bool): Apply NMS across all classes
84
85
Returns:
86
List[int]: Indices of kept predictions
87
"""
88
```
89
90
### Non-Maximum Merging (NMM)
91
92
Advanced algorithm that merges overlapping predictions instead of simply removing them, preserving information from multiple detections.
93
94
```python { .api }
95
class NMMPostprocess(PostprocessPredictions):
96
"""
97
Non-Maximum Merging postprocessing.
98
Merges overlapping predictions instead of removing them, combining confidence scores
99
and bounding box coordinates to create more accurate final predictions.
100
"""
101
102
def nmm(
103
predictions: np.ndarray,
104
match_threshold: float = 0.5,
105
class_agnostic: bool = False,
106
) -> List[int]:
107
"""
108
Non-Maximum Merging algorithm implementation.
109
110
Parameters:
111
- predictions (np.ndarray): Predictions array with bbox and scores
112
- match_threshold (float): Overlap threshold for merging
113
- class_agnostic (bool): Whether to merge across all classes
114
115
Returns:
116
List[int]: Indices of final merged predictions
117
"""
118
119
def batched_nmm(
120
predictions: np.ndarray,
121
match_threshold: float = 0.5,
122
class_agnostic: bool = False,
123
) -> List[int]:
124
"""
125
Batched Non-Maximum Merging for efficient processing.
126
127
Parameters:
128
- predictions (np.ndarray): Predictions with bbox, scores, and classes
129
- match_threshold (float): Overlap threshold for merging
130
- class_agnostic (bool): Merge across all classes
131
132
Returns:
133
List[int]: Indices of merged predictions
134
"""
135
```
136
137
### Greedy Non-Maximum Merging
138
139
Greedy variant of NMM that processes predictions in confidence order for improved performance on sliced inference results.
140
141
```python { .api }
142
class GreedyNMMPostprocess(PostprocessPredictions):
143
"""
144
Greedy Non-Maximum Merging postprocessing.
145
Processes predictions in descending confidence order, greedily merging
146
overlapping detections. Optimized for sliced inference scenarios.
147
"""
148
149
def greedy_nmm(
150
predictions: np.ndarray,
151
match_threshold: float = 0.5,
152
class_agnostic: bool = False,
153
) -> List[int]:
154
"""
155
Greedy Non-Maximum Merging algorithm implementation.
156
157
Parameters:
158
- predictions (np.ndarray): Predictions array with bbox and scores
159
- match_threshold (float): Overlap threshold for merging
160
- class_agnostic (bool): Whether to merge across classes
161
162
Returns:
163
List[int]: Indices of merged predictions
164
"""
165
166
def batched_greedy_nmm(
167
predictions: np.ndarray,
168
match_threshold: float = 0.5,
169
class_agnostic: bool = False,
170
) -> List[int]:
171
"""
172
Batched Greedy Non-Maximum Merging for efficient processing.
173
174
Parameters:
175
- predictions (np.ndarray): Predictions with bbox, scores, and classes
176
- match_threshold (float): Overlap threshold for merging
177
- class_agnostic (bool): Merge predictions across all classes
178
179
Returns:
180
List[int]: Indices of kept predictions after merging
181
"""
182
```
183
184
### Linear Soft NMS
185
186
Soft NMS variant that gradually reduces confidence scores of overlapping predictions instead of hard removal.
187
188
```python { .api }
189
class LSNMSPostprocess(PostprocessPredictions):
190
"""
191
Linear Soft NMS postprocessing.
192
Applies soft suppression by linearly reducing confidence scores of overlapping
193
predictions instead of hard removal, preserving more detections.
194
"""
195
```
196
197
### Postprocessing Algorithm Mapping
198
199
```python { .api }
200
POSTPROCESS_NAME_TO_CLASS = {
201
"GREEDYNMM": GreedyNMMPostprocess,
202
"NMM": NMMPostprocess,
203
"NMS": NMSPostprocess,
204
"LSNMS": LSNMSPostprocess,
205
}
206
```
207
208
## Usage Examples
209
210
### Basic NMS Postprocessing
211
212
```python
213
from sahi.postprocess.combine import NMSPostprocess
214
from sahi import get_sliced_prediction
215
216
# Create NMS postprocessor
217
nms_postprocess = NMSPostprocess(
218
match_threshold=0.5,
219
match_metric="IOU",
220
class_agnostic=False
221
)
222
223
# Apply to sliced prediction
224
result = get_sliced_prediction(
225
image="large_image.jpg",
226
detection_model=model,
227
slice_height=640,
228
slice_width=640,
229
postprocess=nms_postprocess
230
)
231
232
print(f"Found {len(result.object_prediction_list)} objects after NMS")
233
```
234
235
### Greedy NMM for Sliced Inference
236
237
```python
238
from sahi.postprocess.combine import GreedyNMMPostprocess
239
240
# Recommended for sliced inference
241
greedy_nmm = GreedyNMMPostprocess(
242
match_threshold=0.5,
243
match_metric="IOS", # Intersection over Smaller area
244
class_agnostic=False
245
)
246
247
result = get_sliced_prediction(
248
image="satellite_image.tif",
249
detection_model=model,
250
slice_height=1024,
251
slice_width=1024,
252
postprocess=greedy_nmm
253
)
254
```
255
256
### Comparing Postprocessing Methods
257
258
```python
259
from sahi.postprocess.combine import (
260
NMSPostprocess,
261
GreedyNMMPostprocess,
262
NMMPostprocess
263
)
264
265
# Test different postprocessing approaches
266
postprocessors = {
267
"NMS": NMSPostprocess(match_threshold=0.5),
268
"NMM": NMMPostprocess(match_threshold=0.5),
269
"GreedyNMM": GreedyNMMPostprocess(match_threshold=0.5)
270
}
271
272
results = {}
273
for name, postprocessor in postprocessors.items():
274
result = get_sliced_prediction(
275
image="test_image.jpg",
276
detection_model=model,
277
postprocess=postprocessor
278
)
279
results[name] = len(result.object_prediction_list)
280
print(f"{name}: {results[name]} detections")
281
```
282
283
### Custom Postprocessing Configuration
284
285
```python
286
from sahi.postprocess.combine import GreedyNMMPostprocess
287
288
# Fine-tuned for specific use case
289
custom_postprocess = GreedyNMMPostprocess(
290
match_threshold=0.3, # Lower threshold for aggressive merging
291
match_metric="IOS", # Use Intersection over Smaller area
292
class_agnostic=True # Merge across different classes
293
)
294
295
# Apply with sliced prediction
296
result = get_sliced_prediction(
297
image="crowded_scene.jpg",
298
detection_model=model,
299
slice_height=512,
300
slice_width=512,
301
overlap_height_ratio=0.3, # Higher overlap
302
overlap_width_ratio=0.3,
303
postprocess=custom_postprocess,
304
verbose=2
305
)
306
```
307
308
### Using String-based Postprocessing
309
310
```python
311
from sahi.predict import get_sliced_prediction
312
313
# Use string identifiers for postprocessing
314
result = get_sliced_prediction(
315
image="image.jpg",
316
detection_model=model,
317
postprocess_type="GREEDYNMM", # Algorithm name
318
postprocess_match_metric="IOS", # Overlap metric
319
postprocess_match_threshold=0.5, # Threshold
320
postprocess_class_agnostic=False # Class-aware processing
321
)
322
```
323
324
### Direct Algorithm Usage
325
326
```python
327
from sahi.postprocess.combine import greedy_nmm, nms
328
import numpy as np
329
330
# Prepare predictions array [x1, y1, x2, y2, score, class_id]
331
predictions = np.array([
332
[10, 10, 50, 50, 0.9, 0], # High confidence person
333
[15, 15, 55, 55, 0.7, 0], # Overlapping person detection
334
[100, 100, 150, 150, 0.8, 1] # Car detection
335
])
336
337
# Apply Greedy NMM directly
338
kept_indices = greedy_nmm(
339
predictions=predictions,
340
match_threshold=0.5,
341
class_agnostic=False
342
)
343
344
final_predictions = predictions[kept_indices]
345
print(f"Kept {len(final_predictions)} predictions after merging")
346
347
# Compare with standard NMS
348
nms_indices = nms(
349
predictions=predictions,
350
match_threshold=0.5,
351
class_agnostic=False
352
)
353
354
print(f"NMS would keep {len(nms_indices)} predictions")
355
```
356
357
### Class-Agnostic vs Class-Aware Processing
358
359
```python
360
# Class-aware: merge only predictions of same class
361
class_aware = GreedyNMMPostprocess(
362
match_threshold=0.5,
363
class_agnostic=False # Only merge same-class predictions
364
)
365
366
# Class-agnostic: merge any overlapping predictions
367
class_agnostic = GreedyNMMPostprocess(
368
match_threshold=0.5,
369
class_agnostic=True # Merge overlapping predictions regardless of class
370
)
371
372
# Compare results
373
aware_result = get_sliced_prediction(
374
image="multi_class_scene.jpg",
375
detection_model=model,
376
postprocess=class_aware
377
)
378
379
agnostic_result = get_sliced_prediction(
380
image="multi_class_scene.jpg",
381
detection_model=model,
382
postprocess=class_agnostic
383
)
384
385
print(f"Class-aware: {len(aware_result.object_prediction_list)} detections")
386
print(f"Class-agnostic: {len(agnostic_result.object_prediction_list)} detections")
387
```