Skip to content

Commit

Permalink
Add support to visualmetrics to identify key frames matching the give…
Browse files Browse the repository at this point in the history
…n colors.

This patch updates the visualmetrics.py and visualmetrics-portable.py
scripts to allow it to identify and record timestamps for frames that
match the given color configuration. This consists of an RGB value with
fuzz (anything +/- fuzz matches) and a fraction between 0 and 1 of the
required percentage of each channel from the histogram that must match it.
  • Loading branch information
aosmond committed May 2, 2024
1 parent f7fe3bc commit 51a0aad
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 2 deletions.
4 changes: 4 additions & 0 deletions lib/chrome/webdriver/setupChromiumOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ export function setupChromiumOptions(
}
}

if (browserOptions.enableVideoAutoplay) {
seleniumOptions.addArguments("--autoplay-policy=no-user-gesture-required");

Check failure on line 175 in lib/chrome/webdriver/setupChromiumOptions.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Replace `"--autoplay-policy=no-user-gesture-required"` with `'--autoplay-policy=no-user-gesture-required'`
}

// It's a new splash screen introduced in Chrome 98
// for new profiles
// disable it with ChromeWhatsNewUI
Expand Down
7 changes: 6 additions & 1 deletion lib/core/engine/iteration.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
addConnectivity,
removeConnectivity
} from '../../connectivity/index.js';
import { jsonifyVisualProgress } from '../../support/util.js';
import { jsonifyVisualProgress, jsonifyKeyColorFrames } from '../../support/util.js';

Check failure on line 19 in lib/core/engine/iteration.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Replace `·jsonifyVisualProgress,·jsonifyKeyColorFrames·` with `⏎··jsonifyVisualProgress,⏎··jsonifyKeyColorFrames⏎`
import { flushDNS } from '../../support/dns.js';

