pythondjangoopencvdjango-imagekit

Creating video thumbnails with imagekit and opencv


I have a model named "Post", which is going to refer to images and videos. I added ImageSpecField for thumbnail storage and created a function, that pulls the desired frame from uploaded video. Is there any way to use this function while generating thumbnail? Because right now ImageSpecField can only use FileField as an input.

I have tried creating a new class inheriting from ImageSpecField, but I quickly realized that this is not going to work because this class was instanced only on server start, thus putting this function in constructor of it would not work.

import cv2 as cv
from django.conf import settings
from django.db import models
from imagekit.processors import ResizeToFit
from imagekit.models import ImageSpecField


def video_to_image(source, frame):
    vid_cap = cv.VideoCapture(settings.MEDIA_ROOT + source.__str__())
    vid_cap.set(cv.CAP_PROP_POS_FRAMES, frame)
    success, image = vid_cap.read()
    vid_cap.release()

    return image


class Post(models.Model):
    IMAGE = 'I'
    VIDEO = 'V'
    FILE_TYPES = [
        (IMAGE, 'Image'),
        (VIDEO, 'Video')
    ]

    file_type = models.CharField(max_length=1, choices=FILE_TYPES)
    file = models.FileField(upload_to='post_images')
    thumbnail_frame = models.IntegerField(default=0)
    image_thumbnail = ImageSpecField(source='file',
                                     processors=[ResizeToFit(width=200, height=200)],
                                     format='JPEG',
                                     options={'quality': 60})

I want imagekit to generate thumbnail from video, and be able to get it via ImageSpecField.


Solution

  • Ok, I think I finally got it. I managed to achieve it by creating another field - thumbnail_source_image and depending on the uploaded file type doing the following:

    I am getting file type using the magic library.

    But there is one small problem with this method - for Django to generate file path, we have to call save() method on our model. This forces us to make two requests to the database, instead of one.

    utils.py file:

    import cv2 as cv
    
    
    def save_frame_from_video(video_path, millisecond, frame_file_path):
        vidcap = cv.VideoCapture(video_path)
    
        vidcap.set(cv.CAP_PROP_POS_MSEC, millisecond)
    
        success, image = vidcap.read()
    
        # save image to temp file
        cv.imwrite(frame_file_path, image)
    
        vidcap.release()
    

    models.py file:

    import os
    from django.conf import settings
    from django.db import models
    from imagekit.processors import ResizeToFit
    from imagekit.models import ImageSpecField
    from .utils import save_frame_from_video
    
    
    class Post(models.Model):
        image_types = ['image/jpeg', 'image/gif', 'image/png']
        video_types = ['video/webm']
    
        IMAGE = 'I'
        VIDEO = 'V'
        TYPES = [
            (IMAGE, 'Image'),
            (VIDEO, 'Video'),
        ]
    
        type = models.CharField(max_length=1, choices=TYPES, blank=True)
        file = models.FileField(upload_to='post_files/%Y/%m/%d/')
    
        thumbnail_millisecond = models.IntegerField(default=0)
        thumbnail_source_image = models.ImageField(upload_to='post_files/%Y/%m/%d/', null=True, blank=True)
        image_thumbnail = ImageSpecField(source='thumbnail_source_image',
                                         processors=[
                                             ResizeToFit(150,
                                                         150,
                                                         mat_color=(230, 230, 230)),
                                         ],
                                         format='JPEG',
                                         options={'quality': 80})
    
        def _set_type(self):
            # max bytes to read for file type detection
            read_size = 5 * (1024 * 1024)  # 5MB
    
            # read mime type of file
            from magic import from_buffer
            mime = from_buffer(self.file.read(read_size), mime=True)
    
            if mime in self.image_types:
                self.type = self.IMAGE
            elif mime in self.video_types:
                self.type = self.VIDEO
    
        def _set_thumbnail_source_image(self):
            if self.type == self.IMAGE:
                self.thumbnail_source_image = self.file
            elif self.type == self.VIDEO:
                # create thumbnail source file
                image_path = os.path.splitext(self.file.path)[0] + '_thumbnail_src_image.jpg'
                save_frame_from_video(self.file.path, int(self.thumbnail_millisecond), image_path)
    
                # generate path relative to media root, because this is the version that ImageField accepts
                media_image_path = os.path.relpath(image_path, settings.MEDIA_ROOT)
    
                self.thumbnail_source_image = media_image_path
    
        def save(self, *args, **kwargs):
            if self.type == '':
                self._set_type()
            # if there is no source image
            if not bool(self.thumbnail_source_image):
                # we need to save first, for django to generate path for file in "file" field
                super().save(*args, **kwargs)
                self._set_thumbnail_source_image()
    
            super().save(*args, **kwargs)