Skip to content

Commit 36ee408

Browse files
authored
Merge pull request #2 from Som5ra/agent/pip-git-install
Package FastVisionOps for Git installs and organize modules
2 parents 2af5cbf + 050d0c3 commit 36ee408

32 files changed

Lines changed: 1013 additions & 597 deletions

.github/workflows/ci.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,58 @@ jobs:
2525
cache: pip
2626
- name: Install package
2727
run: python -m pip install --upgrade pip && python -m pip install .
28+
- name: Verify installed native package
29+
working-directory: ${{ runner.temp }}
30+
run: |
31+
python - <<'PY'
32+
import numpy as np
33+
34+
from fastvisionops import NativeBackend
35+
36+
backend = NativeBackend()
37+
assert backend.library_path.name.startswith("_native")
38+
np.testing.assert_array_equal(
39+
backend.nms(
40+
[[0, 0, 10, 10], [1, 1, 9, 9], [20, 20, 30, 30]],
41+
[0.9, 0.8, 0.7],
42+
),
43+
[0, 2],
44+
)
45+
output = backend.hwc_to_chw_normalize(
46+
np.zeros((4, 5, 3), dtype=np.uint8),
47+
[0, 0, 0],
48+
[1, 1, 1],
49+
threads=2,
50+
)
51+
assert output.shape == (3, 4, 5)
52+
PY
2853
- name: Build native backend
2954
run: python -m fastvisionops.build
55+
- name: Verify portable native build
56+
run: |
57+
python -m fastvisionops.build \
58+
--no-openmp \
59+
--output "${RUNNER_TEMP}/libfastvisionops-portable.so"
60+
python - <<'PY'
61+
import os
62+
import numpy as np
63+
64+
from fastvisionops import NativeBackend
65+
66+
backend = NativeBackend(
67+
os.path.join(
68+
os.environ["RUNNER_TEMP"],
69+
"libfastvisionops-portable.so",
70+
)
71+
)
72+
np.testing.assert_array_equal(
73+
backend.nms(
74+
[[0, 0, 10, 10], [1, 1, 9, 9], [20, 20, 30, 30]],
75+
[0.9, 0.8, 0.7],
76+
),
77+
[0, 2],
78+
)
79+
PY
3080
- name: Run test suite
3181
run: python -m unittest discover -s tests -v
3282
- name: Smoke-test benchmark runners

README.md

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,28 @@ flowchart LR
2929

3030
## Install
3131

32+
Install directly from GitHub over SSH:
33+
3234
```bash
33-
python -m pip install .
34-
python -m fastvisionops.build
35+
python -m pip install "git+ssh://git@github.com/Som5ra/FastVisionOps.git"
3536
```
3637

37-
The NumPy APIs work immediately after installation. The second command builds
38-
the optional native backend with GCC or Clang. It uses OpenMP when supported
39-
and otherwise retries as portable single-threaded C. Use `CC`, `--compiler`,
40-
or `--no-openmp` to control the build.
38+
The equivalent public HTTPS command does not require an SSH key:
39+
40+
```bash
41+
python -m pip install "git+https://github.com/Som5ra/FastVisionOps.git"
42+
```
43+
44+
For a local checkout, use `python -m pip install .`. Each command installs
45+
NumPy when needed and compiles the native backend into the wheel, so
46+
`NativeBackend()` works immediately. GCC or Clang is required; OpenMP is used
47+
when supported and otherwise falls back to portable single-threaded C.
48+
49+
To pin a branch, tag, or commit, append its ref:
50+
51+
```bash
52+
python -m pip install "git+https://github.com/Som5ra/FastVisionOps.git@<ref>"
53+
```
4154

4255
## Quick start
4356

@@ -139,17 +152,32 @@ environment, results, and limitations.
139152
Standalone transpose and normalization remain NumPy operations; the fused
140153
native path avoids intermediate arrays and accelerates the useful hot path.
141154

155+
## Repository layout
156+
157+
| Path | Responsibility |
158+
| --- | --- |
159+
| `fastvisionops/preprocess/` | Validated NumPy layout conversion and normalization |
160+
| `fastvisionops/postprocess/` | Bounding-box and boolean-mask suppression |
161+
| `fastvisionops/native/` | ctypes bindings, builder, and colocated C source |
162+
| `fastvisionops/{bbox,mask,build}.py` | Stable compatibility import paths |
163+
| `nmss/` | Backward-compatible namespace for existing users |
164+
| `legacy/` | Original standalone adapters; excluded from installation |
165+
166+
The maintained implementation flows one way: public APIs delegate to the
167+
stage package, while compatibility modules only re-export those functions.
168+
142169
## Validation
143170

144171
```bash
145172
python -m fastvisionops.build
146173
python -m unittest discover -s tests -v
147174
```
148175

