Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7720bcb

Browse files
committed
Refactor
- Python 3 - Some cleanup - Works in Windows.
1 parent 6d6dfe4 commit 7720bcb

3 files changed

Lines changed: 161 additions & 85 deletions

File tree

constants.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# MP4 quality; lower is better
2+
quality = 15
3+
4+
# Desired framerate
5+
fps = 24
6+
7+
# Minimum number of sequential images that constitute a set. If less than this,
8+
# images are assumed to be single still photos.
9+
min_images_per_set = fps # one seconds' worth
10+
11+
# Minimum length of video; loop will be repeated until this is exceeded.
12+
boomerang_length_seconds = 15
13+
14+
# Extension for a photo file (case sensitive)
15+
photo_ext = '.JPG'
16+
17+
# Everything in the filename up to the image number, e.g. this is for photos
18+
# named DSC05234.JPG
19+
photo_prefix = 'DSC'
20+
21+
# Location of photos, e.g. where you copied the contents of your SD card.
22+
import os
23+
input_dir = os.path.join(os.path.expanduser('~'), 'Desktop', 'DCIM', '100MSDCF')
24+
25+
# Desired video width. Images will be scaled to this. Must be even!
26+
video_width = 1920
27+
28+
# Location of exiftool, as you'd type it into a terminal.
29+
exiftool_call = 'exiftool'
30+
31+
# Location of ffmpeg
32+
ffmpeg_call = 'ffmpeg'
33+
34+
if os.name == 'nt':
35+
dev_null = "NUL"
36+
else:
37+
dev_null = '/dev/null'

sorter.py

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,67 @@
1+
# Sorts images into sets based on capture time.
2+
13
import os, shutil
2-
from video_maker import fps
4+
from constants import *
35

4-
minImagesPerSet = fps # if less than this number of images makes a sequence, don't make it into a set.
6+
set_offset = 0 # number for first set in this run.
57

6-
dir = os.path.expanduser('~/Desktop/DCIM/100MSDCF')
7-
setOffset = 0 # number for first set.
8-
files = os.listdir(dir)
8+
files = os.listdir(input_dir)
99
if len(files) == 0:
10-
print 'No files.'
10+
print('No files.')
1111
exit(0)
1212

13-
lastDate = None
13+
last_date = None
1414
sets = []
15-
thisSet = []
1615
for file in sorted(files):
17-
filename = os.path.join(dir, file)
18-
if os.path.isdir(filename): continue
19-
if not '.JPG' in filename: continue
20-
# mtime is the only one that works, sadly the resolution is not high enough to detect when the camera buffer was full.
21-
thisDate = os.path.getmtime(filename)
22-
if lastDate:
23-
timeDeltaSec = thisDate - lastDate
24-
if timeDeltaSec < 3: # Apparently within a burst, some files will have a time difference of 2 seconds.
25-
thisSet.append(filename) # add file to this set; it was taken soon after the previous file.
16+
filename = os.path.join(input_dir, file)
17+
if os.path.isdir(filename):
18+
continue
19+
if not photo_ext in filename:
20+
continue
21+
# mtime is the only one that works, sadly the resolution is not high enough
22+
# to detect when the camera buffer was full.
23+
this_date = os.path.getmtime(filename)
24+
if last_date is None:
25+
this_set = [filename] # first time around; add file to set.
26+
else:
27+
time_delta_sec = this_date - last_date
28+
# Apparently within a burst, some files will have a time difference of 2
29+
# seconds.
30+
if time_delta_sec < 3:
31+
# Add file to this set; it was taken soon after the previous file.
32+
this_set.append(filename)
2633
else:
2734
# Bigger jump in file date; assume this is the start of a new set.
28-
if len(thisSet) >= minImagesPerSet:
29-
print 'Set with %3d images formed; next file is %10.3f seconds away' % (len(thisSet), timeDeltaSec)
30-
sets.append(thisSet) # store the set
35+
if len(this_set) >= min_images_per_set:
36+
# This sequence has enough images; make a set.
37+
print(f'Set with {len(this_set):3d} images formed; '
38+
f'next file is {time_delta_sec:10.3f} seconds away')
39+
sets.append(this_set) # store the set
3140
else:
32-
print 'Set with %3d images discarded; next file is %10.3f seconds away' % (len(thisSet), timeDeltaSec)
41+
# This sequence doesn't have enough images; ignore them.
42+
print(f'Set with {len(this_set):3d} images discarded (too short); '
43+
f'next file is {time_delta_sec:10.3f} seconds away')
3344

34-
thisSet = [filename] # start a new one.
35-
else:
36-
thisSet.append(filename) # first time around; add file to set.
37-
lastDate = thisDate
45+
this_set = [filename] # start a new set with this image.
46+
last_date = this_date
3847

39-
if len(thisSet) >= minImagesPerSet:
48+
if len(this_set) >= min_images_per_set:
4049
# Trailing images got collected here. That's our last set.
41-
print 'Set with %3d images formed; arrived at last file' % len(thisSet)
42-
sets.append(thisSet)
50+
print(f'Set with {len(this_set):3d} images formed; arrived at last file.')
51+
sets.append(this_set)
4352

