Re-creating (simply) the Take On Me Music Video

Overview

So, what's the endgoal here? Well, if you're anything like me, you may like 80's music. Even better, you may like 80's music videos. Well the "Take On Me" music video is one of my absolute favorites. The smooth morphing from full color to black/white was always super cool to me. As soon as I started working with OpenCV, Edge detectors, etc. I saw a small opportunity to put myself in to the music video, and that's what we are going to do today .

Now, this is going to be a pretty simple rendition to the great music video. We will just be using OpenCV to...:

  • color mask video frames to track a single object
  • find the edges in the above mask
  • overlay the mask on the original frame

In the end, you will have a gif and a video representation to show your friends.

Getting Started

If you're using poetry (like me), the most relevant entries in your toml will be:

imutils = "0.5.4"
opencv-python = "4.5.3.56"
imageio = "2.9.0"

If you're not (and you're using pip), you can pip install the below:

pip install opencv-python==4.5.3.56 imageio==2.9.0 imutils==0.5.4

Now, you're all set up.

The Code

So, lets run through the code...

The Setup

So, for the setup.

The first thing we have to do is import our required libraries.

# Import the required libraries
import cv2
import imutils
import imageio
import time

Next, we can set up our video streaming from our local webcam. Its also important that we set our output stream to have the same FPS, frame width, and frame height as our input stream.

# Create a video capture instance.
# VideoCapture(0) corresponds to your computers
# webcam
cap = cv2.VideoCapture(0)

# Lets grab the frames-per-second (FPS) of the
# webcam so our output has a similar FPS.
# Lets also grab the height and width so our
# output is the same size as the webcam
fps          = cap.get(cv2.CAP_PROP_FPS)
frame_width  = int(cap.get(3))
frame_height = int(cap.get(4))

Moving forward, we can initialize our outputs. The out object is a video writer. What that means is that as we write frames to it, it will save these frames to the file we designate. If we look at the object instantiation below, we are going to write the video stream to output.avi with MJPG encoding. And if you notice, it will have the same FPS and size as our webcam.

The frames list will be used to create our gif in the end.

Finally, we create a named window so we can see what our code is doing realtime!

# Now lets create the video writer. We will
# write our processed frames to this object
# to create the processed video.
out = cv2.VideoWriter('outpy.avi', 
    cv2.VideoWriter_fourcc('M','J','P','G'), 
    fps, 
    (frame_width,frame_height)
)

frames = []

cv2.namedWindow('Video')

As I previously noted, we are going to use simple color masking to create our video. In the below, we will use the lower end of that mask as pure black (0, 0, 0) and the upper end will be a dark grey (50, 50, 50).

blackLower = (0, 0, 0)
blackUpper = (50, 50, 50)

Helper Functions

This helper function is going to do some super fun stuff.

First, it will blur the frame we give it. It's usually important to blur your frames before applying filters to them. The decreased resolution will bring forth the more prominent features of the frame and reduce noise.

Second, we will apply the color mask to the frame.

Third, we will erode the mask. Erosion is another technique to reduce noise by calculating a local minimum over some kernel (in our case, a 5x5 kernel).

def blur_and_mask(frame, lower_color, upper_color):
    blurred = cv2.GaussianBlur(frame, (5, 5), 3)
    mask = cv2.inRange(blurred, blackLower, blackUpper)
    kernel = np.ones((5, 5), np.uint8)
    mask = cv2.erode(mask, kernel)
    return mask

Main Loop

Now that all of the setup is out of the way, we can start playing with the main loop!

The first portion of this loop might look easy to you. We:

  • Read a frame from our webcam (video stream defined above)
  • We use our fancy function above to retrieve a color mask of the frame
  • We then create a greyscale copy of the original frame (we will use this later!)
while(True):
    # Capture frame-by-frame
    ret, frame = cap.read()

    # Grayscale the image and apply a gaussian blur to it

    mask = blur_and_mask(frame, blackLower, blackUpper)
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

