From 203e571aba6b93b58801857df04e90a785ce365c Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期三, 30 七月 2025 22:11:26 +0800
Subject: [PATCH] add face swapper

---
 data/auto_media_publisher.sqlite3                                     |    0 
 /dev/null                                                             |   17 --
 auto_media_publisher/modifiers/face_swap/__init__.py                  |    0 
 auto_media_publisher/modifiers/face_swap/face_nsfw_predictor.py       |    0 
 auto_media_publisher/modifiers/face_swap/face_enhancer.py             |    1 
 auto_media_publisher/modifiers/face_swap/face_swaper_avpy.py          |  166 +++++++++++++++++++++++
 auto_media_publisher/modifiers/face_swap/typing.py                    |    7 +
 auto_media_publisher/utils/media_input_output.py                      |   93 +++++++++++++
 auto_media_publisher/modifiers/face_swap/face_analyser.py             |    1 
 auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg.py        |   79 +++++------
 auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg_stream.py |    2 
 11 files changed, 302 insertions(+), 64 deletions(-)

diff --git a/auto_media_publisher/utils/face_swap/__init__.py b/auto_media_publisher/modifiers/face_swap/__init__.py
similarity index 100%
rename from auto_media_publisher/utils/face_swap/__init__.py
rename to auto_media_publisher/modifiers/face_swap/__init__.py
diff --git a/auto_media_publisher/utils/face_swap/face_analyser.py b/auto_media_publisher/modifiers/face_swap/face_analyser.py
similarity index 98%
rename from auto_media_publisher/utils/face_swap/face_analyser.py
rename to auto_media_publisher/modifiers/face_swap/face_analyser.py
index e927185..1c4538b 100644
--- a/auto_media_publisher/utils/face_swap/face_analyser.py
+++ b/auto_media_publisher/modifiers/face_swap/face_analyser.py
@@ -18,7 +18,6 @@
         self._analyser.prepare(ctx_id=0, det_size=(640, 640))
 
     def _select_device(self):
-        return 'cpu'
         if torch.cuda.is_available():
             return 'cuda'
         elif torch.backends.mps.is_available():
diff --git a/auto_media_publisher/utils/face_swap/face_enhancer.py b/auto_media_publisher/modifiers/face_swap/face_enhancer.py
similarity index 98%
rename from auto_media_publisher/utils/face_swap/face_enhancer.py
rename to auto_media_publisher/modifiers/face_swap/face_enhancer.py
index d4b6beb..1b600fe 100644
--- a/auto_media_publisher/utils/face_swap/face_enhancer.py
+++ b/auto_media_publisher/modifiers/face_swap/face_enhancer.py
@@ -16,7 +16,6 @@
         self._enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=self.device)
 
     def _select_device(self):
-        return 'cpu'
         if torch.cuda.is_available():
             return 'cuda'
         elif torch.backends.mps.is_available():
