Skip to content

Commit 00cd96f

Browse files
authored
Merge pull request #24 from OBA-Research/dev
implement v0.1.9: Introduce model-variant API, branch-based HF loading, and integration test layer
2 parents 35f33fa + 0d697ff commit 00cd96f

11 files changed

Lines changed: 190 additions & 89 deletions

File tree

CONTRIBUTING.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# Contributing to VAAS
22

33
Thank you for your interest in improving **VAAS (Vision-Attention Anomaly Scoring)**.
4-
This document defines the **rules and expectations** for contributing to the VAAS codebase.
54

65
---
76

8-
## Important Design Constraints (Read First)
7+
## Few Important Design Constraints (Read First)
98

109
- **PyTorch must NOT be imported at import-time of public APIs**
1110
- Public entry points (e.g. `vaas.inference.pipeline` and other modules imported by users)
@@ -64,7 +63,7 @@ make build # build wheel + sdist
6463

6564
Formatting and linting are enforced with **ruff**.
6665

67-
Before submitting any change, ensure:
66+
Before submitting any change, please ensure:
6867

6968
```bash
7069
make format
@@ -82,7 +81,7 @@ pre-commit run --all-files
8281

8382
## 4. Testing Requirements
8483

85-
All contributions must pass:
84+
Tests are passing:
8685

8786
```bash
8887
make test-smoke