44-
for i in range(0, len(sets)):
53+
# Now that the sets are formed, physically move the files into set folders.
54+
for i in range(len(sets)):
4555
set = sets[i]
46-
setNum = i + setOffset
47-
print 'set %2d: %s to %s (%3d images)' % (setNum, os.path.basename(set[0]),
48-
os.path.basename(set[-1]), len(set))
49-
50-
newDirName = os.path.join(dir, 'set_%02d' % setNum)
51-
os.mkdir(newDirName)
52-
for oldFile in set:
53-
# By putting a hyphen in the name, we can then use negative file numbers to encode sequences in reverse.
54-
newFile = os.path.join(newDirName, 'image-%s' % os.path.basename(oldFile)[len('DSC'):])
55-
print '\t%s --> %s' % (oldFile, newFile)
56-
shutil.move(oldFile, newFile)
56+
set_num = i + set_offset
57+
print(f'set {set_num:2d}: {os.path.basename(set[0])} to {os.path.basename(set[-1])} '
58+
f'({len(set):3d} images)')
59+
60+
new_dir = os.path.join(input_dir, 'set_%02d' % set_num)
61+
os.mkdir(new_dir)
62+
for old_file in set:
63+
# By putting a hyphen in the name, we can then use negative file numbers
64+
# to encode sequences in reverse.
65+
newFile = os.path.join(new_dir, f'image-{os.path.basename(old_file)[len(photo_prefix):]}')
66+
print(f'\t{old_file} --> {newFile}')
67+
shutil.move(old_file, newFile)

video_maker.py

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,93 @@
1-
import os, glob, math
1+
# Make videos from images that have been sorted into sets.
22

3-
dir = os.path.expanduser('~/Desktop/DCIM/100MSDCF')
3+
import os, glob, math, tempfile
4+
from constants import *
45

5-
quality = 15 # MP4 quality; lower is better
6-
fps = 24
7-
boomerangLengthSeconds = 15 # final video length, minimum.
8-
sets = sorted(glob.glob('%s/set_*' % dir))
6+
temp_dir = tempfile.mkdtemp(prefix='video_maker_')
7+
8+
def get_output(cmd: str) -> str:
9+
# Super lazy way to get the output of a program: output to a file and then
10+
# read that file.
11+
temp_file = os.path.join(temp_dir, 'temp.txt')
12+
os.system(f'{cmd} > {temp_file}')
13+
with open(temp_file, 'r') as f:
14+
return f.read()
15+
16+
sets = sorted(glob.glob(f'{input_dir}/set_*'))
17+
output_dims = None
918
for set in sets:
1019
if not os.path.isdir(set): continue
11-
files = sorted(glob.glob('%s/*.JPG' % set))
12-
if len(files) < 2:
13-
print 'Set does not contain enough images!'
20+
files = sorted(glob.glob(f'{set}/*{photo_ext}'))
21+
if len(files) < min_images_per_set:
22+
print('Set does not contain enough images!')
1423
continue
15-
setName = os.path.basename(set)
16-
print setName
17-
# Check the orientation of these images. Currently only support "horizontal" and "90 CW".
18-
os.system('exiftool -Orientation -n %s > /tmp/orient.txt' % files[0])
19-
with open('/tmp/orient.txt', 'r') as f:
20-
orientation = f.read()
24+
set_name = os.path.basename(set)
25+
print(set_name)
26+
if output_dims is None:
27+
# We have to compute our own output size, maintaining aspect ratio, and
28+
# making sure the height is divisible by 2.
29+
exiftool = f'{exiftool_call} -ImageWidth -s -s -s {files[0]}' # -s 3 times gets value only
30+
width = int(get_output(exiftool))
31+
exiftool = f'{exiftool_call} -ImageHeight -s -s -s {files[0]}' # -s 3 times gets value only
32+
height = int(get_output(exiftool))
33+
AR = height / float(width)
34+
new_width = video_width
35+
new_height = int(new_width * AR)
36+
if new_height % 2:
37+
new_height -=1
38+
output_dims = f'{new_width}x{new_height}'
39+
print(f'{width}x{height} -> {output_dims}')
40+
# Check the orientation of these images. Currently only support "horizontal"
41+
# and "90 CW".
42+
exiftool = f'{exiftool_call} -Orientation -n {files[0]}'
43+
orientation = get_output(exiftool)
2144
if ': 1' in orientation:
22-
vf = 'scale=1920:1280'
23-
print 'Images are horizontal'
45+
vf = f'scale={output_dims}'
46+
print('Images are horizontal')
2447
elif ': 6' in orientation:
25-
vf = 'scale=1920:1280,transpose=1' # see https://stackoverflow.com/a/9570992
26-
print 'Images are vertical'
48+
vf = f'scale={output_dims},transpose=1' # see https://stackoverflow.com/a/9570992
49+
print('Images are vertical')
2750
else:
28-
raise ValueError('Unsupported image rotation!\n%s' % orientation)
51+
raise RuntimeError(f'Unsupported image rotation!\n{orientation}')
2952
# Start at the second frame, because the reverse clip will end at the first frame.
30-
startNum = int(os.path.basename(files[1])[len('image-'):-len('.JPG')])
31-
videoFileForward = os.path.join(dir, '%s_forward.mp4' % setName)
32-
ffmpeg = "ffmpeg -y -start_number %d -r %d -i '%s/image-%%05d.JPG' -vf '%s' -vcodec libx264 " \
33-
"-crf %d -pix_fmt yuv420p %s > /dev/null 2>&1" % (startNum, fps, set, vf, quality, videoFileForward)
34-
print 'Forward'
53+
start_num = int(os.path.basename(files[1])[len('image-'):-len(photo_ext)])
54+
vid_file_fwd = os.path.join(input_dir, f'{set_name}_forward.mp4')
55+
ffmpeg = f"{ffmpeg_call} -y -start_number {start_num} -r {fps} -i \"{set}/image-%05d.JPG\" " \
56+
f"-vf \"{vf}\" -vcodec libx264 -crf {quality} -pix_fmt yuv420p {vid_file_fwd} " \
57+
f"> {dev_null} 2>&1"
58+
print('Forward')
3559
os.system(ffmpeg)
3660