diff --git a/auto_media_publisher/utils/face_swap/face_nsfw_predictor.py b/auto_media_publisher/modifiers/face_swap/face_nsfw_predictor.py
similarity index 100%
rename from auto_media_publisher/utils/face_swap/face_nsfw_predictor.py
rename to auto_media_publisher/modifiers/face_swap/face_nsfw_predictor.py
diff --git a/auto_media_publisher/modifiers/face_swap/face_swaper_avpy.py b/auto_media_publisher/modifiers/face_swap/face_swaper_avpy.py
new file mode 100644
index 0000000..52b0f41
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/face_swaper_avpy.py
@@ -0,0 +1,166 @@
+import av
+import torch
+import threading, queue
+from threading import current_thread
+import numpy as np
+import insightface
+import cv2
+import time
+from insightface.app.common import Face
+from gfpgan.utils import GFPGANer
+from .typing import Frame # 浣犲凡鏈夊畾涔�
+from auto_media_publisher.config.conf_base import FACE_ENHANCER_MODEL_PATH,FACE_SWAPPER_MODEL_PATH
+from insightface.app import FaceAnalysis
+
+SIMILAR_FACE_DISTANCE: float = 0.45     # < 0.4 鍚屼竴浜� 0.4 -0.6 澶ф鏄悓涓�浜� >0.6 涓嶆槸鍚屼竴涓�
+
+def _select_device():
+    if torch.cuda.is_available():
+        return 'cuda:0'
+    elif torch.backends.mps.is_available():
+        return 'mps'
+    else:
+        return 'cpu'
+    
+NUM_WORKERS = 1
+
+#face_swapper = insightface.model_zoo.get_model(FACE_SWAPPER_MODEL_PATH, providers=['CUDAExecutionProvider'])
+#face_enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=_select_device())
+
+frame_q = queue.Queue(maxsize=10)
+processed_q = queue.Queue(maxsize=10)
+
+THREAD_SEMAPHORE =threading.Semaphore(4)  # 涓嶉檺鍒朵簡銆�
+
+def enhance_face(thread_face_enhancer,target_face, temp_frame:np.ndarray) -> np.ndarray:
+    start_x, start_y, end_x, end_y = map(int, target_face['bbox'])
+    padding_x = int((end_x - start_x) * 0.5)
+    padding_y = int((end_y - start_y) * 0.5)
+    start_x = max(0, start_x - padding_x)
+    start_y = max(0, start_y - padding_y)
+    end_x = max(0, end_x + padding_x)
+    end_y = max(0, end_y + padding_y)
+    temp_face = temp_frame[start_y:end_y, start_x:end_x]
+    #print(temp_face.size)
+    if temp_face.size:
+        _, _, temp_face = thread_face_enhancer.enhance(
+                temp_face,
+                paste_back=True
+        )
+        temp_frame[start_y:end_y, start_x:end_x] = temp_face
+    return temp_frame
+    
+
+def decode_video(video_path):
+    container = av.open(video_path)
+    for idx, frame in enumerate(container.decode(video=0)):
+        img = frame.to_ndarray(format='rgb24')
+        frame_q.put((idx, img))
+    for _ in range(NUM_WORKERS):
+        frame_q.put((None, None))
+
+def process_frame_loop(new_face):
+    start_time = time.time()
+    thread_face_analyser = FaceAnalysis(name='buffalo_l', root='./', providers=['CUDAExecutionProvider'])
+    thread_face_analyser.prepare(ctx_id=3, det_size=(640, 640))
+    #thread_face_enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=_select_device())
+    thread_face_enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=_select_device())
+    thread_face_swapper = insightface.model_zoo.get_model(FACE_SWAPPER_MODEL_PATH, providers=['CUDAExecutionProvider'])
+
+    while True:
+        idx, frame = frame_q.get()
+        if idx is None:
+            processed_q.put((None, None))
+            break
+        try:
+            many_faces = thread_face_analyser.get(frame)
+            if many_faces:
+                old_face = many_faces[0]
+                frame = thread_face_swapper.get(frame, old_face, new_face, paste_back=True)
+
+                for sinlge_face in many_faces:
+                    frame_ = enhance_face(thread_face_enhancer,sinlge_face, frame)                
+
+            processed_q.put((idx, frame_))
+            print(f"{current_thread().name} has proceesed frame :{idx}")           
+
+        except Exception as e:
+            print(f"[ERROR] Frame {idx} failed: {e}")
+    
+    print(f"totol time is : {time.time()- start_time}")
+
+def encode_video(output_path, fps, width, height):
+    out = av.open(output_path, mode='w')
+
+    #stream = out.add_stream('h264_nvenc', rate=fps)
+    stream = out.add_stream('libx264', rate=fps)
+    stream.width = width
+    stream.height = height
+    stream.pix_fmt = 'yuv420p'
+
+    stream.options = {'crf': '18', 'preset': 'veryfast'}
+    #stream.options = {'rc': 'constqp', 'qp': '18','preset': 'p7','profile': 'high'}
+
+    next_index = 0
+    buffer = {}
+    end_count = 0
+
+    while True:
+        idx, frame = processed_q.get()
+        if idx is None:
+            end_count += 1
+            if end_count >= NUM_WORKERS:
+                break
+            continue
+        buffer[idx] = frame
+
+        # 鍙鏈夊綋鍓嶈鍐欏叆鐨勫抚锛屽氨杈撳嚭
+        while next_index in buffer:
+            frame = buffer.pop(next_index)
+            video_frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
+            for packet in stream.encode(video_frame):
+                out.mux(packet)
+            next_index += 1
+
+    # 娓呯┖缂栫爜缂撳瓨
+    for packet in stream.encode():
+        out.mux(packet)
+    out.close()
+# 鍚姩浠诲姟
+video_path = './temp/buhui.mp4'
+output_path = './temp/buhui-wangrong-enhancer.mp4'
+new_face_image_path = './temp/wangrong.jpg'
+face_analyser = FaceAnalysis(name='buffalo_l', root='./', providers=['CUDAExecutionProvider'])
+face_analyser.prepare(ctx_id=0, det_size=(640, 640))
+
+faces = face_analyser.get(cv2.imread(new_face_image_path))
+if faces:
+    new_face = sorted(faces, key=lambda x: x.bbox[0])[0]
+        
+    #threading.Thread(target=decode_video, args=(video_path,)).start()
+    #threading.Thread(target=process_frame_loop, args=(new_face,)).start()
+    #threading.Thread(target=encode_video, args=(output_path, 25,576,1080)).start()
+    threads = []
+        
+    # 瑙g爜绾跨▼
+    t_decode = threading.Thread(target=decode_video,args=(video_path,))
+    threads.append(t_decode)
+    t_decode.start()
+    
+    # 澶氬鐞嗙嚎绋�
+    for i in range(NUM_WORKERS):
+        t = threading.Thread(target=process_frame_loop, args=(new_face,),name=f"process_worker_{i}")
+        threads.append(t)
+        t.start()
+    
+    # 缂栫爜绾跨▼
+    t_encode = threading.Thread(target=encode_video,args=(output_path, 25,576,1080))
+    threads.append(t_encode)
+    t_encode.start()
+    
+    for t in threads:
+        t.join()
+    
+    print("Processing complete.")
+
+
diff --git a/auto_media_publisher/utils/face_swap/face_swaper_ffmpeg.py b/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg.py
similarity index 80%
rename from auto_media_publisher/utils/face_swap/face_swaper_ffmpeg.py
rename to auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg.py
index 7b28497..12f4d11 100644
--- a/auto_media_publisher/utils/face_swap/face_swaper_ffmpeg.py
+++ b/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg.py
@@ -15,7 +15,9 @@
 from .face_analyser import FaceAnalyser
 from auto_media_publisher.utils.logger import get_logger
 from auto_media_publisher.config.conf_base import FACE_SWAPPER_MODEL_PATH