import { getNumberOfRunningProcesses } from '../../support/processes.js';
Expand Down Expand Up @@ -235,6 +235,11 @@ export class Iteration {
);
}
}
if (videoMetrics.visualMetrics['KeyColorFrames']) {
videoMetrics.visualMetrics['KeyColorFrames'] = jsonifyKeyColorFrames(

Check failure on line 239 in lib/core/engine/iteration.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Replace `··videoMetrics.visualMetrics['KeyColorFrames']·=` with `videoMetrics.visualMetrics['KeyColorFrames']·=⏎···············`
videoMetrics.visualMetrics['KeyColorFrames']
);
}
result[index_].videoRecordingStart =
videoMetrics.videoRecordingStart;
result[index_].visualMetrics = videoMetrics.visualMetrics;
Expand Down
10 changes: 10 additions & 0 deletions lib/support/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ export function parseCommandLine() {
type: 'boolean',
group: 'chrome'
})
.option('chrome.enableVideoAutoplay', {
describe: 'Allow videos to autoplay.',
type: 'boolean',
group: 'chrome'
})
.option('chrome.timeline', {
alias: 'chrome.trace',
describe:
Expand Down Expand Up @@ -720,6 +725,11 @@ export function parseCommandLine() {
describe:
'Use the portable visual-metrics processing script (no ImageMagick dependencies).'
})
.option('visualMetricsKeyColor', {
type: 'array',
nargs: 6,
describe: 'Collect Key Color frame metrics when you run --visualMetrics. Each --visualMetricsKeyColor supplied must have 6 arguments: key name, red channel (0-255), green channel (0-255), blue channel (0-255), channel fuzz (0-255), fraction (0.0-1.0) of pixels that must match each channel.',

Check failure on line 731 in lib/support/cli.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Replace `·'Collect·Key·Color·frame·metrics·when·you·run·--visualMetrics.·Each·--visualMetricsKeyColor·supplied·must·have·6·arguments:·key·name,·red·channel·(0-255),·green·channel·(0-255),·blue·channel·(0-255),·channel·fuzz·(0-255),·fraction·(0.0-1.0)·of·pixels·that·must·match·each·channel.',` with `⏎········'Collect·Key·Color·frame·metrics·when·you·run·--visualMetrics.·Each·--visualMetricsKeyColor·supplied·must·have·6·arguments:·key·name,·red·channel·(0-255),·green·channel·(0-255),·blue·channel·(0-255),·channel·fuzz·(0-255),·fraction·(0.0-1.0)·of·pixels·that·must·match·each·channel.'`
})
.option('scriptInput.visualElements', {
describe:
'Include specific elements in visual elements. Give the element a name and select it with document.body.querySelector. Use like this: --scriptInput.visualElements name:domSelector see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors. Add multiple instances to measure multiple elements. Visual Metrics will use these elements and calculate when they are visible and fully rendered.'
Expand Down
3 changes: 2 additions & 1 deletion lib/support/har/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ function addExtrasToHAR(
'ContentfulSpeedIndex',
'VisualProgress',
'ContentfulSpeedIndexProgress',
'PerceptualSpeedIndexProgress'
'PerceptualSpeedIndexProgress',
'KeyColorFrames'
]);

for (let key of Object.keys(visualMetricsData)) {
Expand Down
23 changes: 23 additions & 0 deletions lib/support/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,29 @@ export function jsonifyVisualProgress(visualProgress) {
}
return visualProgress;
}
export function jsonifyKeyColorFrames(keyColorFrames) {
// Original data looks like
// "FrameName1=[0-133 255-300], FrameName2=[133-255] FrameName3=[]"
if (typeof keyColorFrames === 'string') {
const keyColorFramesObject = {};
for (const keyColorPair of keyColorFrames.split(', ')) {
const [name, values] = keyColorPair.split('=');
keyColorFramesObject[name] = [];
const rangePairs = values.replace('[', '').replace(']', '');
if (rangePairs) {
for (const rangePair of rangePairs.split(' ')) {
const [start, end] = rangePair.split('-');
keyColorFramesObject[name].push({
startTimestamp: Number.parseInt(start, 10),
endTimestamp: Number.parseInt(end, 10),

Check failure on line 247 in lib/support/util.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Delete `,`
});
}
}
}
return keyColorFramesObject;
}
return keyColorFrames;
}
export function adjustVisualProgressTimestamps(
visualProgress,
profilerStartTime,
Expand Down
9 changes: 9 additions & 0 deletions lib/video/postprocessing/visualmetrics/visualMetrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ export async function run(
scriptArguments.push('--contentful');
}

if (options.visualMetricsKeyColor) {
for (let i = 0; i < options.visualMetricsKeyColor.length; ++i) {
if (i % 6 == 0) {
scriptArguments.push('--keycolor')

Check failure on line 90 in lib/video/postprocessing/visualmetrics/visualMetrics.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `;`
}
scriptArguments.push(options.visualMetricsKeyColor[i]);
}
}

// There seems to be a bug with --startwhite that makes VM bail out
// 11:20:14.950 - Calculating image histograms
// 11:20:14.951 - No video frames found in /private/var/folders/27/xpnvcsbs0nlfbb4qq397z3rh0000gn/T/vis-cn_JMf
Expand Down
132 changes: 132 additions & 0 deletions visualmetrics/visualmetrics-portable.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,8 @@ def calculate_visual_metrics(
dirs,
progress_file,
hero_elements_file,
key_colors,
key_colors_file,
):
metrics = None
histograms = load_histograms(histograms_file, start, end)
Expand All @@ -1225,6 +1227,15 @@ def calculate_visual_metrics(
f = open(progress_file, "w")
json.dump(progress, f)
f.close()
key_color_frames = calculate_key_color_frames(histograms, key_colors)
if key_color_frames and key_colors_file is not None:
file_name, ext = os.path.splitext(key_colors_file)
if ext.lower() == ".gz":
f = gzip.open(key_colors_file, GZIP_TEXT, 7)
else:
f = open(key_colors_file, "w")
json.dump(key_color_frames, f)
f.close()
if len(histograms) > 1:
metrics = [
{"name": "First Visual Change", "value": histograms[1]["time"]},
Expand Down Expand Up @@ -1315,6 +1326,20 @@ def calculate_visual_metrics(
metrics.append({"name": "Perceptual Speed Index", "value": 0})
if contentful:
metrics.append({"name": "Contentful Speed Index", "value": 0})
if key_color_frames:
keysum = ""
for key in key_color_frames:
if len(keysum):
keysum += ", "
framesum = ""
for frame in key_color_frames[key]:
if len(framesum):
framesum += " "
framesum += "{0:d}-{1:d}".format(
frame["start_time"], frame["end_time"]
)
keysum += "{0}=[{1}]".format(key, framesum)
metrics.append({"name": "Key Color Frames", "value": keysum})
prog = ""
for p in progress:
if len(prog):
Expand Down Expand Up @@ -1346,6 +1371,90 @@ def load_histograms(histograms_file, start, end):
return histograms


def calculate_key_color_frames(histograms, key_colors):
if not key_colors:
return {}

key_color_frames = {}
for key in key_colors:
key_color_frames[key] = []

current = None
current_key = None
total = 0
matched = 0
channels = ["r", "g", "b"]
for index, histogram in enumerate(histograms):
buckets = 256
available = [0 for i in range(buckets)]

# Determine how many samples there are for each histogram channel
total = {}
for channel in channels:
total[channel] = 0
for i in range(buckets):
total[channel] += histogram["histogram"][channel][i]

matching_key = None
for key in key_colors:
fuzz = key_colors[key]["fuzz"]
fraction = key_colors[key]["fraction"]

channel_matches = True
for channel in channels:
# Find the acceptable range around the target channel value
target = key_colors[key][channel]
low = max(0, target - fuzz)
high = min(buckets, target + fuzz)

target_total = 0
for j in range(low, high):
target_total += histogram["histogram"][channel][j]

if target_total < total[channel] * fraction:
channel_matches = False
break

if channel_matches:
matching_key = key
break

if matching_key is not None:
if current_key is not None:
if current_key != key:
current["end_time"] = histogram["time"]
key_color_frames[current_key].append(current)
current_key = matching_key
current = {
"frame_count": 1,
"start_time": histogram["time"],
}
else:
current["frame_count"] += 1
else:
current_key = matching_key
current = {
"frame_count": 1,
"start_time": histogram["time"],
}

logging.debug(
"{0:d}ms - Matched key color frame {1}".format(
histogram["time"], matching_key
)
)
elif current_key is not None:
current["end_time"] = histogram["time"]
key_color_frames[current_key].append(current)
current_key = None
current = None

if current_key is not None:
current["end_time"] = histograms[-1]["time"]
key_color_frames[current_key].append(current)
return key_color_frames


def calculate_visual_progress(histograms):
progress = []
first = histograms[0]["histogram"]
Expand Down Expand Up @@ -1760,6 +1869,16 @@ def main():
default=False,
help="Remove orange-colored frames from the beginning of the video.",
)
parser.add_argument(
"--keycolor",
action="append",
nargs=6,
metavar=("key", "red", "green", "blue", "fuzz", "fraction"),
help="Identify frames that match the given channels (0-255) plus or "
"minus the given per channel fuzz. Fraction is the percentage of the "
"pixels per channel that must be in the given range (0-1).",
)
parser.add_argument("--keycolors", help="Key color frames output file.")
parser.add_argument(
"-p",
"--viewport",
Expand Down Expand Up @@ -1916,6 +2035,17 @@ def main():
options.full,
)

key_colors = {}
if options.keycolor:
for key_params in options.keycolor:
key_colors[key_params[0]] = {
"r": int(key_params[1]),
"g": int(key_params[2]),
"b": int(key_params[3]),
"fuzz": int(key_params[4]),
"fraction": float(key_params[5]),
}

# Calculate the histograms and visual metrics
calculate_histograms(directory, histogram_file, options.force)
metrics = calculate_visual_metrics(
Expand All @@ -1927,6 +2057,8 @@ def main():
directory,
options.progress,
options.herodata,
key_colors,
options.keycolors,
)

if options.screenshot is not None:
Expand Down
Loading

0 comments on commit 51a0aad

Please sign in to comment.