Skip to content

Instantly share code, notes, and snippets.

@rygorous
Created January 2, 2025 09:40
Show Gist options
  • Save rygorous/3c031829655031e46f949273d8ef60e0 to your computer and use it in GitHub Desktop.
Save rygorous/3c031829655031e46f949273d8ef60e0 to your computer and use it in GitHub Desktop.
RD chart template for Oodle Texture eval
<!DOCTYPE html>
<html>
<head>
<style>
body { background: #fff; margin: 10px; font: 12pt Calibri,Arial,Helvetica,sans-serif; }
h1 { font-size: 18pt; margin: .5em 0 .5em 0; }
h2 { font-size: 16pt; margin: .5em 0 .5em 0; }
canvas { margin: auto; }
table { border-spacing: 0; }
table.rdresult tbody tr:nth-child(even) { background: #eee; }
table.rdresult tbody tr:nth-child(odd) { background: #e0e0e0; }
table.rdresult tbody tr td, table.rdresult thead tr th { border-left: 1px solid #000; }
table.rdresult tbody tr td:nth-child(1), table.rdresult thead tr th:nth-child(1) { border-left: none; }
table.rdresult tr { padding: 0; }
table.rdresult td, th { padding: 0 .3em; }
table.rdresult th { font-weight: bold; text-align: center; border-bottom: 1px solid #000; }
table.rdresult td.num { text-align: right; }
nav.toc { font-size: .9em; }
</style>
<script>
var renderRDChart = (function () {
"use strict";
var lerp = function(a, b, t) {
return (1.0 - t)*a + t*b;
};
var unlerp = function(x, a, b) {
return (x - a) / (b - a);
};
var segmentLen = function(x0, y0, x1, y1) {
let dx = x1 - x0;
let dy = y1 - y0;
return Math.sqrt(dx*dx + dy*dy);
};
var textWidth = function(ctx, font, text) {
let old_font = ctx.font;
ctx.font = font;
let metrics = ctx.measureText(text);
ctx.font = old_font;
return metrics.width;
};
var drawLineWithArrowTip = function(ctx, x0, y0, x1, y1, width) {
let halfw = width * 0.5;
let seglen = segmentLen(x0, y0, x1, y1);
let tiplen = Math.min(width * 4, seglen);
let tipw = tiplen * 0.5;
let dir_x = (x1 - x0) / seglen;
let dir_y = (y1 - y0) / seglen;
let perp_x = -dir_y;
let perp_y = dir_x;
let tip_t = (seglen - tiplen) / seglen;
let tip_x = lerp(x0, x1, tip_t);
let tip_y = lerp(y0, y1, tip_t);
// arrow with tip
ctx.beginPath();
ctx.moveTo(x0 - halfw * dir_x - halfw * perp_x, y0 - halfw * dir_y - halfw * perp_y);
ctx.lineTo(tip_x - halfw * perp_x, tip_y - halfw * perp_y);
ctx.lineTo(tip_x - tipw * perp_x, tip_y - tipw * perp_y);
ctx.lineTo(x1, y1);
ctx.lineTo(tip_x + tipw * perp_x, tip_y + tipw * perp_y);
ctx.lineTo(tip_x + halfw * perp_x, tip_y + halfw * perp_y);
ctx.lineTo(x0 - halfw * dir_x + halfw * perp_x, y0 - halfw * dir_y + halfw * perp_y);
ctx.closePath();
ctx.fill();
};
var makeFont = function(height, face) {
let font = {
height: height,
baseline: 0.85 * height, // argh?
name: height + "px " + face
};
return font;
};
var make2D = function(x, y) {
return {
x: x,
y: y
};
};
var unpackAnchor = function(anchor) {
if (anchor === "N")
return make2D(0.5, 0.0);
else if (anchor === "NW")
return make2D(0.0, 0.0);
else if (anchor === "NE")
return make2D(1.0, 0.0);
else if (anchor === "W")
return make2D(0.0, 0.5);
else if (anchor === "E")
return make2D(1.0, 0.5);
else if (anchor === "SW")
return make2D(0.0, 1.0);
else if (anchor === "SE")
return make2D(1.0, 1.0);
else if (anchor === "S")
return make2D(0.5, 1.0);
else
return anchor;
};
var measureText = function(ctx, font, text) {
ctx.font = font.name;
return make2D(ctx.measureText(text).width, font.height);
};
var anchorText = function(ctx, x, y, anchor, text, font) {
anchor = unpackAnchor(anchor);
ctx.font = font.name;
// Estimate the bounding rect
let dim = measureText(ctx, font, text);
// Put the anchor at the desired position
// lerp(x0, x0 + w, anchor.x) == x
// lerp(y0, y0 + h, anchor.y) == y
// ->
// x0 = x - w*anchor.x
// y0 = y - h*anchor.y
ctx.fillText(text, x - dim.x*anchor.x, y - dim.y*anchor.y + font.baseline);
};
var getColumnLabel = function(column) {
if (column[1] === "")
return column[0];
else
return column[0] + " (" + column[1] + ")";
};
var makeLayout = function(ctx, data) {
let element = ctx.canvas;
let w = element.width;
let h = element.height;
// Extract legend from data
let legend = data.codecs;
let margin = 5;
let tickmark_size = 6;
let title_font = makeFont(16, "sans-serif");
let label_font = makeFont(12, "sans-serif");
let ymarks_width = Math.ceil(textWidth(ctx, label_font, "999"));
let axis_margin = 16; // distance of furthest-out tick mark from end of axis arrow
// Determine max width of items in legend
let max_legend_label_width = 0;
for (var i = 0; i < legend.length; i++) {
max_legend_label_width = Math.max(max_legend_label_width, measureText(ctx, label_font, legend[i][0]).x);
}
let legend_width = label_font.height*1.5 /*box*/ + max_legend_label_width;
// Determine width of all column labels (for axis labels)
let max_col_label_width = 0;
for (let i = 0; i < data.columns.length; i++) {
// Skip special columns
if (data.columns[i][1].startsWith('#'))
continue;
let label = getColumnLabel(data.columns[i]);
max_col_label_width = Math.max(max_col_label_width, measureText(ctx, label_font, label).x);
}
let side_width = Math.max(legend_width, max_col_label_width);
let layout = {
title_font: title_font,
label_font: label_font,
x0: margin + Math.max(ymarks_width + tickmark_size, max_col_label_width*0.5),
y0: h - margin - tickmark_size - label_font.height,
x1: w - margin - side_width,
y1: margin + title_font.height,
title_y: margin,
axis_width: 1.2,
tickmark_size: tickmark_size,
point_radius: 2.1,
legend: legend,
};
// Region the actual data values are mapped to
// (we have a bit of extra space for the arrows)
layout.data_x1 = layout.x1 - axis_margin;
layout.data_y1 = layout.y1 + axis_margin;
return layout;
};
var linearAxisMap = function(val, val_min, val_max, tgt_min, tgt_max) {
return lerp(tgt_min, tgt_max, unlerp(val, val_min, val_max));
};
var logAxisMap = function(val, val_min, val_max, tgt_min, tgt_max) {
return lerp(tgt_min, tgt_max, unlerp(Math.log(val), Math.log(val_min), Math.log(val_max)));
};
var makeAxisMap = function(column, min_coord, max_coord) {
let minv = column[3];
let maxv = column[4];
if (column[2] === "linear") {
return function(v) { return linearAxisMap(v, minv, maxv, min_coord, max_coord); };
} else if (column[2] == "log2" || column[2] == "log10") {
return function(v) { return logAxisMap(v, minv, maxv, min_coord, max_coord); };
} else {
return {};
}
};
var updateAxisMaps = function(layout, data, cfg) {
cfg.x_map = makeAxisMap(data.columns[cfg.col_x], layout.x0, layout.data_x1);
cfg.y_map = makeAxisMap(data.columns[cfg.col_y], layout.y0, layout.data_y1);
};
var defaultFormatNumber = function(number) {
return number.toString();
}
var drawXTickMark = function(ctx, layout, cfg, x, major, format) {
let mapped_x = cfg.x_map(x);
ctx.beginPath();
ctx.moveTo(mapped_x, layout.y0);
if (major)
ctx.lineTo(mapped_x, layout.y0 + layout.tickmark_size);
else
ctx.lineTo(mapped_x, layout.y0 + layout.tickmark_size*0.5);
ctx.stroke();
if (major)
anchorText(ctx, mapped_x, layout.y0 + layout.tickmark_size, "N", format(x), layout.label_font);
};
var drawYTickMark = function(ctx, layout, cfg, y, major, format) {
let mapped_y = cfg.y_map(y);
ctx.beginPath();
ctx.moveTo(layout.x0, mapped_y);
if (major)
ctx.lineTo(layout.x0 - layout.tickmark_size, mapped_y);
else
ctx.lineTo(layout.x0 - layout.tickmark_size*0.5, mapped_y);
ctx.stroke();
if (major)
anchorText(ctx, layout.x0 - layout.tickmark_size, mapped_y, "E", format(y) + " ", layout.label_font);
};
var drawColumnTickMarks = function(column, draw_func) {
let minv = column[3];
let maxv = column[4];
if (column[2] == "linear") {
// Find target pow10 spacing density that gives no less than 3 and no more than 30 major ticks
let range = maxv - minv;
let coarse_spacing_log10 = Math.floor(Math.log10(range / 3.0));
let coarse_spacing_lin = Math.pow(10.0, coarse_spacing_log10);
let coarse_spacing = coarse_spacing_lin;
// Further adjust the coarse spacing: if we have less than 6 or more than 15 major ticks,
// scale down or up by a power of 2, respectively
let num_ticks_initial_coarse = range / coarse_spacing;
if (num_ticks_initial_coarse < 6.0) {
coarse_spacing /= 2.0;
coarse_spacing_log10 -= 1.0;
coarse_spacing_lin /= 10.0;
} else if (num_ticks_initial_coarse > 15.0) {
coarse_spacing *= 2.0;
}
let spacing = coarse_spacing / 5;
let tick_min = spacing * Math.ceil(minv / spacing);
let tick_max = spacing * Math.floor(maxv / spacing);
// Coarse spacing: one per 5 minor ticks
let initial_phase = Math.ceil(minv / spacing) % 5;
if (initial_phase < 0)
initial_phase += 5;
// Determine how to format numbers
// value differences are on the order of coarse_spacing
// so print values with enough precision to resolve the tick spacing
let max_abs = Math.max(Math.abs(minv), Math.abs(maxv));
let format_number = defaultFormatNumber;
if (max_abs > 0.0) {
format_number = function(number) {
// Print just the integer digits at the target scaling level
if (coarse_spacing_log10 >= 0 && coarse_spacing_log10 <= 2) {
// Small-ish integer
return number.toFixed(0);
} else if (coarse_spacing_log10 >= -2 && coarse_spacing_log10 < 0) {
// Tenths or hundreds are "almost integer", add back a decimal point as required
let digits = (number / coarse_spacing_lin).toFixed(0);
if (digits !== "0") {
digits = digits.padStart(1 - coarse_spacing_log10, "0");
let decimal_point_at = digits.length + coarse_spacing_log10;
digits = digits.slice(0, decimal_point_at) + "." + digits.slice(decimal_point_at, digits.length);
}
return digits;
} else {
return number.toExponential(2);
}
};
}
for (let i = 0; tick_min + i*spacing <= tick_max; i++) {
var phase = (initial_phase + i) % 5;
draw_func(tick_min + i*spacing, phase == 0, format_number);
}
} else if (column[2] == "log2") {
for (let v = minv; v <= maxv; v *= 2) {
draw_func(v, true);
if (v * 1.25 <= maxv) draw_func(v * 1.25, false, defaultFormatNumber);
if (v * 1.50 <= maxv) draw_func(v * 1.50, false, defaultFormatNumber);
if (v * 1.75 <= maxv) draw_func(v * 1.75, false, defaultFormatNumber);
}
} else if (column[3] == "log10") {
// TODO
}
};
var drawAxes = function(ctx, layout, data, cfg) {
anchorText(ctx, 0.5 * (layout.x0 + layout.x1), layout.title_y, "N", cfg.title, layout.title_font);
updateAxisMaps(layout, data, cfg);
let col_x = data.columns[cfg.col_x];
let col_y = data.columns[cfg.col_y];
// The axes themselves
drawLineWithArrowTip(ctx, layout.x0, layout.y0, layout.x1, layout.y0, layout.axis_width);
drawLineWithArrowTip(ctx, layout.x0, layout.y0, layout.x0, layout.y1, layout.axis_width);
// Axis tick marks
drawColumnTickMarks(col_x, function(x, maj, fmt) { drawXTickMark(ctx, layout, cfg, x, maj, fmt) });
drawColumnTickMarks(col_y, function(y, maj, fmt) { drawYTickMark(ctx, layout, cfg, y, maj, fmt) });
// Axis labels
let x_label = getColumnLabel(col_x);
let y_label = getColumnLabel(col_y);
anchorText(ctx, layout.x0, layout.y1, "S", y_label, layout.label_font);
anchorText(ctx, layout.x1, layout.y0, "W", " " + x_label, layout.label_font);
// Legend
let num_rows = layout.legend.length;
let line_spacing = 1.6*layout.label_font.height;
let base_y = 0.5 * (layout.y0 + layout.y1 - ((num_rows-1)*line_spacing + layout.label_font.height));
let box_diam = layout.label_font.height;
for (let i = 0; i < layout.legend.length; i++) {
let y = base_y + i * line_spacing;
ctx.fillStyle = layout.legend[i][1];
ctx.fillRect(layout.x1, y, box_diam, box_diam);
ctx.fillStyle = "black";
anchorText(ctx, layout.x1 + box_diam, y, "NW", " " + layout.legend[i][0], layout.label_font);
}
};
var drawPoint = function(ctx, layout, cfg, x, y) {
ctx.beginPath();
ctx.arc(cfg.x_map(x), cfg.y_map(y), layout.point_radius, 0, 2*Math.PI);
ctx.fill();
};
var render = function(element, data, cfg) {
let ctx = element.getContext("2d");
let layout = makeLayout(ctx, data);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
drawAxes(ctx, layout, data, cfg);
// Clip data points to layout body region
ctx.save();
ctx.beginPath();
ctx.moveTo(layout.x0, layout.y0);
ctx.lineTo(layout.x1, layout.y0);
ctx.lineTo(layout.x1, layout.y1);
ctx.lineTo(layout.x0, layout.y1);
ctx.closePath();
ctx.clip();
// Draw lines between connected data points
// draw these first so they render below the points
let curCodec = null;
ctx.save();
ctx.beginPath(); // dummy empty path at begin
ctx.setLineDash([4,2]);
for (let i = 1; i < data.points.length; i++) {
let x = data.points[i][cfg.col_x];
let y = data.points[i][cfg.col_y];
let codec = data.points[i][0];
if (codec !== curCodec) {
// complete previous path
ctx.stroke();
curCodec = codec;
ctx.strokeStyle = data.codecs[codec][1];
ctx.beginPath();
ctx.moveTo(cfg.x_map(x), cfg.y_map(y));
} else {
ctx.lineTo(cfg.x_map(x), cfg.y_map(y));
}
}
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
// Draw data points
for (let i = 0; i < data.points.length; i++) {
let x = data.points[i][cfg.col_x];
let y = data.points[i][cfg.col_y];
let codec = data.codecs[data.points[i][0]];
ctx.fillStyle = codec[1];
drawPoint(ctx, layout, cfg, x, y);
}
ctx.restore();
};
return render;
})();
var rd_schema = {
codecs: [
[ "baseline", "#008a00" ],
],
columns: [
[ "Codec", "#codec", "", NaN, NaN ],
[ "lambda", "", "log2", 1.0, 128.0 ],
[ "bpb", "", "linear", 2.0, 8.0 ],
[ "RMS error", "", "linear", 1.0, 4.0 ],
[ "VCDiff", "", "linear", 1.0, 6.0 ],
[ "Image", "#image", "", NaN, NaN ],
[ "Alpha image", "#alphaimage", "", NaN, NaN ],
],
};
function insert_numeric_cell(row, value) {
let cell = row.insertCell();
if (typeof value === 'number' && isNaN(value)) {
cell.innerText = "-";
} else {
cell.innerText = value;
}
cell.classList.add("num");
}
function render_rd_results(idPrefix, title, points) {
if (points.length < 1) {
return;
}
// Find value ranges
let minBpb = Infinity, maxBpb = -Infinity;
let minRmse = Infinity, maxRmse = -Infinity;
let minVcdiff = Infinity, maxVcdiff = -Infinity;
points.forEach(dataRow => {
let bpb = dataRow[2];
let rmse = dataRow[3];
let vcdiff = dataRow[4];
minBpb = Math.min(minBpb, bpb);
maxBpb = Math.max(maxBpb, bpb);
minRmse = Math.min(minRmse, rmse);
maxRmse = Math.max(maxRmse, rmse);
minVcdiff = Math.min(minVcdiff, vcdiff);
maxVcdiff = Math.max(maxVcdiff, vcdiff);
});
let data = Object.assign({}, rd_schema);
// Update value ranges in schema, but don't let things go too crazy
data.columns[2][3] = Math.max(Math.floor(minBpb * 5.0) / 5.0, 0.0);
data.columns[2][4] = Math.min(Math.ceil(maxBpb * 5.0) / 5.0, 9.0);
data.columns[3][3] = Math.max(Math.floor(minRmse * 5.0) / 5.0, 0.0);
data.columns[3][4] = Math.min(Math.ceil(maxRmse * 5.0) / 5.0, 50.0);
data.columns[4][3] = Math.max(Math.floor(minVcdiff * 5.0) / 5.0, 0.0);
data.columns[4][4] = Math.min(Math.ceil(maxVcdiff * 5.0) / 5.0, 50.0);
data.points = points;
renderRDChart(document.getElementById(idPrefix + "_rmse"), data,
{
title: title,
col_x: 2,
col_y: 3,
});
renderRDChart(document.getElementById(idPrefix + "_vcdiff"), data,
{
title: title,
col_x: 2,
col_y: 4,
});
// Set up table header
let table = document.getElementById(idPrefix + "_table");
const columnNames = [
"Lambda", "BPB", "RMSE", "VCDiff", "Rate red.", "Image"
];
let tabHeader = table.createTHead();
let headerRow = tabHeader.insertRow();
columnNames.forEach(columnName => {
let hdrElement = document.createElement("th");
hdrElement.innerText = columnName;
headerRow.appendChild(hdrElement);
});
let tabBody = table.createTBody();
let baselineBpb = 0.0;
points.forEach(dataRow => {
// Skip points that are not baseline or prev
let codecType = dataRow[0];
if (codecType == 0) {
baselineBpb = dataRow[2];
} else if (codecType >= 2) {
return;
}
let tabRow = tabBody.insertRow();
insert_numeric_cell(tabRow, dataRow[1]); // lambda
insert_numeric_cell(tabRow, dataRow[2].toFixed(4)); // bpb
insert_numeric_cell(tabRow, dataRow[3].toFixed(4)); // rmse
insert_numeric_cell(tabRow, dataRow[4].toFixed(4)); // vcdiff
insert_numeric_cell(tabRow, (100.0 * (baselineBpb - dataRow[2]) / baselineBpb).toFixed(2) + "%"); // rate reduction
let imageCell = tabRow.insertCell();
let imageName = dataRow[5];
let imageDisplayName = imageName;
let imageLastSlash = imageName.lastIndexOf("/");
if (imageLastSlash != -1) {
imageDisplayName = imageName.substr(imageLastSlash + 1);
}
let imageHtml = '<a href="' + imageName + '">' + imageDisplayName + '</a>';
let alphaImageName = dataRow[6];
if (alphaImageName !== null) {
imageHtml += ' <a href="' + alphaImageName + '">(alpha)</a>';
}
imageCell.innerHTML = imageHtml;
});
}
function getTocTargets() {
// 'toc' the header right before a list with links to all anchors we care about for navigation
const candidateLinks = document.querySelectorAll("#toc+ol>li>a:first-of-type");
let elements = [];
candidateLinks.forEach((candidateLink) => {
// We only care about links to elsewhere in this document
if (candidateLink.hash !== "") {
candidateElement = document.getElementById(candidateLink.hash.slice(1));
if (candidateElement !== null) {
elements.push(candidateElement);
}
}
});
return elements;
}
function findNextTocItem(direction) {
let tocItems = getTocTargets();
if (tocItems.length === 0) {
return null;
}
let currentItemIndex = 0;
// Find the "current" item, which we take to be the first that has a positive "top" coordinate
for (const [index, item] of tocItems.entries()) {
const bounds = item.getBoundingClientRect();
if (bounds.top >= -0.5) {
currentItemIndex = index;
break;
}
}
// For negative direction, the "next" item is our predecessor, if any
if (direction < 0) {
return tocItems[Math.max(currentItemIndex - 1, 0)];
} else if (direction > 0) {
return tocItems[Math.min(currentItemIndex + 1, tocItems.length - 1)];
} else {
return tocItems[currentItemIndex];
}
}
function scrollToNextTocItem(direction) {
findNextTocItem(direction).scrollIntoView({ behavior: "instant", inline: "start" });
}
document.addEventListener("keydown", (event) => {
if (event.key == ",") {
scrollToNextTocItem(-1);
} else if (event.key == ".") {
scrollToNextTocItem(1);
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment