|
| 1 | +#!/usr/bin/env python |
1 | 2 | # |
2 | 3 | # Scene Detection with Python and OpenCV - Example Program |
3 | 4 | # Part 2: Adaptive Fade Detection By: Brandon Castellano |
4 | 5 | # |
5 | 6 | # http://www.bcastell.com/tech-articles/pyscenedetect-tutorial-part-2/ |
6 | 7 | # |
7 | | -# This Python program implements a more advanced and optimized threshold- |
8 | | -# based scene detection algorithm, and improves the output format so |
9 | | -# the scene cuts can be more easily imported into other programs (e.g. |
10 | | -# ffmpeg, mkvmerge). Basic usage is: |
| 8 | +# This Python program implements an optimized, threshold-based |
| 9 | +# scene detection algorithm, and improves the output format so that |
| 10 | +# scene cuts can be more easily imported into other programs (e.g. |
| 11 | +# ffmpeg, mkvmerge). Usage: |
11 | 12 | # |
12 | | -# > python part2-adaptive.py [-h] -i VIDEO_FILE |
| 13 | +# ./python part2-adaptive.py VIDEO_FILE [intensity = 16] |
13 | 14 | # |
14 | | -# Where -i denotes the input video, and -h shows the help message. |
15 | | -# Optional arguments that can be passed after VIDEO_FILE are: |
16 | | -# |
17 | | -# -h, --help show this help message and exit |
18 | | -# -i VIDEO_FILE, --input VIDEO_FILE |
19 | | -# [REQUIRED] Path to input video. (default: None) |
20 | | -# -t intensity, --threshold intensity |
21 | | -# 8-bit intensity value, from 0-255, to use as a fade |
22 | | -# in/out detection threshold. (default: 8) |
23 | | -# -m percent, --minpercent percent |
24 | | -# Amount of pixels in a frame, from 0-100%, that must |
25 | | -# fall under [intensity]. (default: 95) |
26 | | -# -b rows, --blocksize rows |
27 | | -# Number of rows in frame to check at once, can be tuned |
28 | | -# for performance. (default: 32) |
29 | | -# -s offset, --startindex offset |
30 | | -# Starting index for chapter/scene output. (default: 0) |
31 | | -# |
32 | | -# Example: |
33 | | -# > python part2-adaptive.py -i testvideo.mp4 |
34 | | -# > python part2-adaptive.py -i testvideo.mp4 -t 8 -m 95 -b 32 -s 0 |
| 15 | +# Which will export a list of scenes and their timecodes (by default) |
| 16 | +# detected in VIDEO_FILE, using the intensity as a threshold. Other |
| 17 | +# parameters (e.g. minimum match percent and block size) can be |
| 18 | +# modified just above the main function in the source below. |
35 | 19 | # |
36 | 20 | # For each fade/cut that is detected, the timecodes and frame numbers |
37 | 21 | # are printed to stdout. Note that this program depends on the Python |
|
49 | 33 | # OTHER DEALINGS IN THE SOFTWARE. |
50 | 34 | # |
51 | 35 |
|
52 | | - |
53 | 36 | import sys |
54 | | -import argparse |
55 | | - |
56 | 37 | import cv2 |
57 | 38 | import numpy as np |
58 | 39 |
|
59 | 40 |
|
60 | | -def int_type_check(min_val, max_val = None, metavar = None): |
61 | | - if metavar == None: metavar = 'value' |
62 | | - def _type_check(value): |
63 | | - value = int(value) |
64 | | - valid = True |
65 | | - msg = '' |
66 | | - if (max_val == None): |
67 | | - if (value < min_val): valid = False |
68 | | - msg = 'invalid choice: %d (%s must be at least %d)' % ( |
69 | | - value, metavar, min_val ) |
70 | | - else: |
71 | | - if (value < min_val or value > max_val): valid = False |
72 | | - msg = 'invalid choice: %d (%s must be between %d and %d)' % ( |
73 | | - value, metavar, min_val, max_val ) |
74 | | - if not valid: |
75 | | - raise argparse.ArgumentTypeError(msg) |
76 | | - return value |
77 | | - return _type_check |
78 | | - |
79 | | - |
80 | | -def get_cli_parser(): |
81 | | - parser = argparse.ArgumentParser( |
82 | | - formatter_class = argparse.ArgumentDefaultsHelpFormatter) |
83 | | - parser._optionals.title = 'arguments' |
84 | | - |
85 | | - parser.add_argument('-i', '--input', metavar = 'VIDEO_FILE', |
86 | | - type = file, required = True, |
87 | | - help = '[REQUIRED] Path to input video.') |
88 | | - parser.add_argument('-t', '--threshold', metavar = 'intensity', |
89 | | - type = int_type_check(0, 255, 'intensity'), default = 8, |
90 | | - help = '8-bit intensity value, from 0-255, to use as a fade in/out detection threshold.') |
91 | | - parser.add_argument('-m', '--minpercent', metavar = 'percent', |
92 | | - type = int_type_check(0, 100, 'percentage'), default = 95, |
93 | | - help = 'Amount of pixels in a frame, from 0-100%%, that must fall under [intensity].') |
94 | | - parser.add_argument('-b', '--blocksize', metavar = 'rows', |
95 | | - type = int_type_check(0, None, 'number of rows'), default = 32, |
96 | | - help = 'Number of rows in frame to check at once, can be tuned for performance.') |
97 | | - parser.add_argument('-s', '--startindex', metavar = 'offset', |
98 | | - type = int, default = 0, |
99 | | - help = 'Starting index for chapter/scene output.') |
100 | | - |
101 | | - return parser |
| 41 | +# Advanced Scene Detection Parameters |
| 42 | +INTENSITY_THRESHOLD = 16 # Pixel intensity threshold (0-255), default 16 |
| 43 | +MINIMUM_PERCENT = 95 # Min. amount of pixels to be below threshold. |
| 44 | +BLOCK_SIZE = 32 # Num. of rows to sum per iteration. |
102 | 45 |
|
103 | 46 |
|
104 | 47 | def main(): |
105 | | - args = get_cli_parser().parse_args() |
| 48 | + if len(sys.argv) < 2: |
| 49 | + print "Error - file name must be specified as first argument." |
| 50 | + return |
| 51 | + |
| 52 | + cap = cv2.VideoCapture() |
| 53 | + cap.open(sys.argv[1]) |
| 54 | + |
| 55 | + if not cap.isOpened(): |
| 56 | + print "Fatal error - could not open video %s." % sys.argv[1] |
| 57 | + return |
| 58 | + else: |
| 59 | + print "Parsing video %s..." % sys.argv[1] |
| 60 | + |
| 61 | + # Do stuff with cap here. |
106 | 62 |
|
107 | | - print args |
| 63 | + width = cap.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH) |
| 64 | + height = cap.get(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT) |
| 65 | + print "Video Resolution: %d x %d" % (width, height) |
| 66 | + |
| 67 | + # Allow the threshold to be passed as an optional, second argument to the script. |
| 68 | + threshold = 16 |
| 69 | + if len(sys.argv) > 2 and int(sys.argv[2]) > 0: |
| 70 | + threshold = int(sys.argv[2]) |
| 71 | + print "Detecting scenes with threshold = %d" % threshold |
| 72 | + print "Min. pixels under threshold = %d %%" % MINIMUM_PERCENT |
| 73 | + print "Block/row size = %d" % BLOCK_SIZE |
| 74 | + print "" |
| 75 | + |
| 76 | + min_percent = MINIMUM_PERCENT / 100.0 |
| 77 | + num_rows = BLOCK_SIZE |
| 78 | + last_amt = 0 # Number of pixel values above threshold in last frame. |
| 79 | + start_time = cv2.getTickCount() # Used for statistics after loop. |
| 80 | + |
| 81 | + while True: |
| 82 | + # Get next frame from video. |
| 83 | + (rv, im) = cap.read() |
| 84 | + if not rv: # im is a valid image if and only if rv is true |
| 85 | + break |
| 86 | + |
| 87 | + # Compute # of pixel values and minimum amount to trigger fade. |
| 88 | + num_pixel_vals = float(im.shape[0] * im.shape[1] * im.shape[2]) |
| 89 | + min_pixels = int(num_pixel_vals * (1.0 - min_percent)) |
| 90 | + |
| 91 | + # Loop through frame block-by-block, updating current sum. |
| 92 | + frame_amt = 0 |
| 93 | + curr_row = 0 |
| 94 | + while curr_row < im.shape[0]: |
| 95 | + # Add # of pixel values in current block above the threshold. |
| 96 | + frame_amt += np.sum( |
| 97 | + im[curr_row : curr_row + num_rows,:,:] > threshold ) |
| 98 | + if frame_amt > min_pixels: # We can avoid checking the rest of the |
| 99 | + break # frame since we crossed the boundary. |
| 100 | + curr_row += num_rows |
| 101 | + |
| 102 | + # Detect fade in from black. |
| 103 | + if frame_amt >= min_pixels and last_amt < min_pixels: |
| 104 | + print "Detected fade in at %dms (frame %d)." % ( |
| 105 | + cap.get(cv2.cv.CV_CAP_PROP_POS_MSEC), |
| 106 | + cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES) ) |
| 107 | + |
| 108 | + # Detect fade out to black. |
| 109 | + elif frame_amt < min_pixels and last_amt >= min_pixels: |
| 110 | + print "Detected fade out at %dms (frame %d)." % ( |
| 111 | + cap.get(cv2.cv.CV_CAP_PROP_POS_MSEC), |
| 112 | + cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES) ) |
| 113 | + |
| 114 | + last_amt = frame_amt # Store current mean to compare in next iteration. |
| 115 | + |
| 116 | + # Get # of frames in video based on the position of the last frame we read. |
| 117 | + frame_count = cap.get(cv2.cv.CV_CAP_PROP_POS_FRAMES) |
| 118 | + # Compute runtime and average framerate |
| 119 | + total_runtime = float(cv2.getTickCount() - start_time) / cv2.getTickFrequency() |
| 120 | + avg_framerate = float(frame_count) / total_runtime |
| 121 | + |
| 122 | + print "Read %d frames from video in %4.2f seconds (avg. %4.1f FPS)." % ( |
| 123 | + frame_count, total_runtime, avg_framerate) |
| 124 | + |
| 125 | + cap.release() |
108 | 126 |
|
109 | | - pass |
110 | 127 |
|
111 | | - |
112 | 128 | if __name__ == "__main__": |
113 | 129 | main() |
0 commit comments