Merge branch 'feature/basic-pipeline' into dev
This commit is contained in:
commit
708d7058e7
3 changed files with 267 additions and 74 deletions
12
README.md
Normal file
12
README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Autophotographer
|
||||||
|
|
||||||
|
Autophotographer is a tool that helps users filter the best images from a video.
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── docs Report and Project proposal
|
||||||
|
├── img images for the repository
|
||||||
|
├── README.md
|
||||||
|
├── src project source code
|
||||||
|
└── terraform terraform IaC
|
||||||
|
```
|
|
@ -1,87 +1,239 @@
|
||||||
import cv2
|
import cv2
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import pathlib
|
from os.path import abspath
|
||||||
import numpy
|
import pandas
|
||||||
import time
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
from skimage.exposure import is_low_contrast
|
||||||
|
import yaml
|
||||||
|
|
||||||
# Process arguments
|
# Import local packages
|
||||||
def parse_arguments(argv=None):
|
from focusdetection.focusdetection import fast_fourier
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('-i', '--input', dest='inputfile', type=pathlib.Path, help='Specify a video file')
|
|
||||||
parser.add_argument('-o', '--output', dest='outputfolder', type=pathlib.Path, help='Specify a folder to save frames to')
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
# Convert video to frames
|
#import sys
|
||||||
def video_to_frames():
|
#import pathlib
|
||||||
startTime = time.time()
|
#import numpy
|
||||||
print("Converting video to frames...")
|
#import time
|
||||||
capture = cv2.VideoCapture(str(inputfile))
|
|
||||||
success,image = capture.read()
|
|
||||||
count = 0
|
|
||||||
while success:
|
|
||||||
outputfile = outputfolder + "/frame%d.jpg" % count
|
|
||||||
# print(outputfile)
|
|
||||||
cv2.imwrite(outputfile, image)
|
|
||||||
success,image = capture.read()
|
|
||||||
count +=1
|
|
||||||
endTime = time.time()
|
|
||||||
totalTime = endTime - startTime
|
|
||||||
print("Exported " + str(count) + " frames in " + str(totalTime) + " seconds.")
|
|
||||||
|
|
||||||
# Shrink set based on filesize
|
# accepted image formats
|
||||||
def display_file_sizes():
|
image_formats = (".jpg", ".jpeg", ".png")
|
||||||
filesizes = []
|
|
||||||
for filename in os.listdir(outputfolder):
|
|
||||||
filepath = outputfolder + "/" + filename
|
|
||||||
filesize = os.path.getsize(filepath)
|
|
||||||
print(filepath + ": " + str(filesize))
|
|
||||||
filesizes.append(filesize)
|
|
||||||
# work out average
|
|
||||||
average = sum(filesizes)/len(filesizes)
|
|
||||||
print ("Average is: " + str(average))
|
|
||||||
# delete files below average
|
|
||||||
count = 0
|
|
||||||
for filename in os.listdir(outputfolder):
|
|
||||||
filepath = outputfolder + "/" + filename
|
|
||||||
if filesizes[count] < average:
|
|
||||||
# print(filepath + ": " + str(filesizes[count]))
|
|
||||||
os.remove(filepath)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
#def remove_similar_frames():
|
# accepted video formats
|
||||||
def order_frames_by_filesize():
|
video_formats = (".mp4", ".mov", ".avi", ".flv", ".mkv")
|
||||||
frames = os.listdir(outputfolder)
|
|
||||||
frames = sorted(frames, key = lambda x: os.stat(os.path.join(outputfolder, x)).st_size, reverse = True)
|
# load config file
|
||||||
for frame in frames:
|
def load_config(path=os.path.join(os.path.dirname(__file__), "./config.yml")):
|
||||||
filesize = os.stat(os.path.join(outputfolder, frame)).st_size
|
abs_path = os.path.abspath(path)
|
||||||
if filesize > 1024:
|
|
||||||
filesize = filesize / 1024
|
# check if file exists
|
||||||
print(frame + ": " + str(filesize) + " KB")
|
if os.path.exists(abs_path):
|
||||||
|
if abs_path.lower().endswith((".yml", ".yaml")):
|
||||||
|
print("[INFO] Loading config...")
|
||||||
|
# attempt to open file
|
||||||
|
with open(abs_path) as file:
|
||||||
|
return yaml.safe_load(file)
|
||||||
else:
|
else:
|
||||||
print(frame + ": " + str(filesize))
|
print("[ERRO] Please specify a file with extension '.yml' or '.yaml'.")
|
||||||
|
quit()
|
||||||
|
else:
|
||||||
|
print("[ERRO] Path does not exist")
|
||||||
|
quit()
|
||||||
|
|
||||||
def order_frames_by_brightness():
|
def filter_to_function(imagefilter, prettyName=False):
|
||||||
frames_path = os.listdir(outputfolder)
|
if imageFilter == "brightness":
|
||||||
frames_path = sorted(frames_path, key = return_frame_brightness)
|
# filter_brightness()
|
||||||
for frame_path in frames_path:
|
print("[INFO] Filtering based on brightness...")
|
||||||
print(frame_path + ": " + str(return_frame_brightness(frame_path)))
|
elif imageFilter == "filesize":
|
||||||
|
# filter_filesize()
|
||||||
|
print("[INFO] Filtering based on filesize...")
|
||||||
|
elif imageFilter == "contrast":
|
||||||
|
# filter_contrast()
|
||||||
|
print("[INFO] Filtering based on contrast...")
|
||||||
|
elif imageFilter == "focus":
|
||||||
|
# filter_focus()
|
||||||
|
print("[INFO] Filtering based on focus...")
|
||||||
|
else:
|
||||||
|
print("[WARN] Filter not recognised. Ignoring...")
|
||||||
|
|
||||||
def return_frame_brightness(frame_path):
|
# filter paths by accepted file extensions
|
||||||
frame = cv2.imread(os.path.join(outputfolder, frame_path))
|
def filter_paths(paths):
|
||||||
hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
print("[INFO] Filtering paths by filetype...")
|
||||||
h, s, v = cv2.split(hsv_frame)
|
filtered_paths = []
|
||||||
average_v = numpy.average(v)
|
for path in paths:
|
||||||
return average_v
|
if path.lower().endswith(video_formats + image_formats):
|
||||||
|
filtered_paths.append(path)
|
||||||
|
return filtered_paths
|
||||||
|
|
||||||
args = parse_arguments()
|
# load frames from images
|
||||||
inputfile = str(args.inputfile.absolute())
|
def load_video(path):
|
||||||
outputfolder = str(args.outputfolder.absolute())
|
print("[INFO] Loading video...")
|
||||||
|
stream = cv2.VideoCapture(path)
|
||||||
|
frames = []
|
||||||
|
while True:
|
||||||
|
(retrieved, frame) = stream.read()
|
||||||
|
if not retrieved:
|
||||||
|
break
|
||||||
|
frames.append(frame)
|
||||||
|
return frames
|
||||||
|
|
||||||
# Convert video to frames
|
# load images
|
||||||
video_to_frames()
|
def load_image(path):
|
||||||
|
print("[INFO] Loading image...")
|
||||||
|
image = cv2.imread(path)
|
||||||
|
return image
|
||||||
|
|
||||||
display_file_sizes()
|
# load images respective of their file type
|
||||||
#order_frames_by_filesize()
|
def load_files(paths):
|
||||||
#order_frames_by_brightness()
|
print("[INFO] Loading files...")
|
||||||
|
# set loaded counters to 0
|
||||||
|
images_loaded = 0
|
||||||
|
videos_loaded = 0
|
||||||
|
videos = []
|
||||||
|
images = []
|
||||||
|
|
||||||
|
# load in all image/video files
|
||||||
|
for path in paths:
|
||||||
|
# if file is an image, just load it
|
||||||
|
if path.lower().endswith(image_formats):
|
||||||
|
image = load_image(path)
|
||||||
|
images_loaded += 1
|
||||||
|
images.append(image)
|
||||||
|
# if file is a video, break into frames and load them
|
||||||
|
elif path.lower().endswith(video_formats):
|
||||||
|
frames = load_video(path)
|
||||||
|
videos.append(frames)
|
||||||
|
videos_loaded += 1
|
||||||
|
else:
|
||||||
|
print("[INFO] Skipped non-image/non-video file.")
|
||||||
|
|
||||||
|
# return information on number of files loaded
|
||||||
|
if videos_loaded == 0 and images_loaded == 0:
|
||||||
|
print("[INFO] No valid images or videos found.")
|
||||||
|
else:
|
||||||
|
print("[INFO] {} image(s) and {} video(s) loaded.".format(images_loaded, videos_loaded))
|
||||||
|
|
||||||
|
total_frames = 0
|
||||||
|
for video in videos:
|
||||||
|
total_frames += len(video)
|
||||||
|
total_images = total_frames + len(images)
|
||||||
|
print("[INFO] Total images loaded (including video frames): {}".format(total_images))
|
||||||
|
|
||||||
|
return (images, videos)
|
||||||
|
|
||||||
|
# filter images based on filesize
|
||||||
|
def filter_filesize(paths):
|
||||||
|
print("[INFO] Filtering by filesize...")
|
||||||
|
filesizes = []
|
||||||
|
filtered_paths = []
|
||||||
|
# calculate avg filesize
|
||||||
|
for path in paths:
|
||||||
|
filesize = os.path.getsize(path)
|
||||||
|
filesizes.append(filesize)
|
||||||
|
filesize_avg = sum(filesizes)/len(filesizes)
|
||||||
|
filesize_std = pandas.Series(filesizes).std(ddof=0)
|
||||||
|
filesize_min = min(filesizes)
|
||||||
|
filesize_max = max(filesizes)
|
||||||
|
filesize_filter = filesize_avg - filesize_std
|
||||||
|
print("[INFO] min size: {}, max size: {}, avg size: {}, std size: {}".format(
|
||||||
|
filesize_min, filesize_max, filesize_avg, filesize_std))
|
||||||
|
|
||||||
|
# remove images that are 3 std below mean
|
||||||
|
for path in paths:
|
||||||
|
if os.path.getsize(path) > filesize_filter:
|
||||||
|
filtered_paths.append(path)
|
||||||
|
|
||||||
|
return filtered_paths
|
||||||
|
|
||||||
|
# plot the distribution of file sizes for data insight
|
||||||
|
def plot_filesizes(paths):
|
||||||
|
filesizes = []
|
||||||
|
for path in paths:
|
||||||
|
if path.lower().endswith(image_formats):
|
||||||
|
filesize = os.path.getsize(path)
|
||||||
|
filesizes.append(filesize)
|
||||||
|
sorted_fs = sorted(filesizes)
|
||||||
|
data = np.array(sorted_fs)
|
||||||
|
fig = plt.figure(figsize =(10, 7))
|
||||||
|
plt.boxplot(data)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
# resize image and turn to greyscale
|
||||||
|
def process_image(path, width):
|
||||||
|
print("[INFO] Processing image...")
|
||||||
|
image = cv2.imread(path)
|
||||||
|
height = int(image.shape[0] * (width / image.shape[0]))
|
||||||
|
image = cv2.resize(image, (width, height))
|
||||||
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||||
|
return image
|
||||||
|
|
||||||
|
# filter images based on contrast
|
||||||
|
def filter_contrast(paths, thresh=0.35):
|
||||||
|
print("[INFO] Filtering by contrast...")
|
||||||
|
filtered_paths = []
|
||||||
|
for path in paths:
|
||||||
|
if path.lower().endswith(video_formats):
|
||||||
|
continue
|
||||||
|
print(path)
|
||||||
|
image = process_image(path, 500)
|
||||||
|
if is_low_contrast(image, thresh):
|
||||||
|
print("[INFO] Low contrast")
|
||||||
|
else:
|
||||||
|
print("[INFO] High contrast")
|
||||||
|
filtered_paths.append(path)
|
||||||
|
return filtered_paths
|
||||||
|
|
||||||
|
# filter images based on brightness
|
||||||
|
def filter_brightness(paths):
|
||||||
|
print("[INFO] Filtering by brightness...")
|
||||||
|
|
||||||
|
# filter images based on focus/blurriness
|
||||||
|
def filter_focus(paths):
|
||||||
|
print("[INFO] Filtering by focus/blurriness...")
|
||||||
|
filtered_paths = []
|
||||||
|
# read images in greyscale
|
||||||
|
for path in paths:
|
||||||
|
if path.lower().endswith(video_formats):
|
||||||
|
continue
|
||||||
|
print(path)
|
||||||
|
image = process_image(path, 500)
|
||||||
|
(mean, blurry) = fast_fourier(image, size=60, thresh=30)
|
||||||
|
blurStats = "Blurry ({:.4f})" if blurry else "Not blurry ({:.4f})"
|
||||||
|
if blurry:
|
||||||
|
print(("[INFO] {}: " + blurStats).format(path, mean))
|
||||||
|
else:
|
||||||
|
filtered_paths.append(path)
|
||||||
|
return filtered_paths
|
||||||
|
|
||||||
|
# rank remaining images using a CNN
|
||||||
|
def rank_images():
|
||||||
|
print("[INFO] Ranking images using machine learning...")
|
||||||
|
|
||||||
|
# parse commande line arguments
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-i", "--input", type=os.path.abspath, required=True, nargs="+",
|
||||||
|
help="path to video or image folder")
|
||||||
|
parser.add_argument("-c", "--config", type=os.path.abspath, help="path to config file")
|
||||||
|
args = vars(parser.parse_args())
|
||||||
|
|
||||||
|
paths = filter_paths(args["input"])
|
||||||
|
|
||||||
|
# load in config file
|
||||||
|
if args["config"] is not None:
|
||||||
|
autophotoConf = load_config(args["config"])
|
||||||
|
else:
|
||||||
|
autophotoConf = load_config()
|
||||||
|
|
||||||
|
# Order and selection of operations from config file
|
||||||
|
for imageFilter in autophotoConf["filters"]:
|
||||||
|
n_of_images_before = len(paths)
|
||||||
|
filter_to_function(imageFilter)
|
||||||
|
n_of_images_after = len(paths)
|
||||||
|
|
||||||
|
# calculate set difference after filtering
|
||||||
|
diff = n_of_images_before - n_of_images_after
|
||||||
|
print("[INFO] Filtered {}/{} images via {} filtering.".format(
|
||||||
|
n_of_images_after, n_of_images_before, imageFilter))
|
||||||
|
|
||||||
|
if autophotoConf["CNNrank"]:
|
||||||
|
print("[INFO] Running CNN ranking...")
|
29
src/config.yml
Normal file
29
src/config.yml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
# Config file for autophotographer
|
||||||
|
|
||||||
|
# List of filters to apply in order
|
||||||
|
filters:
|
||||||
|
- brightness
|
||||||
|
- filesize
|
||||||
|
- contrast
|
||||||
|
- focus
|
||||||
|
|
||||||
|
# Whether or not to apply CNN ranking
|
||||||
|
CNNrank: True
|
||||||
|
|
||||||
|
# Options for focus filter
|
||||||
|
brightness_options:
|
||||||
|
threshold: 0.35
|
||||||
|
|
||||||
|
# Options for focus filter
|
||||||
|
filesize_options:
|
||||||
|
threshold: 0.35
|
||||||
|
|
||||||
|
# Options for focus filter
|
||||||
|
contrast_options:
|
||||||
|
threshold: 0.35
|
||||||
|
|
||||||
|
# Options for focus filter
|
||||||
|
focus_options:
|
||||||
|
threshold: 0.35
|
||||||
|
...
|
Reference in a new issue