Create a Moving Photo Slideshow with Weighted Transitions in OpenCV

What will we cover in this tutorial?

In this tutorial you will learn how to make a slideshow of your favorite photos moving across the screen with weighted transitions. This will be done in Python with OpenCV.

See the result in the video below.

Step 1: A simple approach without moving effect

If you want to build something great, start with something simple first. The reason for that is that you will learn along the way. It is difficult to understand all aspects from the beginning.

Start small. Start simple. Learn from each step.

Here we assume that you have all your favorite photos in a folder called pics. In the first run, you just want to show them on your screen one-by-one.

import cv2
import glob
import os

def process():
    path = "pics"
    filenames = glob.glob(os.path.join(path, "*"))

    for filename in filenames:
        print(filename)
        img = cv2.imread(filename)

        cv2.imshow("Slideshow", img)

        if cv2.waitKey(1000) == ord('q'):
            return


process()

As you will realize, this will show the photos in the size they stored. Hence, when photos change, the dimensions of the window will change as well (unless the two consecutive photos have the exact same dimensions). This will not make a good user experience. Also, if the photos dimensions are larger than your screen resolution, they will not be fully visible.

Step 2: Scaling images to fit inside the screen

We want the window size where we show the photos to have fixed size. This is not as simple as it sounds.

Image a photo has dimensions 1000 x 2000 pixels. Then the next one has 2000 x 1000. How would you scale it down? If you scale it down to 500 x 500 by default, then the objects in the images will be flatten or narrowed together.

Hence, what we do in our first attempt, is to scale it based on the dimensions. That is, a photo with dimensions 1000 x 2000 will become 500 x 1000. And a photo of dimensions 2000 x 1000 will become 1000 x 500. Then we will crop it to fit the 500 x 500 dimension. The cropping will take the middle of the photo.

import cv2
import glob
import os

def process():
    path = "pics"
    filenames = glob.glob(os.path.join(path, "*"))

    for filename in filenames:
        print(filename)
        img = cv2.imread(filename)

        height, width, _ = img.shape
        if width < height:
            height = int(height*500/width)
            width = 500
            img = cv2.resize(img, (width, height))
            shift = height - 500
            img = img[shift//2:-shift//2,:,:]
        else:
            width = int(width*500/height)
            height = 500
            shift = width - 500
            img = cv2.resize(img, (width, height))
            img = img[:,shift//2:-shift//2,:]

        cv2.imshow("Slideshow", img)

        if cv2.waitKey(1000) == ord('q'):
            return

process()

This gives a better experience, but not perfect.

Step 3: Make a weighted transition between image switches

To make a weighted transition we make it by adding a transition phase of the photos.

import cv2
import glob
import os
import numpy as np

def process():
    path = "pics"
    filenames = glob.glob(os.path.join(path, "*"))

    prev_image = np.zeros((500, 500, 3), np.uint8)
    for filename in filenames:
        print(filename)
        img = cv2.imread(filename)

        height, width, _ = img.shape
        if width < height:
            height = int(height*500/width)
            width = 500
            img = cv2.resize(img, (width, height))
            shift = height - 500
            img = img[shift//2:-shift//2,:,:]
        else:
            width = int(width*500/height)
            height = 500
            shift = width - 500
            img = cv2.resize(img, (width, height))
            img = img[:,shift//2:-shift//2,:]

        for i in range(101):
            alpha = i/100
            beta = 1.0 - alpha
            dst = cv2.addWeighted(img, alpha, prev_image, beta, 0.0)

            cv2.imshow("Slideshow", dst)
            if cv2.waitKey(1) == ord('q'):
                return

        prev_image = img

        if cv2.waitKey(1000) == ord('q'):
            return

process()

Notice the prev_image variable that is needed. It is set to a black image when it enters the loop. The transition is made by using cv2.addWeighted(…) to get the effect.

Step 4: Make the photos move while showing

The idea is to let the photo move. Say, if the photo is scaled to dimension 500 x 1000. Then we want to create a view of that photo of size 500 x 500 that slides from one end to the other while it is showing.

This requires that we have a state for the photo, which stores where we are in of the current view.

For this purpose we create a class to represent a photo that keeps the current view. It also includes the resizing.

import cv2
import numpy as np
import glob
import os
import random


class Image:
    def __init__(self, filename, time=500, size=500):
        self.size = size
        self.time = time
        self.shifted = 0.0
        self.img = cv2.imread(filename)
        self.height, self.width, _ = self.img.shape
        if self.width < self.height:
            self.height = int(self.height*size/self.width)
            self.width = size
            self.img = cv2.resize(self.img, (self.width, self.height))
            self.shift = self.height - size
            self.shift_height = True
        else:
            self.width = int(self.width*size/self.height)
            self.height = size
            self.shift = self.width - size
            self.img = cv2.resize(self.img, (self.width, self.height))
            self.shift_height = False
        self.delta_shift = self.shift/self.time

    def reset(self):
        if random.randint(0, 1) == 0:
            self.shifted = 0.0
            self.delta_shift = abs(self.delta_shift)
        else:
            self.shifted = self.shift
            self.delta_shift = -abs(self.delta_shift)

    def get_frame(self):
        if self.shift_height:
            roi = self.img[int(self.shifted):int(self.shifted) + self.size, :, :]
        else:
            roi = self.img[:, int(self.shifted):int(self.shifted) + self.size, :]
        self.shifted += self.delta_shift
        if self.shifted > self.shift:
            self.shifted = self.shift
        if self.shifted < 0:
            self.shifted = 0
        return roi


def process():
    path = "pics"
    filenames = glob.glob(os.path.join(path, "*"))

    cnt = 0
    images = []
    for filename in filenames:
        print(filename)

        img = Image(filename)

        images.append(img)
        if cnt > 300:
            break
        cnt += 1

    prev_image = images[random.randrange(0, len(images))]
    prev_image.reset()

    while True:
        while True:
            img = images[random.randrange(0, len(images))]
            if img != prev_image:
                break
        img.reset()

        for i in range(100):
            alpha = i/100
            beta = 1.0 - alpha
            dst = cv2.addWeighted(img.get_frame(), alpha, prev_image.get_frame(), beta, 0.0)

            cv2.imshow("Slide", dst)
            if cv2.waitKey(1) == ord('q'):
                return

        prev_image = img
        for _ in range(300):
            cv2.imshow("Slide", img.get_frame())
            if cv2.waitKey(1) == ord('q'):
                return


process()

This results in a nice way where the photos slowly move through the view. It also has added some randomness. First of all, it takes a random photo. Also, the direction is set to be random.

This is a good start of having a nice slideshow of your favorite photos.

Leave a Reply