browsertime 19.1.0 → 19.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +113 -89
- package/lib/core/engine/command/chromeTrace.js +4 -1
- package/lib/video/postprocessing/visualmetrics/visualMetrics.js +2 -2
- package/package.json +10 -10
- package/browsertime/__init__.py +0 -1
- package/browsertime/test_data/ms_000000.png +0 -0
- package/browsertime/test_data/ms_000920.png +0 -0
- package/browsertime/test_data/ms_001000.png +0 -0
- package/browsertime/test_data/ms_001080.png +0 -0
- package/browsertime/test_data/ms_001200.png +0 -0
- package/browsertime/test_data/ms_001240.png +0 -0
- package/browsertime/test_data/ms_001280.png +0 -0
- package/browsertime/test_data/ms_001360.png +0 -0
- package/browsertime/test_data/ms_001400.png +0 -0
- package/browsertime/test_data/ms_001520.png +0 -0
- package/browsertime/test_data/ms_002040.png +0 -0
- package/browsertime/test_data/ms_002600.png +0 -0
- package/browsertime/test_data/ms_003160.png +0 -0
- package/browsertime/test_data/ms_003720.png +0 -0
- package/browsertime/test_data/ms_004280.png +0 -0
- package/browsertime/test_data/ms_004880.png +0 -0
- package/browsertime/test_data/ms_005440.png +0 -0
- package/browsertime/test_data/ms_006000.png +0 -0
- package/browsertime/test_visualmetrics.py +0 -34
- package/browsertime/visualmetrics-portable.py +0 -1969
- package/browsertime/visualmetrics.py +0 -2518
|
@@ -1,1969 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Copyright (c) 2014, Google Inc.
|
|
4
|
-
All rights reserved.
|
|
5
|
-
|
|
6
|
-
Redistribution and use in source and binary forms, with or without modification,
|
|
7
|
-
are permitted provided that the following conditions are met:
|
|
8
|
-
|
|
9
|
-
* Redistributions of source code must retain the above copyright notice,
|
|
10
|
-
this list of conditions and the following disclaimer.
|
|
11
|
-
* Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
-
this list of conditions and the following disclaimer in the documentation
|
|
13
|
-
and/or other materials provided with the distribution.
|
|
14
|
-
* Neither the name of the company nor the names of its contributors may be
|
|
15
|
-
used to endorse or promote products derived from this software without
|
|
16
|
-
specific prior written permission.
|
|
17
|
-
|
|
18
|
-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
19
|
-
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
20
|
-
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
21
|
-
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
22
|
-
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
23
|
-
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
24
|
-
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
25
|
-
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
26
|
-
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
27
|
-
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
28
|
-
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."""
|
|
29
|
-
#
|
|
30
|
-
# The original script from Google was heavily modified for the Browsertime
|
|
31
|
-
# project.
|
|
32
|
-
#
|
|
33
|
-
import gc
|
|
34
|
-
import glob
|
|
35
|
-
import gzip
|
|
36
|
-
import json
|
|
37
|
-
import logging
|
|
38
|
-
import math
|
|
39
|
-
import os
|
|
40
|
-
import re
|
|
41
|
-
import sys
|
|
42
|
-
import shutil
|
|
43
|
-
import subprocess
|
|
44
|
-
import tempfile
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
GZIP_TEXT = "wt"
|
|
48
|
-
GZIP_READ_TEXT = "rt"
|
|
49
|
-
|
|
50
|
-
# Globals
|
|
51
|
-
options = None
|
|
52
|
-
client_viewport = None
|
|
53
|
-
frame_cache = {}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# #################################################################################################
|
|
57
|
-
# Replacement methods for ImageMagick to Python conversion
|
|
58
|
-
# #################################################################################################
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def compare(img1, img2, fuzz=0.10):
|
|
62
|
-
"""Calculate the Absolute Error count between given images."""
|
|
63
|
-
try:
|
|
64
|
-
import numpy as np
|
|
65
|
-
|
|
66
|
-
img1_data = np.array(img1)
|
|
67
|
-
img2_data = np.array(img2)
|
|
68
|
-
|
|
69
|
-
inds = np.argwhere(
|
|
70
|
-
np.isclose(img1_data[:, :, 0], img2_data[:, :, 0], atol=fuzz * 255)
|
|
71
|
-
& np.isclose(img1_data[:, :, 1], img2_data[:, :, 1], atol=fuzz * 255)
|
|
72
|
-
& np.isclose(img1_data[:, :, 2], img2_data[:, :, 2], atol=fuzz * 255)
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
return (img1_data.shape[0] * img1_data.shape[1]) - len(inds)
|
|
76
|
-
except BaseException as e:
|
|
77
|
-
logging.exception(e)
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def crop_im(img, crop_x, crop_y, crop_x_offset, crop_y_offset, gravity=None):
|
|
82
|
-
"""Crop an image.
|
|
83
|
-
|
|
84
|
-
If gravity is equal to "center", the crop region will
|
|
85
|
-
first be centered before applying the crop.
|
|
86
|
-
"""
|
|
87
|
-
try:
|
|
88
|
-
import numpy as np
|
|
89
|
-
from PIL import Image
|
|
90
|
-
|
|
91
|
-
img = np.array(img)
|
|
92
|
-
|
|
93
|
-
base_x = 0
|
|
94
|
-
base_y = 0
|
|
95
|
-
|
|
96
|
-
height, width, _ = img.shape
|
|
97
|
-
if gravity == "center":
|
|
98
|
-
base_x = width // 2
|
|
99
|
-
base_y = height // 2
|
|
100
|
-
|
|
101
|
-
base_x -= crop_x // 2
|
|
102
|
-
base_y -= crop_y // 2
|
|
103
|
-
|
|
104
|
-
# Handle the boundaries of the crop using max to prevent
|
|
105
|
-
# negatives, and min to prevent going over the othersde of
|
|
106
|
-
# the image
|
|
107
|
-
start_x = min(width - 1, max(base_x + crop_x_offset, 0))
|
|
108
|
-
start_y = min(height - 1, max(base_y + crop_y_offset, 0))
|
|
109
|
-
|
|
110
|
-
end_x = min(width - 1, max(start_x + crop_x, 0))
|
|
111
|
-
end_y = min(height - 1, max(start_y + crop_y, 0))
|
|
112
|
-
|
|
113
|
-
if len(img[start_y:end_y, start_x:end_x, :]) == 0:
|
|
114
|
-
raise Exception(
|
|
115
|
-
f"Cropped image is empty. Image dimensions: {img.shape}, "
|
|
116
|
-
f"Crop Region: {crop_x}, {crop_y}, {crop_x_offset}, {crop_y_offset}"
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
return Image.fromarray(img[start_y:end_y, start_x:end_x, :])
|
|
120
|
-
except BaseException as e:
|
|
121
|
-
logging.exception(e)
|
|
122
|
-
return None
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def resize(img, width, height):
|
|
126
|
-
"""Resize an image to the given width, and height."""
|
|
127
|
-
|
|
128
|
-
try:
|
|
129
|
-
from PIL import Image
|
|
130
|
-
|
|
131
|
-
try:
|
|
132
|
-
# If it's a numpy array, convert it first
|
|
133
|
-
img = Image.fromarray(img)
|
|
134
|
-
except:
|
|
135
|
-
pass
|
|
136
|
-
|
|
137
|
-
return img.resize((width, height), resample=Image.LANCZOS)
|
|
138
|
-
except BaseException as e:
|
|
139
|
-
logging.exception(e)
|
|
140
|
-
return None
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def scale(img, maxsize):
|
|
144
|
-
"""Scale an image to the given max size."""
|
|
145
|
-
width, height = img.size
|
|
146
|
-
ratio = min(float(maxsize) / width, float(maxsize) / height)
|
|
147
|
-
return resize(img, int(width * ratio), int(height * ratio))
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def mask(
|
|
151
|
-
img, x_mask, y_mask, x_offset, y_offset, color=(255, 255, 255), insert_img=None
|
|
152
|
-
):
|
|
153
|
-
"""Mask an image.
|
|
154
|
-
|
|
155
|
-
If insert_img is provided, the image given will mask the region
|
|
156
|
-
specified. Otherwise, by default, the region specified will be covered
|
|
157
|
-
in white - change color to change the mask color.
|
|
158
|
-
"""
|
|
159
|
-
try:
|
|
160
|
-
import numpy as np
|
|
161
|
-
from PIL import Image
|
|
162
|
-
|
|
163
|
-
img_data = np.array(img)
|
|
164
|
-
if insert_img is not None:
|
|
165
|
-
insert_img_data = np.array(insert_img)
|
|
166
|
-
img_data[
|
|
167
|
-
y_offset : y_offset + y_mask, x_offset : x_offset + x_mask, :
|
|
168
|
-
] = insert_img
|
|
169
|
-
else:
|
|
170
|
-
img_data[
|
|
171
|
-
y_offset : y_offset + y_mask, x_offset : x_offset + x_mask, :
|
|
172
|
-
] = color
|
|
173
|
-
|
|
174
|
-
return Image.fromarray(img_data)
|
|
175
|
-
except BaseException as e:
|
|
176
|
-
logging.exception(e)
|
|
177
|
-
return None
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def blank_frame(file, color="white"):
|
|
181
|
-
"""Return a new blank frame that has the same dimensions as file."""
|
|
182
|
-
try:
|
|
183
|
-
from PIL import Image
|
|
184
|
-
|
|
185
|
-
with Image.open(file) as im:
|
|
186
|
-
width, height = im.size
|
|
187
|
-
return Image.new("RGB", (width, height), color=color)
|
|
188
|
-
except BaseException as e:
|
|
189
|
-
logging.exception(e)
|
|
190
|
-
return None
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def edges_im(img):
|
|
194
|
-
"""Find the edges of the given image.
|
|
195
|
-
|
|
196
|
-
First, we apply a gaussian filter using a kernal of radius=13,
|
|
197
|
-
and sigma=1 to a grayscale version of the image. Then we apply
|
|
198
|
-
CED to find the edges.
|
|
199
|
-
|
|
200
|
-
We calculate the hysterisis thresholds for the CED using the min and max
|
|
201
|
-
vaues of the blurred image. We use 10% as the lower threshold,
|
|
202
|
-
and 30% as the upper threshold.
|
|
203
|
-
"""
|
|
204
|
-
try:
|
|
205
|
-
import cv2
|
|
206
|
-
import numpy as np
|
|
207
|
-
from PIL import Image, ImageOps
|
|
208
|
-
|
|
209
|
-
gs_img = np.array(ImageOps.grayscale(img))
|
|
210
|
-
blurred_img = cv2.GaussianBlur(gs_img, (13, 13), 1)
|
|
211
|
-
|
|
212
|
-
# Calculate the threshold values for double-thresholding
|
|
213
|
-
min_g = np.min(blurred_img[:])
|
|
214
|
-
max_g = np.max(blurred_img[:])
|
|
215
|
-
edge_img = cv2.Canny(
|
|
216
|
-
blurred_img, 0.10 * (max_g - min_g) + min_g, 0.30 * (max_g - min_g) + min_g
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
return Image.fromarray(edge_img)
|
|
220
|
-
except BaseException as e:
|
|
221
|
-
logging.exception(e)
|
|
222
|
-
return None
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def contentful_value(img):
|
|
226
|
-
"""
|
|
227
|
-
Get the contentful value by counting the number of
|
|
228
|
-
defined pixels in the image of the edges.
|
|
229
|
-
"""
|
|
230
|
-
try:
|
|
231
|
-
import numpy as np
|
|
232
|
-
|
|
233
|
-
edge_img = np.array(edges_im(img))
|
|
234
|
-
white_pixels = np.where(edge_img != 0)
|
|
235
|
-
|
|
236
|
-
return len(white_pixels[0])
|
|
237
|
-
except BaseException as e:
|
|
238
|
-
logging.exception(e)
|
|
239
|
-
return None
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def build_edge_video(video_path, viewport):
|
|
243
|
-
"""Compute, and highlight the edges of a given video.
|
|
244
|
-
|
|
245
|
-
Makes use of the same technique as the contentful value
|
|
246
|
-
calculation. However, it crops, and scales the image using
|
|
247
|
-
our own method rather than FFMPEG. This creates two videos
|
|
248
|
-
suffixed with `-edges` and `-edges-overlay` that contain
|
|
249
|
-
the raw edges, and a video with the edges overlaid. They will
|
|
250
|
-
be found in the same location as the original video.
|
|
251
|
-
|
|
252
|
-
These videos will only be produced when --contentful-video is
|
|
253
|
-
used.
|
|
254
|
-
"""
|
|
255
|
-
logging.debug("Creating edge video for {0}".format(video_path))
|
|
256
|
-
|
|
257
|
-
output_dir, video_name = os.path.split(video_path)
|
|
258
|
-
video_name, _ = os.path.splitext(video_name)
|
|
259
|
-
|
|
260
|
-
try:
|
|
261
|
-
import cv2
|
|
262
|
-
import numpy as np
|
|
263
|
-
from PIL import Image, ImageOps
|
|
264
|
-
|
|
265
|
-
# Get the edges of all frames
|
|
266
|
-
edge_video = []
|
|
267
|
-
resized_video = []
|
|
268
|
-
video = cv2.VideoCapture(video_path)
|
|
269
|
-
frame_count = video.get(cv2.CAP_PROP_FPS)
|
|
270
|
-
while video.isOpened():
|
|
271
|
-
ret, frame = video.read()
|
|
272
|
-
if ret:
|
|
273
|
-
cropped_im = frame
|
|
274
|
-
if viewport:
|
|
275
|
-
cropped_im = crop_im(
|
|
276
|
-
frame,
|
|
277
|
-
viewport["width"],
|
|
278
|
-
viewport["height"],
|
|
279
|
-
viewport["x"],
|
|
280
|
-
viewport["y"],
|
|
281
|
-
)
|
|
282
|
-
resized_video.append(scale(cropped_im, options.thumbsize))
|
|
283
|
-
edge_video.append(np.array(edges_im(resized_video[-1])))
|
|
284
|
-
else:
|
|
285
|
-
video.release()
|
|
286
|
-
break
|
|
287
|
-
|
|
288
|
-
out_size = edge_video[-1].shape
|
|
289
|
-
out_edges = cv2.VideoWriter(
|
|
290
|
-
os.path.join(output_dir, video_name + "-edges.mp4"),
|
|
291
|
-
cv2.VideoWriter_fourcc(*"MP4V"),
|
|
292
|
-
frame_count,
|
|
293
|
-
(out_size[1], out_size[0]),
|
|
294
|
-
1,
|
|
295
|
-
)
|
|
296
|
-
out_edges_overlay = cv2.VideoWriter(
|
|
297
|
-
os.path.join(output_dir, video_name + "-edges-overlay.mp4"),
|
|
298
|
-
cv2.VideoWriter_fourcc(*"MP4V"),
|
|
299
|
-
frame_count,
|
|
300
|
-
(out_size[1], out_size[0]),
|
|
301
|
-
1,
|
|
302
|
-
)
|
|
303
|
-
for i, frame in enumerate(edge_video):
|
|
304
|
-
cframe = np.zeros((out_size[0], out_size[1], 3))
|
|
305
|
-
overlayframe = np.array(resized_video[i])
|
|
306
|
-
for x in range(cframe.shape[0]):
|
|
307
|
-
for y in range(cframe.shape[1]):
|
|
308
|
-
if frame[x, y] != 0:
|
|
309
|
-
cframe[x, y, :] = (0, 0, 255)
|
|
310
|
-
overlayframe[x, y, :] = (0, 0, 255)
|
|
311
|
-
out_edges.write(np.uint8(cframe))
|
|
312
|
-
out_edges_overlay.write(np.uint8(overlayframe))
|
|
313
|
-
|
|
314
|
-
out_edges.release()
|
|
315
|
-
out_edges_overlay.release()
|
|
316
|
-
|
|
317
|
-
logging.debug("Finished creating edge videos for {0}".format(video_path))
|
|
318
|
-
except BaseException as e:
|
|
319
|
-
logging.exception(e)
|
|
320
|
-
return
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def convert_to_srgb(img):
|
|
324
|
-
"""Convert PIL image to sRGB color space (if possible)"""
|
|
325
|
-
try:
|
|
326
|
-
import io
|
|
327
|
-
from PIL import Image, ImageCms
|
|
328
|
-
|
|
329
|
-
icc = img.info.get("icc_profile", "")
|
|
330
|
-
|
|
331
|
-
if icc:
|
|
332
|
-
return ImageCms.profileToProfile(
|
|
333
|
-
img,
|
|
334
|
-
ImageCms.ImageCmsProfile(io.BytesIO(icc)),
|
|
335
|
-
ImageCms.createProfile("sRGB"),
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
logging.debug(
|
|
339
|
-
"Unable to convert image to sRGB as there is no color "
|
|
340
|
-
"profile to transform from."
|
|
341
|
-
)
|
|
342
|
-
return img
|
|
343
|
-
except BaseException as e:
|
|
344
|
-
logging.exception(e)
|
|
345
|
-
return None
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def convert_img_to_jpeg(src, dest, quality=30):
|
|
349
|
-
"""Convert an image to a JPEG with the given quality."""
|
|
350
|
-
try:
|
|
351
|
-
from PIL import Image
|
|
352
|
-
|
|
353
|
-
with Image.open(src) as img:
|
|
354
|
-
img = convert_to_srgb(img)
|
|
355
|
-
img.save(dest, quality=quality)
|
|
356
|
-
except BaseException as e:
|
|
357
|
-
logging.exception(e)
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
# #################################################################################################
|
|
362
|
-
# Frame Extraction and de-duplication
|
|
363
|
-
# #################################################################################################
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def video_to_frames(
|
|
367
|
-
video,
|
|
368
|
-
directory,
|
|
369
|
-
force,
|
|
370
|
-
orange_file,
|
|
371
|
-
find_viewport,
|
|
372
|
-
viewport_retries,
|
|
373
|
-
viewport_min_height,
|
|
374
|
-
viewport_min_width,
|
|
375
|
-
full_resolution,
|
|
376
|
-
):
|
|
377
|
-
"""Extract the video frames"""
|
|
378
|
-
global client_viewport
|
|
379
|
-
first_frame = os.path.join(directory, "ms_000000")
|
|
380
|
-
if (
|
|
381
|
-
not os.path.isfile(first_frame + ".png")
|
|
382
|
-
and not os.path.isfile(first_frame + ".jpg")
|
|
383
|
-
) or force:
|
|
384
|
-
if os.path.isfile(video):
|
|
385
|
-
video = os.path.realpath(video)
|
|
386
|
-
logging.info("Processing frames from video " + video + " to " + directory)
|
|
387
|
-
is_mobile = find_recording_platform(video)
|
|
388
|
-
if os.path.isdir(directory):
|
|
389
|
-
shutil.rmtree(directory, True)
|
|
390
|
-
if not os.path.isdir(directory):
|
|
391
|
-
os.mkdir(directory, 0o755)
|
|
392
|
-
if os.path.isdir(directory):
|
|
393
|
-
directory = os.path.realpath(directory)
|
|
394
|
-
viewport, cropped = find_video_viewport(
|
|
395
|
-
video,
|
|
396
|
-
directory,
|
|
397
|
-
find_viewport,
|
|
398
|
-
viewport_retries,
|
|
399
|
-
viewport_min_height,
|
|
400
|
-
viewport_min_width,
|
|
401
|
-
is_mobile,
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
if options.contentful_video:
|
|
405
|
-
# Create some videos with the edges
|
|
406
|
-
build_edge_video(video, viewport)
|
|
407
|
-
|
|
408
|
-
gc.collect()
|
|
409
|
-
if extract_frames(video, directory, full_resolution, viewport):
|
|
410
|
-
client_viewport = None
|
|
411
|
-
directories = [directory]
|
|
412
|
-
for dir in directories:
|
|
413
|
-
if orange_file is not None:
|
|
414
|
-
remove_frames_before_orange(dir, orange_file)
|
|
415
|
-
remove_orange_frames(dir, orange_file)
|
|
416
|
-
find_render_start(dir, orange_file, cropped, is_mobile)
|
|
417
|
-
adjust_frame_times(dir)
|
|
418
|
-
eliminate_duplicate_frames(dir, cropped, is_mobile)
|
|
419
|
-
crop_viewport(dir)
|
|
420
|
-
gc.collect()
|
|
421
|
-
else:
|
|
422
|
-
logging.critical("Error extracting the video frames from %s", video)
|
|
423
|
-
else:
|
|
424
|
-
logging.critical("Error creating output directory: %s", directory)
|
|
425
|
-
else:
|
|
426
|
-
logging.critical("Input video file %s does not exist", video)
|
|
427
|
-
else:
|
|
428
|
-
logging.info("Extracted video already exists in %s", directory)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def extract_frames(video, directory, full_resolution, viewport):
|
|
432
|
-
"""Extract and number the video frames"""
|
|
433
|
-
ret = False
|
|
434
|
-
logging.info("Extracting frames from " + video + " to " + directory)
|
|
435
|
-
decimate = get_decimate_filter()
|
|
436
|
-
if decimate is not None:
|
|
437
|
-
crop = ""
|
|
438
|
-
if viewport is not None:
|
|
439
|
-
crop = "crop={0}:{1}:{2}:{3},".format(
|
|
440
|
-
viewport["width"], viewport["height"], viewport["x"], viewport["y"]
|
|
441
|
-
)
|
|
442
|
-
scale = "scale=iw*min({0:d}/iw\\,{0:d}/ih):ih*min({0:d}/iw\\,{0:d}/ih),".format(
|
|
443
|
-
options.thumbsize
|
|
444
|
-
)
|
|
445
|
-
if full_resolution:
|
|
446
|
-
scale = ""
|
|
447
|
-
# escape directory name
|
|
448
|
-
# see https://en.wikibooks.org/wiki/FFMPEG_An_Intermediate_Guide/image_sequence#Percent_in_filename
|
|
449
|
-
dir_escaped = directory.replace("%", "%%")
|
|
450
|
-
command = [
|
|
451
|
-
"ffmpeg",
|
|
452
|
-
"-v",
|
|
453
|
-
"debug",
|
|
454
|
-
"-i",
|
|
455
|
-
video,
|
|
456
|
-
"-vsync",
|
|
457
|
-
"0",
|
|
458
|
-
"-vf",
|
|
459
|
-
crop + scale + decimate + "=0:64:640:0.001",
|
|
460
|
-
os.path.join(dir_escaped, "img-%d.png"),
|
|
461
|
-
]
|
|
462
|
-
logging.debug(" ".join(command))
|
|
463
|
-
lines = []
|
|
464
|
-
|
|
465
|
-
proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
|
|
466
|
-
while proc.poll() is None:
|
|
467
|
-
lines.extend(iter(proc.stderr.readline, ""))
|
|
468
|
-
|
|
469
|
-
pattern = re.compile(r"keep pts:[0-9]+ pts_time:(?P<timecode>[0-9\.]+)")
|
|
470
|
-
frame_count = 0
|
|
471
|
-
for line in lines:
|
|
472
|
-
match = re.search(pattern, line)
|
|
473
|
-
if match:
|
|
474
|
-
frame_count += 1
|
|
475
|
-
frame_time = int(
|
|
476
|
-
math.floor(float(match.groupdict().get("timecode")) * 1000)
|
|
477
|
-
)
|
|
478
|
-
src = os.path.join(directory, "img-{0:d}.png".format(frame_count))
|
|
479
|
-
dest = os.path.join(directory, "video-{0:06d}.png".format(frame_time))
|
|
480
|
-
logging.debug("Renaming " + src + " to " + dest)
|
|
481
|
-
os.rename(src, dest)
|
|
482
|
-
ret = True
|
|
483
|
-
return ret
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
def find_recording_platform(video):
|
|
487
|
-
"""Find the platform that this video was recorded on.
|
|
488
|
-
|
|
489
|
-
We can make use of a field called `com.android.version` to
|
|
490
|
-
determine if we've recorded on mobile or not.
|
|
491
|
-
"""
|
|
492
|
-
command = ["ffprobe", video]
|
|
493
|
-
logging.debug(command)
|
|
494
|
-
|
|
495
|
-
lines = []
|
|
496
|
-
proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
|
|
497
|
-
|
|
498
|
-
while proc.poll() is None:
|
|
499
|
-
lines.extend(iter(proc.stderr.readline, ""))
|
|
500
|
-
|
|
501
|
-
is_mobile = False
|
|
502
|
-
matcher = re.compile(".*com\.android\.version.*")
|
|
503
|
-
for line in lines:
|
|
504
|
-
if matcher.search(line):
|
|
505
|
-
is_mobile = True
|
|
506
|
-
|
|
507
|
-
return is_mobile
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
def remove_frames_before_orange(directory, orange_file):
|
|
511
|
-
"""Remove stray frames from the start of the video"""
|
|
512
|
-
frames = sorted(glob.glob(os.path.join(directory, "video-*.png")))
|
|
513
|
-
if len(frames):
|
|
514
|
-
# go through the first 20 frames and remove any that come before the first orange frame.
|
|
515
|
-
# iOS video capture starts with a blank white frame and then flips to
|
|
516
|
-
# orange before starting.
|
|
517
|
-
logging.debug("Scanning for non-orange frames...")
|
|
518
|
-
found_orange = False
|
|
519
|
-
remove_frames = []
|
|
520
|
-
frame_count = 0
|
|
521
|
-
for frame in frames:
|
|
522
|
-
frame_count += 1
|
|
523
|
-
if is_color_frame(frame, orange_file):
|
|
524
|
-
found_orange = True
|
|
525
|
-
break
|
|
526
|
-
if frame_count > 20:
|
|
527
|
-
break
|
|
528
|
-
remove_frames.append(frame)
|
|
529
|
-
|
|
530
|
-
if found_orange and len(remove_frames):
|
|
531
|
-
for frame in remove_frames:
|
|
532
|
-
logging.debug("Removing pre-orange frame %s", frame)
|
|
533
|
-
os.remove(frame)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
def remove_orange_frames(directory, orange_file):
|
|
537
|
-
"""Remove orange frames from the beginning of the video"""
|
|
538
|
-
frames = sorted(glob.glob(os.path.join(directory, "video-*.png")))
|
|
539
|
-
if len(frames):
|
|
540
|
-
logging.debug("Scanning for orange frames...")
|
|
541
|
-
for frame in frames:
|
|
542
|
-
if is_color_frame(frame, orange_file):
|
|
543
|
-
logging.debug("Removing Orange frame: %s", frame)
|
|
544
|
-
os.remove(frame)
|
|
545
|
-
else:
|
|
546
|
-
break
|
|
547
|
-
for frame in reversed(frames):
|
|
548
|
-
if is_color_frame(frame, orange_file):
|
|
549
|
-
logging.debug("Removing orange frame %s from the end", frame)
|
|
550
|
-
os.remove(frame)
|
|
551
|
-
else:
|
|
552
|
-
break
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
def find_image_viewport(file, is_mobile):
|
|
556
|
-
logging.debug("Finding the viewport for %s", file)
|
|
557
|
-
try:
|
|
558
|
-
from PIL import Image
|
|
559
|
-
|
|
560
|
-
im = Image.open(file)
|
|
561
|
-
width, height = im.size
|
|
562
|
-
x = int(math.floor(width / 2))
|
|
563
|
-
y = int(math.floor(height / 2))
|
|
564
|
-
pixels = im.load()
|
|
565
|
-
background = pixels[x, y]
|
|
566
|
-
|
|
567
|
-
# Find the left edge
|
|
568
|
-
left = None
|
|
569
|
-
while left is None and x >= 0:
|
|
570
|
-
if not colors_are_similar(background, pixels[x, y]):
|
|
571
|
-
left = x + 1
|
|
572
|
-
else:
|
|
573
|
-
x -= 1
|
|
574
|
-
if left is None:
|
|
575
|
-
left = 0
|
|
576
|
-
logging.debug("Viewport left edge is %d", left)
|
|
577
|
-
|
|
578
|
-
# Find the right edge
|
|
579
|
-
x = int(math.floor(width / 2))
|
|
580
|
-
right = None
|
|
581
|
-
while right is None and x < width:
|
|
582
|
-
if not colors_are_similar(background, pixels[x, y]):
|
|
583
|
-
right = x - 1
|
|
584
|
-
else:
|
|
585
|
-
x += 1
|
|
586
|
-
if right is None:
|
|
587
|
-
right = width
|
|
588
|
-
logging.debug("Viewport right edge is {0:d}".format(right))
|
|
589
|
-
|
|
590
|
-
# Find the top edge
|
|
591
|
-
x = int(math.floor(width / 2))
|
|
592
|
-
top = None
|
|
593
|
-
while top is None and y >= 0:
|
|
594
|
-
if not colors_are_similar(background, pixels[x, y]):
|
|
595
|
-
top = y + 1
|
|
596
|
-
else:
|
|
597
|
-
y -= 1
|
|
598
|
-
if top is None:
|
|
599
|
-
top = 0
|
|
600
|
-
logging.debug("Viewport top edge is {0:d}".format(top))
|
|
601
|
-
|
|
602
|
-
# Find the bottom edge
|
|
603
|
-
y = int(math.floor(height / 2))
|
|
604
|
-
bottom = None
|
|
605
|
-
while bottom is None and y < height:
|
|
606
|
-
if not colors_are_similar(background, pixels[x, y]):
|
|
607
|
-
bottom = y - 1
|
|
608
|
-
else:
|
|
609
|
-
y += 1
|
|
610
|
-
if bottom is None:
|
|
611
|
-
bottom = height
|
|
612
|
-
logging.debug("Viewport bottom edge is {0:d}".format(bottom))
|
|
613
|
-
|
|
614
|
-
viewport = {
|
|
615
|
-
"x": left,
|
|
616
|
-
"y": top,
|
|
617
|
-
"width": (right - left),
|
|
618
|
-
"height": (bottom - top),
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if is_mobile:
|
|
622
|
-
# On mobile we need to ignore the top ~10 pixels because
|
|
623
|
-
# there is a visible progress bar there on some browsers.
|
|
624
|
-
viewport["y"] += 10
|
|
625
|
-
viewport["height"] -= 10
|
|
626
|
-
|
|
627
|
-
except Exception:
|
|
628
|
-
viewport = None
|
|
629
|
-
|
|
630
|
-
return viewport
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
def find_video_viewport(
|
|
634
|
-
video,
|
|
635
|
-
directory,
|
|
636
|
-
find_viewport,
|
|
637
|
-
viewport_retries,
|
|
638
|
-
viewport_min_height,
|
|
639
|
-
viewport_min_width,
|
|
640
|
-
is_mobile,
|
|
641
|
-
):
|
|
642
|
-
logging.debug("Finding Video Viewport...")
|
|
643
|
-
viewport = None
|
|
644
|
-
|
|
645
|
-
# cropped will be True if the viewport setting changes
|
|
646
|
-
# the original frame
|
|
647
|
-
cropped = False
|
|
648
|
-
|
|
649
|
-
try:
|
|
650
|
-
from PIL import Image
|
|
651
|
-
|
|
652
|
-
retries = -1
|
|
653
|
-
|
|
654
|
-
while (
|
|
655
|
-
viewport is None
|
|
656
|
-
or viewport["height"] <= viewport_min_height
|
|
657
|
-
or viewport["width"] <= viewport_min_width
|
|
658
|
-
):
|
|
659
|
-
retries += 1
|
|
660
|
-
if retries >= 1:
|
|
661
|
-
# In some cases, the first frame is not an orange screen or a screen
|
|
662
|
-
# with a solid color. In this case, we need to try finding the viewport
|
|
663
|
-
# using the next frame. The `viewport_retries` dictates the maximum number
|
|
664
|
-
# of frames to check.
|
|
665
|
-
if retries >= viewport_retries:
|
|
666
|
-
logging.exception(
|
|
667
|
-
"Could not calculate a viewport after %s tries.",
|
|
668
|
-
viewport_retries,
|
|
669
|
-
)
|
|
670
|
-
break
|
|
671
|
-
logging.info("Failed to find a good viewport. Retrying...")
|
|
672
|
-
|
|
673
|
-
logging.debug("Using frame " + str(retries))
|
|
674
|
-
|
|
675
|
-
frame = os.path.join(directory, "viewport.png")
|
|
676
|
-
if os.path.isfile(frame):
|
|
677
|
-
os.remove(frame)
|
|
678
|
-
|
|
679
|
-
command = ["ffmpeg", "-i", video]
|
|
680
|
-
|
|
681
|
-
# Pull one frame from the video starting with the frame at
|
|
682
|
-
# the `retries` index
|
|
683
|
-
command.extend(["-vf", "select=gte(n\\,%s)" % retries])
|
|
684
|
-
command.extend(["-frames:v", "1", frame])
|
|
685
|
-
subprocess.check_output(command)
|
|
686
|
-
|
|
687
|
-
if os.path.isfile(frame):
|
|
688
|
-
with Image.open(frame) as im:
|
|
689
|
-
width, height = im.size
|
|
690
|
-
logging.debug("%s is %dx%d", frame, width, height)
|
|
691
|
-
if find_viewport:
|
|
692
|
-
viewport = find_image_viewport(frame, is_mobile)
|
|
693
|
-
else:
|
|
694
|
-
viewport = {"x": 0, "y": 0, "width": width, "height": height}
|
|
695
|
-
|
|
696
|
-
os.remove(frame)
|
|
697
|
-
|
|
698
|
-
if viewport is not None and viewport != {
|
|
699
|
-
"x": 0,
|
|
700
|
-
"y": 0,
|
|
701
|
-
"width": width,
|
|
702
|
-
"height": height,
|
|
703
|
-
}:
|
|
704
|
-
cropped = True
|
|
705
|
-
|
|
706
|
-
except Exception as e:
|
|
707
|
-
viewport = None
|
|
708
|
-
|
|
709
|
-
return viewport, cropped
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
def adjust_frame_times(directory):
|
|
713
|
-
offset = None
|
|
714
|
-
frames = sorted(glob.glob(os.path.join(directory, "video-*.png")))
|
|
715
|
-
# Special hack to the the video start
|
|
716
|
-
# Let us tune this in the future to skip using a global
|
|
717
|
-
global videoRecordingStart
|
|
718
|
-
match = re.compile(r"video-(?P<ms>[0-9]+)\.png")
|
|
719
|
-
if len(frames):
|
|
720
|
-
for frame in frames:
|
|
721
|
-
m = re.search(match, frame)
|
|
722
|
-
if m is not None:
|
|
723
|
-
frame_time = int(m.groupdict().get("ms"))
|
|
724
|
-
if offset is None:
|
|
725
|
-
# This is the first frame.
|
|
726
|
-
videoRecordingStart = frame_time
|
|
727
|
-
offset = frame_time
|
|
728
|
-
new_time = frame_time - offset
|
|
729
|
-
dest = os.path.join(directory, "ms_{0:06d}.png".format(new_time))
|
|
730
|
-
os.rename(frame, dest)
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
def find_render_start(directory, orange_file, cropped, is_mobile):
|
|
734
|
-
logging.debug("Finding Render Start...")
|
|
735
|
-
try:
|
|
736
|
-
if (
|
|
737
|
-
client_viewport is not None
|
|
738
|
-
or options.viewport is not None
|
|
739
|
-
or (options.renderignore > 0 and options.renderignore <= 100)
|
|
740
|
-
):
|
|
741
|
-
files = sorted(glob.glob(os.path.join(directory, "video-*.png")))
|
|
742
|
-
count = len(files)
|
|
743
|
-
if count > 1:
|
|
744
|
-
from PIL import Image
|
|
745
|
-
|
|
746
|
-
first = files[0]
|
|
747
|
-
with Image.open(first) as im:
|
|
748
|
-
width, height = im.size
|
|
749
|
-
if options.renderignore > 0 and options.renderignore <= 100:
|
|
750
|
-
mask = {}
|
|
751
|
-
mask["width"] = int(math.floor(width * options.renderignore / 100))
|
|
752
|
-
mask["height"] = int(
|
|
753
|
-
math.floor(height * options.renderignore / 100)
|
|
754
|
-
)
|
|
755
|
-
mask["x"] = int(math.floor(width / 2 - mask["width"] / 2))
|
|
756
|
-
mask["y"] = int(math.floor(height / 2 - mask["height"] / 2))
|
|
757
|
-
else:
|
|
758
|
-
mask = None
|
|
759
|
-
|
|
760
|
-
im_width = width
|
|
761
|
-
im_height = height
|
|
762
|
-
|
|
763
|
-
top = 10
|
|
764
|
-
right_margin = 10
|
|
765
|
-
bottom_margin = 24
|
|
766
|
-
if height > 400 or width > 400:
|
|
767
|
-
top = max(top, int(math.ceil(float(height) * 0.03)))
|
|
768
|
-
right_margin = max(
|
|
769
|
-
right_margin, int(math.ceil(float(width) * 0.04))
|
|
770
|
-
)
|
|
771
|
-
bottom_margin = max(
|
|
772
|
-
bottom_margin, int(math.ceil(float(width) * 0.04))
|
|
773
|
-
)
|
|
774
|
-
height = max(height - top - bottom_margin, 1)
|
|
775
|
-
left = 0
|
|
776
|
-
width = max(width - right_margin, 1)
|
|
777
|
-
if client_viewport is not None:
|
|
778
|
-
height = max(client_viewport["height"] - top - bottom_margin, 1)
|
|
779
|
-
width = max(client_viewport["width"] - right_margin, 1)
|
|
780
|
-
left += client_viewport["x"]
|
|
781
|
-
top += client_viewport["y"]
|
|
782
|
-
elif cropped:
|
|
783
|
-
# The image was already cropped, so only cutout the bottom
|
|
784
|
-
# to get rid of the network request/etc. information for
|
|
785
|
-
# desktop videos, and nothing extra on mobile.
|
|
786
|
-
top = 0
|
|
787
|
-
left = 0
|
|
788
|
-
width = im_width
|
|
789
|
-
|
|
790
|
-
if is_mobile:
|
|
791
|
-
height = im_height
|
|
792
|
-
else:
|
|
793
|
-
height = im_height - bottom_margin
|
|
794
|
-
|
|
795
|
-
crop = (width, height, left, top)
|
|
796
|
-
|
|
797
|
-
for i in range(1, count):
|
|
798
|
-
if frames_match(first, files[i], 10, 0, crop, mask):
|
|
799
|
-
logging.debug("Removing pre-render frame %s", files[i])
|
|
800
|
-
os.remove(files[i])
|
|
801
|
-
elif orange_file is not None and is_color_frame(
|
|
802
|
-
files[i], orange_file
|
|
803
|
-
):
|
|
804
|
-
logging.debug("Removing orange frame %s", files[i])
|
|
805
|
-
os.remove(files[i])
|
|
806
|
-
else:
|
|
807
|
-
break
|
|
808
|
-
except BaseException:
|
|
809
|
-
logging.exception("Error getting render start")
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
def eliminate_duplicate_frames(directory, cropped, is_mobile):
|
|
813
|
-
logging.debug("Eliminating Duplicate Frames...")
|
|
814
|
-
global client_viewport
|
|
815
|
-
try:
|
|
816
|
-
files = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
|
|
817
|
-
if len(files) > 1:
|
|
818
|
-
from PIL import Image
|
|
819
|
-
|
|
820
|
-
blank = files[0]
|
|
821
|
-
with Image.open(blank) as im:
|
|
822
|
-
width, height = im.size
|
|
823
|
-
im_width = width
|
|
824
|
-
im_height = height
|
|
825
|
-
|
|
826
|
-
# Figure out the region of the image that we care about
|
|
827
|
-
top = 40
|
|
828
|
-
right_margin = 10
|
|
829
|
-
bottom_margin = 10
|
|
830
|
-
if height > 400 or width > 400:
|
|
831
|
-
top = int(math.ceil(float(height) * 0.04))
|
|
832
|
-
right_margin = int(math.ceil(float(width) * 0.04))
|
|
833
|
-
bottom_margin = int(math.ceil(float(width) * 0.06))
|
|
834
|
-
height = max(height - top - bottom_margin, 1)
|
|
835
|
-
left = 0
|
|
836
|
-
width = max(width - right_margin, 1)
|
|
837
|
-
|
|
838
|
-
if client_viewport is not None:
|
|
839
|
-
height = max(client_viewport["height"] - top - bottom_margin, 1)
|
|
840
|
-
width = max(client_viewport["width"] - right_margin, 1)
|
|
841
|
-
left += client_viewport["x"]
|
|
842
|
-
top += client_viewport["y"]
|
|
843
|
-
elif cropped:
|
|
844
|
-
# The image was already cropped, so only cutout the bottom
|
|
845
|
-
# to get rid of the network request/etc. information for
|
|
846
|
-
# desktop videos, and nothing extra on mobile.
|
|
847
|
-
top = 0
|
|
848
|
-
left = 0
|
|
849
|
-
width = im_width
|
|
850
|
-
|
|
851
|
-
if is_mobile:
|
|
852
|
-
height = im_height
|
|
853
|
-
else:
|
|
854
|
-
height = im_height - bottom_margin
|
|
855
|
-
|
|
856
|
-
crop = (width, height, left, top)
|
|
857
|
-
logging.debug("Viewport cropping set to (W, H, L, T): " + str(crop))
|
|
858
|
-
|
|
859
|
-
# Do a pass looking for the first non-blank frame with an allowance
|
|
860
|
-
# for up to a 10% per-pixel difference for noise in the white
|
|
861
|
-
# field.
|
|
862
|
-
count = len(files)
|
|
863
|
-
for i in range(1, count):
|
|
864
|
-
if frames_match(blank, files[i], 10, 0, crop, None):
|
|
865
|
-
logging.debug(
|
|
866
|
-
"Removing duplicate frame {0} from the beginning".format(
|
|
867
|
-
files[i]
|
|
868
|
-
)
|
|
869
|
-
)
|
|
870
|
-
os.remove(files[i])
|
|
871
|
-
else:
|
|
872
|
-
break
|
|
873
|
-
|
|
874
|
-
# Do another pass looking for the last frame but with an allowance for up
|
|
875
|
-
# to a 15% difference in individual pixels to deal with noise
|
|
876
|
-
# around text.
|
|
877
|
-
files = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
|
|
878
|
-
count = len(files)
|
|
879
|
-
duplicates = []
|
|
880
|
-
if count > 2:
|
|
881
|
-
files.reverse()
|
|
882
|
-
baseline = files[0]
|
|
883
|
-
previous_frame = baseline
|
|
884
|
-
for i in range(1, count):
|
|
885
|
-
if frames_match(baseline, files[i], 15, 5, crop, None):
|
|
886
|
-
if previous_frame is baseline:
|
|
887
|
-
duplicates.append(previous_frame)
|
|
888
|
-
else:
|
|
889
|
-
logging.debug(
|
|
890
|
-
"Removing duplicate frame {0} from the end".format(
|
|
891
|
-
previous_frame
|
|
892
|
-
)
|
|
893
|
-
)
|
|
894
|
-
os.remove(previous_frame)
|
|
895
|
-
previous_frame = files[i]
|
|
896
|
-
else:
|
|
897
|
-
break
|
|
898
|
-
for duplicate in duplicates:
|
|
899
|
-
logging.debug(
|
|
900
|
-
"Removing duplicate frame {0} from the end".format(duplicate)
|
|
901
|
-
)
|
|
902
|
-
os.remove(duplicate)
|
|
903
|
-
|
|
904
|
-
except BaseException:
|
|
905
|
-
logging.exception("Error processing frames for duplicates")
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
def crop_viewport(directory):
|
|
909
|
-
if client_viewport is not None:
|
|
910
|
-
try:
|
|
911
|
-
from PIL import Image
|
|
912
|
-
|
|
913
|
-
files = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
|
|
914
|
-
count = len(files)
|
|
915
|
-
if count > 0:
|
|
916
|
-
for i in range(count):
|
|
917
|
-
with Image.open(files[i]) as im:
|
|
918
|
-
new_img = crop_im(
|
|
919
|
-
im,
|
|
920
|
-
client_viewport["width"],
|
|
921
|
-
client_viewport["height"],
|
|
922
|
-
client_viewport["x"],
|
|
923
|
-
client_viewport["y"],
|
|
924
|
-
)
|
|
925
|
-
new_img.save(files[i])
|
|
926
|
-
|
|
927
|
-
except BaseException:
|
|
928
|
-
logging.exception("Error cropping to viewport")
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
def get_decimate_filter():
|
|
932
|
-
decimate = None
|
|
933
|
-
try:
|
|
934
|
-
filters = subprocess.check_output(
|
|
935
|
-
["ffmpeg", "-filters"], stderr=subprocess.STDOUT, encoding="UTF-8"
|
|
936
|
-
)
|
|
937
|
-
lines = filters.split("\n")
|
|
938
|
-
match = re.compile(
|
|
939
|
-
r"(?P<filter>[\w]*decimate).*V->V.*Remove near-duplicate frames"
|
|
940
|
-
)
|
|
941
|
-
for line in lines:
|
|
942
|
-
m = re.search(match, line)
|
|
943
|
-
if m is not None:
|
|
944
|
-
decimate = m.groupdict().get("filter")
|
|
945
|
-
break
|
|
946
|
-
except BaseException:
|
|
947
|
-
logging.critical("Error checking ffmpeg filters for decimate")
|
|
948
|
-
decimate = None
|
|
949
|
-
return decimate
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
def clean_directory(directory):
|
|
953
|
-
files = glob.glob(os.path.join(directory, "*.png"))
|
|
954
|
-
for file in files:
|
|
955
|
-
os.remove(file)
|
|
956
|
-
files = glob.glob(os.path.join(directory, "*.jpg"))
|
|
957
|
-
for file in files:
|
|
958
|
-
os.remove(file)
|
|
959
|
-
files = glob.glob(os.path.join(directory, "*.json"))
|
|
960
|
-
for file in files:
|
|
961
|
-
os.remove(file)
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
def is_color_frame(file, color_file):
|
|
965
|
-
"""Check a section from the middle, top and bottom of the viewport to see if it matches"""
|
|
966
|
-
global frame_cache
|
|
967
|
-
if file in frame_cache and color_file in frame_cache[file]:
|
|
968
|
-
return bool(frame_cache[file][color_file])
|
|
969
|
-
match = False
|
|
970
|
-
if os.path.isfile(color_file):
|
|
971
|
-
try:
|
|
972
|
-
from PIL import Image
|
|
973
|
-
|
|
974
|
-
with Image.open(file) as img:
|
|
975
|
-
width, height = img.size
|
|
976
|
-
crops = []
|
|
977
|
-
|
|
978
|
-
# Middle
|
|
979
|
-
crops.append(
|
|
980
|
-
(int(width / 2), int(height / 3), int(width / 4), int(height / 3))
|
|
981
|
-
)
|
|
982
|
-
# Top
|
|
983
|
-
crops.append((int(width / 2), int(height / 5), int(width / 4), 50))
|
|
984
|
-
# Bottom
|
|
985
|
-
crops.append(
|
|
986
|
-
(
|
|
987
|
-
int(width / 2),
|
|
988
|
-
int(height / 5),
|
|
989
|
-
int(width / 4),
|
|
990
|
-
height - int(height / 5),
|
|
991
|
-
)
|
|
992
|
-
)
|
|
993
|
-
|
|
994
|
-
for crop in crops:
|
|
995
|
-
with Image.open(file) as im:
|
|
996
|
-
crop_i = crop_im(im, crop[0], crop[1], crop[2], crop[3])
|
|
997
|
-
resized_im = resize(crop_i, 200, 200)
|
|
998
|
-
|
|
999
|
-
with Image.open(color_file) as color_im:
|
|
1000
|
-
different_pixels = compare(resized_im, color_im, fuzz=0.15)
|
|
1001
|
-
|
|
1002
|
-
if different_pixels < 10000:
|
|
1003
|
-
match = True
|
|
1004
|
-
break
|
|
1005
|
-
except Exception as e:
|
|
1006
|
-
logging.debug(e)
|
|
1007
|
-
pass
|
|
1008
|
-
if file not in frame_cache:
|
|
1009
|
-
frame_cache[file] = {}
|
|
1010
|
-
frame_cache[file][color_file] = bool(match)
|
|
1011
|
-
return match
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
def colors_are_similar(a, b, threshold=15):
|
|
1015
|
-
similar = True
|
|
1016
|
-
sum = 0
|
|
1017
|
-
for x in range(3):
|
|
1018
|
-
delta = abs(a[x] - b[x])
|
|
1019
|
-
sum += delta
|
|
1020
|
-
if delta > threshold:
|
|
1021
|
-
similar = False
|
|
1022
|
-
if sum > threshold:
|
|
1023
|
-
similar = False
|
|
1024
|
-
|
|
1025
|
-
return similar
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
def frames_match(image1, image2, fuzz_percent, max_differences, crop_region, mask_rect):
|
|
1029
|
-
match = False
|
|
1030
|
-
|
|
1031
|
-
try:
|
|
1032
|
-
from PIL import Image
|
|
1033
|
-
|
|
1034
|
-
with Image.open(image1) as i1, Image.open(image2) as i2:
|
|
1035
|
-
if mask_rect:
|
|
1036
|
-
i1 = mask(
|
|
1037
|
-
i1,
|
|
1038
|
-
mask_rect["width"],
|
|
1039
|
-
mask_rect["height"],
|
|
1040
|
-
mask_rect["x"],
|
|
1041
|
-
mask_rect["y"],
|
|
1042
|
-
)
|
|
1043
|
-
i2 = mask(
|
|
1044
|
-
i2,
|
|
1045
|
-
mask_rect["width"],
|
|
1046
|
-
mask_rect["height"],
|
|
1047
|
-
mask_rect["x"],
|
|
1048
|
-
mask_rect["y"],
|
|
1049
|
-
)
|
|
1050
|
-
|
|
1051
|
-
if crop_region:
|
|
1052
|
-
i1 = crop_im(
|
|
1053
|
-
i1, crop_region[0], crop_region[1], crop_region[2], crop_region[3]
|
|
1054
|
-
)
|
|
1055
|
-
i2 = crop_im(
|
|
1056
|
-
i2, crop_region[0], crop_region[1], crop_region[2], crop_region[3]
|
|
1057
|
-
)
|
|
1058
|
-
|
|
1059
|
-
different_pixels = compare(i1, i2, fuzz=fuzz_percent / 100)
|
|
1060
|
-
if different_pixels <= max_differences:
|
|
1061
|
-
match = True
|
|
1062
|
-
|
|
1063
|
-
except BaseException as e:
|
|
1064
|
-
logging.exception(e)
|
|
1065
|
-
return None
|
|
1066
|
-
|
|
1067
|
-
return match
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
def generate_orange_png(orange_file):
|
|
1071
|
-
try:
|
|
1072
|
-
from PIL import Image, ImageDraw
|
|
1073
|
-
|
|
1074
|
-
im = Image.new("RGB", (200, 200))
|
|
1075
|
-
draw = ImageDraw.Draw(im)
|
|
1076
|
-
draw.rectangle([0, 0, 200, 200], fill=(222, 100, 13))
|
|
1077
|
-
del draw
|
|
1078
|
-
im.save(orange_file, "PNG")
|
|
1079
|
-
except BaseException:
|
|
1080
|
-
logging.exception("Error generating orange png " + orange_file)
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
##########################################################################
|
|
1084
|
-
# Histogram calculations
|
|
1085
|
-
##########################################################################
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
def calculate_histograms(directory, histograms_file, force):
|
|
1089
|
-
logging.debug("Calculating image histograms")
|
|
1090
|
-
if not os.path.isfile(histograms_file) or force:
|
|
1091
|
-
try:
|
|
1092
|
-
extension = None
|
|
1093
|
-
directory = os.path.realpath(directory)
|
|
1094
|
-
first_frame = os.path.join(directory, "ms_000000")
|
|
1095
|
-
if os.path.isfile(first_frame + ".png"):
|
|
1096
|
-
extension = ".png"
|
|
1097
|
-
elif os.path.isfile(first_frame + ".jpg"):
|
|
1098
|
-
extension = ".jpg"
|
|
1099
|
-
if extension is not None:
|
|
1100
|
-
histograms = []
|
|
1101
|
-
frames = sorted(glob.glob(os.path.join(directory, "ms_*" + extension)))
|
|
1102
|
-
match = re.compile(r"ms_(?P<ms>[0-9]+)\.")
|
|
1103
|
-
for frame in frames:
|
|
1104
|
-
m = re.search(match, frame)
|
|
1105
|
-
if m is not None:
|
|
1106
|
-
frame_time = int(m.groupdict().get("ms"))
|
|
1107
|
-
histogram = calculate_image_histogram(frame)
|
|
1108
|
-
gc.collect()
|
|
1109
|
-
if histogram is not None:
|
|
1110
|
-
histograms.append(
|
|
1111
|
-
{
|
|
1112
|
-
"time": frame_time,
|
|
1113
|
-
"file": os.path.basename(frame),
|
|
1114
|
-
"histogram": histogram,
|
|
1115
|
-
}
|
|
1116
|
-
)
|
|
1117
|
-
if os.path.isfile(histograms_file):
|
|
1118
|
-
os.remove(histograms_file)
|
|
1119
|
-
f = gzip.open(histograms_file, GZIP_TEXT)
|
|
1120
|
-
json.dump(histograms, f)
|
|
1121
|
-
f.close()
|
|
1122
|
-
else:
|
|
1123
|
-
logging.critical("No video frames found in " + directory)
|
|
1124
|
-
except BaseException:
|
|
1125
|
-
logging.exception("Error calculating histograms")
|
|
1126
|
-
else:
|
|
1127
|
-
logging.debug("Histograms file {0} already exists".format(histograms_file))
|
|
1128
|
-
logging.debug("Done calculating histograms")
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
def calculate_image_histogram(file):
|
|
1132
|
-
logging.debug("Calculating histogram for " + file)
|
|
1133
|
-
try:
|
|
1134
|
-
from PIL import Image
|
|
1135
|
-
|
|
1136
|
-
im = Image.open(file)
|
|
1137
|
-
width, height = im.size
|
|
1138
|
-
colors = im.getcolors(width * height)
|
|
1139
|
-
histogram = {
|
|
1140
|
-
"r": [0 for i in range(256)],
|
|
1141
|
-
"g": [0 for i in range(256)],
|
|
1142
|
-
"b": [0 for i in range(256)],
|
|
1143
|
-
}
|
|
1144
|
-
for entry in colors:
|
|
1145
|
-
try:
|
|
1146
|
-
count = entry[0]
|
|
1147
|
-
pixel = entry[1]
|
|
1148
|
-
# Don't include White pixels (with a tiny bit of slop for
|
|
1149
|
-
# compression artifacts)
|
|
1150
|
-
if pixel[0] < 250 or pixel[1] < 250 or pixel[2] < 250:
|
|
1151
|
-
histogram["r"][pixel[0]] += count
|
|
1152
|
-
histogram["g"][pixel[1]] += count
|
|
1153
|
-
histogram["b"][pixel[2]] += count
|
|
1154
|
-
except Exception:
|
|
1155
|
-
pass
|
|
1156
|
-
colors = None
|
|
1157
|
-
except Exception:
|
|
1158
|
-
histogram = None
|
|
1159
|
-
logging.exception("Error calculating histogram for " + file)
|
|
1160
|
-
return histogram
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
##########################################################################
|
|
1164
|
-
# Screen Shots
|
|
1165
|
-
##########################################################################
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
def save_screenshot(directory, dest, quality):
|
|
1169
|
-
directory = os.path.realpath(directory)
|
|
1170
|
-
files = sorted(glob.glob(os.path.join(directory, "ms_*.png")))
|
|
1171
|
-
if files is not None and len(files) >= 1:
|
|
1172
|
-
src = files[-1]
|
|
1173
|
-
if dest[-4:] == ".jpg":
|
|
1174
|
-
convert_img_to_jpeg(src, dest, quality=quality)
|
|
1175
|
-
else:
|
|
1176
|
-
shutil.copy(src, dest)
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
##########################################################################
|
|
1180
|
-
# JPEG conversion
|
|
1181
|
-
##########################################################################
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
def convert_to_jpeg(directory, quality):
|
|
1185
|
-
logging.debug("Converting video frames to JPEG")
|
|
1186
|
-
directory = os.path.realpath(directory)
|
|
1187
|
-
pattern = os.path.join(directory, "ms_*.png")
|
|
1188
|
-
|
|
1189
|
-
files = sorted(glob.glob(pattern))
|
|
1190
|
-
for file in files:
|
|
1191
|
-
_, filename = os.path.split(file)
|
|
1192
|
-
filen, ext = os.path.splitext(filename)
|
|
1193
|
-
convert_img_to_jpeg(
|
|
1194
|
-
file, os.path.join(directory, filen + ".jpg"), quality=quality
|
|
1195
|
-
)
|
|
1196
|
-
os.remove(file)
|
|
1197
|
-
|
|
1198
|
-
logging.debug("Done converting video frames to JPEG")
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
##########################################################################
|
|
1202
|
-
# Visual Metrics
|
|
1203
|
-
##########################################################################
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
def calculate_visual_metrics(
|
|
1207
|
-
histograms_file,
|
|
1208
|
-
start,
|
|
1209
|
-
end,
|
|
1210
|
-
perceptual,
|
|
1211
|
-
contentful,
|
|
1212
|
-
dirs,
|
|
1213
|
-
progress_file,
|
|
1214
|
-
hero_elements_file,
|
|
1215
|
-
):
|
|
1216
|
-
metrics = None
|
|
1217
|
-
histograms = load_histograms(histograms_file, start, end)
|
|
1218
|
-
if histograms is not None and len(histograms) > 0:
|
|
1219
|
-
progress = calculate_visual_progress(histograms)
|
|
1220
|
-
if progress and progress_file is not None:
|
|
1221
|
-
file_name, ext = os.path.splitext(progress_file)
|
|
1222
|
-
if ext.lower() == ".gz":
|
|
1223
|
-
f = gzip.open(progress_file, GZIP_TEXT, 7)
|
|
1224
|
-
else:
|
|
1225
|
-
f = open(progress_file, "w")
|
|
1226
|
-
json.dump(progress, f)
|
|
1227
|
-
f.close()
|
|
1228
|
-
if len(histograms) > 1:
|
|
1229
|
-
metrics = [
|
|
1230
|
-
{"name": "First Visual Change", "value": histograms[1]["time"]},
|
|
1231
|
-
{"name": "Last Visual Change", "value": histograms[-1]["time"]},
|
|
1232
|
-
{"name": "Speed Index", "value": calculate_speed_index(progress)},
|
|
1233
|
-
]
|
|
1234
|
-
if perceptual:
|
|
1235
|
-
value, value_progress = calculate_perceptual_speed_index(progress, dirs)
|
|
1236
|
-
metrics.extend(
|
|
1237
|
-
(
|
|
1238
|
-
{"name": "Perceptual Speed Index", "value": value},
|
|
1239
|
-
{
|
|
1240
|
-
"name": "Perceptual Speed Index Progress",
|
|
1241
|
-
"value": value_progress,
|
|
1242
|
-
},
|
|
1243
|
-
)
|
|
1244
|
-
)
|
|
1245
|
-
if contentful:
|
|
1246
|
-
value, value_progress = calculate_contentful_speed_index(progress, dirs)
|
|
1247
|
-
|
|
1248
|
-
metrics.extend(
|
|
1249
|
-
(
|
|
1250
|
-
{"name": "Contentful Speed Index", "value": value},
|
|
1251
|
-
{
|
|
1252
|
-
"name": "Contentful Speed Index Progress",
|
|
1253
|
-
"value": value_progress,
|
|
1254
|
-
},
|
|
1255
|
-
)
|
|
1256
|
-
)
|
|
1257
|
-
if hero_elements_file is not None and os.path.isfile(hero_elements_file):
|
|
1258
|
-
logging.debug("Calculating hero element times")
|
|
1259
|
-
hero_data = None
|
|
1260
|
-
hero_f_in = gzip.open(hero_elements_file, GZIP_READ_TEXT)
|
|
1261
|
-
try:
|
|
1262
|
-
hero_data = json.load(hero_f_in)
|
|
1263
|
-
except Exception as e:
|
|
1264
|
-
logging.exception("Could not load hero elements data")
|
|
1265
|
-
logging.exception(e)
|
|
1266
|
-
hero_f_in.close()
|
|
1267
|
-
|
|
1268
|
-
if (
|
|
1269
|
-
hero_data is not None
|
|
1270
|
-
and hero_data["heroes"] is not None
|
|
1271
|
-
and hero_data["viewport"] is not None
|
|
1272
|
-
and len(hero_data["heroes"]) > 0
|
|
1273
|
-
):
|
|
1274
|
-
viewport = hero_data["viewport"]
|
|
1275
|
-
hero_timings = []
|
|
1276
|
-
for hero in hero_data["heroes"]:
|
|
1277
|
-
hero_time = calculate_hero_time(progress, dirs, hero, viewport)
|
|
1278
|
-
if hero_time is not None:
|
|
1279
|
-
hero_timings.append(
|
|
1280
|
-
{
|
|
1281
|
-
"name": hero["name"],
|
|
1282
|
-
"value": hero_time,
|
|
1283
|
-
}
|
|
1284
|
-
)
|
|
1285
|
-
hero_timings_sorted = sorted(
|
|
1286
|
-
hero_timings, key=lambda timing: timing["value"]
|
|
1287
|
-
)
|
|
1288
|
-
# hero_timings.append({'name': 'FirstPaintedHero',
|
|
1289
|
-
# 'value': hero_timings_sorted[0]['value']})
|
|
1290
|
-
if (len(hero_timings_sorted) > 0):
|
|
1291
|
-
hero_timings.append(
|
|
1292
|
-
{
|
|
1293
|
-
"name": "LastMeaningfulPaint",
|
|
1294
|
-
"value": hero_timings_sorted[-1]["value"],
|
|
1295
|
-
}
|
|
1296
|
-
)
|
|
1297
|
-
hero_data["timings"] = hero_timings
|
|
1298
|
-
metrics += hero_timings
|
|
1299
|
-
|
|
1300
|
-
hero_f_out = gzip.open(hero_elements_file, GZIP_TEXT, 7)
|
|
1301
|
-
json.dump(hero_data, hero_f_out)
|
|
1302
|
-
hero_f_out.close()
|
|
1303
|
-
else:
|
|
1304
|
-
logging.warn(
|
|
1305
|
-
"Hero elements file is not valid: " + str(hero_elements_file)
|
|
1306
|
-
)
|
|
1307
|
-
else:
|
|
1308
|
-
metrics = [
|
|
1309
|
-
{"name": "First Visual Change", "value": histograms[0]["time"]},
|
|
1310
|
-
{"name": "Last Visual Change", "value": histograms[0]["time"]},
|
|
1311
|
-
{"name": "Visually Complete", "value": histograms[0]["time"]},
|
|
1312
|
-
{"name": "Speed Index", "value": 0},
|
|
1313
|
-
]
|
|
1314
|
-
if perceptual:
|
|
1315
|
-
metrics.append({"name": "Perceptual Speed Index", "value": 0})
|
|
1316
|
-
if contentful:
|
|
1317
|
-
metrics.append({"name": "Contentful Speed Index", "value": 0})
|
|
1318
|
-
prog = ""
|
|
1319
|
-
for p in progress:
|
|
1320
|
-
if len(prog):
|
|
1321
|
-
prog += ", "
|
|
1322
|
-
prog += "{0:d}={1:d}".format(p["time"], int(p["progress"]))
|
|
1323
|
-
metrics.append({"name": "Visual Progress", "value": prog})
|
|
1324
|
-
|
|
1325
|
-
return metrics
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
def load_histograms(histograms_file, start, end):
|
|
1329
|
-
histograms = None
|
|
1330
|
-
if os.path.isfile(histograms_file):
|
|
1331
|
-
f = gzip.open(histograms_file)
|
|
1332
|
-
original = json.load(f)
|
|
1333
|
-
f.close()
|
|
1334
|
-
if start != 0 or end != 0:
|
|
1335
|
-
histograms = []
|
|
1336
|
-
for histogram in original:
|
|
1337
|
-
if histogram["time"] <= start:
|
|
1338
|
-
histogram["time"] = start
|
|
1339
|
-
histograms = [histogram]
|
|
1340
|
-
elif histogram["time"] <= end:
|
|
1341
|
-
histograms.append(histogram)
|
|
1342
|
-
else:
|
|
1343
|
-
break
|
|
1344
|
-
else:
|
|
1345
|
-
histograms = original
|
|
1346
|
-
return histograms
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
def calculate_visual_progress(histograms):
|
|
1350
|
-
progress = []
|
|
1351
|
-
first = histograms[0]["histogram"]
|
|
1352
|
-
last = histograms[-1]["histogram"]
|
|
1353
|
-
for index, histogram in enumerate(histograms):
|
|
1354
|
-
p = calculate_frame_progress(histogram["histogram"], first, last)
|
|
1355
|
-
file_name, ext = os.path.splitext(histogram["file"])
|
|
1356
|
-
progress.append({"time": histogram["time"], "file": file_name, "progress": p})
|
|
1357
|
-
logging.debug("{0:d}ms - {1:d}% Complete".format(histogram["time"], int(p)))
|
|
1358
|
-
return progress
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
def calculate_frame_progress(histogram, start, final):
|
|
1362
|
-
"""Calculate the progress percentage of a given frame histogram.
|
|
1363
|
-
|
|
1364
|
-
This method finds the visually-complete progress by taking a sum of
|
|
1365
|
-
all the differences in the histogram between the current frame, and the
|
|
1366
|
-
final frame. The initial/first frame is used to remove values that are
|
|
1367
|
-
consitent between the first, and final frame (i.e. it's a baseline).
|
|
1368
|
-
|
|
1369
|
-
Note that this method should not be using a slop/fuzz because we aren't
|
|
1370
|
-
looking at individual pixel intensities which is where the fuzz can be
|
|
1371
|
-
found. Within individual channels, we hit an issue where we cannot tell
|
|
1372
|
-
if a value in the red channel is purely from a red colour in the image, or
|
|
1373
|
-
if it's mixed with other colours such as grey (e.g. red with rbg(240,0,0),
|
|
1374
|
-
and light grey with rgb(245,245,245)).
|
|
1375
|
-
"""
|
|
1376
|
-
total = 0
|
|
1377
|
-
matched = 0
|
|
1378
|
-
for channel in ("r", "g", "b"):
|
|
1379
|
-
for pixel_ind in range(256):
|
|
1380
|
-
curr = histogram[channel][pixel_ind]
|
|
1381
|
-
init = start[channel][pixel_ind]
|
|
1382
|
-
target = final[channel][pixel_ind]
|
|
1383
|
-
|
|
1384
|
-
curr_diff = abs(curr - init)
|
|
1385
|
-
target_diff = abs(target - init)
|
|
1386
|
-
|
|
1387
|
-
matched += min(curr_diff, target_diff)
|
|
1388
|
-
total += target_diff
|
|
1389
|
-
|
|
1390
|
-
progress = (float(matched) / float(total)) if total else 1
|
|
1391
|
-
return math.floor(progress * 100)
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
def find_visually_complete(progress):
|
|
1395
|
-
time = 0
|
|
1396
|
-
for p in progress:
|
|
1397
|
-
if int(p["progress"]) == 100:
|
|
1398
|
-
time = p["time"]
|
|
1399
|
-
break
|
|
1400
|
-
elif time == 0:
|
|
1401
|
-
time = p["time"]
|
|
1402
|
-
return time
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
def calculate_speed_index(progress):
|
|
1406
|
-
si = 0
|
|
1407
|
-
last_ms = progress[0]["time"]
|
|
1408
|
-
last_progress = progress[0]["progress"]
|
|
1409
|
-
for p in progress:
|
|
1410
|
-
elapsed = p["time"] - last_ms
|
|
1411
|
-
si += elapsed * (1.0 - last_progress)
|
|
1412
|
-
last_ms = p["time"]
|
|
1413
|
-
last_progress = p["progress"] / 100.0
|
|
1414
|
-
return int(si)
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
def calculate_contentful_speed_index(progress, directory):
|
|
1418
|
-
# convert output comes out with lines that have this format:
|
|
1419
|
-
# <pixel count>: <rgb color> #<hex color> <gray color>
|
|
1420
|
-
# This is CLI dependant and very fragile
|
|
1421
|
-
matcher = re.compile(r"(\d+?):")
|
|
1422
|
-
|
|
1423
|
-
try:
|
|
1424
|
-
from PIL import Image
|
|
1425
|
-
|
|
1426
|
-
dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory)
|
|
1427
|
-
content = []
|
|
1428
|
-
maxContent = 0
|
|
1429
|
-
for p in progress[1:]:
|
|
1430
|
-
# Full Path of the Current Frame
|
|
1431
|
-
current_frame = os.path.join(dir, "ms_{0:06d}.png".format(p["time"]))
|
|
1432
|
-
logging.debug("contentfulSpeedIndex: Current frame is %s" % current_frame)
|
|
1433
|
-
|
|
1434
|
-
value = 0
|
|
1435
|
-
with Image.open(current_frame) as current_frame_img:
|
|
1436
|
-
value = contentful_value(current_frame_img)
|
|
1437
|
-
|
|
1438
|
-
logging.debug("contentfulSpeedIndex: Contentful value {0}".format(value))
|
|
1439
|
-
|
|
1440
|
-
if value > maxContent:
|
|
1441
|
-
maxContent = value
|
|
1442
|
-
content.append(value)
|
|
1443
|
-
|
|
1444
|
-
for i, value in enumerate(content):
|
|
1445
|
-
if maxContent > 0:
|
|
1446
|
-
content[i] = float(content[i]) / float(maxContent)
|
|
1447
|
-
else:
|
|
1448
|
-
content[i] = 0.0
|
|
1449
|
-
|
|
1450
|
-
# Assume 0 content for first frame
|
|
1451
|
-
cont_si = 1 * (progress[1]["time"] - progress[0]["time"])
|
|
1452
|
-
completeness_value = [(progress[1]["time"], int(cont_si))]
|
|
1453
|
-
for i in range(1, len(progress) - 1):
|
|
1454
|
-
elapsed = progress[i + 1]["time"] - progress[i]["time"]
|
|
1455
|
-
# print i,' time =',p['time'],'elapsed =',elapsed,'content = ',content[i]
|
|
1456
|
-
cont_si += elapsed * (1.0 - content[i])
|
|
1457
|
-
completeness_value.append((progress[i + 1]["time"], int(cont_si)))
|
|
1458
|
-
|
|
1459
|
-
cont_si = int(cont_si)
|
|
1460
|
-
raw_progress_value = ["0=0"]
|
|
1461
|
-
for timestamp, percent in completeness_value:
|
|
1462
|
-
p = int(100 * float(percent) / float(cont_si))
|
|
1463
|
-
raw_progress_value.append("%d=%d" % (timestamp, p))
|
|
1464
|
-
|
|
1465
|
-
return cont_si, ", ".join(raw_progress_value)
|
|
1466
|
-
except Exception as e:
|
|
1467
|
-
logging.exception(e)
|
|
1468
|
-
return None, None
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
def calculate_perceptual_speed_index(progress, directory):
|
|
1472
|
-
from ssim import compute_ssim
|
|
1473
|
-
|
|
1474
|
-
x = len(progress)
|
|
1475
|
-
dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory)
|
|
1476
|
-
first_paint_frame = os.path.join(dir, "ms_{0:06d}.png".format(progress[1]["time"]))
|
|
1477
|
-
target_frame = os.path.join(dir, "ms_{0:06d}.png".format(progress[x - 1]["time"]))
|
|
1478
|
-
ssim_1 = compute_ssim(first_paint_frame, target_frame)
|
|
1479
|
-
per_si = float(progress[1]["time"])
|
|
1480
|
-
last_ms = progress[1]["time"]
|
|
1481
|
-
# Full Path of the Target Frame
|
|
1482
|
-
logging.debug("Target image for perSI is %s" % target_frame)
|
|
1483
|
-
ssim = ssim_1
|
|
1484
|
-
completeness_value = []
|
|
1485
|
-
for p in progress[1:]:
|
|
1486
|
-
elapsed = p["time"] - last_ms
|
|
1487
|
-
# print '*******elapsed %f'%elapsed
|
|
1488
|
-
# Full Path of the Current Frame
|
|
1489
|
-
current_frame = os.path.join(dir, "ms_{0:06d}.png".format(p["time"]))
|
|
1490
|
-
logging.debug("Current Image is %s" % current_frame)
|
|
1491
|
-
# Takes full path of PNG frames to compute SSIM value
|
|
1492
|
-
per_si += elapsed * (1.0 - ssim)
|
|
1493
|
-
ssim = compute_ssim(current_frame, target_frame)
|
|
1494
|
-
gc.collect()
|
|
1495
|
-
last_ms = p["time"]
|
|
1496
|
-
completeness_value.append((p["time"], int(per_si)))
|
|
1497
|
-
|
|
1498
|
-
per_si = int(per_si)
|
|
1499
|
-
raw_progress_value = ["0=0"]
|
|
1500
|
-
for timestamp, percent in completeness_value:
|
|
1501
|
-
p = int(100 * float(percent) / float(per_si))
|
|
1502
|
-
raw_progress_value.append("%d=%d" % (timestamp, p))
|
|
1503
|
-
|
|
1504
|
-
return per_si, ", ".join(raw_progress_value)
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
def calculate_hero_time(progress, directory, hero, viewport):
|
|
1508
|
-
try:
|
|
1509
|
-
dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory)
|
|
1510
|
-
n = len(progress)
|
|
1511
|
-
target_frame = os.path.join(dir, "ms_{0:06d}".format(progress[n - 1]["time"]))
|
|
1512
|
-
|
|
1513
|
-
extension = None
|
|
1514
|
-
if os.path.isfile(target_frame + ".png"):
|
|
1515
|
-
extension = ".png"
|
|
1516
|
-
elif os.path.isfile(target_frame + ".jpg"):
|
|
1517
|
-
extension = ".jpg"
|
|
1518
|
-
if extension is not None:
|
|
1519
|
-
hero_width = int(hero["width"])
|
|
1520
|
-
hero_height = int(hero["height"])
|
|
1521
|
-
hero_x = int(hero["x"])
|
|
1522
|
-
hero_y = int(hero["y"])
|
|
1523
|
-
target_frame = target_frame + extension
|
|
1524
|
-
logging.debug(
|
|
1525
|
-
"Target image for hero %s is %s" % (hero["name"], target_frame)
|
|
1526
|
-
)
|
|
1527
|
-
|
|
1528
|
-
from PIL import Image
|
|
1529
|
-
|
|
1530
|
-
with Image.open(target_frame) as im:
|
|
1531
|
-
width, height = im.size
|
|
1532
|
-
if width != viewport["width"]:
|
|
1533
|
-
scale = float(width) / float(viewport["width"])
|
|
1534
|
-
logging.debug(
|
|
1535
|
-
"Frames are %dpx wide but viewport was %dpx. Scaling by %f"
|
|
1536
|
-
% (width, viewport["width"], scale)
|
|
1537
|
-
)
|
|
1538
|
-
hero_width = int(hero["width"] * scale)
|
|
1539
|
-
hero_height = int(hero["height"] * scale)
|
|
1540
|
-
hero_x = int(hero["x"] * scale)
|
|
1541
|
-
hero_y = int(hero["y"] * scale)
|
|
1542
|
-
|
|
1543
|
-
logging.debug(
|
|
1544
|
-
'Calculating render time for hero element "%s" at position [%d, %d, %d, %d]'
|
|
1545
|
-
% (hero["name"], hero_x, hero_y, hero_width, hero_height)
|
|
1546
|
-
)
|
|
1547
|
-
|
|
1548
|
-
# Apply the mask to the target frame to create the reference frame
|
|
1549
|
-
target_mask_path = os.path.join(
|
|
1550
|
-
dir,
|
|
1551
|
-
"hero_{0}_ms_{1:06d}.png".format(hero["name"], progress[n - 1]["time"]),
|
|
1552
|
-
)
|
|
1553
|
-
|
|
1554
|
-
def __apply_hero_mask(cur_frame):
|
|
1555
|
-
"""Helper method for re-applying the same mask."""
|
|
1556
|
-
cropped_frame = None
|
|
1557
|
-
with Image.open(cur_frame) as im:
|
|
1558
|
-
cropped_frame = crop_im(im, hero_width, hero_height, hero_x, hero_y)
|
|
1559
|
-
return cropped_frame
|
|
1560
|
-
|
|
1561
|
-
target_mask = __apply_hero_mask(target_frame)
|
|
1562
|
-
target_mask.save(target_mask_path)
|
|
1563
|
-
|
|
1564
|
-
def cleanup():
|
|
1565
|
-
if os.path.isfile(target_mask_path):
|
|
1566
|
-
os.remove(target_mask_path)
|
|
1567
|
-
|
|
1568
|
-
# Allow for small differences like scrollbars and overlaid UI elements
|
|
1569
|
-
# by applying a 10% fuzz and allowing for up to 2% of the pixels to be
|
|
1570
|
-
# different.
|
|
1571
|
-
fuzz = 10
|
|
1572
|
-
max_pixel_diff = math.ceil(hero_width * hero_height * 0.02)
|
|
1573
|
-
|
|
1574
|
-
for p in progress:
|
|
1575
|
-
current_frame = os.path.join(dir, "ms_{0:06d}".format(p["time"]))
|
|
1576
|
-
extension = None
|
|
1577
|
-
if os.path.isfile(current_frame + ".png"):
|
|
1578
|
-
extension = ".png"
|
|
1579
|
-
elif os.path.isfile(current_frame + ".jpg"):
|
|
1580
|
-
extension = ".jpg"
|
|
1581
|
-
if extension is not None:
|
|
1582
|
-
current_mask_path = os.path.join(
|
|
1583
|
-
dir, "hero_{0}_ms_{1:06d}.png".format(hero["name"], p["time"])
|
|
1584
|
-
)
|
|
1585
|
-
|
|
1586
|
-
current_mask = __apply_hero_mask(current_frame + extension)
|
|
1587
|
-
current_mask.save(current_mask_path)
|
|
1588
|
-
|
|
1589
|
-
match = frames_match(
|
|
1590
|
-
target_mask_path,
|
|
1591
|
-
current_mask_path,
|
|
1592
|
-
fuzz,
|
|
1593
|
-
max_pixel_diff,
|
|
1594
|
-
None,
|
|
1595
|
-
None,
|
|
1596
|
-
)
|
|
1597
|
-
|
|
1598
|
-
# Remove each mask after using it
|
|
1599
|
-
os.remove(current_mask_path)
|
|
1600
|
-
|
|
1601
|
-
if match:
|
|
1602
|
-
# Clean up masks as soon as a match is found
|
|
1603
|
-
cleanup()
|
|
1604
|
-
return p["time"]
|
|
1605
|
-
|
|
1606
|
-
# No matches found; clean up masks
|
|
1607
|
-
cleanup()
|
|
1608
|
-
|
|
1609
|
-
return None
|
|
1610
|
-
except Exception as e:
|
|
1611
|
-
logging.exception(e)
|
|
1612
|
-
return None
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
##########################################################################
|
|
1616
|
-
# Check any dependencies
|
|
1617
|
-
##########################################################################
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
def check_config():
|
|
1621
|
-
ok = True
|
|
1622
|
-
|
|
1623
|
-
if get_decimate_filter() is not None:
|
|
1624
|
-
logging.debug("FFMPEG found")
|
|
1625
|
-
else:
|
|
1626
|
-
print("ffmpeg: FAIL")
|
|
1627
|
-
ok = False
|
|
1628
|
-
|
|
1629
|
-
if sys.version_info >= (3, 6):
|
|
1630
|
-
logging.debug("Python 3.6+ found")
|
|
1631
|
-
else:
|
|
1632
|
-
print("Python 3.6+: FAIL")
|
|
1633
|
-
ok = False
|
|
1634
|
-
|
|
1635
|
-
try:
|
|
1636
|
-
import numpy as np
|
|
1637
|
-
|
|
1638
|
-
logging.debug("Numpy found")
|
|
1639
|
-
except BaseException:
|
|
1640
|
-
print("Numpy: FAIL")
|
|
1641
|
-
ok = False
|
|
1642
|
-
|
|
1643
|
-
try:
|
|
1644
|
-
import cv2
|
|
1645
|
-
|
|
1646
|
-
logging.debug("OpenCV-Python found")
|
|
1647
|
-
except BaseException:
|
|
1648
|
-
print("OpenCV-Python: FAIL")
|
|
1649
|
-
ok = False
|
|
1650
|
-
|
|
1651
|
-
try:
|
|
1652
|
-
from PIL import Image, ImageCms, ImageDraw, ImageOps # noqa
|
|
1653
|
-
|
|
1654
|
-
logging.debug("Pillow found")
|
|
1655
|
-
except BaseException:
|
|
1656
|
-
print("Pillow: FAIL")
|
|
1657
|
-
ok = False
|
|
1658
|
-
|
|
1659
|
-
try:
|
|
1660
|
-
from ssim import compute_ssim # noqa
|
|
1661
|
-
|
|
1662
|
-
logging.debug("SSIM found")
|
|
1663
|
-
except BaseException:
|
|
1664
|
-
print("SSIM: FAIL")
|
|
1665
|
-
ok = False
|
|
1666
|
-
|
|
1667
|
-
return ok
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
def check_process(command, output):
|
|
1671
|
-
ok = False
|
|
1672
|
-
try:
|
|
1673
|
-
out = subprocess.check_output(
|
|
1674
|
-
command, stderr=subprocess.STDOUT, shell=True, encoding="UTF-8"
|
|
1675
|
-
)
|
|
1676
|
-
if out.find(output) > -1:
|
|
1677
|
-
ok = True
|
|
1678
|
-
except BaseException:
|
|
1679
|
-
ok = False
|
|
1680
|
-
return ok
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
##########################################################################
|
|
1684
|
-
# Main Entry Point
|
|
1685
|
-
##########################################################################
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
def main():
|
|
1689
|
-
import argparse
|
|
1690
|
-
|
|
1691
|
-
global options
|
|
1692
|
-
|
|
1693
|
-
parser = argparse.ArgumentParser(
|
|
1694
|
-
description="Calculate visual performance metrics from a video.",
|
|
1695
|
-
prog="visualmetrics",
|
|
1696
|
-
)
|
|
1697
|
-
parser.add_argument("--version", action="version", version="%(prog)s 0.1")
|
|
1698
|
-
parser.add_argument(
|
|
1699
|
-
"-c",
|
|
1700
|
-
"--check",
|
|
1701
|
-
action="store_true",
|
|
1702
|
-
default=False,
|
|
1703
|
-
help="Check dependencies (ffmpeg, Numpy, OpenCV-Python, PIL, SSIM).",
|
|
1704
|
-
)
|
|
1705
|
-
parser.add_argument(
|
|
1706
|
-
"-v",
|
|
1707
|
-
"--verbose",
|
|
1708
|
-
action="count",
|
|
1709
|
-
help="Increase verbosity (specify multiple times for more).",
|
|
1710
|
-
)
|
|
1711
|
-
parser.add_argument(
|
|
1712
|
-
"--logfile", help="Write log messages to given file instead of stdout"
|
|
1713
|
-
)
|
|
1714
|
-
parser.add_argument(
|
|
1715
|
-
"--logformat",
|
|
1716
|
-
help="Formatting for the log messages",
|
|
1717
|
-
default="%(asctime)s.%(msecs)03d - %(message)s",
|
|
1718
|
-
)
|
|
1719
|
-
parser.add_argument("-i", "--video", help="Input video file.")
|
|
1720
|
-
parser.add_argument(
|
|
1721
|
-
"-d",
|
|
1722
|
-
"--dir",
|
|
1723
|
-
help="Directory of video frames "
|
|
1724
|
-
"(as input if exists or as output if a video file is specified).",
|
|
1725
|
-
)
|
|
1726
|
-
parser.add_argument(
|
|
1727
|
-
"--render", help="Render the video frames to the given mp4 video file."
|
|
1728
|
-
)
|
|
1729
|
-
parser.add_argument(
|
|
1730
|
-
"--screenshot",
|
|
1731
|
-
help="Save the last frame of video as an image to the path provided.",
|
|
1732
|
-
)
|
|
1733
|
-
parser.add_argument(
|
|
1734
|
-
"-q",
|
|
1735
|
-
"--quality",
|
|
1736
|
-
type=int,
|
|
1737
|
-
help="JPEG Quality " "(if specified, frames will be converted to JPEG).",
|
|
1738
|
-
)
|
|
1739
|
-
parser.add_argument(
|
|
1740
|
-
"-l",
|
|
1741
|
-
"--full",
|
|
1742
|
-
action="store_true",
|
|
1743
|
-
default=False,
|
|
1744
|
-
help="Keep full-resolution images instead of resizing to 400x400 pixels",
|
|
1745
|
-
)
|
|
1746
|
-
parser.add_argument(
|
|
1747
|
-
"--thumbsize", type=int, default=400, help="Thumbnail size (defaults to 400)."
|
|
1748
|
-
)
|
|
1749
|
-
parser.add_argument(
|
|
1750
|
-
"-f",
|
|
1751
|
-
"--force",
|
|
1752
|
-
action="store_true",
|
|
1753
|
-
default=False,
|
|
1754
|
-
help="Force processing of a video file (overwrite existing directory).",
|
|
1755
|
-
)
|
|
1756
|
-
parser.add_argument(
|
|
1757
|
-
"-o",
|
|
1758
|
-
"--orange",
|
|
1759
|
-
action="store_true",
|
|
1760
|
-
default=False,
|
|
1761
|
-
help="Remove orange-colored frames from the beginning of the video.",
|
|
1762
|
-
)
|
|
1763
|
-
parser.add_argument(
|
|
1764
|
-
"-p",
|
|
1765
|
-
"--viewport",
|
|
1766
|
-
action="store_true",
|
|
1767
|
-
default=False,
|
|
1768
|
-
help="Locate and use the viewport from the first video frame.",
|
|
1769
|
-
)
|
|
1770
|
-
parser.add_argument(
|
|
1771
|
-
"--viewportretries",
|
|
1772
|
-
type=int,
|
|
1773
|
-
default=5,
|
|
1774
|
-
help="Number of times to attempt to obtain a viewport. Analagous to the "
|
|
1775
|
-
"number of frames to try to find a viewport with. By default, up to the "
|
|
1776
|
-
"first 5 frames are used.",
|
|
1777
|
-
)
|
|
1778
|
-
parser.add_argument(
|
|
1779
|
-
"--viewportminheight",
|
|
1780
|
-
type=int,
|
|
1781
|
-
default=0,
|
|
1782
|
-
help="The minimum possible height (in pixels) for the viewport. Used when "
|
|
1783
|
-
"attempting to find the viewport size. Defaults to 0.",
|
|
1784
|
-
)
|
|
1785
|
-
parser.add_argument(
|
|
1786
|
-
"--viewportminwidth",
|
|
1787
|
-
type=int,
|
|
1788
|
-
default=0,
|
|
1789
|
-
help="The minimum possible width (in pixels) for the viewport. Used when "
|
|
1790
|
-
"attempting to find the viewport size. Defaults to 0.",
|
|
1791
|
-
)
|
|
1792
|
-
parser.add_argument(
|
|
1793
|
-
"-s",
|
|
1794
|
-
"--start",
|
|
1795
|
-
type=int,
|
|
1796
|
-
default=0,
|
|
1797
|
-
help="Start time (in milliseconds) for calculating visual metrics.",
|
|
1798
|
-
)
|
|
1799
|
-
parser.add_argument(
|
|
1800
|
-
"-e",
|
|
1801
|
-
"--end",
|
|
1802
|
-
type=int,
|
|
1803
|
-
default=0,
|
|
1804
|
-
help="End time (in milliseconds) for calculating visual metrics.",
|
|
1805
|
-
)
|
|
1806
|
-
parser.add_argument(
|
|
1807
|
-
"--renderignore",
|
|
1808
|
-
type=int,
|
|
1809
|
-
default=0,
|
|
1810
|
-
help="Ignore the center X%% of the frame when looking for "
|
|
1811
|
-
"the first rendered frame (useful for Opera mini).",
|
|
1812
|
-
)
|
|
1813
|
-
parser.add_argument(
|
|
1814
|
-
"-k",
|
|
1815
|
-
"--perceptual",
|
|
1816
|
-
action="store_true",
|
|
1817
|
-
default=False,
|
|
1818
|
-
help="Calculate perceptual Speed Index",
|
|
1819
|
-
)
|
|
1820
|
-
parser.add_argument(
|
|
1821
|
-
"--contentful",
|
|
1822
|
-
action="store_true",
|
|
1823
|
-
default=False,
|
|
1824
|
-
help="Calculate contentful Speed Index",
|
|
1825
|
-
)
|
|
1826
|
-
parser.add_argument(
|
|
1827
|
-
"--contentful-video",
|
|
1828
|
-
action="store_true",
|
|
1829
|
-
default=False,
|
|
1830
|
-
help="Produce a video of the edges used in the ContentfulSpeedIndex "
|
|
1831
|
-
"calculation. The resulting videos are suffixed with -edge and "
|
|
1832
|
-
"-edge-overlay.",
|
|
1833
|
-
)
|
|
1834
|
-
parser.add_argument(
|
|
1835
|
-
"-j",
|
|
1836
|
-
"--json",
|
|
1837
|
-
action="store_true",
|
|
1838
|
-
default=False,
|
|
1839
|
-
help="Set output format to JSON",
|
|
1840
|
-
)
|
|
1841
|
-
parser.add_argument("--progress", help="Visual progress output file.")
|
|
1842
|
-
parser.add_argument("--herodata", help="Hero elements data file.")
|
|
1843
|
-
|
|
1844
|
-
options = parser.parse_args()
|
|
1845
|
-
|
|
1846
|
-
if not options.check and not options.dir and not options.video:
|
|
1847
|
-
parser.error(
|
|
1848
|
-
"A video, Directory of images or histograms file needs to be provided.\n\n"
|
|
1849
|
-
"Use -h to see available options"
|
|
1850
|
-
)
|
|
1851
|
-
|
|
1852
|
-
if options.perceptual or options.contentful:
|
|
1853
|
-
if not options.video:
|
|
1854
|
-
parser.error(
|
|
1855
|
-
"A video file needs to be provided.\n\n"
|
|
1856
|
-
"Use -h to see available options"
|
|
1857
|
-
)
|
|
1858
|
-
|
|
1859
|
-
temp_dir = tempfile.mkdtemp(prefix="vis-")
|
|
1860
|
-
colors_temp_dir = tempfile.mkdtemp(prefix="vis-color-")
|
|
1861
|
-
directory = temp_dir
|
|
1862
|
-
if options.dir is not None:
|
|
1863
|
-
directory = options.dir
|
|
1864
|
-
histogram_file = os.path.join(temp_dir, "histograms.json.gz")
|
|
1865
|
-
|
|
1866
|
-
# Set up logging
|
|
1867
|
-
log_level = logging.CRITICAL
|
|
1868
|
-
if options.verbose == 1:
|
|
1869
|
-
log_level = logging.ERROR
|
|
1870
|
-
elif options.verbose == 2:
|
|
1871
|
-
log_level = logging.WARNING
|
|
1872
|
-
elif options.verbose == 3:
|
|
1873
|
-
log_level = logging.INFO
|
|
1874
|
-
elif options.verbose == 4:
|
|
1875
|
-
log_level = logging.DEBUG
|
|
1876
|
-
if options.logfile is not None:
|
|
1877
|
-
logging.basicConfig(
|
|
1878
|
-
filename=options.logfile,
|
|
1879
|
-
level=log_level,
|
|
1880
|
-
format=options.logformat,
|
|
1881
|
-
datefmt="%H:%M:%S",
|
|
1882
|
-
)
|
|
1883
|
-
else:
|
|
1884
|
-
logging.basicConfig(
|
|
1885
|
-
level=log_level, format=options.logformat, datefmt="%H:%M:%S"
|
|
1886
|
-
)
|
|
1887
|
-
|
|
1888
|
-
ok = False
|
|
1889
|
-
try:
|
|
1890
|
-
if not options.check:
|
|
1891
|
-
# Run a quick check to make sure all requirements exist,
|
|
1892
|
-
# otherwise failures might be silent due to how this code is
|
|
1893
|
-
# structured.
|
|
1894
|
-
ok = check_config()
|
|
1895
|
-
if not ok:
|
|
1896
|
-
raise Exception("Please install requirements before running.")
|
|
1897
|
-
|
|
1898
|
-
if options.video:
|
|
1899
|
-
orange_file = None
|
|
1900
|
-
if options.orange:
|
|
1901
|
-
orange_file = os.path.join(
|
|
1902
|
-
os.path.dirname(os.path.realpath(__file__)), "orange.png"
|
|
1903
|
-
)
|
|
1904
|
-
if not os.path.isfile(orange_file):
|
|
1905
|
-
orange_file = os.path.join(colors_temp_dir, "orange.png")
|
|
1906
|
-
generate_orange_png(orange_file)
|
|
1907
|
-
video_to_frames(
|
|
1908
|
-
options.video,
|
|
1909
|
-
directory,
|
|
1910
|
-
options.force,
|
|
1911
|
-
orange_file,
|
|
1912
|
-
options.viewport,
|
|
1913
|
-
options.viewportretries,
|
|
1914
|
-
options.viewportminheight,
|
|
1915
|
-
options.viewportminwidth,
|
|
1916
|
-
options.full,
|
|
1917
|
-
)
|
|
1918
|
-
|
|
1919
|
-
# Calculate the histograms and visual metrics
|
|
1920
|
-
calculate_histograms(directory, histogram_file, options.force)
|
|
1921
|
-
metrics = calculate_visual_metrics(
|
|
1922
|
-
histogram_file,
|
|
1923
|
-
options.start,
|
|
1924
|
-
options.end,
|
|
1925
|
-
options.perceptual,
|
|
1926
|
-
options.contentful,
|
|
1927
|
-
directory,
|
|
1928
|
-
options.progress,
|
|
1929
|
-
options.herodata,
|
|
1930
|
-
)
|
|
1931
|
-
|
|
1932
|
-
if options.screenshot is not None:
|
|
1933
|
-
quality = 30
|
|
1934
|
-
if options.quality is not None:
|
|
1935
|
-
quality = options.quality
|
|
1936
|
-
save_screenshot(directory, options.screenshot, quality)
|
|
1937
|
-
# JPEG conversion
|
|
1938
|
-
if options.dir is not None and options.quality is not None:
|
|
1939
|
-
convert_to_jpeg(directory, options.quality)
|
|
1940
|
-
|
|
1941
|
-
if metrics is not None:
|
|
1942
|
-
ok = True
|
|
1943
|
-
if options.json:
|
|
1944
|
-
data = dict()
|
|
1945
|
-
for metric in metrics:
|
|
1946
|
-
data[metric["name"].replace(" ", "")] = metric["value"]
|
|
1947
|
-
if "videoRecordingStart" in globals():
|
|
1948
|
-
data["videoRecordingStart"] = videoRecordingStart
|
|
1949
|
-
print(json.dumps(data))
|
|
1950
|
-
else:
|
|
1951
|
-
for metric in metrics:
|
|
1952
|
-
print("{0}: {1}".format(metric["name"], metric["value"]))
|
|
1953
|
-
else:
|
|
1954
|
-
ok = check_config()
|
|
1955
|
-
except Exception as e:
|
|
1956
|
-
logging.exception(e)
|
|
1957
|
-
ok = False
|
|
1958
|
-
|
|
1959
|
-
# Clean up
|
|
1960
|
-
shutil.rmtree(temp_dir)
|
|
1961
|
-
shutil.rmtree(colors_temp_dir)
|
|
1962
|
-
if ok:
|
|
1963
|
-
exit(0)
|
|
1964
|
-
else:
|
|
1965
|
-
exit(1)
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
if "__main__" == __name__:
|
|
1969
|
-
main()
|