But, now its starting to get interesting! We use cv2.findContours to grab all of our contours from our image mask. The cv2.RETR_EXTERNAL tells us to only retrieve the extreme outer contours. The cv2.CHAIN_APPROX_SIMPLE tells openCV to compress all of the directional line segments (horizontal, vertical, diagonal) and leave only their end points.

Finally, we use imutils.grab_contours which just helps us differentiate which version of OpenCV is running and use the contours appropriately.

    cnts = cv2.findContours(
        mask.copy(), 
        cv2.RETR_EXTERNAL, 
        cv2.CHAIN_APPROX_SIMPLE
    )

    cnts = imutils.grab_contours(cnts)

Next is probably the most complex piece of code, but its not too bad once we break it down!

First, we retrieve the biggest contour from our frame. This will be object that had the most points matching the specified color range. We can then build a bounding rectangle around that contour.

    if len(cnts) > 0:
        c    = max(cnts, key=cv2.contourArea)
        rect = cv2.boundingRect(c)

        # This area is too small to be of our
        # interest, disregard it and go to the
        # next frame
        if rect[2] < 100 or rect[3] < 100: continue

The problem with the above is that cv2.boundingRect(c) doesn't actually draw anything on the frame for us. So, if we want to draw it on there (and we DO), we will have to do that ourselves. What cv2.boundingRect(c) does give us is a tuple of starting x-coordinate, starting y-coordinate, rectangle width, and rectangle height.

Coincidentally, this is exactly what we need to draw a box on the frame!

        # Unpack the bounding box
        x,y,w,h = rect
        y1 = y
        y2 = y + h
        x1 = x
        x2 = x + w
        # Draw the bounding box on the frame
        cv2.rectangle(
            frame,
            (x,y),
            (x+w,y+h),
            (0,255,0),
            2
        )

Up to this point, we have a frame with a box drawn on it. That's close, but not exactly what we were going for! Remember, we want the inside of that box to look like the "Take On Me" music video (where the color is subtracted).

To do that, we will need two things:

  • The greyscale frame from above
  • The bounding rectangle from above

The gray[y:y+h, x:x+w] will take the pixels from the greyscale image that fall within the bounding box. Always remember, images and frames are just matrices!

Once we have that area of interest, we can apply the Canny edge detection filter to it. This filter will give us a frame that is all black except for the detected edges, they will be white. The next action is to do a bitwise_not of the edge detected frame. That way, everything will be white except for the edges (they will be black).

Finally, we can overlay that new frame-segment on to our original frame, write it to our output stream, and bam... were done!

        # Take a canny edge detection of the newly drawn
        # rectangle from the original grey scale image.
        # This will perform edge detection in only the 
        # area of interect (the cv2 rectangle defined above).
        # This will make all the edges white and the
        # rest of the pixels black. So, we have to invert it
        # so the black becomes white and the 
        # white becomes black (to fit the AHA video)
        to_canny = cv2.GaussianBlur(
            gray[y:y+h, x:x+w],
            (5, 5),
            3
        )

        edges = cv2.bitwise_not(
            cv2.Canny(to_canny, 0, 50)
        )

        # Since the edges are only a 2-channel frame,
        # we can overlay it on to each channel in the 
        # original frame
        frame[y1:y2, x1:x2, 0] = edges
        frame[y1:y2, x1:x2, 1] = edges
        frame[y1:y2, x1:x2, 2] = edges

    out.write(frame)
    frames.append(frame)
    # Display the resulting frame
    cv2.imshow('Video',frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

Making a gif

So at this point, we have our music video and its written to disk. I also wanted to create a little gif of this to use as a thumbnail. Below is some code using imageio to do just that.

fp_out = "../../../static/assets/img/take-on-me.gif"
with imageio.get_writer(fp_out, mode="I") as writer:
  for idx, frame in enumerate(frames[0:60]):
        print("Adding frame to GIF file: ", idx + 1)
        rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        writer.append_data(rgb_frame)

Running

Running can simply be done with python main.py!