|
|
|
|
|
import os, pdb, subprocess, glob, cv2 |
|
import numpy as np |
|
from shutil import rmtree |
|
import torch |
|
|
|
from scenedetect.video_manager import VideoManager |
|
from scenedetect.scene_manager import SceneManager |
|
from scenedetect.stats_manager import StatsManager |
|
from scenedetect.detectors import ContentDetector |
|
|
|
from scipy.interpolate import interp1d |
|
from scipy.io import wavfile |
|
from scipy import signal |
|
|
|
from eval.detectors import S3FD |
|
|
|
|
|
class SyncNetDetector: |
|
def __init__(self, device, detect_results_dir="detect_results"): |
|
self.s3f_detector = S3FD(device=device) |
|
self.detect_results_dir = detect_results_dir |
|
|
|
def __call__(self, video_path: str, min_track=50, scale=False): |
|
crop_dir = os.path.join(self.detect_results_dir, "crop") |
|
video_dir = os.path.join(self.detect_results_dir, "video") |
|
frames_dir = os.path.join(self.detect_results_dir, "frames") |
|
temp_dir = os.path.join(self.detect_results_dir, "temp") |
|
|
|
|
|
if os.path.exists(crop_dir): |
|
rmtree(crop_dir) |
|
|
|
if os.path.exists(video_dir): |
|
rmtree(video_dir) |
|
|
|
if os.path.exists(frames_dir): |
|
rmtree(frames_dir) |
|
|
|
if os.path.exists(temp_dir): |
|
rmtree(temp_dir) |
|
|
|
|
|
|
|
os.makedirs(crop_dir) |
|
os.makedirs(video_dir) |
|
os.makedirs(frames_dir) |
|
os.makedirs(temp_dir) |
|
|
|
|
|
|
|
if scale: |
|
scaled_video_path = os.path.join(video_dir, "scaled.mp4") |
|
command = f"ffmpeg -loglevel error -y -nostdin -i {video_path} -vf scale='224:224' {scaled_video_path}" |
|
subprocess.run(command, shell=True) |
|
video_path = scaled_video_path |
|
|
|
command = f"ffmpeg -y -nostdin -loglevel error -i {video_path} -qscale:v 2 -async 1 -r 25 {os.path.join(video_dir, 'video.mp4')}" |
|
subprocess.run(command, shell=True, stdout=None) |
|
|
|
command = f"ffmpeg -y -nostdin -loglevel error -i {os.path.join(video_dir, 'video.mp4')} -qscale:v 2 -f image2 {os.path.join(frames_dir, '%06d.jpg')}" |
|
subprocess.run(command, shell=True, stdout=None) |
|
|
|
command = f"ffmpeg -y -nostdin -loglevel error -i {os.path.join(video_dir, 'video.mp4')} -ac 1 -vn -acodec pcm_s16le -ar 16000 {os.path.join(video_dir, 'audio.wav')}" |
|
subprocess.run(command, shell=True, stdout=None) |
|
|
|
faces = self.detect_face(frames_dir) |
|
|
|
scene = self.scene_detect(video_dir) |
|
|
|
|
|
alltracks = [] |
|
|
|
for shot in scene: |
|
if shot[1].frame_num - shot[0].frame_num >= min_track: |
|
alltracks.extend(self.track_face(faces[shot[0].frame_num : shot[1].frame_num], min_track=min_track)) |
|
|
|
|
|
for ii, track in enumerate(alltracks): |
|
self.crop_video(track, os.path.join(crop_dir, "%05d" % ii), frames_dir, 25, temp_dir, video_dir) |
|
|
|
rmtree(temp_dir) |
|
|
|
def scene_detect(self, video_dir): |
|
video_manager = VideoManager([os.path.join(video_dir, "video.mp4")]) |
|
stats_manager = StatsManager() |
|
scene_manager = SceneManager(stats_manager) |
|
|
|
scene_manager.add_detector(ContentDetector()) |
|
base_timecode = video_manager.get_base_timecode() |
|
|
|
video_manager.set_downscale_factor() |
|
|
|
video_manager.start() |
|
|
|
scene_manager.detect_scenes(frame_source=video_manager) |
|
|
|
scene_list = scene_manager.get_scene_list(base_timecode) |
|
|
|
if scene_list == []: |
|
scene_list = [(video_manager.get_base_timecode(), video_manager.get_current_timecode())] |
|
|
|
return scene_list |
|
|
|
def track_face(self, scenefaces, num_failed_det=25, min_track=50, min_face_size=100): |
|
|
|
iouThres = 0.5 |
|
tracks = [] |
|
|
|
while True: |
|
track = [] |
|
for framefaces in scenefaces: |
|
for face in framefaces: |
|
if track == []: |
|
track.append(face) |
|
framefaces.remove(face) |
|
elif face["frame"] - track[-1]["frame"] <= num_failed_det: |
|
iou = bounding_box_iou(face["bbox"], track[-1]["bbox"]) |
|
if iou > iouThres: |
|
track.append(face) |
|
framefaces.remove(face) |
|
continue |
|
else: |
|
break |
|
|
|
if track == []: |
|
break |
|
elif len(track) > min_track: |
|
|
|
framenum = np.array([f["frame"] for f in track]) |
|
bboxes = np.array([np.array(f["bbox"]) for f in track]) |
|
|
|
frame_i = np.arange(framenum[0], framenum[-1] + 1) |
|
|
|
bboxes_i = [] |
|
for ij in range(0, 4): |
|
interpfn = interp1d(framenum, bboxes[:, ij]) |
|
bboxes_i.append(interpfn(frame_i)) |
|
bboxes_i = np.stack(bboxes_i, axis=1) |
|
|
|
if ( |
|
max(np.mean(bboxes_i[:, 2] - bboxes_i[:, 0]), np.mean(bboxes_i[:, 3] - bboxes_i[:, 1])) |
|
> min_face_size |
|
): |
|
tracks.append({"frame": frame_i, "bbox": bboxes_i}) |
|
|
|
return tracks |
|
|
|
def detect_face(self, frames_dir, facedet_scale=0.25): |
|
flist = glob.glob(os.path.join(frames_dir, "*.jpg")) |
|
flist.sort() |
|
|
|
dets = [] |
|
|
|
for fidx, fname in enumerate(flist): |
|
image = cv2.imread(fname) |
|
|
|
image_np = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) |
|
bboxes = self.s3f_detector.detect_faces(image_np, conf_th=0.9, scales=[facedet_scale]) |
|
|
|
dets.append([]) |
|
for bbox in bboxes: |
|
dets[-1].append({"frame": fidx, "bbox": (bbox[:-1]).tolist(), "conf": bbox[-1]}) |
|
|
|
return dets |
|
|
|
def crop_video(self, track, cropfile, frames_dir, frame_rate, temp_dir, video_dir, crop_scale=0.4): |
|
|
|
flist = glob.glob(os.path.join(frames_dir, "*.jpg")) |
|
flist.sort() |
|
|
|
fourcc = cv2.VideoWriter_fourcc(*"mp4v") |
|
vOut = cv2.VideoWriter(cropfile + "t.mp4", fourcc, frame_rate, (224, 224)) |
|
|
|
dets = {"x": [], "y": [], "s": []} |
|
|
|
for det in track["bbox"]: |
|
|
|
dets["s"].append(max((det[3] - det[1]), (det[2] - det[0])) / 2) |
|
dets["y"].append((det[1] + det[3]) / 2) |
|
dets["x"].append((det[0] + det[2]) / 2) |
|
|
|
|
|
dets["s"] = signal.medfilt(dets["s"], kernel_size=13) |
|
dets["x"] = signal.medfilt(dets["x"], kernel_size=13) |
|
dets["y"] = signal.medfilt(dets["y"], kernel_size=13) |
|
|
|
for fidx, frame in enumerate(track["frame"]): |
|
|
|
cs = crop_scale |
|
|
|
bs = dets["s"][fidx] |
|
bsi = int(bs * (1 + 2 * cs)) |
|
|
|
image = cv2.imread(flist[frame]) |
|
|
|
frame = np.pad(image, ((bsi, bsi), (bsi, bsi), (0, 0)), "constant", constant_values=(110, 110)) |
|
my = dets["y"][fidx] + bsi |
|
mx = dets["x"][fidx] + bsi |
|
|
|
face = frame[int(my - bs) : int(my + bs * (1 + 2 * cs)), int(mx - bs * (1 + cs)) : int(mx + bs * (1 + cs))] |
|
|
|
vOut.write(cv2.resize(face, (224, 224))) |
|
|
|
audiotmp = os.path.join(temp_dir, "audio.wav") |
|
audiostart = (track["frame"][0]) / frame_rate |
|
audioend = (track["frame"][-1] + 1) / frame_rate |
|
|
|
vOut.release() |
|
|
|
|
|
|
|
command = "ffmpeg -y -nostdin -loglevel error -i %s -ss %.3f -to %.3f %s" % ( |
|
os.path.join(video_dir, "audio.wav"), |
|
audiostart, |
|
audioend, |
|
audiotmp, |
|
) |
|
output = subprocess.run(command, shell=True, stdout=None) |
|
|
|
sample_rate, audio = wavfile.read(audiotmp) |
|
|
|
|
|
|
|
command = "ffmpeg -y -nostdin -loglevel error -i %st.mp4 -i %s -c:v copy -c:a aac %s.mp4" % ( |
|
cropfile, |
|
audiotmp, |
|
cropfile, |
|
) |
|
output = subprocess.run(command, shell=True, stdout=None) |
|
|
|
os.remove(cropfile + "t.mp4") |
|
|
|
return {"track": track, "proc_track": dets} |
|
|
|
|
|
def bounding_box_iou(boxA, boxB): |
|
xA = max(boxA[0], boxB[0]) |
|
yA = max(boxA[1], boxB[1]) |
|
xB = min(boxA[2], boxB[2]) |
|
yB = min(boxA[3], boxB[3]) |
|
|
|
interArea = max(0, xB - xA) * max(0, yB - yA) |
|
|
|
boxAArea = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1]) |
|
boxBArea = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1]) |
|
|
|
iou = interArea / float(boxAArea + boxBArea - interArea) |
|
|
|
return iou |
|
|