Skip to content

Commit db9f1d1

Browse files
committed
Initial public release of NinjaDesc
Reference implementation for the CVPR 2022 paper "NinjaDesc: Content-Concealing Visual Descriptors via Adversarial Learning" (Ng, Kim, Lee, DeTone, Yang, Shen, Ilg, Balntas, Mikolajczyk, Sweeney).
0 parents  commit db9f1d1

78 files changed

Lines changed: 4753 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
__pycache__/
2+
*.py[cod]
3+
*.egg-info/
4+
*.egg/
5+
build/
6+
dist/
7+
8+
.venv/
9+
venv/
10+
.idea/
11+
.vscode/
12+
.DS_Store
13+
14+
outputs/
15+
data/

README.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# NinjaDesc: Content-Concealing Visual Descriptors via Adversarial Learning
2+
3+
Reference implementation for the CVPR 2022 paper *"NinjaDesc: Content-Concealing Visual Descriptors via Adversarial Learning"* by Tony Ng, Hyo Jin Kim, Vincent T. Lee, Daniel DeTone, Tsun-Yi Yang, Tianwei Shen, Eddy Ilg, Vassileios Balntas, Krystian Mikolajczyk, and Chris Sweeney (Reality Labs, Meta and Imperial College London).
4+
5+
[Paper (arXiv 2112.12785)](https://arxiv.org/abs/2112.12785)
6+
7+
## Abstract
8+
9+
In the light of recent analyses on privacy-concerning scene revelation from visual descriptors, we develop descriptors that conceal the input image content. We propose an adversarial learning framework for training visual descriptors that prevent image reconstruction, while maintaining the matching accuracy. We let a feature encoding network (**NinjaNet**) and an image reconstruction network compete with each other, such that the encoder tries to impede the image reconstruction with its generated descriptors (**NinjaDesc**), while the reconstructor tries to recover the input image from the descriptors. The experimental results demonstrate that the resulting visual descriptors significantly deteriorate the image reconstruction quality with minimal impact on correspondence matching and camera localization performance.
10+
11+
## Citation
12+
13+
```bibtex
14+
@inproceedings{ng2022ninjadesc,
15+
title = {NinjaDesc: Content-Concealing Visual Descriptors via Adversarial Learning},
16+
author = {Ng, Tony and Kim, Hyo Jin and Lee, Vincent T. and DeTone, Daniel
17+
and Yang, Tsun-Yi and Shen, Tianwei and Ilg, Eddy
18+
and Balntas, Vassileios and Mikolajczyk, Krystian and Sweeney, Chris},
19+
booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
20+
year = {2022}
21+
}
22+
```
23+
24+
## Installation
25+
26+
Tested with Python 3.8+ and PyTorch >= 1.10.
27+
28+
```bash
29+
git clone https://github.com/facebookresearch/ninjadesc.git
30+
cd ninjadesc
31+
pip install -r requirements.txt
32+
pip install -e .
33+
```
34+
35+
Set the data and output roots that the configs read from:
36+
37+
```bash
38+
export NINJADESC_DATA_ROOT=/path/to/data # raw + prepared datasets
39+
export NINJADESC_OUTPUT_ROOT=/path/to/outputs # checkpoints, tensorboard logs
40+
```
41+
42+
## Pretrained models
43+
44+
Weights are released as GitHub Release assets. They are downloaded automatically into `~/.cache/ninjadesc/` the first time a model is requested via `ninjadesc._compat.weights.download_weights(name)`.
45+
46+
| Logical name | File |
47+
| -------------------------------------------------- | ---------------------------------------------------------- |
48+
| `pa_desc_sosnet_init` | `pa_desc_sos_init_torchscript.pt` |
49+
| `pa_desc_sift_init` | `pa_desc_sift_init_torchscript.pt` |
50+
| `pa_desc_hardnet_init` | `pa_desc_hardnet_init_torchscript.pt` |
51+
| `pa_desc_hardnet_init_normalize` | `pa_desc_hardnet_init_normalize_torchscript.pt` |
52+
| `lemuria_unet_padesc_sos_init` | `LemuriaNet_Unet_PADesc_INIT.pth` |
53+
| `lemuria_unet_padesc_sift_init` | `LemuriaNet_UNet_PADesc_SIFT_INIT.pth` |
54+
| `lemuria_unet_padesc_hardnet_init` | `LemuriaNet_UNet_PADesc_HARDNET_INIT.pth` |
55+
| `lemuria_unet_padesc_hardnet_nontorchscript_init` | `LemuriaNet_UNet_PADesc_HARDNET_NONTORCHSCRIPT_INIT.pth` |
56+
| `lemuria_unet_sos` | `LemuriaNet_UNet_SOS.pth` |
57+
| `lemuria_unet_hard` | `LemuriaNet_UNet_HARD.pth` |
58+
| `lemuria_unet_sift` | `LemuriaNet_UNet_SIFT.pth` |
59+
| `lemuria_sos_mpa_3303` | `SOS_MPA_3303_epoch_249.ckpt` |
60+
| `lemuria_sift_mpa_1302` | `SIFT_MPA_1302_epoch_248.ckpt` |
61+
| `sosnet_hpatches` | `sosnet-32x32-hpatches_a.pth` |
62+
| `sosnet_liberty` | `sosnet-32x32-liberty.pth` |
63+
| `sosnet_scape_pipeline` | `sosnet_scape.pt` |
64+
| `sosnet_padesc_liberty` | `padesc_liberty.pt` |
65+
| `hardnet_lib` | `hardnet_lib.pt` |
66+
| `hardnet_liberty_aug` | `checkpoint_liberty_with_aug.pth` |
67+
68+
## Dataset preparation
69+
70+
The image-based reconstruction trainer/tester uses MegaDepth features pre-extracted into `.h5` files. Generate them with:
71+
72+
```bash
73+
python -m ninjadesc.pa_desc.megadepth_prep.prep \
74+
--data_root /path/to/MegaDepth_v1 \
75+
--kpt SOS \
76+
--worker_id 0
77+
```
78+
79+
This writes `${NINJADESC_DATA_ROOT}/megadepth_h5s_<kpt>_hpatches-a_original/*.hdf5`. Repeat with `--kpt sift` and `--kpt hardnet` for the other base descriptors. The patch-based descriptor trainer additionally uses the UBC PhotoTour dataset (Liberty / Notredame / Yosemite); torchvision downloads it automatically into `${NINJADESC_DATA_ROOT}/PhotoTour/`.
80+
81+
## Usage
82+
83+
All trainers/testers are Hydra entry points; CLI overrides take the form `key=value`.
84+
85+
**Inference** — extract a NinjaDesc from base SOSNet descriptors:
86+
87+
```python
88+
import torch
89+
from ninjadesc.pa_desc.models.privacy import PrivacyEncoder
90+
91+
encoder = PrivacyEncoder().eval()
92+
ninja = encoder(torch.randn(N, 128)) # NinjaDesc, shape (N, 128)
93+
```
94+
95+
**Step 1 — utility initialization of NinjaNet** (UBC PhotoTour):
96+
97+
```bash
98+
python -m ninjadesc.pa_desc.tools.desc_trainer \
99+
profiler=simple datasets@data.train.ds=liberty \
100+
trainer.max_epochs=300 trainer.gpus=4
101+
```
102+
103+
**Step 2 — initialize the reconstruction adversary** (MegaDepth):
104+
105+
```bash
106+
python -m ninjadesc.pa_desc.tools.recon_trainer \
107+
profiler=simple trainer.max_epochs=300 trainer.gpus=4
108+
```
109+
110+
**Step 3 — joint adversarial training**:
111+
112+
```bash
113+
python -m ninjadesc.pa_desc.tools.trainer \
114+
profiler=simple trainer.max_epochs=300 trainer.gpus=4 loss._lambda=1.5
115+
```
116+
117+
`loss._lambda` is the privacy parameter that trades off matching utility against image-reconstruction concealment.
118+
119+
**Evaluation** on MegaDepth:
120+
121+
```bash
122+
python -m ninjadesc.pa_desc.tools.tester \
123+
base_desc=sosnet \
124+
padesc_checkpoint.base_dir=${NINJADESC_OUTPUT_ROOT}/pa_desc_joint/<run> \
125+
padesc_checkpoint.epoch=21 padesc_checkpoint.step=13749
126+
```
127+
128+
## Repository layout
129+
130+
```
131+
ninjadesc/
132+
├── _compat/ # thin shims (Hydra/PL helpers, weight download, IO)
133+
├── lemuria/recon/ # image reconstruction networks (UNet, UResNet, VGG, Discriminator)
134+
└── pa_desc/
135+
├── core/ # losses (perceptual loss)
136+
├── data/ # MegaDepth, PhotoTour, HPatches dataset adapters
137+
├── engine/ # PyTorch Lightning modules (PADesc, joint, lemurianet)
138+
├── megadepth_prep/ # h5 feature extraction script
139+
├── models/ # SOSNet, HardNet, NinjaNet (PrivacyEncoder), reconstructor
140+
├── tools/ # desc_trainer, recon_trainer, trainer (joint), tester
141+
└── config/ # Hydra configs (yaml)
142+
```
143+
144+
## Acknowledgements
145+
146+
This codebase builds on a number of open-source projects:
147+
- **SOSNet** ([Tian et al., 2019](https://github.com/scape-research/SOSNet)) — base descriptor.
148+
- **HardNet** ([Mishchuk et al., 2017](https://github.com/DagnyT/hardnet)) — base descriptor.
149+
- **InvSfM / `dangwal21`** — descriptor inversion baseline.
150+
- **MegaDepth** ([Li & Snavely, 2018](https://www.cs.cornell.edu/projects/megadepth/)) — adversarial training data.
151+
- **HPatches** ([Balntas et al., 2017](https://hpatches.github.io/)) — matching benchmark.
152+
- **PyTorch Lightning**, **Hydra**, **kornia**, **torchvision** — frameworks.
153+
154+
## Known limitations
155+
156+
- The `HPatches` dataset adapter (`ninjadesc/pa_desc/data/hpatches.py`) is a stub; populate it with a loader for the official HPatches release before evaluating on it.
157+
- The `MegaDepthDataset` requires h5 features pre-extracted via the dataset preparation step above.
158+
- No unit tests are included.
159+
160+
## License
161+
162+
This project is released under CC-BY-NC 4.0. See `LICENSE`.

ninjadesc/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.

ninjadesc/_compat/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.

ninjadesc/_compat/hydra.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
from typing import Any, List, Optional
8+
9+
import hydra
10+
import torchvision.transforms as T
11+
from omegaconf import DictConfig
12+
13+
14+
def instantiate_hydra_or_python(x: Any, *args: Any, **kwargs: Any) -> Any:
15+
if isinstance(x, (DictConfig, dict)) and "_target_" in x:
16+
return hydra.utils.instantiate(x, *args, **kwargs)
17+
if callable(x):
18+
return x(*args, **kwargs)
19+
return x
20+
21+
22+
def maybe_instantiate(cfg: Optional[DictConfig], key: str) -> Any:
23+
if cfg is None or key not in cfg or cfg[key] is None:
24+
return None
25+
return hydra.utils.instantiate(cfg[key])
26+
27+
28+
def maybe_instantiate_list(cfg: Optional[DictConfig], key: str) -> List[Any]:
29+
if cfg is None or key not in cfg or cfg[key] is None:
30+
return []
31+
return [hydra.utils.instantiate(x) for x in cfg[key]]
32+
33+
34+
def compose_transforms_from_hydra(cfg: Optional[DictConfig]) -> T.Compose:
35+
if cfg is None:
36+
return T.Compose([])
37+
return T.Compose([hydra.utils.instantiate(t) for t in cfg])

ninjadesc/_compat/io.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
9+
10+
class _PathManager:
11+
def open(self, path, mode="r"):
12+
return open(path, mode)
13+
14+
def exists(self, path):
15+
return os.path.exists(path)
16+
17+
def mkdirs(self, path):
18+
os.makedirs(path, exist_ok=True)
19+
20+
def get_local_path(self, path):
21+
return path
22+
23+
24+
path_manager = _PathManager()
25+
26+
27+
def clean_path(path):
28+
return os.path.normpath(path)

ninjadesc/_compat/logging.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import logging
8+
9+
10+
def sudo_make_me_a_logger(name: str) -> logging.Logger:
11+
return logging.getLogger(name)

ninjadesc/_compat/metrics.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import torch
8+
9+
10+
def fpr_at_recall(
11+
distances: torch.Tensor,
12+
labels: torch.Tensor,
13+
recall_target: float = 0.95,
14+
) -> torch.Tensor:
15+
distances = distances.flatten()
16+
labels = labels.flatten().to(torch.bool)
17+
18+
sort_idx = torch.argsort(distances)
19+
sorted_labels = labels[sort_idx]
20+
21+
num_pos = sorted_labels.sum().item()
22+
if num_pos == 0:
23+
return torch.tensor(float("nan"), device=distances.device)
24+
25+
cum_pos = torch.cumsum(sorted_labels.to(torch.float32), dim=0)
26+
threshold_idx = torch.searchsorted(cum_pos, torch.tensor(recall_target * num_pos))
27+
threshold_idx = int(threshold_idx.clamp(max=len(sorted_labels) - 1).item())
28+
29+
num_neg_below = (~sorted_labels[: threshold_idx + 1]).sum().to(torch.float32)
30+
num_neg_total = (~labels).sum().to(torch.float32).clamp(min=1.0)
31+
return num_neg_below / num_neg_total

ninjadesc/_compat/weights.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
from pathlib import Path
9+
10+
import torch.hub
11+
12+
_RELEASE_BASE = "https://github.com/facebookresearch/ninjadesc/releases/download/v1.0"
13+
14+
WEIGHT_FILES = {
15+
"pa_desc_sosnet_init": "pa_desc_sos_init_torchscript.pt",
16+
"pa_desc_sift_init": "pa_desc_sift_init_torchscript.pt",
17+
"pa_desc_hardnet_init": "pa_desc_hardnet_init_torchscript.pt",
18+
"pa_desc_hardnet_init_normalize": "pa_desc_hardnet_init_normalize_torchscript.pt",
19+
"lemuria_unet_padesc_sos_init": "LemuriaNet_Unet_PADesc_INIT.pth",
20+
"lemuria_unet_padesc_sift_init": "LemuriaNet_UNet_PADesc_SIFT_INIT.pth",
21+
"lemuria_unet_padesc_hardnet_init": "LemuriaNet_UNet_PADesc_HARDNET_INIT.pth",
22+
"lemuria_unet_padesc_hardnet_nontorchscript_init": "LemuriaNet_UNet_PADesc_HARDNET_NONTORCHSCRIPT_INIT.pth",
23+
"lemuria_unet_sos": "LemuriaNet_UNet_SOS.pth",
24+
"lemuria_unet_hard": "LemuriaNet_UNet_HARD.pth",
25+
"lemuria_unet_sift": "LemuriaNet_UNet_SIFT.pth",
26+
"lemuria_sos_mpa_3303": "SOS_MPA_3303_epoch_249.ckpt",
27+
"lemuria_sift_mpa_1302": "SIFT_MPA_1302_epoch_248.ckpt",
28+
"sosnet_hpatches": "sosnet-32x32-hpatches_a.pth",
29+
"sosnet_liberty": "sosnet-32x32-liberty.pth",
30+
"sosnet_scape_pipeline": "sosnet_scape.pt",
31+
"sosnet_padesc_liberty": "padesc_liberty.pt",
32+
"hardnet_lib": "hardnet_lib.pt",
33+
"hardnet_liberty_aug": "checkpoint_liberty_with_aug.pth",
34+
}
35+
36+
37+
def _cache_root() -> Path:
38+
base = os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache")
39+
return Path(base) / "ninjadesc"
40+
41+
42+
def download_weights(name: str) -> str:
43+
if name not in WEIGHT_FILES:
44+
raise KeyError(
45+
f"Unknown weight name {name!r}. Known: {sorted(WEIGHT_FILES)}"
46+
)
47+
filename = WEIGHT_FILES[name]
48+
cache_dir = _cache_root()
49+
cache_dir.mkdir(parents=True, exist_ok=True)
50+
local_path = cache_dir / filename
51+
if not local_path.exists():
52+
url = f"{_RELEASE_BASE}/{filename}"
53+
torch.hub.download_url_to_file(url, str(local_path))
54+
return str(local_path)

ninjadesc/lemuria/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.

0 commit comments

Comments
 (0)