Adnan commited on
Commit
9380604
·
verified ·
1 Parent(s): 8fff660

Create frame_interpolator.py

Browse files
Files changed (1) hide show
  1. frame_interpolator.py +157 -0
frame_interpolator.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TimeLapseForge — Frame Interpolation Module
3
+ Creates smooth intermediate frames between panels using
4
+ alpha blending and optical flow warping.
5
+ """
6
+
7
+ import cv2
8
+ import numpy as np
9
+ from PIL import Image
10
+ from typing import List, Optional, Callable
11
+
12
+
13
+ class FrameInterpolator:
14
+ """Generate intermediate frames between timelapse panels for smooth video."""
15
+
16
+ @staticmethod
17
+ def pil_to_cv2(img: Image.Image) -> np.ndarray:
18
+ """Convert PIL Image to OpenCV BGR format."""
19
+ rgb = np.array(img)
20
+ return cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
21
+
22
+ @staticmethod
23
+ def cv2_to_pil(img: np.ndarray) -> Image.Image:
24
+ """Convert OpenCV BGR image to PIL Image."""
25
+ rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
26
+ return Image.fromarray(rgb)
27
+
28
+ def blend_interpolate(
29
+ self, img1: Image.Image, img2: Image.Image, num_frames: int = 4
30
+ ) -> List[Image.Image]:
31
+ """
32
+ Simple alpha blending between two frames.
33
+ Fast and reliable, good for most cases.
34
+ """
35
+ arr1 = np.array(img1).astype(np.float32)
36
+ arr2 = np.array(img2).astype(np.float32)
37
+
38
+ frames = []
39
+ for i in range(1, num_frames + 1):
40
+ alpha = i / (num_frames + 1)
41
+ blended = ((1 - alpha) * arr1 + alpha * arr2).astype(np.uint8)
42
+ frames.append(Image.fromarray(blended))
43
+
44
+ return frames
45
+
46
+ def flow_interpolate(
47
+ self, img1: Image.Image, img2: Image.Image, num_frames: int = 4
48
+ ) -> List[Image.Image]:
49
+ """
50
+ Optical flow based interpolation.
51
+ Better quality than blending — objects move smoothly instead of fading.
52
+ """
53
+ cv_img1 = self.pil_to_cv2(img1)
54
+ cv_img2 = self.pil_to_cv2(img2)
55
+
56
+ gray1 = cv2.cvtColor(cv_img1, cv2.COLOR_BGR2GRAY)
57
+ gray2 = cv2.cvtColor(cv_img2, cv2.COLOR_BGR2GRAY)
58
+
59
+ # Calculate optical flow (Farneback method)
60
+ try:
61
+ flow = cv2.calcOpticalFlowFarneback(
62
+ gray1, gray2, None,
63
+ pyr_scale=0.5, levels=3, winsize=15,
64
+ iterations=3, poly_n=5, poly_sigma=1.2, flags=0
65
+ )
66
+ except Exception:
67
+ # Fallback to blend if flow calculation fails
68
+ return self.blend_interpolate(img1, img2, num_frames)
69
+
70
+ h, w = cv_img1.shape[:2]
71
+ frames = []
72
+
73
+ for i in range(1, num_frames + 1):
74
+ alpha = i / (num_frames + 1)
75
+
76
+ # Warp img1 towards img2 using scaled flow
77
+ flow_scaled = flow * alpha
78
+ map_x = np.float32(np.tile(np.arange(w), (h, 1)) + flow_scaled[..., 0])
79
+ map_y = np.float32(np.tile(np.arange(h).reshape(-1, 1), (1, w)) + flow_scaled[..., 1])
80
+
81
+ warped_1 = cv2.remap(cv_img1, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
82
+
83
+ # Also warp img2 backwards
84
+ flow_back = flow * (alpha - 1)
85
+ map_x_b = np.float32(np.tile(np.arange(w), (h, 1)) + flow_back[..., 0])
86
+ map_y_b = np.float32(np.tile(np.arange(h).reshape(-1, 1), (1, w)) + flow_back[..., 1])
87
+
88
+ warped_2 = cv2.remap(cv_img2, map_x_b, map_y_b, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
89
+
90
+ # Blend the two warped images
91
+ blended = cv2.addWeighted(warped_1, 1 - alpha, warped_2, alpha, 0)
92
+ frames.append(self.cv2_to_pil(blended))
93
+
94
+ return frames
95
+
96
+ def interpolate_sequence(
97
+ self,
98
+ images: List[Image.Image],
99
+ multiplier: int = 4,
100
+ method: str = "blend",
101
+ progress_callback: Optional[Callable] = None,
102
+ ) -> List[Image.Image]:
103
+ """
104
+ Interpolate an entire sequence of images.
105
+
106
+ Args:
107
+ images: List of panel images
108
+ multiplier: How many intermediate frames between each panel pair
109
+ method: "blend" for alpha blending, "flow" for optical flow
110
+ progress_callback: Optional progress callback(current, total)
111
+
112
+ Returns:
113
+ Expanded list of frames with intermediate frames inserted
114
+ """
115
+ if len(images) < 2:
116
+ return images
117
+
118
+ interpolate_fn = self.flow_interpolate if method == "flow" else self.blend_interpolate
119
+ result = [images[0]]
120
+ total_pairs = len(images) - 1
121
+
122
+ for i in range(total_pairs):
123
+ intermediate = interpolate_fn(images[i], images[i + 1], num_frames=multiplier)
124
+ result.extend(intermediate)
125
+ result.append(images[i + 1])
126
+
127
+ if progress_callback:
128
+ progress_callback(i + 1, total_pairs)
129
+
130
+ return result
131
+
132
+ def create_morph_transition(
133
+ self, img1: Image.Image, img2: Image.Image, num_frames: int = 8
134
+ ) -> List[Image.Image]:
135
+ """
136
+ Create a morphing transition effect (blend + subtle zoom).
137
+ Good for dramatic reveal moments.
138
+ """
139
+ frames = []
140
+ arr1 = np.array(img1).astype(np.float32)
141
+ arr2 = np.array(img2).astype(np.float32)
142
+ h, w = arr1.shape[:2]
143
+
144
+ for i in range(1, num_frames + 1):
145
+ alpha = i / (num_frames + 1)
146
+
147
+ # Subtle zoom effect (1.0 to 1.02 scale)
148
+ scale = 1.0 + 0.02 * alpha
149
+ center_x, center_y = w // 2, h // 2
150
+
151
+ M = cv2.getRotationMatrix2D((center_x, center_y), 0, scale)
152
+ zoomed_1 = cv2.warpAffine(arr1.astype(np.uint8), M, (w, h), borderMode=cv2.BORDER_REFLECT)
153
+
154
+ blended = ((1 - alpha) * zoomed_1.astype(np.float32) + alpha * arr2).astype(np.uint8)
155
+ frames.append(Image.fromarray(blended))
156
+
157
+ return frames