pseudopros 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pseudopros/__init__.py +26 -0
- pseudopros/_cli.py +159 -0
- pseudopros/_fomm/__init__.py +0 -0
- pseudopros/_fomm/modules/__init__.py +0 -0
- pseudopros/_fomm/modules/dense_motion.py +113 -0
- pseudopros/_fomm/modules/generator.py +97 -0
- pseudopros/_fomm/modules/keypoint_detector.py +75 -0
- pseudopros/_fomm/modules/util.py +245 -0
- pseudopros/_fomm/sync_batchnorm/__init__.py +12 -0
- pseudopros/_fomm/sync_batchnorm/batchnorm.py +315 -0
- pseudopros/_fomm/sync_batchnorm/comm.py +137 -0
- pseudopros/_fomm/sync_batchnorm/replicate.py +94 -0
- pseudopros/anonymizer.py +227 -0
- pseudopros/embedding.py +115 -0
- pseudopros/fomm.py +299 -0
- pseudopros/gst.py +231 -0
- pseudopros/keying.py +113 -0
- pseudopros/security.py +177 -0
- pseudopros/synthesizer.py +262 -0
- pseudopros/unified_renderer/__init__.py +12 -0
- pseudopros/unified_renderer/model.py +234 -0
- pseudopros-0.1.0.dist-info/METADATA +366 -0
- pseudopros-0.1.0.dist-info/RECORD +26 -0
- pseudopros-0.1.0.dist-info/WHEEL +4 -0
- pseudopros-0.1.0.dist-info/entry_points.txt +2 -0
- pseudopros-0.1.0.dist-info/licenses/LICENSE +21 -0
pseudopros/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pseudopros.anonymizer import Anonymizer
|
|
2
|
+
from pseudopros.keying import PseudonymKey
|
|
3
|
+
from pseudopros.security import SecurityAudit
|
|
4
|
+
from pseudopros.synthesizer import (
|
|
5
|
+
NoiseSynthesizer,
|
|
6
|
+
RapidSynthesizer,
|
|
7
|
+
RosterSynthesizer,
|
|
8
|
+
build_face_bank,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from importlib.metadata import metadata as _metadata, version as _version
|
|
13
|
+
__version__ = _version('pseudopros')
|
|
14
|
+
__doc__ = _metadata('pseudopros')['Description']
|
|
15
|
+
except Exception:
|
|
16
|
+
__version__ = "0.0.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Anonymizer",
|
|
20
|
+
"NoiseSynthesizer",
|
|
21
|
+
"RosterSynthesizer",
|
|
22
|
+
"PseudonymKey",
|
|
23
|
+
"RapidSynthesizer",
|
|
24
|
+
"SecurityAudit",
|
|
25
|
+
"build_face_bank",
|
|
26
|
+
]
|
pseudopros/_cli.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Command-line interface for pseudopros."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import cv2
|
|
11
|
+
import imageio
|
|
12
|
+
|
|
13
|
+
from pseudopros.anonymizer import Anonymizer
|
|
14
|
+
from pseudopros.synthesizer import NoiseSynthesizer, RosterSynthesizer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_secret(args: argparse.Namespace) -> bytes:
|
|
18
|
+
if args.secret_file:
|
|
19
|
+
return Path(args.secret_file).read_bytes()
|
|
20
|
+
if args.secret_hex:
|
|
21
|
+
return bytes.fromhex(args.secret_hex)
|
|
22
|
+
env = os.environ.get("PSEUDOPROS_SECRET")
|
|
23
|
+
if env:
|
|
24
|
+
return bytes.fromhex(env)
|
|
25
|
+
print(
|
|
26
|
+
"error: provide a secret via --secret-file, --secret-hex, or "
|
|
27
|
+
"PSEUDOPROS_SECRET env var",
|
|
28
|
+
file=sys.stderr,
|
|
29
|
+
)
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_salt(args: argparse.Namespace) -> bytes:
|
|
34
|
+
if args.salt_hex:
|
|
35
|
+
return bytes.fromhex(args.salt_hex)
|
|
36
|
+
env = os.environ.get("PSEUDOPROS_SALT")
|
|
37
|
+
if env:
|
|
38
|
+
return bytes.fromhex(env)
|
|
39
|
+
return b"pseudopros"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def anonymize_video(
|
|
43
|
+
input_path: str,
|
|
44
|
+
output_path: str,
|
|
45
|
+
anon,
|
|
46
|
+
enrollment_path: str,
|
|
47
|
+
) -> int:
|
|
48
|
+
"""Process a video file frame-by-frame. Returns frame count processed."""
|
|
49
|
+
enroll_frame = imageio.v2.imread(enrollment_path)
|
|
50
|
+
if not anon.enroll(enroll_frame, "subject"):
|
|
51
|
+
print(
|
|
52
|
+
f"warning: no face detected in enrollment image {enrollment_path!r}",
|
|
53
|
+
file=sys.stderr,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
cap = cv2.VideoCapture(input_path)
|
|
57
|
+
if not cap.isOpened():
|
|
58
|
+
print(f"error: cannot open input {input_path!r}", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
|
|
62
|
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
63
|
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
64
|
+
|
|
65
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
66
|
+
writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
|
67
|
+
|
|
68
|
+
count = 0
|
|
69
|
+
while True:
|
|
70
|
+
ret, frame_bgr = cap.read()
|
|
71
|
+
if not ret:
|
|
72
|
+
break
|
|
73
|
+
frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
|
|
74
|
+
result_rgb = anon.process(frame_rgb)
|
|
75
|
+
result_bgr = cv2.cvtColor(result_rgb, cv2.COLOR_RGB2BGR)
|
|
76
|
+
writer.write(result_bgr)
|
|
77
|
+
count += 1
|
|
78
|
+
|
|
79
|
+
cap.release()
|
|
80
|
+
writer.release()
|
|
81
|
+
return count
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _cmd_anonymize(args: argparse.Namespace) -> None:
|
|
85
|
+
secret = _load_secret(args)
|
|
86
|
+
salt = _load_salt(args)
|
|
87
|
+
synth = RosterSynthesizer() if args.synthesizer == "roster" else NoiseSynthesizer()
|
|
88
|
+
|
|
89
|
+
print(
|
|
90
|
+
f"Loading FOMM model {'from ' + args.checkpoint if args.checkpoint else '(downloading from HuggingFace)'}...",
|
|
91
|
+
file=sys.stderr,
|
|
92
|
+
)
|
|
93
|
+
anon = Anonymizer.build(
|
|
94
|
+
secret=secret,
|
|
95
|
+
checkpoint_path=args.checkpoint or None,
|
|
96
|
+
config_path=args.config or None,
|
|
97
|
+
salt=salt,
|
|
98
|
+
device=args.device,
|
|
99
|
+
synthesizer=synth,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
print(f"Processing {args.input!r} -> {args.output!r} ...", file=sys.stderr)
|
|
103
|
+
count = anonymize_video(args.input, args.output, anon, args.enrollment)
|
|
104
|
+
print(f"Done. {count} frame(s) written to {args.output!r}", file=sys.stderr)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main(argv: list[str] | None = None) -> None:
|
|
108
|
+
parser = argparse.ArgumentParser(
|
|
109
|
+
prog="pseudopros",
|
|
110
|
+
description="Cryptographic face anonymization for video pipelines.",
|
|
111
|
+
)
|
|
112
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
113
|
+
|
|
114
|
+
anon_p = sub.add_parser("anonymize", help="Anonymize faces in a video file.")
|
|
115
|
+
anon_p.add_argument("input", help="Input video path.")
|
|
116
|
+
anon_p.add_argument("output", help="Output video path.")
|
|
117
|
+
anon_p.add_argument(
|
|
118
|
+
"--enrollment", required=True, metavar="IMAGE",
|
|
119
|
+
help="Image file containing the subject's face for enrollment.",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
secret_g = anon_p.add_mutually_exclusive_group()
|
|
123
|
+
secret_g.add_argument(
|
|
124
|
+
"--secret-file", metavar="PATH",
|
|
125
|
+
help="Path to a file containing the raw secret bytes. Recommended for production.",
|
|
126
|
+
)
|
|
127
|
+
secret_g.add_argument(
|
|
128
|
+
"--secret-hex", metavar="HEX",
|
|
129
|
+
help="Secret key as a hex string. Or set PSEUDOPROS_SECRET env var.",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
anon_p.add_argument(
|
|
133
|
+
"--salt-hex", metavar="HEX",
|
|
134
|
+
help="Deployment salt as a hex string. Or set PSEUDOPROS_SALT env var. "
|
|
135
|
+
"Default: hex of b'pseudopros'.",
|
|
136
|
+
)
|
|
137
|
+
anon_p.add_argument(
|
|
138
|
+
"--synthesizer", choices=["noise", "roster"], default="noise",
|
|
139
|
+
help="Synthesizer backend. 'noise' is a placeholder; "
|
|
140
|
+
"'roster' uses LDM (~1.3 GB download on first use). Default: noise.",
|
|
141
|
+
)
|
|
142
|
+
anon_p.add_argument("--device", default="cpu", help="Torch device. Default: cpu.")
|
|
143
|
+
anon_p.add_argument(
|
|
144
|
+
"--checkpoint", metavar="PATH",
|
|
145
|
+
help="Path to a local FOMM checkpoint (.pth.tar). "
|
|
146
|
+
"If omitted, weights are downloaded from HuggingFace Hub.",
|
|
147
|
+
)
|
|
148
|
+
anon_p.add_argument(
|
|
149
|
+
"--config", metavar="PATH",
|
|
150
|
+
help="Path to the FOMM config YAML. Required when --checkpoint is set.",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
args = parser.parse_args(argv)
|
|
154
|
+
if args.command == "anonymize":
|
|
155
|
+
_cmd_anonymize(args)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from torch import nn
|
|
2
|
+
import torch.nn.functional as F
|
|
3
|
+
import torch
|
|
4
|
+
from .util import Hourglass, AntiAliasInterpolation2d, make_coordinate_grid, kp2gaussian
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DenseMotionNetwork(nn.Module):
|
|
8
|
+
"""
|
|
9
|
+
Module that predicting a dense motion from sparse motion representation given by kp_source and kp_driving
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, block_expansion, num_blocks, max_features, num_kp, num_channels, estimate_occlusion_map=False,
|
|
13
|
+
scale_factor=1, kp_variance=0.01):
|
|
14
|
+
super(DenseMotionNetwork, self).__init__()
|
|
15
|
+
self.hourglass = Hourglass(block_expansion=block_expansion, in_features=(num_kp + 1) * (num_channels + 1),
|
|
16
|
+
max_features=max_features, num_blocks=num_blocks)
|
|
17
|
+
|
|
18
|
+
self.mask = nn.Conv2d(self.hourglass.out_filters, num_kp + 1, kernel_size=(7, 7), padding=(3, 3))
|
|
19
|
+
|
|
20
|
+
if estimate_occlusion_map:
|
|
21
|
+
self.occlusion = nn.Conv2d(self.hourglass.out_filters, 1, kernel_size=(7, 7), padding=(3, 3))
|
|
22
|
+
else:
|
|
23
|
+
self.occlusion = None
|
|
24
|
+
|
|
25
|
+
self.num_kp = num_kp
|
|
26
|
+
self.scale_factor = scale_factor
|
|
27
|
+
self.kp_variance = kp_variance
|
|
28
|
+
|
|
29
|
+
if self.scale_factor != 1:
|
|
30
|
+
self.down = AntiAliasInterpolation2d(num_channels, self.scale_factor)
|
|
31
|
+
|
|
32
|
+
def create_heatmap_representations(self, source_image, kp_driving, kp_source):
|
|
33
|
+
"""
|
|
34
|
+
Eq 6. in the paper H_k(z)
|
|
35
|
+
"""
|
|
36
|
+
spatial_size = source_image.shape[2:]
|
|
37
|
+
gaussian_driving = kp2gaussian(kp_driving, spatial_size=spatial_size, kp_variance=self.kp_variance)
|
|
38
|
+
gaussian_source = kp2gaussian(kp_source, spatial_size=spatial_size, kp_variance=self.kp_variance)
|
|
39
|
+
heatmap = gaussian_driving - gaussian_source
|
|
40
|
+
|
|
41
|
+
#adding background feature
|
|
42
|
+
zeros = torch.zeros(heatmap.shape[0], 1, spatial_size[0], spatial_size[1]).type(heatmap.type())
|
|
43
|
+
heatmap = torch.cat([zeros, heatmap], dim=1)
|
|
44
|
+
heatmap = heatmap.unsqueeze(2)
|
|
45
|
+
return heatmap
|
|
46
|
+
|
|
47
|
+
def create_sparse_motions(self, source_image, kp_driving, kp_source):
|
|
48
|
+
"""
|
|
49
|
+
Eq 4. in the paper T_{s<-d}(z)
|
|
50
|
+
"""
|
|
51
|
+
bs, _, h, w = source_image.shape
|
|
52
|
+
identity_grid = make_coordinate_grid((h, w), type=kp_source['value'].type())
|
|
53
|
+
identity_grid = identity_grid.view(1, 1, h, w, 2)
|
|
54
|
+
coordinate_grid = identity_grid - kp_driving['value'].view(bs, self.num_kp, 1, 1, 2)
|
|
55
|
+
if 'jacobian' in kp_driving:
|
|
56
|
+
jacobian = torch.matmul(kp_source['jacobian'], torch.inverse(kp_driving['jacobian']))
|
|
57
|
+
jacobian = jacobian.unsqueeze(-3).unsqueeze(-3)
|
|
58
|
+
jacobian = jacobian.repeat(1, 1, h, w, 1, 1)
|
|
59
|
+
coordinate_grid = torch.matmul(jacobian, coordinate_grid.unsqueeze(-1))
|
|
60
|
+
coordinate_grid = coordinate_grid.squeeze(-1)
|
|
61
|
+
|
|
62
|
+
driving_to_source = coordinate_grid + kp_source['value'].view(bs, self.num_kp, 1, 1, 2)
|
|
63
|
+
|
|
64
|
+
#adding background feature
|
|
65
|
+
identity_grid = identity_grid.repeat(bs, 1, 1, 1, 1)
|
|
66
|
+
sparse_motions = torch.cat([identity_grid, driving_to_source], dim=1)
|
|
67
|
+
return sparse_motions
|
|
68
|
+
|
|
69
|
+
def create_deformed_source_image(self, source_image, sparse_motions):
|
|
70
|
+
r"""
|
|
71
|
+
Eq 7. in the paper \hat{T}_{s<-d}(z)
|
|
72
|
+
"""
|
|
73
|
+
bs, _, h, w = source_image.shape
|
|
74
|
+
source_repeat = source_image.unsqueeze(1).unsqueeze(1).repeat(1, self.num_kp + 1, 1, 1, 1, 1)
|
|
75
|
+
source_repeat = source_repeat.view(bs * (self.num_kp + 1), -1, h, w)
|
|
76
|
+
sparse_motions = sparse_motions.view((bs * (self.num_kp + 1), h, w, -1))
|
|
77
|
+
sparse_deformed = F.grid_sample(source_repeat, sparse_motions, align_corners=True)
|
|
78
|
+
sparse_deformed = sparse_deformed.view((bs, self.num_kp + 1, -1, h, w))
|
|
79
|
+
return sparse_deformed
|
|
80
|
+
|
|
81
|
+
def forward(self, source_image, kp_driving, kp_source):
|
|
82
|
+
if self.scale_factor != 1:
|
|
83
|
+
source_image = self.down(source_image)
|
|
84
|
+
|
|
85
|
+
bs, _, h, w = source_image.shape
|
|
86
|
+
|
|
87
|
+
out_dict = dict()
|
|
88
|
+
heatmap_representation = self.create_heatmap_representations(source_image, kp_driving, kp_source)
|
|
89
|
+
sparse_motion = self.create_sparse_motions(source_image, kp_driving, kp_source)
|
|
90
|
+
deformed_source = self.create_deformed_source_image(source_image, sparse_motion)
|
|
91
|
+
out_dict['sparse_deformed'] = deformed_source
|
|
92
|
+
|
|
93
|
+
input = torch.cat([heatmap_representation, deformed_source], dim=2)
|
|
94
|
+
input = input.view(bs, -1, h, w)
|
|
95
|
+
|
|
96
|
+
prediction = self.hourglass(input)
|
|
97
|
+
|
|
98
|
+
mask = self.mask(prediction)
|
|
99
|
+
mask = F.softmax(mask, dim=1)
|
|
100
|
+
out_dict['mask'] = mask
|
|
101
|
+
mask = mask.unsqueeze(2)
|
|
102
|
+
sparse_motion = sparse_motion.permute(0, 1, 4, 2, 3)
|
|
103
|
+
deformation = (sparse_motion * mask).sum(dim=1)
|
|
104
|
+
deformation = deformation.permute(0, 2, 3, 1)
|
|
105
|
+
|
|
106
|
+
out_dict['deformation'] = deformation
|
|
107
|
+
|
|
108
|
+
# Sec. 3.2 in the paper
|
|
109
|
+
if self.occlusion:
|
|
110
|
+
occlusion_map = torch.sigmoid(self.occlusion(prediction))
|
|
111
|
+
out_dict['occlusion_map'] = occlusion_map
|
|
112
|
+
|
|
113
|
+
return out_dict
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
from .util import ResBlock2d, SameBlock2d, UpBlock2d, DownBlock2d
|
|
5
|
+
from .dense_motion import DenseMotionNetwork
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OcclusionAwareGenerator(nn.Module):
|
|
9
|
+
"""
|
|
10
|
+
Generator that given source image and and keypoints try to transform image according to movement trajectories
|
|
11
|
+
induced by keypoints. Generator follows Johnson architecture.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, num_channels, num_kp, block_expansion, max_features, num_down_blocks,
|
|
15
|
+
num_bottleneck_blocks, estimate_occlusion_map=False, dense_motion_params=None, estimate_jacobian=False):
|
|
16
|
+
super(OcclusionAwareGenerator, self).__init__()
|
|
17
|
+
|
|
18
|
+
if dense_motion_params is not None:
|
|
19
|
+
self.dense_motion_network = DenseMotionNetwork(num_kp=num_kp, num_channels=num_channels,
|
|
20
|
+
estimate_occlusion_map=estimate_occlusion_map,
|
|
21
|
+
**dense_motion_params)
|
|
22
|
+
else:
|
|
23
|
+
self.dense_motion_network = None
|
|
24
|
+
|
|
25
|
+
self.first = SameBlock2d(num_channels, block_expansion, kernel_size=(7, 7), padding=(3, 3))
|
|
26
|
+
|
|
27
|
+
down_blocks = []
|
|
28
|
+
for i in range(num_down_blocks):
|
|
29
|
+
in_features = min(max_features, block_expansion * (2 ** i))
|
|
30
|
+
out_features = min(max_features, block_expansion * (2 ** (i + 1)))
|
|
31
|
+
down_blocks.append(DownBlock2d(in_features, out_features, kernel_size=(3, 3), padding=(1, 1)))
|
|
32
|
+
self.down_blocks = nn.ModuleList(down_blocks)
|
|
33
|
+
|
|
34
|
+
up_blocks = []
|
|
35
|
+
for i in range(num_down_blocks):
|
|
36
|
+
in_features = min(max_features, block_expansion * (2 ** (num_down_blocks - i)))
|
|
37
|
+
out_features = min(max_features, block_expansion * (2 ** (num_down_blocks - i - 1)))
|
|
38
|
+
up_blocks.append(UpBlock2d(in_features, out_features, kernel_size=(3, 3), padding=(1, 1)))
|
|
39
|
+
self.up_blocks = nn.ModuleList(up_blocks)
|
|
40
|
+
|
|
41
|
+
self.bottleneck = torch.nn.Sequential()
|
|
42
|
+
in_features = min(max_features, block_expansion * (2 ** num_down_blocks))
|
|
43
|
+
for i in range(num_bottleneck_blocks):
|
|
44
|
+
self.bottleneck.add_module('r' + str(i), ResBlock2d(in_features, kernel_size=(3, 3), padding=(1, 1)))
|
|
45
|
+
|
|
46
|
+
self.final = nn.Conv2d(block_expansion, num_channels, kernel_size=(7, 7), padding=(3, 3))
|
|
47
|
+
self.estimate_occlusion_map = estimate_occlusion_map
|
|
48
|
+
self.num_channels = num_channels
|
|
49
|
+
|
|
50
|
+
def deform_input(self, inp, deformation):
|
|
51
|
+
_, h_old, w_old, _ = deformation.shape
|
|
52
|
+
_, _, h, w = inp.shape
|
|
53
|
+
if h_old != h or w_old != w:
|
|
54
|
+
deformation = deformation.permute(0, 3, 1, 2)
|
|
55
|
+
deformation = F.interpolate(deformation, size=(h, w), mode='bilinear')
|
|
56
|
+
deformation = deformation.permute(0, 2, 3, 1)
|
|
57
|
+
return F.grid_sample(inp, deformation, align_corners=True)
|
|
58
|
+
|
|
59
|
+
def forward(self, source_image, kp_driving, kp_source):
|
|
60
|
+
# Encoding (downsampling) part
|
|
61
|
+
out = self.first(source_image)
|
|
62
|
+
for i in range(len(self.down_blocks)):
|
|
63
|
+
out = self.down_blocks[i](out)
|
|
64
|
+
|
|
65
|
+
# Transforming feature representation according to deformation and occlusion
|
|
66
|
+
output_dict = {}
|
|
67
|
+
if self.dense_motion_network is not None:
|
|
68
|
+
dense_motion = self.dense_motion_network(source_image=source_image, kp_driving=kp_driving,
|
|
69
|
+
kp_source=kp_source)
|
|
70
|
+
output_dict['mask'] = dense_motion['mask']
|
|
71
|
+
output_dict['sparse_deformed'] = dense_motion['sparse_deformed']
|
|
72
|
+
|
|
73
|
+
if 'occlusion_map' in dense_motion:
|
|
74
|
+
occlusion_map = dense_motion['occlusion_map']
|
|
75
|
+
output_dict['occlusion_map'] = occlusion_map
|
|
76
|
+
else:
|
|
77
|
+
occlusion_map = None
|
|
78
|
+
deformation = dense_motion['deformation']
|
|
79
|
+
out = self.deform_input(out, deformation)
|
|
80
|
+
|
|
81
|
+
if occlusion_map is not None:
|
|
82
|
+
if out.shape[2] != occlusion_map.shape[2] or out.shape[3] != occlusion_map.shape[3]:
|
|
83
|
+
occlusion_map = F.interpolate(occlusion_map, size=out.shape[2:], mode='bilinear')
|
|
84
|
+
out = out * occlusion_map
|
|
85
|
+
|
|
86
|
+
output_dict["deformed"] = self.deform_input(source_image, deformation)
|
|
87
|
+
|
|
88
|
+
# Decoding part
|
|
89
|
+
out = self.bottleneck(out)
|
|
90
|
+
for i in range(len(self.up_blocks)):
|
|
91
|
+
out = self.up_blocks[i](out)
|
|
92
|
+
out = self.final(out)
|
|
93
|
+
out = F.sigmoid(out)
|
|
94
|
+
|
|
95
|
+
output_dict["prediction"] = out
|
|
96
|
+
|
|
97
|
+
return output_dict
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from torch import nn
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
from .util import Hourglass, make_coordinate_grid, AntiAliasInterpolation2d
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KPDetector(nn.Module):
|
|
8
|
+
"""
|
|
9
|
+
Detecting a keypoints. Return keypoint position and jacobian near each keypoint.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, block_expansion, num_kp, num_channels, max_features,
|
|
13
|
+
num_blocks, temperature, estimate_jacobian=False, scale_factor=1,
|
|
14
|
+
single_jacobian_map=False, pad=0):
|
|
15
|
+
super(KPDetector, self).__init__()
|
|
16
|
+
|
|
17
|
+
self.predictor = Hourglass(block_expansion, in_features=num_channels,
|
|
18
|
+
max_features=max_features, num_blocks=num_blocks)
|
|
19
|
+
|
|
20
|
+
self.kp = nn.Conv2d(in_channels=self.predictor.out_filters, out_channels=num_kp, kernel_size=(7, 7),
|
|
21
|
+
padding=pad)
|
|
22
|
+
|
|
23
|
+
if estimate_jacobian:
|
|
24
|
+
self.num_jacobian_maps = 1 if single_jacobian_map else num_kp
|
|
25
|
+
self.jacobian = nn.Conv2d(in_channels=self.predictor.out_filters,
|
|
26
|
+
out_channels=4 * self.num_jacobian_maps, kernel_size=(7, 7), padding=pad)
|
|
27
|
+
self.jacobian.weight.data.zero_()
|
|
28
|
+
self.jacobian.bias.data.copy_(torch.tensor([1, 0, 0, 1] * self.num_jacobian_maps, dtype=torch.float))
|
|
29
|
+
else:
|
|
30
|
+
self.jacobian = None
|
|
31
|
+
|
|
32
|
+
self.temperature = temperature
|
|
33
|
+
self.scale_factor = scale_factor
|
|
34
|
+
if self.scale_factor != 1:
|
|
35
|
+
self.down = AntiAliasInterpolation2d(num_channels, self.scale_factor)
|
|
36
|
+
|
|
37
|
+
def gaussian2kp(self, heatmap):
|
|
38
|
+
"""
|
|
39
|
+
Extract the mean and from a heatmap
|
|
40
|
+
"""
|
|
41
|
+
shape = heatmap.shape
|
|
42
|
+
heatmap = heatmap.unsqueeze(-1)
|
|
43
|
+
grid = make_coordinate_grid(shape[2:], heatmap.type()).unsqueeze_(0).unsqueeze_(0)
|
|
44
|
+
value = (heatmap * grid).sum(dim=(2, 3))
|
|
45
|
+
kp = {'value': value}
|
|
46
|
+
|
|
47
|
+
return kp
|
|
48
|
+
|
|
49
|
+
def forward(self, x):
|
|
50
|
+
if self.scale_factor != 1:
|
|
51
|
+
x = self.down(x)
|
|
52
|
+
|
|
53
|
+
feature_map = self.predictor(x)
|
|
54
|
+
prediction = self.kp(feature_map)
|
|
55
|
+
|
|
56
|
+
final_shape = prediction.shape
|
|
57
|
+
heatmap = prediction.view(final_shape[0], final_shape[1], -1)
|
|
58
|
+
heatmap = F.softmax(heatmap / self.temperature, dim=2)
|
|
59
|
+
heatmap = heatmap.view(*final_shape)
|
|
60
|
+
|
|
61
|
+
out = self.gaussian2kp(heatmap)
|
|
62
|
+
|
|
63
|
+
if self.jacobian is not None:
|
|
64
|
+
jacobian_map = self.jacobian(feature_map)
|
|
65
|
+
jacobian_map = jacobian_map.reshape(final_shape[0], self.num_jacobian_maps, 4, final_shape[2],
|
|
66
|
+
final_shape[3])
|
|
67
|
+
heatmap = heatmap.unsqueeze(2)
|
|
68
|
+
|
|
69
|
+
jacobian = heatmap * jacobian_map
|
|
70
|
+
jacobian = jacobian.view(final_shape[0], final_shape[1], 4, -1)
|
|
71
|
+
jacobian = jacobian.sum(dim=-1)
|
|
72
|
+
jacobian = jacobian.view(jacobian.shape[0], jacobian.shape[1], 2, 2)
|
|
73
|
+
out['jacobian'] = jacobian
|
|
74
|
+
|
|
75
|
+
return out
|