149-
The 41 tests cover exact and randomized NumPy/native equivalence, empty and
176+
The 47 tests cover exact and randomized NumPy/native equivalence, empty and
150177
noncontiguous inputs, channel reversal, deterministic ties, multiclass
151-
behavior, malformed controls, portable builds, and serial/concurrent batches.
152-
CI runs the suite and benchmark smoke tests on Python 3.9, 3.12, and 3.13.
178+
behavior, malformed controls, compatibility imports, package layout, portable
179+
builds, and serial/concurrent batches. CI runs the suite and benchmark smoke
180+
tests on Python 3.9, 3.12, and 3.13.
153181

154182
## Migration
155183

fastvisionops/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
from typing import TYPE_CHECKING
44

5-
from nmss.bbox import (
5+
from .postprocess.bbox import (
66
bbox_iou,
77
multiclass_nms,
88
multiclass_nms_class_aware,
99
multiclass_nms_class_unaware,
1010
nms,
1111
)
12-
from nmss.mask import mask_iou, mask_nms, multiclass_mask_nms
12+
from .postprocess.mask import mask_iou, mask_nms, multiclass_mask_nms
1313

1414
from .preprocess import (
1515
chw_channel_normalize,

fastvisionops/_validation.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Shared validation helpers for FastVisionOps operations."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
7+
import numpy as np
8+
from numpy.typing import ArrayLike, NDArray
9+
10+
11+
def validate_threshold(name: str, value: float) -> float:
12+
value = float(value)
13+
if not np.isfinite(value) or not 0.0 <= value <= 1.0:
14+
raise ValueError(f"{name} must be finite and in [0, 1], got {value!r}")
15+
return value
16+
17+
18+
def validate_offset(offset: float) -> float:
19+
offset = float(offset)
20+
if offset not in (0.0, 1.0):
21+
raise ValueError(f"offset must be 0 or 1, got {offset!r}")
22+
return offset
23+
24+
25+
def validate_max_detections(value: int | None) -> int | None:
26+
if value is None:
27+
return None
28+
if (
29+
isinstance(value, (bool, np.bool_))
30+
or not isinstance(value, (int, np.integer))
31+
or value < 0
32+
):
33+
raise ValueError("max_detections must be a non-negative integer or None")
34+
return int(value)
35+
36+
37+
def validate_boxes(boxes: ArrayLike) -> NDArray[np.float64]:
38+
result = np.ascontiguousarray(boxes, dtype=np.float64)
39+
if result.ndim != 2 or result.shape[1:] != (4,):
40+
raise ValueError(f"boxes must have shape (N, 4), got {result.shape}")
41+
if not np.isfinite(result).all():
42+
raise ValueError("boxes must contain only finite values")
43+
if result.size and (
44+
np.any(result[:, 2] < result[:, 0])
45+
or np.any(result[:, 3] < result[:, 1])
46+
):
47+
raise ValueError("each box must satisfy x2 >= x1 and y2 >= y1")
48+
return result
49+
50+
51+
def validate_scores(
52+
scores: ArrayLike,
53+
num_items: int,
54+
*,
55+
ndim: int,
56+
) -> NDArray[np.float64]:
57+
result = np.ascontiguousarray(scores, dtype=np.float64)
58+
if result.ndim != ndim:
59+
shape = "(N,)" if ndim == 1 else "(N, C)"
60+
raise ValueError(f"scores must have shape {shape}, got {result.shape}")
61+
if result.shape[0] != num_items:
62+
raise ValueError(
63+
"boxes/masks and scores must contain the same number of items, "
64+
f"got {num_items} and {result.shape[0]}"
65+
)
66+
if ndim == 2 and result.shape[1] == 0:
67+
raise ValueError("scores must contain at least one class")
68+
if not np.isfinite(result).all():
69+
raise ValueError("scores must contain only finite values")
70+
return result
71+
72+
73+
def validate_masks(masks: ArrayLike) -> NDArray[np.bool_]:
74+
result = np.asarray(masks)
75+
if result.ndim < 2:
76+
raise ValueError(f"masks must have shape (N, ...), got {result.shape}")
77+
if result.dtype != np.bool_:
78+
raise TypeError(f"masks must have boolean dtype, got {result.dtype}")
79+
return np.ascontiguousarray(result)
80+
81+
82+
def validate_batch(
83+
boxes: Sequence[ArrayLike],
84+
scores: Sequence[ArrayLike],
85+
) -> None:
86+
if len(boxes) != len(scores):
87+
raise ValueError(
88+
"boxes and scores batches must have equal length, "
89+
f"got {len(boxes)} and {len(scores)}"
90+
)

fastvisionops/bbox.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
"""Bounding-box operations exposed under the FastVisionOps namespace."""
1+
"""Compatibility import for :mod:`fastvisionops.postprocess.bbox`."""
22

3-
from nmss.bbox import (
3+
from .postprocess.bbox import (
44
bbox_iou,
55
multiclass_nms,
66
multiclass_nms_class_aware,

fastvisionops/build.py

Lines changed: 24 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,27 @@
1-
"""Build the optional FastVisionOps native backend."""
2-
3-
from __future__ import annotations
4-
5-
import argparse
6-
import os
7-
from pathlib import Path
8-
import shutil
9-
import subprocess
10-
import sys
11-
12-
13-
PACKAGE_ROOT = Path(__file__).resolve().parent
14-
SOURCE = PACKAGE_ROOT / "csrc" / "vision_ops.c"
15-
DEFAULT_OUTPUT = PACKAGE_ROOT / "lib" / "libfastvisionops.so"
16-
17-
18-
def _compile(command: list[str]) -> subprocess.CompletedProcess[str]:
19-
return subprocess.run(command, text=True, capture_output=True)
20-
21-
22-
def build_native_backend(
23-
output: str | os.PathLike[str] | None = None,
24-
*,
25-
compiler: str | None = None,
26-
openmp: bool = True,
27-
) -> Path:
28-
"""Compile the shared C library and return its path.
29-
30-
OpenMP is attempted by default. If the compiler does not support it, the
31-
same source is rebuilt as a portable single-threaded library.
32-
"""
33-
output_path = Path(output).resolve() if output else DEFAULT_OUTPUT
34-
compiler = compiler or os.environ.get("CC", "cc")
35-
if shutil.which(compiler) is None:
36-
raise RuntimeError(
37-
f"C compiler {compiler!r} was not found; install GCC or Clang "
38-
"or set the CC environment variable"
39-
)
40-
output_path.parent.mkdir(parents=True, exist_ok=True)
41-
base_command = [
42-
compiler,
43-
"-O3",
44-
"-std=c11",
45-
"-DNDEBUG",
46-
"-fPIC",
47-
"-shared",
48-
str(SOURCE),
49-
"-lm",
50-
"-o",
51-
str(output_path),
52-
]
53-
command = base_command[:1] + (["-fopenmp"] if openmp else []) + base_command[1:]
54-
result = _compile(command)
55-
if result.returncode and openmp:
56-
result = _compile(base_command)
57-
if result.returncode:
58-
detail = result.stderr.strip() or result.stdout.strip()
59-
raise RuntimeError(f"native backend build failed: {detail}")
60-
return output_path
61-
62-
63-
build_c_backend = build_native_backend
64-
65-
66-
def main(argv: list[str] | None = None) -> int:
67-
parser = argparse.ArgumentParser(
68-
description="Compile the optional FastVisionOps C backend."
69-
)
70-
parser.add_argument("--output", help="custom output library path")
71-
parser.add_argument("--compiler", help="C compiler executable")
72-
parser.add_argument(
73-
"--no-openmp",
74-
action="store_true",
75-
help="build a portable single-threaded backend",
76-
)
77-
arguments = parser.parse_args(argv)
78-
try:
79-
output = build_native_backend(
80-
arguments.output,
81-
compiler=arguments.compiler,
82-
openmp=not arguments.no_openmp,
83-
)
84-
except RuntimeError as error:
85-
parser.exit(1, f"error: {error}\n")
86-
print(output)
87-
return 0
1+
"""Public native build API.
2+
3+
The implementation lives under :mod:`fastvisionops.native` alongside the
4+
backend and C source. This module preserves the original command and imports.
5+
"""
6+
7+
from .native.build import (
8+
DEFAULT_OUTPUT,
9+
PACKAGE_ROOT,
10+
SOURCE,
11+
build_c_backend,
12+
build_native_backend,
13+
main,
14+
)
15+
16+
__all__ = [
17+
"DEFAULT_OUTPUT",
18+
"PACKAGE_ROOT",
19+
"SOURCE",
20+
"build_c_backend",
21+
"build_native_backend",
22+
"main",
23+
]
8824

8925

9026
if __name__ == "__main__":
91-
sys.exit(main())
27+
raise SystemExit(main())

fastvisionops/mask.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
"""Mask operations exposed under the FastVisionOps namespace."""
1+
"""Compatibility import for :mod:`fastvisionops.postprocess.mask`."""
22

3-
from nmss.mask import (
3+
from .postprocess.mask import (
44
mask_iou,
55
mask_nms,
66
mask_nms_cpu,

fastvisionops/native/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Compiled backend and build helpers."""
2+
3+
from .backend import (
4+
CBackend,
5+
NativeBackend,
6+
batch_multiclass_nms,
7+
hwc_to_chw_normalize,
8+
hwc_to_chw_normalize_batched,
9+
load_backend,
10+
multiclass_nms,
11+
nms,
12+
)
13+
from .build import DEFAULT_OUTPUT
14+
15+
__all__ = [
16+
"CBackend",
17+
"DEFAULT_OUTPUT",
18+
"NativeBackend",
19+
"batch_multiclass_nms",
20+
"hwc_to_chw_normalize",
21+
"hwc_to_chw_normalize_batched",
22+
"load_backend",
23+
"multiclass_nms",
24+
"nms",
25+
]

0 commit comments

Comments
 (0)