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 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