Created
January 2, 2025 09:40
-
-
Save rygorous/3c031829655031e46f949273d8ef60e0 to your computer and use it in GitHub Desktop.
RD chart template for Oodle Texture eval
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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