-
+from auto_media_publisher.utils.media_input_output import VideoReader,VideoWriter
+from torch.utils.data import DataLoader
+from torchvision import transforms
 
 class FaceSwapWorker:
     def __init__(self, enable_enhancer:bool = False,enable_nsfw_predictor:bool=False):
@@ -25,6 +27,7 @@
         self.main_analyser = FaceAnalyser()
         self.enable_enhancer = enable_enhancer
         self.enable_nsfw_predictor = enable_nsfw_predictor
+        self.batch_size = 4
         if self.enable_nsfw_predictor:
             self.nsfw_predictor = NSFWPredictor()
         self._init()
@@ -48,56 +51,46 @@
             return 'mps'
         else:
             return 'cpu'
+    
+    def auto_downsample_ratio(self,h, w):
+        """
+        Automatically find a downsample ratio so that the largest side of the resolution be 512px.
+        """
+        return min(512 / max(h, w), 1)
 
     def _decode_video(self):
-        # === Step 1: 鐢� ffprobe 鑾峰彇瑙嗛淇℃伅 ===
-        probe_cmd = [
-            "ffprobe",
-            "-v", "error",
-            "-select_streams", "v:0",
-            "-show_entries", "stream=width,height,r_frame_rate",
-            "-of", "json",
-            self.input_video_path
-        ]
-        probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
-        probe_data = json.loads(probe_result.stdout)
-        video_stream = probe_data['streams'][0]
-        self.width = int(video_stream['width'])
-        self.height = int(video_stream['height'])
-        self.fps = eval(video_stream['r_frame_rate'])  # e.g. "30/1"
+        transform = transforms.ToTensor()
+        source = VideoReader(self.input_video_path, transform)
+       
+        reader = DataLoader(source, batch_size=self.batch_size, pin_memory=True, num_workers=self.num_processors)
 