3761
# Reverse clip starts at the second to last frame, because the forward clip ends at the last frame.
38-
startNum = int(os.path.basename(files[-2])[len('image'):-len('.JPG')])
39-
videoFileReverse = os.path.join(dir, '%s_reverse.mp4' % setName)
40-
ffmpeg = "ffmpeg -y -start_number %d -r %d -i '%s/image%%05d.JPG' -vf '%s' -vcodec libx264 " \
41-
"-crf %d -pix_fmt yuv420p %s > /dev/null 2>&1" % (startNum, fps, set, vf, quality, videoFileReverse)
42-
print 'Reverse'
62+
start_num = int(os.path.basename(files[-2])[len('image'):-len(photo_ext)])
63+
vid_file_rev = os.path.join(input_dir, f'{set_name}_reverse.mp4')
64+
ffmpeg = f"{ffmpeg_call} -y -start_number {start_num} -r {fps} -i \"{set}/image%05d.JPG\" " \
65+
f"-vf \"{vf}\" -vcodec libx264 -crf {quality} -pix_fmt yuv420p {vid_file_rev} " \
66+
f"> {dev_null} 2>&1"
67+
print('Reverse')
4368
os.system(ffmpeg)
4469

4570
# How long is our sequence?
46-
numFrames = len(files)
47-
lengthSeconds = numFrames / float(fps) * 2 # 2 for forward and reverse
48-
numPairs = int(math.ceil(boomerangLengthSeconds / lengthSeconds))
71+
num_frames = len(files)
72+
len_seconds = num_frames / float(fps) * 2 # 2 for forward and reverse
73+
num_pairs = int(math.ceil(boomerang_length_seconds / len_seconds))
74+
75+
print(f'Pair is {len_seconds:.2f} seconds long. Will require {num_pairs} pairs to hit '
76+
f'{boomerang_length_seconds} seconds.')
4977

50-
print 'Pair is %.2f seconds long. Will require %d pairs to hit %d seconds.' % (
51-
lengthSeconds, numPairs, boomerangLengthSeconds
52-
)
5378
# Make the concat list.
54-
with open('/tmp/concat.txt', 'w') as f:
55-
for i in range(0, numPairs):
56-
f.write('file %s\n' % videoFileForward)
57-
f.write('file %s\n' % videoFileReverse)
79+
concat_file = os.path.join(temp_dir, 'concat.txt')
80+
# ffmpeg needs the backslashes escaped when loading a file list from a file.
81+
fwd = vid_file_fwd.replace("\\", "\\\\")
82+
rev = vid_file_rev.replace("\\", "\\\\")
83+
with open(concat_file, 'w') as f:
84+
for i in range(num_pairs):
85+
f.write(f'file {fwd}\n')
86+
f.write(f'file {rev}\n')
5887

59-
videoFile = os.path.join(dir, '%s_boomerang.mp4' % setName)
88+
video_file = os.path.join(input_dir, f'{set_name}_boomerang.mp4')
6089

61-
ffmpeg = "ffmpeg -y -safe 0 -f concat -i '/tmp/concat.txt' -c copy %s > /dev/null 2>&1" % (videoFile)
62-
print 'Concat'
63-
# print ffmpeg
90+
ffmpeg = f"{ffmpeg_call} -y -safe 0 -f concat -i \"{concat_file}\" -c copy {video_file} > {dev_null} 2>&1"
91+
print('Concat')
6492
os.system(ffmpeg)
65-
print ''
93+
print()

0 commit comments

Comments
 (0)