annotation_gui_gcp/js/ImageView.js (237 lines of code) (raw):

let canvas; let imageListBox; let context; const Measurements = {}; const image = new Image(); let currentPointID = null; let currentImageID; let currentImageScale; const RedirectCache = {}; let maxImageSize = 1024; const defaultSigma = 0.004 // 70/95/99% CI = 2.6/3.8/7.6 px @ VGA resolution. const preloadLock = new Set() let pointColors = {}; window.addEventListener('DOMContentLoaded', onDOMLoaded); function onDOMLoaded() { canvas = document.getElementById("imgCanvas"); imageListBox = document.getElementById("imageSelectBox"); context = canvas.getContext("2d"); window.addEventListener('load', initialize); } function changeImage(image_key) { image.onload = function () { preloadNeigbors(image_key); resizeCanvas(); displayImage(image_key); drawMeasurements(); }; let url = `${window.location.href}/image/${maxImageSize}/${image_key}`; if (url in RedirectCache){ url = RedirectCache[url] } image.src = url; } function displayImage(image_key) { currentImageID = image_key; // Clear Canvas context.fillStyle = "#FFF"; context.fillRect(0, 0, canvas.width, canvas.height); if (!currentImageID){return;} // Scale image to fit and draw const w = image.width; const h = image.height; const scale_x = canvas.width / image.width; const scale_y = canvas.height / image.height; currentImageScale = Math.min(scale_x, scale_y); context.drawImage(image, 0, 0, w * currentImageScale, h * currentImageScale); for (let i = 0; i < imageListBox.length; i++) { const opt = imageListBox.options[i]; opt.style.fontWeight = (opt.value == currentImageID) ? "bold" : "normal" } imageListBox.value = currentImageID; } function onImageSelect() { const opt = imageListBox.options[imageListBox.options.selectedIndex]; changeImage(opt.value); } function populateImageList(images) { const L = imageListBox.options.length - 1; for (let i = L; i >= 0; i--) { imageListBox.remove(i); } let index = 0; for (let image_id in images) { const opt = document.createElement("option"); opt.text = index; opt.value = image_id; imageListBox.options.add(opt); if (opt.value == currentImageID) { opt.style.fontWeight = "bold"; imageListBox.selectedIndex = index;; } index +=1; } if (imageListBox.selectedIndex == -1){ currentImageID = null; } else{ preloadNeigbors(currentImageID); } redrawWindow(); } function preloadImage(key){ const url = `${window.location.href}/image/${maxImageSize}/${key}`; if (!(url in RedirectCache) && !(preloadLock.has(url))){ preloadLock.add(url); const req = new XMLHttpRequest(); req.onload = function () { preloadLock.delete(url); RedirectCache[url] = req.responseURL; }; req.open("GET", url, true); req.send(); } } function preloadNeigbors(image_key){ // Find the index of the current image let image_index; for (let i = 0; i < imageListBox.options.length; i++) { if (image_key == imageListBox.options[i].value){ image_index = i; break; } } // Preload the neighbors for (let i = 0; i < imageListBox.options.length; i++) { const image_key = imageListBox.options[i].value; if (i != image_index && Math.abs(i - image_index) < 10){ preloadImage(image_key); } } } function populateMeasurements(points) { for (let image_id in points) { Measurements[image_id] = {}; for (let point_id in points[image_id]) { const norm_point = points[image_id][point_id]; const measurement = new Measurement(norm_point[0], norm_point[1], point_id, image_id, norm_point[2]); Measurements[image_id][point_id] = measurement; } } redrawWindow(); } function onSyncHandler(data) { pointColors = data["colors"]; populateImageList(data["points"]); populateMeasurements(data["points"]); currentPointID = data["selected_point"]; redrawWindow(); } function initialize() { const sse = initialize_event_source([ { event: "sync", handler: onSyncHandler } ]); window.onunload = () => { sse.close(); } canvas.addEventListener("mousedown", mouseClicked, false); canvas.addEventListener("mousewheel", mouseWheelTurned, false); window.addEventListener("resize", onWindowResize); imageListBox.addEventListener('change', onImageSelect); post_json({ event: "init" }); } function resizeCanvas() { context.canvas.width = window.innerWidth - imageListBox.offsetWidth - 30; context.canvas.height = window.innerHeight - 30; } function redrawWindow() { // box.size = box.options.length; resizeCanvas(); displayImage(currentImageID); drawMeasurements(); } function onWindowResize() { redrawWindow(); } class Measurement { constructor(norm_x, norm_y, id, image_id, norm_precision) { this.norm_x = norm_x; // x, y in normalized pixels this.norm_y = norm_y; this.id = id; this.image_id = image_id; this.norm_precision = norm_precision; // std. deviation in pixels / max(w,h) } } function drawCircle(x, y, radius_px, color){ context.beginPath(); context.arc(x, y, radius_px, 0, 2 * Math.PI, false); // context.fillStyle = 'red'; // context.fill(); context.lineWidth = Math.max(1, Math.min(10, radius_px / 10)); context.strokeStyle = color; context.stroke(); } function drawOneMeasurement(measurement) { // Draw measurement const color = pointColors[measurement.id]; const normalizer = Math.max(image.width, image.height); const x = (image.width / 2 + measurement.norm_x * normalizer) * currentImageScale; const y = (image.height / 2 + measurement.norm_y * normalizer) * currentImageScale; const radius_px = measurement.norm_precision * normalizer * currentImageScale; // drawCircle(x, y, radius_px, color); // 70% confidence interval drawCircle(x, y, radius_px * 2, color); // 95% confidence interval drawCircle(x, y, radius_px * 3, color); // 99% confidence interval context.font = "20px Arial"; const markerText = measurement.id; const textMeasurements = context.measureText(markerText); context.fillStyle = "#000"; context.fillText(markerText, x + textMeasurements.width / 2, y); } function drawMeasurements() { if (!(currentImageID in Measurements)) { return; } // Draw measurements for (const [id, measurement] of Object.entries(Measurements[currentImageID])) { drawOneMeasurement(measurement); } }; function remove_point_observation(image_id, point_id) { const data = { event: "remove_point_observation", image_id: image_id, point_id: point_id, }; post_json(data); } function add_or_update_point_observation(measurement) { const data = { event: "add_or_update_point_observation", point_id: measurement.id, norm_precision: measurement.norm_precision, xy: [measurement.norm_x, measurement.norm_y], image_id: measurement.image_id, }; post_json(data); } const mouseWheelTurned = function (wheel) { // Find index of selected image let selected_i = null; for (let i = 0; i < imageListBox.options.length; i++) { const opt = imageListBox.options[i]; if (currentImageID == opt.value) { selected_i = i; break; } } selected_i = (wheel.deltaY > 0) ? selected_i - 1 : selected_i + 1 if (selected_i >= 0 && selected_i < imageListBox.options.length) { changeImage(imageListBox.options[selected_i].value); } } const mouseClicked = function (mouse) { if (currentPointID === null) { console.log("No point selected, ignoring click") return; } if (mouse.button == 0) { // native pixel coordinates const rect = canvas.getBoundingClientRect(); const normalizer = Math.max(image.width, image.height); const norm_x = ((mouse.x - rect.left) / currentImageScale - image.width / 2) / normalizer; const norm_y = ((mouse.y - rect.top) / currentImageScale - image.height / 2) / normalizer; const measurement = new Measurement(norm_x, norm_y, currentPointID, currentImageID, defaultSigma); // Send the clicked point to the backend. Will be draw on next sync add_or_update_point_observation(measurement); } else { remove_point_observation(currentImageID, currentPointID); } }