-        print(f"Video info: {self.width}x{self.height} @ {self.fps}fps")
+        writer = VideoWriter(
+                path=self.output_video_path,
+                frame_rate=30,
+                bit_rate=int(1 * 1000000))
+        
+        for src in reader:
 
-        # === Step 2: 鐢� ffmpeg 瑙g爜杈撳嚭 rawvideo 鍒� stdout pipe ===
-        decode_cmd = [
-            "ffmpeg",
-            "-loglevel", "info",
-            "-i", self.input_video_path,
-            "-f", "rawvideo",
-            "-pix_fmt", "rgb24",
-            "pipe:1"
-        ]
-        process = subprocess.Popen(decode_cmd, stdout=subprocess.PIPE, bufsize=10**8)
+                if downsample_ratio is None:
+                    downsample_ratio = auto_downsample_ratio(*src.shape[2:])
 
-        frame_size = self.width * self.height * 3
-        idx = 0
-        while True:
-            in_bytes = process.stdout.read(frame_size)
-            if not in_bytes:
-                break
-            frame = np.frombuffer(in_bytes, dtype=np.uint8).reshape((self.height, self.width, 3))
-            self.frame_queue.put((idx, frame))  # 鏀逛负 tuple 褰㈠紡
-            idx += 1
+                src = src.to(device, dtype, non_blocking=True).unsqueeze(0) # [B, T, C, H, W]
+                fgr, pha, *rec = model(src, *rec, downsample_ratio)
 
-        self.total_frames = idx
-        print(f"_decode_video Total frames: {self.total_frames}")
+                if output_foreground is not None:
+                    writer_fgr.write(fgr[0])
+                if output_alpha is not None:
+                    writer_pha.write(pha[0])
+                if output_composition is not None:
+                    if output_type == 'video':
+                        com = fgr * pha + bgr * (1 - pha)
+                    else:
+                        fgr = fgr * pha.gt(0)
+                        com = torch.cat([fgr, pha], dim=-3)
+                    writer_com.write(com[0])
+        
 
-        process.stdout.close()
-        process.wait()
 
-        # 閫氱煡澶勭悊绾跨▼缁撴潫
-        for _ in range(self.num_processors):
-            self.frame_queue.put(None)
 
     def _process_frame(self):
         thread_face_analyser = FaceAnalyser()
diff --git a/auto_media_publisher/utils/face_swap/face_swaper_ffmpeg_stream.py b/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg_stream.py
similarity index 99%
rename from auto_media_publisher/utils/face_swap/face_swaper_ffmpeg_stream.py
rename to auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg_stream.py
index 5966fe3..0f2452d 100644
--- a/auto_media_publisher/utils/face_swap/face_swaper_ffmpeg_stream.py
+++ b/auto_media_publisher/modifiers/face_swap/face_swaper_ffmpeg_stream.py
@@ -8,7 +8,6 @@
 import torch
 import json
 import insightface
