Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install package
run: python -m pip install --upgrade pip && python -m pip install .
- name: Build native backend
run: python -m fastvisionops.build
- name: Run test suite
run: python -m unittest discover -s tests -v
- name: Smoke-test benchmark runners
run: |
python -m benchmarks.benchmark_bbox --sizes 16 64 --warmup 0 --repeats 1 --batch-size 2 --batch-boxes 32 --workers 2 --format json
python -m benchmarks.benchmark_preprocess --batches 1 2 --height 32 --width 32 --warmup 0 --repeats 1 --threads 2 --format json
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
__pycache__/
*.py[cod]
*.so
.pytest_cache/
.coverage
build/
dist/
*.egg-info/
.venv/
201 changes: 150 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,173 @@
# NMS (Non-maximum Suppression) Tools
# FastVisionOps

[![CI](https://github.com/Som5ra/FastVisionOps/actions/workflows/ci.yml/badge.svg)](https://github.com/Som5ra/FastVisionOps/actions/workflows/ci.yml)
[![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-3776AB.svg)](https://www.python.org/)
[![Backend](https://img.shields.io/badge/backend-NumPy%20%2B%20C-4D77CF.svg)](https://numpy.org/)

**Validated, framework-independent CPU operations for vision inference.**

FastVisionOps combines the former FastPreProcess and NMSs projects behind one
Python package and one rebuildable native library. It covers image layout
conversion and normalization, bounding-box NMS, boolean-mask NMS, multiclass
suppression, and batched execution.

- **Unified:** one install, validation policy, native build, and test suite.
- **Measured:** benchmarks report absolute latency and speedup after checking
native output against NumPy.
- **Defensive:** malformed images, statistics, boxes, scores, and controls fail
early with actionable errors.
- **Reproducible:** portable C source replaces opaque platform binaries and
falls back cleanly when OpenMP is unavailable.

```mermaid
flowchart LR
A["uint8 HWC / NHWC"] --> B["Layout + normalize"]
B --> C["Model inference"]
C --> D["Boxes, scores, masks"]
D --> E["Deterministic NMS"]
```

## Install

<details>
<summary>Bounding Box NMS</summary>
- Refer to ./bbox-nms/nms.py
</details>
```bash
python -m pip install .
python -m fastvisionops.build
```

<details>
<summary>Bounding Box NMS - C language version</summary>
The NumPy APIs work immediately after installation. The second command builds
the optional native backend with GCC or Clang. It uses OpenMP when supported
and otherwise retries as portable single-threaded C. Use `CC`, `--compiler`,
or `--no-openmp` to control the build.

## Bounding Box NMS - C language version
### Benchmark (Single Batch / s)
## Quick start

- Each Single-Batch-Data have 2000 bounding boxes
- Each test run 1000 times to obtain results
### Preprocess images

- Speed(ms) : including: preprocessing, nms
- W/O processing (ms): only including: nms
```python
import numpy as np

| Algo / Paramters | Python | C | C | Batch Pallel C | Batch Pallel C |
|------------------|-----------|---------------|---------------------|----------------|---------------------|
| Batch Num | Speed(ms)| Speed(ms) | W/O processing (ms)| Speed(ms) | W/O processing (ms)|
| 1 | 0.611 | **0.258** | 0.211 | 0.834 | 0.735 |
| 10 | 0.610 | **0.256** | 0.211 | 0.343 | 0.175 |
| 100 | 0.603 | **0.260** | 0.214 | 0.354 | 0.094 |
from fastvisionops import NativeBackend, hwc_to_chw_normalize

### Usage: Refer to batch_parallel_nms.py
image = np.zeros((427, 640, 3), dtype=np.uint8)
mean = [123.675, 116.28, 103.53]
std = [58.395, 57.12, 57.375]

```Python
num_classes = 80
score_thr = 0.5
nms_thr = 0.5
# NumPy reference: float32 CHW, shape (3, 427, 640)
tensor = hwc_to_chw_normalize(image, mean, std, flip_rb=True)

batched_bboxes = [np.ones((2000, 4)), np.ones((123, 4)), np.ones((321, 4)), ...]
batched_scores = [np.ones((2000, num_classes)), np.ones((123, num_classes)), np.ones((321, num_classes)), ...]
# Native fused path
backend = NativeBackend()
fast_tensor = backend.hwc_to_chw_normalize(
image,
mean,
std,
flip_rb=True,
threads=8,
)
np.testing.assert_allclose(fast_tensor, tensor, rtol=1e-6, atol=1e-6)
```

nms_c = Batch_Parallel_Nms()
Single-image HWC and batched NHWC inputs are supported. Outputs are contiguous
CHW or NCHW arrays. Noncontiguous inputs are handled correctly.

### Suppress detections

```python
from fastvisionops import nms

boxes = np.array(
[
[0.0, 0.0, 10.0, 10.0],
[1.0, 1.0, 9.0, 9.0],
[20.0, 20.0, 30.0, 30.0],
]
)
scores = np.array([0.90, 0.80, 0.70])

keep = nms(
boxes,
scores,
score_threshold=0.50,
iou_threshold=0.50,
)
# array([0, 2])
```

# NMS
for boxes, scores in zip(batched_bboxes, batched_scores):
indices_to_keep, nms_out_cls = nms_c.nms(boxes, scores, score_thr, nms_thr)
Use `NativeBackend.nms` for native bbox execution. `multiclass_nms` supports
class-aware and class-unaware suppression. `mask_nms` and
`multiclass_mask_nms` operate on boolean masks.

# BATCH PARALLEL
indices_to_keep, nms_out_cls = nms_c.batch_parallel_nms(batched_bboxes, batched_scores, score_thr, nms_thr)
```
## Measured performance

Each row reports the median duration of one complete public API call after two
warm-ups and nine measured runs. Validation and allocation are included.

| Workload | Baseline | Optimized | Speedup |
| --- | ---: | ---: | ---: |
| Preprocess 1 × 427×640×3 | NumPy 4.492 ms | Native 0.249 ms | **18.07x** |
| Preprocess 32 × 427×640×3 | NumPy 139.105 ms | Native 9.758 ms | **14.25x** |
| NMS, 250 boxes | NumPy 4.798 ms | Native 0.272 ms | **17.66x** |
| NMS, 2,500 boxes | NumPy 75.396 ms | Native 17.121 ms | **4.40x** |
| 8 images × 1,000 boxes | Serial C 27.595 ms | Parallel C 9.995 ms | **2.76x** |

Recorded on Linux x86_64 with Python 3.12.13, NumPy 2.3.5, GCC 13.3, and
9 available Intel Xeon Platinum 8573C vCPUs. Reproduce the measurements:

### If there is any modified
```bash
gcc -O3 -msse2 -mfpmath=sse -ftree-vectorizer-verbose=5 -fopenmp -fPIC -shared -o c/compiled/batch_parallel_nms.so c/batch_parallel_nms.c
python -m benchmarks.benchmark_preprocess
python -m benchmarks.benchmark_bbox
```
</details>

<details>
<summary>Mask NMS</summary>
Add `--format json` for machine-readable output. The
[evaluation report](docs/evaluation.md) documents the complete methodology,
environment, results, and limitations.

### Mutli class mask NMS (class-aware)
## API

- Class-unaware: a proposal can belong to mutiple single class
| API | Purpose | Backend |
| --- | --- | --- |
| `hwc_to_chw` | HWC → CHW layout conversion | NumPy |
| `chw_channel_normalize` | Per-channel CHW normalization | NumPy |
| `hwc_to_chw_normalize` | Fused HWC → normalized CHW | NumPy |
| `hwc_to_chw_normalize_batched` | Fused NHWC → normalized NCHW | NumPy |
| `NativeBackend.hwc_to_chw_normalize*` | Fused single/batch preprocessing | C / OpenMP |
| `nms` / `multiclass_nms` | Bounding-box NMS | NumPy |
| `mask_nms` / `multiclass_mask_nms` | Boolean-mask NMS | NumPy |
| `NativeBackend.nms` / `multiclass_nms` | Bounding-box NMS | C |
| `NativeBackend.batch_multiclass_nms` | Concurrent image batches | C |

- inputs:
- masks: NDArray (num_masks, W, H) (type: Boolean)
- scores: NDArray (num_masks, num_classes) in [0, 1]
- score_thr: float (score threshold of bounding box)
- nms_thr: float (intersection threshold of mask)
- output:
- [NDArray of indices to keep, NDArray of class id]
Standalone transpose and normalization remain NumPy operations; the fused
native path avoids intermediate arrays and accelerates the useful hot path.

## Validation

```bash
python -m fastvisionops.build
python -m unittest discover -s tests -v
```
pip install numba
pip install numpy
```

</details>
The 41 tests cover exact and randomized NumPy/native equivalence, empty and
noncontiguous inputs, channel reversal, deterministic ties, multiclass
behavior, malformed controls, portable builds, and serial/concurrent batches.
CI runs the suite and benchmark smoke tests on Python 3.9, 3.12, and 3.13.

## Migration

New integrations should import from `fastvisionops`.

| Previous API | FastVisionOps API |
| --- | --- |
| `fastpreprocess.hwc_to_chw_normalize` | `fastvisionops.hwc_to_chw_normalize` |
| `fastpreprocess.hwc_to_chw_normalize_batched` | `fastvisionops.hwc_to_chw_normalize_batched` |
| FastPreProcess compiled functions | `fastvisionops.NativeBackend` methods |
| `nmss.nms` and related imports | `fastvisionops.nms` and related imports |
| `nmss.c_backend.CBackend` | `fastvisionops.NativeBackend` |

The `nmss` namespace remains backward compatible and points to the maintained
implementation. The unsafe FastPreProcess binary is not shipped; its
replacement validates inputs, uses NumPy-owned memory, supports noncontiguous
arrays, and removes the unused OpenCV and pybind11 dependencies.

FastVisionOps currently targets deterministic CPU utilities. GPU kernels,
resize/color conversion, Soft-NMS, DIoU-NMS, native mask kernels, and prebuilt
wheels remain outside this release.
57 changes: 22 additions & 35 deletions bbox-nms-c-version/README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,29 @@
## NMS(Non-maximum Suppression) Acceleration
# Legacy C API

This directory preserves the original `Batch_Parallel_Nms` import path.
Maintained native code now lives in `fastvisionops/csrc`, and the shared
library is rebuilt locally:

### Benchmark (Single Batch / s)

- Each Single-Batch-Data have 2000 bounding boxes
- Each test run 1000 times to obtain results

- Speed(ms) : including: preprocessing, nms
- W/O processing (ms): only including: nms

| Algo / Paramters | Python | C | C | Batch Pallel C | Batch Pallel C |
|------------------|-----------|---------------|---------------------|----------------|---------------------|
| Batch Num | Speed(ms)| Speed(ms) | W/O processing (ms)| Speed(ms) | W/O processing (ms)|
| 1 | 0.611 | **0.258** | 0.211 | 0.834 | 0.735 |
| 10 | 0.610 | **0.256** | 0.211 | 0.343 | 0.175 |
| 100 | 0.603 | **0.260** | 0.214 | 0.354 | 0.094 |

### Usage: Refer to batch_parallel_nms.py

```Python
num_classes = 80
score_thr = 0.5
nms_thr = 0.5

batched_bboxes = [np.ones((2000, 4)), np.ones((123, 4)), np.ones((321, 4)), ...]
batched_scores = [np.ones((2000, num_classes)), np.ones((123, num_classes)), np.ones((321, num_classes)), ...]
```bash
python -m fastvisionops.build
```

nms_c = Batch_Parallel_Nms()
Existing calls continue to work:

# NMS
for boxes, scores in zip(batched_bboxes, batched_scores):
indices_to_keep, nms_out_cls = nms_c.nms(boxes, scores, score_thr, nms_thr)
```python
from batch_parallel_nms import Batch_Parallel_Nms

# BATCH PARALLEL
indices_to_keep, nms_out_cls = nms_c.batch_parallel_nms(batched_bboxes, batched_scores, score_thr, nms_thr)
backend = Batch_Parallel_Nms()
indices, class_ids = backend.nms(boxes, scores, 0.5, 0.5)
batch_indices, batch_class_ids = backend.batch_parallel_nms(
boxes_batch,
scores_batch,
0.5,
0.5,
)
```

### If there is any modified
```bash
gcc -O3 -msse2 -mfpmath=sse -ftree-vectorizer-verbose=5 -fopenmp -fPIC -shared -o c/compiled/batch_parallel_nms.so c/batch_parallel_nms.c
```
New code should use `fastvisionops.NativeBackend`. See the
[root README](../README.md) and
[evaluation report](../docs/evaluation.md) for the current API and verified
benchmark.
Loading
Loading