Makefile

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,9 @@ test-integration:
3232
uv venv .venv_test
3333
. .venv_test/bin/activate && \
3434
uv sync && \
35-
uv sync --group torch && \
36-
uv sync --group dev && \
37-
uv pip install dist/*.whl pytest && \
35+
uv sync --group torch --group dev
36+
uv pip install -e . && \
3837
uv run --active pytest -m integration -v ; \
39-
deactivate
4038
rm -rf .venv_test
4139

4240

README.md

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,17 @@ from PIL import Image
8181
import requests
8282
from io import BytesIO
8383

84+
repo_id = "OBA-Research/vaas"
85+
model_variant = "v1-base-df2023" # v1-medium-df2023 and v1-large-df2023 are other model variants you can use.
86+
8487
pipeline = VAASPipeline.from_pretrained(
85-
"OBA-Research/vaas-v1-df2023",
88+
repo_id,
8689
device="cpu",
87-
alpha=0.5
90+
alpha=0.5,
91+
model_variant=model_variant,
8892
)
8993

94+
9095
# # Option A: Using a local image
9196
# image = Image.open("example.jpg").convert("RGB")
9297
# result = pipeline(image)
@@ -171,12 +176,12 @@ If you would like to contribute a notebook, see **[CONTRIBUTING.md](CONTRIBUTIN
171176

172177
## Model Variants (Planned & Released)
173178

174-
| Version | Training Data | Description | Reported Evaluation (Paper) | Hugging Face Model |
179+
| Models | Training Data | Description | Reported Evaluation (Paper) | Hugging Face Model |
175180
|--------|----------------|-------------|-----------------------------|--------------------|
176-
| v1 | DF2023 (10%) | Initial public inference release | F1 / IoU reported on DF2023 & CASIA v2.0 | [vaas-v1-df2023](https://huggingface.co/OBA-Research/vaas-v1-df2023) |
177-
| v2 | DF2023 (≈50%) | Planned scale-up experiment | Planned | TBD |
178-
| v3 | DF2023 (100%) | Full-dataset training (planned) | Planned | TBD |
179-
| v4 | DF2023 + CASIA2.0 | Cross-dataset study (planned) | Cross-dataset eval planned | TBD |
181+
| vaas-v1-base-df2023 | DF2023 (10%) | Initial public inference release | [F1 & IoU are reported in research paper](https://arxiv.org/pdf/2512.15512) | [vaas-v1-base-df2023](https://huggingface.co/OBA-Research/vaas) |
182+
| vaas-v1-medium-df2023 | DF2023 (≈50%) | Scale-up experiment | 5% better than base | [vaas-v1-medium-df2023](https://huggingface.co/OBA-Research/vaas/tree/v1-medium-df2023) |
183+
| vaas-v1-large-df2023 | DF2023 (100%) | Full-dataset training | 9% better than medium | [vaas-v1-large-df2023](https://huggingface.co/OBA-Research/vaas/tree/v1-large-df2023) |
184+
| v4 | DF2023 + Others | Cross-dataset study (planned) | Cross-dataset eval planned | TBD |
180185
| v5 | Other datasets | Exploratory generalisation study | TBD | TBD |
181186

182187

@@ -193,27 +198,6 @@ differ in training data composition and calibration depending on the target
193198
integrity focus.
194199

195200

196-
## Reported Quantitative Performance
197-
198-
Quantitative detection and localisation metrics for VAAS are
199-
reported in the accompanying [paper](https://arxiv.org/abs/2512.15512) under a defined evaluation
200-
protocol.
201-
202-
Under the experimental setup described in the [paper](https://arxiv.org/abs/2512.15512):
203-
204-
- **DF2023 (10% subset)**
205-
F1: 94.9%
206-
IoU: 91.1%
207-
208-
- **CASIA v2.0**
209-
F1: 94.1%
210-
IoU: 89.0%
211-
212-
These metrics are dataset- and protocol-specific and should be
213-
interpreted in conjunction with the methodology described in
214-
the paper.
215-
216-
217201
## Roadmap (Inference-Focused)
218202

219203
- Batch inference and folder-level CLI
@@ -224,7 +208,7 @@ the paper.
224208
- Model compression
225209
- Extended anomaly-map visualisation
226210
- ONNX / TorchScript export
227-
- Use cases with Streamlit / Gradio
211+
- Use cases and extentions
228212

229213
---
230214

@@ -242,6 +226,8 @@ Please see the full guidelines in **[CONTRIBUTING.md](CONTRIBUTING.md)**.
242226

243227
If you use VAAS in your research, please cite both the software and the associated paper as appropriate.
244228

229+
### Software Citation
230+
245231
```python
246232
@software{vaas,
247233
title = {VAAS: Vision-Attention Anomaly Scoring},
@@ -254,6 +240,8 @@ If you use VAAS in your research, please cite both the software and the associat
254240

255241
```
256242

243+
### Paper Citation
244+
257245
```python
258246
@article{bamigbade2025vaas,
259247
title={VAAS: Vision-Attention Anomaly Scoring for Image Manipulation Detection in Digital Forensics},

pyproject.toml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@ authors = [
1111
{ name = "John Sheppard" },
1212
]
1313

14+
keywords = [
15+
"anomaly detection",
16+
"computer vision",
17+
"vision transformers",
18+
"deep learning",
19+
"image forensics",
20+
"representation learning",
21+
]
22+
23+
classifiers = [
24+
"Development Status :: 4 - Beta",
25+
"Intended Audience :: Science/Research",
26+
"Intended Audience :: Developers",
27+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
28+
"Topic :: Scientific/Engineering :: Image Processing",
29+
"License :: OSI Approved :: MIT License",
30+
"Programming Language :: Python :: 3",
31+
"Programming Language :: Python :: 3.10",
32+
"Programming Language :: Python :: 3.11",
33+
"Programming Language :: Python :: 3.12",
34+
"Operating System :: OS Independent",
35+
]
36+
1437
dependencies = [
1538
"transformers>=4.40",
1639
"numpy",
@@ -73,4 +96,9 @@ lint.select = [
7396
quote-style = "double"
7497
indent-style = "space"
7598

76-
99+
[project.urls]
100+
Homepage = "https://github.com/OBA-Research/VAAS"
101+
Repository = "https://github.com/OBA-Research/VAAS"
102+
Documentation = "https://github.com/OBA-Research/VAAS#readme"
103+
Issues = "https://github.com/OBA-Research/VAAS/issues"
104+
HuggingFace = "https://huggingface.co/OBA-Research/vaas"

scripts/publish_hf.py

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,42 @@
1111

1212
def main():
1313
parser = argparse.ArgumentParser("Publish VAAS model to Hugging Face")
14+
1415
parser.add_argument("--checkpoint-dir", type=str, required=True)
1516
parser.add_argument("--repo-id", type=str, required=True)
1617
parser.add_argument("--private", action="store_true")
1718
parser.add_argument("--alpha", type=float, default=0.5)
1819
parser.add_argument("--device", type=str, default="cpu")
20+
21+
parser.add_argument("--variant-name", type=str, required=True)
22+
parser.add_argument("--dataset-name", type=str, required=True)
23+
parser.add_argument("--dataset-fraction", type=str, required=True)
24+
parser.add_argument("--architecture-version", type=str, default="v1")
25+
1926
args = parser.parse_args()
2027

28+
# Deterministic revision generation
29+
revision = (
30+
f"{args.architecture_version}-{args.variant_name}-{args.dataset_name.lower()}"
31+
)
32+
2133
output_dir = "hf_artifact"
22-
os.makedirs(output_dir, exist_ok=True)
34+
35+
if os.path.exists(output_dir):
36+
shutil.rmtree(output_dir)
37+
38+
os.makedirs(output_dir)
2339

2440
pipeline = VAASPipeline.from_checkpoint(
2541
checkpoint_dir=args.checkpoint_dir,
2642
device=args.device,
2743
alpha=args.alpha,
44+
variant=args.variant_name,
45+
metadata={
46+
"architecture_version": args.architecture_version,
47+
"dataset": args.dataset_name,
48+
"dataset_fraction": args.dataset_fraction,
49+
},
2850
)
2951

3052
model_path = os.path.join(output_dir, "model")
@@ -45,7 +67,11 @@ def main():
4567

4668
config = {
4769
"architecture": "VAAS",
48-
"version": "v1",
70+
"architecture_version": args.architecture_version,
71+
"revision": revision,
72+
"variant": args.variant_name,
73+
"dataset": args.dataset_name,
74+
"dataset_fraction": args.dataset_fraction,
4975
"alpha": args.alpha,
5076
"input_size": [224, 224],
5177
"px_checkpoint": "px_model.pth",
@@ -57,56 +83,29 @@ def main():
5783
json.dump(config, f, indent=2)
5884

5985
api = HfApi()
60-
try:
61-
create_repo(args.repo_id, private=args.private, exist_ok=True)
62-
except Exception as e:
63-
print(f"Repository creation skipped or failed: {e}")
64-
65-
src_pipeline_dir = os.path.join("vaas", "inference")
66-
dst_pipeline_dir = os.path.join(output_dir, "vaas", "inference")
6786

68-
os.makedirs(dst_pipeline_dir, exist_ok=True)
69-
vaas_root = os.path.join(output_dir, "vaas")
70-
os.makedirs(vaas_root, exist_ok=True)
87+
# Create revision branch if it does not exist
88+
create_repo(args.repo_id, private=args.private, exist_ok=True)
7189

72-
open(os.path.join(vaas_root, "__init__.py"), "w").close()
73-
open(os.path.join(dst_pipeline_dir, "__init__.py"), "w").close()
74-
75-
shutil.copy(
76-
os.path.join(src_pipeline_dir, "pipeline.py"),
77-
os.path.join(dst_pipeline_dir, "pipeline.py"),
78-
)
79-
80-
shutil.copy(
81-
os.path.join(src_pipeline_dir, "utils.py"),
82-
os.path.join(dst_pipeline_dir, "utils.py"),
83-
)
84-
85-
shutil.copy(
86-
os.path.join(src_pipeline_dir, "visualize.py"),
87-
os.path.join(dst_pipeline_dir, "visualize.py"),
88-
)
89-
90-
src_doc_dir = os.path.join("docs")
91-
dst_doc_dir = os.path.join(output_dir, "docs")
92-
93-
if os.path.exists(dst_doc_dir):
94-
shutil.rmtree(dst_doc_dir)
95-
96-
shutil.copytree(src_doc_dir, dst_doc_dir)
97-
98-
shutil.copy(
99-
"hfREADME.md",
100-
os.path.join(output_dir, "README.md"),
90+
api.create_branch(
91+
repo_id=args.repo_id,
92+
branch=revision,
93+
exist_ok=True,
10194
)
10295

10396
api.upload_folder(
10497
folder_path=output_dir,
10598
repo_id=args.repo_id,
10699
repo_type="model",
100+
revision=revision,
101+
create_pr=False,
107102
)
108103

109-
print(f"Published VAAS model to https://huggingface.co/{args.repo_id}")
104+
print(
105+
f"Published VAAS {args.variant_name} "
106+
f"(revision={revision}) "
107+
f"to https://huggingface.co/{args.repo_id}"
108+
)
110109

111110

112111
if __name__ == "__main__":

tests/test_integration_pipeline.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import numpy as np
2+
import pytest
3+
from PIL import Image
4+
5+
from vaas.inference.pipeline import VAASPipeline
6+
7+
8+
@pytest.mark.integration
9+
def test_hf_model_loading_and_inference():
10+
pipe = VAASPipeline.from_pretrained(
11+
repo_id="OBA-Research/vaas",
12+
model_variant="v1-base-df2023",
13+
device="cpu",
14+
)
15+
16+
img = Image.new("RGB", (224, 224), color="white")
17+
18+
result = pipe(img)
19+
20+
assert "S_H" in result
21+
assert isinstance(result["S_H"], float)
22+
assert isinstance(result["anomaly_map"], np.ndarray)
23+
assert result["anomaly_map"].shape == (224, 224)
24+
25+
26+
@pytest.mark.integration
27+
def test_visualization_pipeline(tmp_path):
28+
pipe = VAASPipeline.from_pretrained(
29+
repo_id="OBA-Research/vaas",
30+
model_variant="v1-base-df2023",
31+
device="cpu",
32+
)
33+
34+
img = Image.new("RGB", (224, 224), color="white")
35+
output_path = tmp_path / "viz.png"
36+
37+
pipe.visualize(
38+
image=img,
39+
save_path=str(output_path),
40+
)
41+
42+
assert output_path.exists()
43+
assert output_path.stat().st_size > 0

tests/test_local_pipeline.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ def test_local_pipeline():
3232

3333
assert isinstance(result, dict)
3434
assert {"S_F", "S_P", "S_H", "anomaly_map"} <= result.keys()
35+
print(pipeline.variant)
36+
print(pipeline.metadata)
37+
38+
39+
test_local_pipeline()

tests/test_pipeline.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ def test_vaas_pipeline_smoke():
1010

1111
from vaas.inference.pipeline import VAASPipeline
1212

13-
repo_id = "OBA-Research/vaas-v1-df2023"
13+
repo_id = "OBA-Research/vaas"
14+
model_variant = "v1-base-df2023"
1415

1516
pipeline = VAASPipeline.from_pretrained(
1617
repo_id,
1718
device="cpu",
1819
alpha=0.5,
20+
model_variant=model_variant,
1921
)
2022

2123
img = Image.fromarray(
@@ -35,3 +37,7 @@ def test_vaas_pipeline_smoke():
3537
anomaly_map = result["anomaly_map"]
3638
assert isinstance(anomaly_map, np.ndarray)
3739
assert anomaly_map.ndim == 2
40+
print(pipeline.metadata)
41+
42+
43+
test_vaas_pipeline_smoke()

vaas/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.7"
1+
__version__ = "0.1.8"

vaas/fx/fx_utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ def compute_reference_stats(dataset, vit_model, device, transform, max_samples=2
1919
# Case 1: Authentic samples available
2020
if len(authentic_indices) > 0:
2121
print(
22-
f"Found {len(authentic_indices)}"
23-
"authentic samples for reference statistics."
22+
f"Found {len(authentic_indices)}authentic samples for reference statistics."
2423
)
2524
selected_indices = random.sample(
2625
authentic_indices, min(len(authentic_indices), max_samples)

0 commit comments

Comments
 (0)