openva/rs-video-processor

View on GitHub
bin/rs_video.py

Summary

Maintainability
A
1 hr
Test Coverage
import os
import glob
import random
import cv2
import pytesseract
from PIL import Image
from collections import Counter
import numpy as np

def extract_frames(video_path, output_folder):
    # Open the video file
    cap = cv2.VideoCapture(video_path)
    
    if not cap.isOpened():
        print('Error: Could not open video.')
        return

    fps = int(cap.get(cv2.CAP_PROP_FPS))  # Get frames per second of the video
    frame_count = 0

    while cap.isOpened():
        ret, frame = cap.read()
        
        # If frame is read correctly ret is True
        if not ret:
            break  # Exit the loop if there are no frames left to read

        # Save a frame every 'fps' frames (i.e., once every second)
        if frame_count % fps == 0:
            frame_time = int(frame_count / fps)
            output_path = f"{output_folder}/{frame_time}.jpg"
            cv2.imwrite(output_path, frame)
            print(f"Saved {output_path}")

        frame_count += 1

    cap.release()

def ocr(image_path, chyrons):
    # Read the image using OpenCV
    image = cv2.imread(image_path)
    
    # Convert to grayscale
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    ocr_results = []

    for chyron in chyrons:

        # Crop function
        def crop_image(image, chyron):
            x, y, w, h = chyron
            return image[y:y+h, x:x+w]
        
        # OCR function
        def ocr_cropped_image(cropped_image):
            pil_image = Image.fromarray(cropped_image)
            return pytesseract.image_to_string(pil_image, lang='eng', config='--psm 6').strip()
        
        # The bottom chyron needs to be pulled in a bit, to avoid including the state seal
        if chyron[1] > 100:
            chyron_list = list(chyron)
            chyron_list[0] = chyron_list[0] + 12
            chyron_list[2] = chyron_list[2] - 12
            chyron = tuple(chyron_list)

        # Crop images based on the coordinates
        cropped = crop_image(gray_image, chyron)
        
        # Perform OCR
        text = ocr_cropped_image(cropped)
   
        # Combine the outputs
        chyron_results = {'coordinates': chyron, 'text': text}
        
        ocr_results.append(chyron_results)
    
    return ocr_results

def sample_chyrons(directory_path):
    # Select a bunch of random screenshots
    screenshots = glob.glob(os.path.join(directory_path, '*.jpg'))
    random_screenshots = random.sample(screenshots, 200)

    boundaries = []

    for random_screenshot in random_screenshots:
        image_path = directory_path + '/' + random_screenshot
        output_path = image_path + '-outlined.jpg'
        chyrons = find_chyrons(image_path, output_path)
        for chyron in chyrons:
            boundaries.append(chyron)

    # Define a function to compare two boundaries
    def are_similar(boundary1, boundary2, threshold=10):
        return all(abs(a - b) <= threshold for a, b in zip(boundary1, boundary2))

    # Find nearly-identical boundaries
    similar_groups = []
    visited = set()

    for i, boundary in enumerate(boundaries):
        if i in visited:
            continue
        # Create a new group for each unvisited boundary
        current_group = [boundary]
        for j, other_boundary in enumerate(boundaries[i+1:], start=i+1):
            if are_similar(boundary, other_boundary):
                current_group.append(other_boundary)
                visited.add(j)
        similar_groups.append(current_group)

    # Save the two most-found similar boundaries
    sorted_similar_groups = sorted(similar_groups, key=lambda group: len(group), reverse=True)
    sorted_similar_groups = sorted_similar_groups[:2]

    def mean_boundary(group):
        # Transpose the list of tuples to easily calculate mean for each boundary dimension
        transposed = list(zip(*group))
        # Calculate mean for each dimension
        mean = tuple(round(sum(dim) / len(dim)) for dim in transposed)
        return mean

    # Calculate the mean boundary for each group
    mean_boundaries = [mean_boundary(group) for group in sorted_similar_groups]

    return mean_boundaries
    
def find_chyrons(image_path, output_path):
    # Load the image
    image = cv2.imread(image_path)

    # Convert the image to HSV color space
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

    # Define the range of blue color in HSV
    lower_blue = np.array([100, 150, 50])
    upper_blue = np.array([140, 255, 255])

    # Threshold the HSV image to get only blue colors
    mask = cv2.inRange(hsv, lower_blue, upper_blue)

    # Find contours in the mask
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # Filter contours to find large blue rectangles
    chyrons = []
    for contour in contours:
        x, y, w, h = cv2.boundingRect(contour)
        # Filtering criteria for large rectangles
        if w >= 200 and h >= 18:
            chyrons.append((x, y, w, h))
            # Draw a green rectangle around the detected object
            #cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)

    # Save the result
    #cv2.imwrite(output_path, image)

    return chyrons

def get_average_color(image_path, bounding_box):
    # Load the image
    image = cv2.imread(image_path)
    
    # Extract the bounding box coordinates
    x, y, w, h = bounding_box
    
    # Crop the region of interest based on the bounding box
    roi = image[y:y+h, x:x+w]
    
    # Calculate the average color of the region of interest
    average_color_per_channel_bgr = np.mean(roi, axis=(0, 1))
    
    # Convert BGR to RGB
    average_color_per_channel = average_color_per_channel_bgr[::-1]
    
    # Convert the averages to integers
    average_color = tuple(map(int, average_color_per_channel))
    
    return average_color