syncnet-python 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.
- syncnet_python/SyncNetInstance.py +210 -0
- syncnet_python/SyncNetModel.py +99 -0
- syncnet_python/__init__.py +29 -0
- syncnet_python/cli.py +127 -0
- syncnet_python/detectors/__init__.py +1 -0
- syncnet_python/detectors/s3fd/__init__.py +66 -0
- syncnet_python/detectors/s3fd/box_utils.py +233 -0
- syncnet_python/detectors/s3fd/nets.py +177 -0
- syncnet_python/run_syncnet_pipeline_on_1example.py +28 -0
- syncnet_python/run_syncnet_pipeline_on_mocha_generation_on_mocha_bench.py +157 -0
- syncnet_python/run_syncnet_pipeline_on_your_own_model_results.py +158 -0
- syncnet_python/syncnet_pipeline.py +332 -0
- syncnet_python-0.1.0.dist-info/METADATA +150 -0
- syncnet_python-0.1.0.dist-info/RECORD +18 -0
- syncnet_python-0.1.0.dist-info/WHEEL +5 -0
- syncnet_python-0.1.0.dist-info/entry_points.txt +2 -0
- syncnet_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- syncnet_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import math
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy
|
|
9
|
+
import python_speech_features
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from scipy import signal
|
|
13
|
+
from scipy.io import wavfile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ==================== Get OFFSET ====================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def calc_pdist(feat1, feat2, vshift=10):
|
|
20
|
+
win_size = vshift * 2 + 1
|
|
21
|
+
|
|
22
|
+
feat2p = torch.nn.functional.pad(feat2, (0, 0, vshift, vshift))
|
|
23
|
+
|
|
24
|
+
dists = []
|
|
25
|
+
|
|
26
|
+
for i in range(0, len(feat1)):
|
|
27
|
+
dists.append(
|
|
28
|
+
torch.nn.functional.pairwise_distance(
|
|
29
|
+
feat1[[i], :].repeat(win_size, 1), feat2p[i : i + win_size, :]
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return dists
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ==================== MAIN DEF ====================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SyncNetInstance(torch.nn.Module):
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
net: torch.nn.Module,
|
|
43
|
+
device: str = "cuda",
|
|
44
|
+
dropout: float = 0,
|
|
45
|
+
num_layers_in_fc_layers: int = 1024,
|
|
46
|
+
):
|
|
47
|
+
super(SyncNetInstance, self).__init__()
|
|
48
|
+
self.__S__ = net
|
|
49
|
+
self.device = device
|
|
50
|
+
|
|
51
|
+
def evaluate(self, opt):
|
|
52
|
+
self.__S__.to(self.device)
|
|
53
|
+
self.__S__.eval()
|
|
54
|
+
|
|
55
|
+
# ========== ==========
|
|
56
|
+
# Load video
|
|
57
|
+
# ========== ==========
|
|
58
|
+
|
|
59
|
+
images = []
|
|
60
|
+
|
|
61
|
+
flist = glob.glob(os.path.join(opt.tmp_dir, "*.jpg"))
|
|
62
|
+
flist.sort()
|
|
63
|
+
|
|
64
|
+
for fname in flist:
|
|
65
|
+
images.append(cv2.imread(fname))
|
|
66
|
+
|
|
67
|
+
im = numpy.stack(images, axis=3)
|
|
68
|
+
im = numpy.expand_dims(im, axis=0)
|
|
69
|
+
im = numpy.transpose(im, (0, 3, 4, 1, 2))
|
|
70
|
+
|
|
71
|
+
imtv = torch.autograd.Variable(torch.from_numpy(im.astype(float)).float())
|
|
72
|
+
|
|
73
|
+
# ========== ==========
|
|
74
|
+
# Load audio
|
|
75
|
+
# ========== ==========
|
|
76
|
+
|
|
77
|
+
sample_rate, audio = wavfile.read(os.path.join(opt.tmp_dir, "audio.wav"))
|
|
78
|
+
mfcc = zip(*python_speech_features.mfcc(audio, sample_rate))
|
|
79
|
+
mfcc = numpy.stack([numpy.array(i) for i in mfcc])
|
|
80
|
+
|
|
81
|
+
cc = numpy.expand_dims(numpy.expand_dims(mfcc, axis=0), axis=0)
|
|
82
|
+
cct = torch.autograd.Variable(torch.from_numpy(cc.astype(float)).float())
|
|
83
|
+
|
|
84
|
+
# ========== ==========
|
|
85
|
+
# Check audio and video input length
|
|
86
|
+
# ========== ==========
|
|
87
|
+
|
|
88
|
+
if (float(len(audio)) / 16000) != (float(len(images)) / 25):
|
|
89
|
+
print(
|
|
90
|
+
"WARNING: Audio (%.4fs) and video (%.4fs) lengths are different."
|
|
91
|
+
% (float(len(audio)) / 16000, float(len(images)) / 25)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
min_length = min(len(images), math.floor(len(audio) / 640))
|
|
95
|
+
|
|
96
|
+
# ========== ==========
|
|
97
|
+
# Generate video and audio feats
|
|
98
|
+
# ========== ==========
|
|
99
|
+
|
|
100
|
+
lastframe = min_length - 5
|
|
101
|
+
im_feat = []
|
|
102
|
+
cc_feat = []
|
|
103
|
+
|
|
104
|
+
tS = time.time()
|
|
105
|
+
for i in range(0, lastframe, opt.batch_size):
|
|
106
|
+
im_batch = [
|
|
107
|
+
imtv[:, :, vframe : vframe + 5, :, :]
|
|
108
|
+
for vframe in range(i, min(lastframe, i + opt.batch_size))
|
|
109
|
+
]
|
|
110
|
+
im_in = torch.cat(im_batch, 0).to(self.device)
|
|
111
|
+
im_out = self.__S__.forward_lip(im_in)
|
|
112
|
+
im_feat.append(im_out.data.cpu())
|
|
113
|
+
|
|
114
|
+
cc_batch = [
|
|
115
|
+
cct[:, :, :, vframe * 4 : vframe * 4 + 20]
|
|
116
|
+
for vframe in range(i, min(lastframe, i + opt.batch_size))
|
|
117
|
+
]
|
|
118
|
+
cc_in = torch.cat(cc_batch, 0).to(self.device)
|
|
119
|
+
cc_out = self.__S__.forward_aud(cc_in)
|
|
120
|
+
cc_feat.append(cc_out.data.cpu())
|
|
121
|
+
|
|
122
|
+
im_feat = torch.cat(im_feat, 0)
|
|
123
|
+
cc_feat = torch.cat(cc_feat, 0)
|
|
124
|
+
|
|
125
|
+
# ========== ==========
|
|
126
|
+
# Compute offset
|
|
127
|
+
# ========== ==========
|
|
128
|
+
|
|
129
|
+
print("Compute time %.3f sec." % (time.time() - tS))
|
|
130
|
+
|
|
131
|
+
dists = calc_pdist(im_feat, cc_feat, vshift=opt.vshift)
|
|
132
|
+
mdist = torch.mean(torch.stack(dists, 1), 1)
|
|
133
|
+
|
|
134
|
+
minval, minidx = torch.min(mdist, 0)
|
|
135
|
+
|
|
136
|
+
offset = opt.vshift - minidx
|
|
137
|
+
conf = torch.median(mdist) - minval
|
|
138
|
+
|
|
139
|
+
fdist = numpy.stack([dist[minidx].numpy() for dist in dists])
|
|
140
|
+
# fdist = numpy.pad(fdist, (3,3), 'constant', constant_values=15)
|
|
141
|
+
fconf = torch.median(mdist).numpy() - fdist
|
|
142
|
+
fconfm = signal.medfilt(fconf, kernel_size=9)
|
|
143
|
+
|
|
144
|
+
numpy.set_printoptions(formatter={"float": "{: 0.3f}".format})
|
|
145
|
+
print("Framewise conf: ")
|
|
146
|
+
print(fconfm)
|
|
147
|
+
print(
|
|
148
|
+
"AV offset: \t%d \nMin dist: \t%.3f\nConfidence: \t%.3f"
|
|
149
|
+
% (offset, minval, conf)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# dists_npy = numpy.array([dist.numpy() for dist in dists])
|
|
153
|
+
# return offset.numpy(), conf.numpy(), minval.numpy()
|
|
154
|
+
return int(offset), float(conf), float(minval)
|
|
155
|
+
|
|
156
|
+
def extract_feature(self, opt, videofile):
|
|
157
|
+
self.__S__.eval()
|
|
158
|
+
# ========== ==========
|
|
159
|
+
# Load video
|
|
160
|
+
# ========== ==========
|
|
161
|
+
cap = cv2.VideoCapture(videofile)
|
|
162
|
+
|
|
163
|
+
frame_num = 1
|
|
164
|
+
images = []
|
|
165
|
+
while frame_num:
|
|
166
|
+
frame_num += 1
|
|
167
|
+
ret, image = cap.read()
|
|
168
|
+
if ret == 0:
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
images.append(image)
|
|
172
|
+
|
|
173
|
+
im = numpy.stack(images, axis=3)
|
|
174
|
+
im = numpy.expand_dims(im, axis=0)
|
|
175
|
+
im = numpy.transpose(im, (0, 3, 4, 1, 2))
|
|
176
|
+
|
|
177
|
+
imtv = torch.autograd.Variable(torch.from_numpy(im.astype(float)).float())
|
|
178
|
+
|
|
179
|
+
# ========== ==========
|
|
180
|
+
# Generate video feats
|
|
181
|
+
# ========== ==========
|
|
182
|
+
|
|
183
|
+
lastframe = len(images) - 4
|
|
184
|
+
im_feat = []
|
|
185
|
+
|
|
186
|
+
tS = time.time()
|
|
187
|
+
for i in range(0, lastframe, opt.batch_size):
|
|
188
|
+
im_batch = [
|
|
189
|
+
imtv[:, :, vframe : vframe + 5, :, :]
|
|
190
|
+
for vframe in range(i, min(lastframe, i + opt.batch_size))
|
|
191
|
+
]
|
|
192
|
+
im_in = torch.cat(im_batch, 0).to(self.device)
|
|
193
|
+
im_out = self.__S__.forward_lipfeat(im_in)
|
|
194
|
+
im_feat.append(im_out.data.cpu())
|
|
195
|
+
|
|
196
|
+
im_feat = torch.cat(im_feat, 0)
|
|
197
|
+
|
|
198
|
+
# ========== ==========
|
|
199
|
+
# Compute offset
|
|
200
|
+
# ========== ==========
|
|
201
|
+
|
|
202
|
+
print("Compute time %.3f sec." % (time.time() - tS))
|
|
203
|
+
|
|
204
|
+
return im_feat
|
|
205
|
+
|
|
206
|
+
def loadParameters(self, path):
|
|
207
|
+
loaded_state = torch.load(path, map_location=lambda storage, loc: storage)
|
|
208
|
+
self_state = self.__S__.state_dict()
|
|
209
|
+
for name, param in loaded_state.items():
|
|
210
|
+
self_state[name].copy_(param)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def save(model, filename):
|
|
6
|
+
with open(filename, "wb") as f:
|
|
7
|
+
torch.save(model, f)
|
|
8
|
+
print("%s saved." % filename)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load(filename):
|
|
12
|
+
net = torch.load(filename)
|
|
13
|
+
return net
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class S(nn.Module):
|
|
17
|
+
def __init__(self, num_layers_in_fc_layers=1024):
|
|
18
|
+
super(S, self).__init__()
|
|
19
|
+
self.__nFeatures__ = 24
|
|
20
|
+
self.__nChs__ = 32
|
|
21
|
+
self.__midChs__ = 32
|
|
22
|
+
self.netcnnaud = nn.Sequential(
|
|
23
|
+
nn.Conv2d(1, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
|
|
24
|
+
nn.BatchNorm2d(64),
|
|
25
|
+
nn.ReLU(inplace=True),
|
|
26
|
+
nn.MaxPool2d(kernel_size=(1, 1), stride=(1, 1)),
|
|
27
|
+
nn.Conv2d(64, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
|
|
28
|
+
nn.BatchNorm2d(192),
|
|
29
|
+
nn.ReLU(inplace=True),
|
|
30
|
+
nn.MaxPool2d(kernel_size=(3, 3), stride=(1, 2)),
|
|
31
|
+
nn.Conv2d(192, 384, kernel_size=(3, 3), padding=(1, 1)),
|
|
32
|
+
nn.BatchNorm2d(384),
|
|
33
|
+
nn.ReLU(inplace=True),
|
|
34
|
+
nn.Conv2d(384, 256, kernel_size=(3, 3), padding=(1, 1)),
|
|
35
|
+
nn.BatchNorm2d(256),
|
|
36
|
+
nn.ReLU(inplace=True),
|
|
37
|
+
nn.Conv2d(256, 256, kernel_size=(3, 3), padding=(1, 1)),
|
|
38
|
+
nn.BatchNorm2d(256),
|
|
39
|
+
nn.ReLU(inplace=True),
|
|
40
|
+
nn.MaxPool2d(kernel_size=(3, 3), stride=(2, 2)),
|
|
41
|
+
nn.Conv2d(256, 512, kernel_size=(5, 4), padding=(0, 0)),
|
|
42
|
+
nn.BatchNorm2d(512),
|
|
43
|
+
nn.ReLU(),
|
|
44
|
+
)
|
|
45
|
+
self.netfcaud = nn.Sequential(
|
|
46
|
+
nn.Linear(512, 512),
|
|
47
|
+
nn.BatchNorm1d(512),
|
|
48
|
+
nn.ReLU(),
|
|
49
|
+
nn.Linear(512, num_layers_in_fc_layers),
|
|
50
|
+
)
|
|
51
|
+
self.netfclip = nn.Sequential(
|
|
52
|
+
nn.Linear(512, 512),
|
|
53
|
+
nn.BatchNorm1d(512),
|
|
54
|
+
nn.ReLU(),
|
|
55
|
+
nn.Linear(512, num_layers_in_fc_layers),
|
|
56
|
+
)
|
|
57
|
+
self.netcnnlip = nn.Sequential(
|
|
58
|
+
nn.Conv3d(3, 96, kernel_size=(5, 7, 7), stride=(1, 2, 2), padding=0),
|
|
59
|
+
nn.BatchNorm3d(96),
|
|
60
|
+
nn.ReLU(inplace=True),
|
|
61
|
+
nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 2, 2)),
|
|
62
|
+
nn.Conv3d(
|
|
63
|
+
96, 256, kernel_size=(1, 5, 5), stride=(1, 2, 2), padding=(0, 1, 1)
|
|
64
|
+
),
|
|
65
|
+
nn.BatchNorm3d(256),
|
|
66
|
+
nn.ReLU(inplace=True),
|
|
67
|
+
nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 2, 2), padding=(0, 1, 1)),
|
|
68
|
+
nn.Conv3d(256, 256, kernel_size=(1, 3, 3), padding=(0, 1, 1)),
|
|
69
|
+
nn.BatchNorm3d(256),
|
|
70
|
+
nn.ReLU(inplace=True),
|
|
71
|
+
nn.Conv3d(256, 256, kernel_size=(1, 3, 3), padding=(0, 1, 1)),
|
|
72
|
+
nn.BatchNorm3d(256),
|
|
73
|
+
nn.ReLU(inplace=True),
|
|
74
|
+
nn.Conv3d(256, 256, kernel_size=(1, 3, 3), padding=(0, 1, 1)),
|
|
75
|
+
nn.BatchNorm3d(256),
|
|
76
|
+
nn.ReLU(inplace=True),
|
|
77
|
+
nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 2, 2)),
|
|
78
|
+
nn.Conv3d(256, 512, kernel_size=(1, 6, 6), padding=0),
|
|
79
|
+
nn.BatchNorm3d(512),
|
|
80
|
+
nn.ReLU(inplace=True),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def forward_aud(self, x):
|
|
84
|
+
mid = self.netcnnaud(x) # N x ch x 24 x M
|
|
85
|
+
mid = mid.view((mid.size()[0], -1)) # N x (ch x 24)
|
|
86
|
+
out = self.netfcaud(mid)
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
def forward_lip(self, x):
|
|
90
|
+
mid = self.netcnnlip(x)
|
|
91
|
+
mid = mid.view((mid.size()[0], -1)) # N x (ch x 24)
|
|
92
|
+
out = self.netfclip(mid)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
def forward_lipfeat(self, x):
|
|
96
|
+
mid = self.netcnnlip(x)
|
|
97
|
+
out = mid.view((mid.size()[0], -1)) # N x (ch x 24)
|
|
98
|
+
|
|
99
|
+
return out
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""SyncNet Python: Audio-visual synchronization detection.
|
|
2
|
+
|
|
3
|
+
This package provides a PyTorch implementation of SyncNet for detecting
|
|
4
|
+
synchronization between audio and video in multimedia content.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
# Import main components
|
|
10
|
+
try:
|
|
11
|
+
from .syncnet_pipeline import SyncNetPipeline
|
|
12
|
+
from .SyncNetModel import S as SyncNetModel
|
|
13
|
+
from .SyncNetInstance import SyncNetInstance
|
|
14
|
+
except ImportError:
|
|
15
|
+
# Fallback for development
|
|
16
|
+
SyncNetPipeline = None
|
|
17
|
+
SyncNetModel = None
|
|
18
|
+
SyncNetInstance = None
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SyncNetPipeline",
|
|
22
|
+
"SyncNetModel",
|
|
23
|
+
"SyncNetInstance",
|
|
24
|
+
"__version__"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def get_version():
|
|
28
|
+
"""Get package version."""
|
|
29
|
+
return __version__
|
syncnet_python/cli.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Command-line interface for SyncNet Python."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .syncnet_pipeline import SyncNetPipeline
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
"""Main CLI entry point."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="SyncNet: Audio-visual synchronization detection",
|
|
16
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
17
|
+
epilog="""
|
|
18
|
+
Examples:
|
|
19
|
+
# Process a single video
|
|
20
|
+
syncnet-python video.mp4
|
|
21
|
+
|
|
22
|
+
# Process multiple videos
|
|
23
|
+
syncnet-python video1.mp4 video2.mp4 --output results.json
|
|
24
|
+
|
|
25
|
+
# Use CPU instead of GPU
|
|
26
|
+
syncnet-python video.mp4 --device cpu
|
|
27
|
+
"""
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
'videos',
|
|
32
|
+
nargs='+',
|
|
33
|
+
help='Video file(s) to process'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
'-o', '--output',
|
|
38
|
+
help='Output JSON file for results'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
'--device',
|
|
43
|
+
default='cuda',
|
|
44
|
+
choices=['cuda', 'cpu'],
|
|
45
|
+
help='Device to run models on (default: cuda)'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
'--s3fd-weights',
|
|
50
|
+
default='weights/sfd_face.pth',
|
|
51
|
+
help='Path to S3FD weights'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
'--syncnet-weights',
|
|
56
|
+
default='weights/syncnet_v2.model',
|
|
57
|
+
help='Path to SyncNet weights'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
'--cache-dir',
|
|
62
|
+
help='Directory for temporary files'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
|
|
67
|
+
# Initialize pipeline
|
|
68
|
+
try:
|
|
69
|
+
pipeline = SyncNetPipeline(
|
|
70
|
+
{
|
|
71
|
+
"s3fd_weights": args.s3fd_weights,
|
|
72
|
+
"syncnet_weights": args.syncnet_weights,
|
|
73
|
+
},
|
|
74
|
+
device=args.device,
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(f"Error initializing pipeline: {e}", file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
|
|
80
|
+
results_all = []
|
|
81
|
+
|
|
82
|
+
# Process videos
|
|
83
|
+
for video_path in args.videos:
|
|
84
|
+
print(f"Processing: {video_path}")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
results = pipeline.inference(
|
|
88
|
+
video_path=video_path,
|
|
89
|
+
audio_path=None,
|
|
90
|
+
cache_dir=args.cache_dir
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
offsets, confs, dists, max_conf, min_dist, s3fd_json, has_face = results
|
|
94
|
+
|
|
95
|
+
result_dict = {
|
|
96
|
+
"video": video_path,
|
|
97
|
+
"status": "success",
|
|
98
|
+
"offset": int(offsets[0]) if offsets else None,
|
|
99
|
+
"confidence": float(max_conf),
|
|
100
|
+
"min_distance": float(min_dist),
|
|
101
|
+
"has_face": has_face
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
print(f" Offset: {offsets[0] if offsets else 'N/A'} frames")
|
|
105
|
+
print(f" Confidence: {max_conf:.3f}")
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
result_dict = {
|
|
109
|
+
"video": video_path,
|
|
110
|
+
"status": "error",
|
|
111
|
+
"error": str(e)
|
|
112
|
+
}
|
|
113
|
+
print(f" Error: {e}")
|
|
114
|
+
|
|
115
|
+
results_all.append(result_dict)
|
|
116
|
+
|
|
117
|
+
# Output results
|
|
118
|
+
if args.output:
|
|
119
|
+
with open(args.output, 'w') as f:
|
|
120
|
+
json.dump(results_all, f, indent=2)
|
|
121
|
+
print(f"\nResults saved to: {args.output}")
|
|
122
|
+
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == '__main__':
|
|
127
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .s3fd import S3FD
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
from .box_utils import nms_
|
|
9
|
+
from .nets import S3FDNet
|
|
10
|
+
|
|
11
|
+
img_mean = np.array([104.0, 117.0, 123.0])[:, np.newaxis, np.newaxis].astype("float32")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class S3FD:
|
|
15
|
+
def __init__(self, net: S3FDNet, device="cuda"):
|
|
16
|
+
"""
|
|
17
|
+
We now accept an *already-initialized* S3FDNet as `net`,
|
|
18
|
+
instead of loading weights here.
|
|
19
|
+
"""
|
|
20
|
+
tstamp = time.time()
|
|
21
|
+
self.device = device
|
|
22
|
+
self.net = net.to(self.device)
|
|
23
|
+
self.net.eval()
|
|
24
|
+
logging.info(
|
|
25
|
+
f"[S3FD] S3FDNet instance is ready (initialized in {time.time()-tstamp:.4f} sec)."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def detect_faces(self, image, conf_th=0.8, scales=[1]):
|
|
29
|
+
"""
|
|
30
|
+
Same detection code as before, but we no longer load the model here.
|
|
31
|
+
"""
|
|
32
|
+
self.net.to(self.device)
|
|
33
|
+
self.net.eval()
|
|
34
|
+
w, h = image.shape[1], image.shape[0]
|
|
35
|
+
bboxes = np.empty(shape=(0, 5))
|
|
36
|
+
|
|
37
|
+
with torch.no_grad():
|
|
38
|
+
for s in scales:
|
|
39
|
+
scaled_img = cv2.resize(
|
|
40
|
+
image, dsize=(0, 0), fx=s, fy=s, interpolation=cv2.INTER_LINEAR
|
|
41
|
+
)
|
|
42
|
+
scaled_img = np.swapaxes(scaled_img, 1, 2)
|
|
43
|
+
scaled_img = np.swapaxes(scaled_img, 1, 0)
|
|
44
|
+
scaled_img = scaled_img[[2, 1, 0], :, :]
|
|
45
|
+
scaled_img = scaled_img.astype("float32")
|
|
46
|
+
scaled_img -= img_mean
|
|
47
|
+
scaled_img = scaled_img[[2, 1, 0], :, :]
|
|
48
|
+
x = torch.from_numpy(scaled_img).unsqueeze(0).to(self.device)
|
|
49
|
+
|
|
50
|
+
y = self.net(x) # forward pass
|
|
51
|
+
detections = y.data.to(self.device)
|
|
52
|
+
scale_tensor = torch.Tensor([w, h, w, h]).to(self.device)
|
|
53
|
+
|
|
54
|
+
for i in range(detections.size(1)):
|
|
55
|
+
j = 0
|
|
56
|
+
while detections[0, i, j, 0] > conf_th:
|
|
57
|
+
score = detections[0, i, j, 0].item()
|
|
58
|
+
pt = (detections[0, i, j, 1:] * scale_tensor).cpu().numpy()
|
|
59
|
+
bbox = (pt[0], pt[1], pt[2], pt[3], score)
|
|
60
|
+
bboxes = np.vstack((bboxes, bbox))
|
|
61
|
+
j += 1
|
|
62
|
+
|
|
63
|
+
# NMS, etc. (unchanged)
|
|
64
|
+
keep = nms_(bboxes, 0.1)
|
|
65
|
+
bboxes = bboxes[keep]
|
|
66
|
+
return bboxes
|