-from insightface.app.common import Face
 from .face_nsfw_predictor import NSFWPredictor
 from .face_enhancer import FaceEnhancer
 from .face_analyser import FaceAnalyser
@@ -41,7 +40,6 @@
         self.stop_event = threading.Event()
 
     def _select_device(self):
-        return 'cpu'
         if torch.cuda.is_available():
             return 'cuda'
         elif torch.backends.mps.is_available():
diff --git a/auto_media_publisher/modifiers/face_swap/typing.py b/auto_media_publisher/modifiers/face_swap/typing.py
new file mode 100644
index 0000000..bc8d6fc
--- /dev/null
+++ b/auto_media_publisher/modifiers/face_swap/typing.py
@@ -0,0 +1,7 @@
+
+from typing import Any
+from insightface.app.common import Face
+import numpy
+
+Face = Face
+Frame = numpy.ndarray[Any, Any]
\ No newline at end of file
diff --git a/auto_media_publisher/utils/face_swap/face_swaper_cv2.py b/auto_media_publisher/utils/face_swap/face_swaper_cv2.py
deleted file mode 100644
index 1e2dc41..0000000
--- a/auto_media_publisher/utils/face_swap/face_swaper_cv2.py
+++ /dev/null
@@ -1,138 +0,0 @@
-import cv2
-import numpy as np
-import insightface
-from insightface.app import FaceAnalysis
-from insightface.app.common import Face
-import threading
-from gfpgan.utils import GFPGANer
-from typing import Any
-
-Frame = np.ndarray
-
-
-class FaceProcessor:
-    """
-    璋冪敤cv鐨勬ā鍧楀垎瑙p4锛岀粍瑁卪p4锛岃棰戣川閲忓樊 
-    """
-    def __init__(self, face_enhancer_model: str = 'GFPGANv1.4.pth', face_swapper_model: str = 'inswapper_128.onnx', device: str = 'cuda'):
-        self.FACE_ENHANCER = None
-        self.FACE_SWAPPER = None
-        self.THREAD_SEMAPHORE = threading.Semaphore()
-        self.THREAD_LOCK = threading.Lock()
-
-        # Set device
-        self.device = device  # 'cuda' for GPU, 'cpu' for CPU
-
-        # Load models
-        self.face_enhancer_model = face_enhancer_model
-        self.face_swapper_model = face_swapper_model
-
-        # Initialize face analysis app
-        self.app = FaceAnalysis(providers=['CUDAExecutionProvider' if device == 'cuda' else 'CPUExecutionProvider'])
-        self.app.prepare(ctx_id=0, det_size=(640, 640))
-
-    def get_face_enhancer(self) -> GFPGANer:
-        with self.THREAD_LOCK:
-            if self.FACE_ENHANCER is None:
-                self.FACE_ENHANCER = GFPGANer(
-                    model_path=self.face_enhancer_model, upscale=1, device=self.device)
-        return self.FACE_ENHANCER
-
-    def get_face_swapper(self) -> Any:
-        with self.THREAD_LOCK:
-            if self.FACE_SWAPPER is None:
-                self.FACE_SWAPPER = insightface.model_zoo.get_model(
-                    self.face_swapper_model, providers=['CUDAExecutionProvider' if self.device == 'cuda' else 'CPUExecutionProvider'])
-        return self.FACE_SWAPPER
-
-    def enhance_face(self, target_face: Face, temp_frame: Frame) -> Frame:
-        start_x, start_y, end_x, end_y = map(int, target_face['bbox'])
-        padding_x = int((end_x - start_x) * 0.5)
-        padding_y = int((end_y - start_y) * 0.5)
-        start_x = max(0, start_x - padding_x)
-        start_y = max(0, start_y - padding_y)
-        end_x = max(0, end_x + padding_x)
-        end_y = max(0, end_y + padding_y)
-        temp_face = temp_frame[start_y:end_y, start_x:end_x]
-        if temp_face.size:
-            with self.THREAD_SEMAPHORE:
-                _, _, temp_face = self.get_face_enhancer().enhance(
-                    temp_face,
-                    paste_back=True
-                )
-            temp_frame[start_y:end_y, start_x:end_x] = temp_face
-        return temp_frame
-
-    def swap_face(self, source_face: Face, target_face: Face, temp_frame: Frame) -> Frame:
-        return self.get_face_swapper().get(temp_frame, target_face, source_face, paste_back=True)
-
-    def process_video(self, src_mp4: str, image_path: str, dst_mp4: str) -> None:
-        # Read the target image (source face)
-        target_image = cv2.imread(image_path)
-        target_faces = self.app.get(target_image)
-        if len(target_faces) == 0:
-            print("No face detected in the target image.")
-            return
-        target_face = target_faces[0]
-
-        # Open the source video
-        cap = cv2.VideoCapture(src_mp4)
-        if not cap.isOpened():
-            print("Error opening video stream or file")
-            return
-
-        # Get video properties
-        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
-        fps = cap.get(cv2.CAP_PROP_FPS)
-        frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
-        frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
-
-        # Create VideoWriter object to save the output video
-        out = cv2.VideoWriter(dst_mp4, fourcc, fps, (frame_width, frame_height))
-
-        # Get the total frame count
-        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
-
-        # Process each frame of the video
-        frame_count = 0
-        while cap.isOpened():
-            ret, frame = cap.read()
-            if not ret:
-                break
-
-            # Detect faces in the current frame
-            faces = self.app.get(frame)
-            if len(faces) > 0:
-                # Swap face from the target image to the current frame
-                swapped_face_frame = self.swap_face(target_face, faces[0], frame)
-                self.enhance_face(faces[0], swapped_face_frame)
-                out.write(swapped_face_frame)
-            else:
-                # If no face detected, just write the original frame
-                out.write(frame)
-
-            # Increment frame count
-            frame_count += 1
-
-            # Calculate and print progress every 5%
-            progress = (frame_count / total_frames) * 100
-            if progress >= 5 * (frame_count // (total_frames // 20)):
-                print(f"Processing: {int(progress)}% completed")
-
-        cap.release()
-        out.release()
-        print(f"Video processing complete. Output saved to {dst_mp4}")
-
-
-# Usage example:
-
-# Initialize the FaceProcessor (use 'cuda' for GPU)
-face_processor = FaceProcessor(device='cpu')
-
-# Process the video by swapping faces from the target image
-# face_processor.process_video('g1.mp4', 'from-face/girl-1.png', './g1-demo_output.mp4')
-# face_processor.process_video('g1.mp4', 'from-face/girl-2.png', './g2-demo_output.mp4')
-face_processor.process_video('g3.mp4', 'lilizhen.jpg', './lilizhen_output.mp4')
-face_processor.process_video('g3.mp4', 'from-face/girl-1.png', './g1-demo_output.mp4')
-face_processor.process_video('g3.mp4', 'from-face/girl-2.png', './g2-demo_output.mp4')
-
diff --git a/auto_media_publisher/utils/face_swap/typing.py b/auto_media_publisher/utils/face_swap/typing.py
deleted file mode 100644
index 30791ef..0000000
--- a/auto_media_publisher/utils/face_swap/typing.py
+++ /dev/null
@@ -1,17 +0,0 @@
-
-from typing import Any
-from insightface.app.common import Face
-import numpy
-
-Face = Face
-Frame = numpy.ndarray[Any, Any]
-'''
-from typing import Any
-import numpy
-import numpy.typing as npt  # Import numpy.typing for NDArray
-
-from insightface.app.common import Face
-
-Face = Face
-Frame = npt.NDArray[numpy.uint8]  # Specify the dtype, e.g., uint8 for image frames
-'''
\ No newline at end of file
diff --git a/auto_media_publisher/utils/media_input_output.py b/auto_media_publisher/utils/media_input_output.py
new file mode 100644
index 0000000..4e2e209
--- /dev/null
+++ b/auto_media_publisher/utils/media_input_output.py
@@ -0,0 +1,93 @@
+import av
+import os
+import pims
+import numpy as np
+from torch.utils.data import Dataset
+from torchvision.transforms.functional import to_pil_image
+from PIL import Image
+
+
+class VideoReader(Dataset):
+    def __init__(self, path, transform=None):
+        self.video = pims.PyAVVideoReader(path)
+        self.rate = self.video.frame_rate
+        self.transform = transform
+        
+    @property
+    def frame_rate(self):
+        return self.rate
+        
+    def __len__(self):
+        return len(self.video)
+        
+    def __getitem__(self, idx):
+        frame = self.video[idx]
+        frame = Image.fromarray(np.asarray(frame))
+        if self.transform is not None:
+            frame = self.transform(frame)
+        return frame
+
+
+class VideoWriter:
+    def __init__(self, path, frame_rate, bit_rate=1000000):
+        self.container = av.open(path, mode='w')
+        self.stream = self.container.add_stream('h264', rate=round(frame_rate))
+        self.stream.pix_fmt = 'yuv420p'
+        self.stream.bit_rate = bit_rate
+        self.stream.codec_context.options = {
+            'crf': '18',  # 鎺у埗鐢昏川锛岃秺灏忚秺娓呮櫚锛�18鏄珮璐ㄩ噺锛�
+            'preset': 'veryfast',     # 褰卞搷缂栫爜閫熷害锛宮edium 鏄�1x锛宖ast鏄�2x faster鏄�3x veryfast鏄�4x superfast鏄�6x ultrafast鏄�10x锛屾枃浠朵綋绉ぇ绾︿細澧炲姞(浠edium涓哄熀鍑�) 10%锛�15%锛�20% 30% 50%
+            'tune': 'film',
+        }
+    
+    def write(self, frames):
+        # frames: [T, C, H, W]
+        self.stream.width = frames.size(3)
+        self.stream.height = frames.size(2)
+        if frames.size(1) == 1:
+            frames = frames.repeat(1, 3, 1, 1) # convert grayscale to RGB
+        frames = frames.mul(255).byte().cpu().permute(0, 2, 3, 1).numpy()
+        for t in range(frames.shape[0]):
+            frame = frames[t]
+            frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
+            self.container.mux(self.stream.encode(frame))
+                
+    def close(self):
+        self.container.mux(self.stream.encode())
+        self.container.close()
+
+
+class ImageSequenceReader(Dataset):
+    def __init__(self, path, transform=None):
+        self.path = path
+        self.files = sorted(os.listdir(path))
+        self.transform = transform
+        
+    def __len__(self):
+        return len(self.files)
+    
+    def __getitem__(self, idx):
+        with Image.open(os.path.join(self.path, self.files[idx])) as img:
+            img.load()
+        if self.transform is not None:
+            return self.transform(img)
+        return img
+
+
+class ImageSequenceWriter:
+    def __init__(self, path, extension='jpg'):
+        self.path = path
+        self.extension = extension
+        self.counter = 0
+        os.makedirs(path, exist_ok=True)
+    
+    def write(self, frames):
+        # frames: [T, C, H, W]
+        for t in range(frames.shape[0]):
+            to_pil_image(frames[t]).save(os.path.join(
+                self.path, str(self.counter).zfill(4) + '.' + self.extension))
+            self.counter += 1
+            
+    def close(self):
+        pass
+        
\ No newline at end of file
diff --git a/data/auto_media_publisher.sqlite3 b/data/auto_media_publisher.sqlite3
index 2a3b573..05e7791 100644
--- a/data/auto_media_publisher.sqlite3
+++ b/data/auto_media_publisher.sqlite3
Binary files differ

--
Gitblit v1.9.1