import cv2 import numpy as np ''' List of functions to create transition between two images similar to animations available in Microsoft PowerPoint. ''' def prepareImages(img1, img2, target_size=None, fill_color=(0, 0, 0)): ''' Resize and pad images so they have the same dimensions. If target_size is provided (width, height), force to that size. Otherwise scale both to the max width and height among them, preserving aspect ratio and padding. ''' def resizeAndPad(img, size, fill_color): h, w = img.shape[:2] scale = min(size[0] / w, size[1] / h) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA) pad_w = size[0] - new_w pad_h = size[1] - new_h top = pad_h // 2 bottom = pad_h - top left = pad_w // 2 right = pad_w - left return cv2.copyMakeBorder(resized, top, bottom, left, right, borderType=cv2.BORDER_CONSTANT, value=fill_color) h1, w1 = img1.shape[:2] h2, w2 = img2.shape[:2] if target_size: size = target_size else: size = (max(w1, w2), max(h1, h2)) img1_ready = resizeAndPad(img1, size, fill_color) img2_ready = resizeAndPad(img2, size, fill_color) return img1_ready, img2_ready def fadeTransition(img1, img2, num_frames=30): ''' Cross-fade from img1 to img2. ''' frames = [] for i in range(num_frames + 1): alpha = i / num_frames frame = cv2.addWeighted(img1, 1 - alpha, img2, alpha, 0) frames.append(frame) return frames def wipeTransition(img1, img2, direction='left_to_right', num_frames=30): ''' Wipe from one side. ''' h, w = img1.shape[:2] frames = [] for i in range(num_frames + 1): frac = i / num_frames if direction == 'left_to_right': cutoff = int(w * frac) frame = np.hstack([img2[:, :cutoff], img1[:, cutoff:]]) elif direction == 'right_to_left': cutoff = int(w * frac) frame = np.hstack([img1[:, :-cutoff] if cutoff else img1, img2[:, -cutoff:] if cutoff else img1]) elif direction == 'top_to_bottom': cutoff = int(h * frac) frame = np.vstack([img2[:cutoff, :], img1[cutoff:, :]]) elif direction == 'bottom_to_top': cutoff = int(h * frac) frame = np.vstack([img1[:-cutoff, :] if cutoff else img1, img2[-cutoff:, :] if cutoff else img1]) else: raise ValueError("Invalid direction") frames.append(frame) return frames def slideTransition(img1, img2, direction='left_to_right', num_frames=30): ''' Push slide: img1 moves out while img2 moves in. ''' h, w = img1.shape[:2] frames = [] for i in range(num_frames + 1): dx = int(w * i / num_frames) if direction == 'left_to_right': off1 = -dx off2 = w - dx frame = np.zeros_like(img1) frame[:, max(0, off1):max(0, off1) + w] = img1[:, max(0, -off1):min(w, w - off1)] frame[:, max(0, off2):max(0, off2) + w] = img2[:, max(0, -off2):min(w, w - off2)] elif direction == 'right_to_left': off1 = dx off2 = - (w - dx) frame = np.zeros_like(img1) frame[:, off1:off1 + w] = img1[:, :w - off1] frame[:, max(0, off2):max(0, off2) + w] = img2[:, max(0, -off2):min(w, w - off2)] elif direction == 'top_to_bottom': dy = int(h * i / num_frames) frame = np.zeros_like(img1) frame[max(0, -dy):max(0, -dy) + h, :] = img1[max(0, dy):min(h, h + dy), :] frame[max(0, h - dy):max(0, h - dy) + h, :] = img2[max(0, - (h - dy)):h, :] elif direction == 'bottom_to_top': dy = int(h * i / num_frames) frame = np.zeros_like(img1) frame[dy:dy + h, :] = img1[:h - dy, :] frame[max(0, dy - h):dy, :] = img2[:h - (dy - h) if dy > h else 0:, :] else: raise ValueError("Invalid direction") frames.append(frame) return frames def splitTransition(img1, img2, orientation='vertical', num_frames=30): ''' Split either from center outward (outward) or inward (inward). ''' h, w = img1.shape[:2] frames = [] for i in range(num_frames + 1): frac = i / num_frames if orientation == 'vertical': cutoff = int((w / 2) * frac) left = img1[:, :w // 2 - cutoff] right = img1[:, w // 2 + cutoff:] center = img2[:, w // 2 - cutoff:w // 2 + cutoff] frame = np.hstack([left, center, right]) elif orientation == 'horizontal': cutoff = int((h / 2) * frac) top = img1[:h // 2 - cutoff, :] bottom = img1[h // 2 + cutoff:, :] center = img2[h // 2 - cutoff:h // 2 + cutoff, :] frame = np.vstack([top, center, bottom]) else: raise ValueError("Invalid orientation") frames.append(frame) return frames def peekTransition(img1, img2, direction='left', num_frames=30): ''' Peek‑through from one side (like 'reveal' in PowerPoint). ''' h, w = img1.shape[:2] frames = [] for i in range(num_frames + 1): frac = i / num_frames if direction == 'left': cutoff = int(w * frac) frame = img1.copy() frame[:, :cutoff] = img2[:, :cutoff] elif direction == 'right': cutoff = int(w * frac) frame = img1.copy() frame[:, w - cutoff:] = img2[:, w - cutoff:] elif direction == 'top': cutoff = int(h * frac) frame = img1.copy() frame[:cutoff, :] = img2[:cutoff, :] elif direction == 'bottom': cutoff = int(h * frac) frame = img1.copy() frame[h - cutoff:, :] = img2[h - cutoff:, :] else: raise ValueError("Invalid direction") frames.append(frame) return frames def zoomTransition(img1, img2, num_frames=30, zoom_in=True): ''' Zoom into/out of the center and swap images mid‑zoom. ''' h, w = img1.shape[:2] frames = [] for i in range(num_frames + 1): frac = i / num_frames if zoom_in: scale = 1 - frac img = img1 if frac < 0.5 else img2 local_frac = frac if frac < 0.5 else frac - 0.5 scale = 1 - local_frac * 2 else: scale = frac img = img2 if frac > 0.5 else img1 local_frac = frac if frac <= 0.5 else frac - 0.5 scale = local_frac * 2 new_w = max(1, int(w * scale)) new_h = max(1, int(h * scale)) resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) frame = np.zeros_like(img1) x = (w - new_w) // 2 y = (h - new_h) // 2 frame[y:y+new_h, x:x+new_w] = resized frames.append(frame) return frames def checkerTransition(img1, img2, num_frames=30, rows=10, cols=10): ''' Checker‑drop transition, flipping squares from img1 to img2. ''' h, w = img1.shape[:2] block_h = h // rows block_w = w // cols frames = [] idx_list = [(r, c) for r in range(rows) for c in range(cols)] total = len(idx_list) for i in range(num_frames + 1): count = int(total * (i / num_frames)) chosen = idx_list[:count] frame = img1.copy() for r, c in chosen: y0 = r * block_h x0 = c * block_w frame[y0:y0+block_h, x0:x0+block_w] = img2[y0:y0+block_h, x0:x0+block_w] frames.append(frame) return frames def checkerboardTransition(img1, img2, num_frames=30, rows=10, cols=10, mode='across'): h, w = img1.shape[:2] block_h = h // rows block_w = w // cols frames = [] idx_list = [(r, c) for r in range(rows) for c in range(cols)] if mode == 'down': idx_list = sorted(idx_list, key=lambda x: x[0]*cols + x[1]) else: idx_list = sorted(idx_list, key=lambda x: x[1]*rows + x[0]) total = len(idx_list) for i in range(num_frames + 1): count = int(total * (i / num_frames)) chosen = idx_list[:count] frame = img1.copy() for r, c in chosen: y0, x0 = r * block_h, c * block_w frame[y0:y0+block_h, x0:x0+block_w] = img2[y0:y0+block_h, x0:x0+block_w] frames.append(frame) return frames def applyTransition(img1, img2, transition_name='fade', **kwargs): ''' Apply a named transition between img1 and img2. Supported names and kwargs: - 'fade' - 'wipe_left_to_right', 'wipe_right_to_left', 'wipe_top_to_bottom', 'wipe_bottom_to_top' - 'slide_left_to_right', ... - 'split_vertical', 'split_horizontal' - 'peek_left', 'peek_right', 'peek_top', 'peek_bottom' - 'zoom_in', 'zoom_out' - 'checker' Common kwargs: - num_frames (default 30) - rows, cols (for checker) ''' img1_ready, img2_ready = prepareImages(img1, img2, target_size=kwargs.get('target_size'), fill_color=kwargs.get('fill_color', (0,0,0))) name = transition_name.lower() nf = kwargs.get('num_frames', 30) if name == 'fade': return fadeTransition(img1_ready, img2_ready, num_frames=nf) if name.startswith('wipe_'): dirn = name.replace('wipe_', '') return wipeTransition(img1_ready, img2_ready, direction=dirn, num_frames=nf) if name.startswith('slide_'): dirn = name.replace('slide_', '') return slideTransition(img1_ready, img2_ready, direction=dirn, num_frames=nf) if name.startswith('split_'): orient = name.replace('split_', '') return splitTransition(img1_ready, img2_ready, orientation=orient, num_frames=nf) if name.startswith('peek_'): dirn = name.replace('peek_', '') return peekTransition(img1_ready, img2_ready, direction=dirn, num_frames=nf) if name == 'zoom_in': return zoomTransition(img1_ready, img2_ready, num_frames=nf, zoom_in=True) if name == 'zoom_out': return zoomTransition(img1_ready, img2_ready, num_frames=nf, zoom_in=False) if name == 'checker': return checkerTransition(img1_ready, img2_ready, num_frames=nf, rows=kwargs.get('rows', 10), cols=kwargs.get('cols', 10)) if name == 'checkerboard_across': return checkerboard_transition(img1_ready, img2_ready, num_frames=nf, rows=kwargs.get('rows',10), cols=kwargs.get('cols',10), mode='across') if name == 'checkerboard_down': return checkerboard_transition(img1_ready, img2_ready, num_frames=nf, rows=kwargs.get('rows', 10), cols=kwargs.get('cols', 10), mode='down') raise ValueError(f"Unsupported transition: {transition_name}") if __name__ == "__main__": img1 = cv2.imread("image-1.png") img2 = cv2.imread("image-2.png") ''' if img1 is None or img2 is None: print("Images could not be found or read. Exiting!") exit() ''' transition_name="split_vertical" frames = applyTransition(img1, img2, transition_name, num_frames=60) # Save the video h, w = frames[0].shape[:2] out = cv2.VideoWriter("pptTransition.mp4", cv2.VideoWriter_fourcc(*"mp4v"), 30, (w, h)) for f in frames: out.write(f) out.release() print(f"PPT like Transition '{transition_name}' Saved.")