/* globals Utils, GetOptimalBoxWidth */
// global functions from drawsteps.js
/* globals
drawSpotProfileEdit, drawSubregionImage, drawSpotContent, drawSpotSignal,
drawProbeLayout, drawProbeLayoutSampling, drawResampled, drawGroundtruthImage,
drawVirtualSEM
*/
/* exported
G_UpdateResampled,
G_UpdateVirtualSEMConfig,
ResampleFullImage,
G_Update_GroundTruth
G_Update_InfoDisplays
G_update_ImgMetrics
G_UpdateRuler
G_UpdateFilters
G_UpdateStageSettings
G_AUTO_PREVIEW_LIMIT
G_VSEM_PAUSED
G_SHOW_SUBREGION_OVERLAY
G_IMG_METRIC_ENABLED
G_APP_NAME
G_INPUT_IMAGE
G_PRELOADED_IMAGES
G_PRELOADED_IMAGES_ROOT
G_IMG_METRICS
G_STAGES
*/
/** Name of the application */
const G_APP_NAME = "ImgBeamer";
var G_DEBUG = false;
Konva.autoDrawEnabled = true;
/** The number of cells in the raster grid at which auto-preview stops, for responsiveness */
// eslint-disable-next-line no-magic-numbers
var G_AUTO_PREVIEW_LIMIT = 16 * 16;
/** Toggle value to pause the continously draw the Resulting Image / Virtual SEM view */
var G_VSEM_PAUSED = false;
/** Toggle value to show/hide the subregion overlay on the Sample Ground Truth stage. */
var G_SHOW_SUBREGION_OVERLAY = true;
/** Toggle value to pause/hide the image quality metric calculation of the Resulting Image / Virtual SEM view */
var G_IMG_METRIC_ENABLED = true;
/** The root folder for all the preloaded images specified by {@link G_PRELOADED_IMAGES}. */
const G_PRELOADED_IMAGES_ROOT = "src/testimages/";
/**
* The list of default/preloaded images to use with the application.
* @see G_PRELOADED_IMAGES_ROOT
*/
const G_PRELOADED_IMAGES = [
'grains1.png',
'grains2full.png',
'grains2tl.png',
'grains2nc.png',
'APT_needle.png',
'tephra_448nm.png',
'tephra_200nm.png',
];
/** global variable to set the input ground truth image */
// var G_INPUT_IMAGE = Utils.getGroundtruthImage();
var G_INPUT_IMAGE = G_PRELOADED_IMAGES_ROOT + 'grains2tl.png';
// Preload the larger image files in the background
// without blocking the UI for improved responsiveness
window.addEventListener('load', function(){
// https://stackoverflow.com/a/59861857/883015
// var images = G_PRELOADED_IMAGES;
var images = ['grains2full.png', 'APT_needle.png', 'tephra_448nm.png', 'tephra_200nm.png'];
var preload = '';
for(let i = 0; i < images.length; i++) {
preload += '<link rel="preload" href="' + G_PRELOADED_IMAGES_ROOT
+ images[i] + '" as="image">\n';
}
$('head').append(preload);
});
/** The list of image quality metrics supported by the application. */
const G_IMG_METRICS = [
'SSIM',
'MS-SSIM',
'MSE',
'PSNR',
'iNRMSE',
'iNMSE',
];
/** global reference to update the resampling steps (spot layout,
* sampled subregion, resulting subregion) displays,
* This is mainly to be called when the auto-preview is disabled/off */
var G_UpdateResampled = null;
/** global reference to update the beam values for the Virtual SEM view */
var G_UpdateVirtualSEMConfig = null;
/** global reference to update the Groundtruth view */
var G_Update_GroundTruth = null;
/** global reference to update the Information displays */
var G_Update_InfoDisplays = null;
/** global reference to update the just the Image Metrics information display */
var G_update_ImgMetrics = null;
/** global reference to update the ruler */
var G_UpdateRuler = null;
/** global reference to update/apply image filters */
var G_UpdateFilters = null;
/** global reference to update stage related settings */
var G_UpdateStageSettings = null;
/** a global reference to the main body container that holds the boxes/stages.
* @todo do we still need this? Maybe remove... */
var G_MAIN_CONTAINER = $('#main-container');
/** The calculated size of each box/stage */
var G_BOX_SIZE = GetOptimalBoxWidth();
/** The number of stages to create */
const nStages = 9;
/** The array/list of all the stages. */
var G_STAGES = [];
// first create the stages
for (let i = 0; i < nStages; i++) {
let stage = Utils.newStageTemplate(G_MAIN_CONTAINER, G_BOX_SIZE, G_BOX_SIZE);
G_STAGES.push(stage);
}
/////////////////////
/**Currently only used by {@link ResampleFullImage}
* @todo Possibly, to be removed along with it. */
var G_MAIN_IMAGE_OBJ = null;
// call once on App start
UpdateBaseImage();
// update event for ground truth image change
$(document.body).on('OnGroundtruthImageChange', UpdateBaseImage);
/** Updates everything needed assuming that {@link G_INPUT_IMAGE} has changed,
* updates/draws all the stages/boxes once. */
function UpdateBaseImage(){
// load image and wait for when ready
Utils.loadImage(G_INPUT_IMAGE, function(event){
var imageObj = event.target;
G_MAIN_IMAGE_OBJ = imageObj;
OnImageLoaded(imageObj, G_STAGES);
});
}
/**
* Called by {@link UpdateBaseImage} once the image data has been loaded,
* Draws and manages all the drawing stages with each their event handlers.
* @param {*} eImg The Element/Object of the loaded image.
* @param {*} stages The array of stages to use for drawing.
*/
function OnImageLoaded(eImg, stages){
/* eslint-disable no-magic-numbers */
// Edit these numbers to change the display order
var baseImageStage = stages[1];
var spotProfileStage = stages[2];
var spotContentStage = stages[3];
var spotSignalStage = stages[4];
var probeLayoutStage = stages[5];
var layoutSampledStage = stages[6];
var resampledStage = stages[7];
var groundtruthMapStage = stages[0];
var virtualSEMStage = stages[8];
/* eslint-enable no-magic-numbers */
/** called when a change occurs in the spot profile, subregion, or spot content */
function doUpdate(){
// don't update spot signal if not shown
if ($(spotSignalStage.getContainer()).is(':visible')) {
updateSpotSignal();
}
updateProbeLayout();
updateResamplingSteps();
updateGroundtruthMap();
updateVirtualSEM_Config();
updateInfoDisplays();
}
var updateImgMetrics = function(){
Utils.updateImageMetricsInfo(groundtruthMapStage, virtualSEMStage);
};
G_update_ImgMetrics = updateImgMetrics;
var updateInfoDisplays = function(){
// update spot/beam info: size, rotation, shape
var cellSize = Utils.computeCellSize(subregionImage.getStage());
Utils.updateDisplayBeamParams(spotProfileStage, layoutBeam, cellSize, spotScaling, promptForSpotWidth);
Utils.updateMagInfo(baseImageStage, subregionImage);
Utils.updateSubregionPixelSize(resampledStage, subregionImage, eImg);
updateImgMetrics();
};
G_Update_InfoDisplays = updateInfoDisplays;
/** prompts the user for the spot width % */
function promptForSpotWidth(){
var spotWidth = prompt("Spot width (%) - Default is 100%", 100);
if (spotWidth > 0) {
Utils_SetSpotWidth(spotWidth);
}
}
// draw Spot Profile
$(spotProfileStage.getContainer())
.attr('box_label', 'Spot Profile')
.attr('note', 'Press [R] to reset shape\nScroll to change size')
.css('border-color', 'red');
var _spotProfileInfo = drawSpotProfileEdit(spotProfileStage, doUpdate);
var beam = _spotProfileInfo.beam;
var spotScaling = _spotProfileInfo.spotSize;
// Subregion View
// draw base image (can pan & zoom)
$(baseImageStage.getContainer())
.addClass('grabCursor')
.attr('box_label', 'Subregion View / FOV')
.attr('note', 'Pan & Zoom: Drag and Scroll\nPress [R] to reset')
.css('border-color', 'blue');
var subregionImage = drawSubregionImage(baseImageStage, eImg, G_BOX_SIZE, doUpdate);
// draw Spot Content
$(spotContentStage.getContainer())
.addClass('advancedMode')
.addClass('grabCursor')
.attr('box_label', 'Spot Content')
.attr('note', 'Scroll to adjust spot size\nHold [Shift] for half rate');
var spotContentBeam = beam.clone();
// make a clone without copying over the event bindings
var imageCopy = subregionImage.clone().off();
drawSpotContent(spotContentStage, imageCopy, spotContentBeam, doUpdate);
/**(temporary) publicly exposed function to set the spot width
* @param {number} spotWidth the spot width in percent (%), ex. use 130 for 130%.
* @todo move into separate file if possible */
function Utils_SetSpotWidth(spotWidth=100){
var beam = spotContentBeam;
var spotScaler = spotScaling;
// calculate the new scale for spot-content image, based on the given spot width
var cellSize = Utils.computeCellSize(spotScaler);
var maxScale = Math.max(beam.scaleX(), beam.scaleY());
var eccScaled = beam.scaleX() / maxScale;
var newScale = ((beam.width() * eccScaled) / (spotWidth/100)) / cellSize.w;
Utils.centeredScale(spotScaler, newScale);
// propagate changes and update stages
updateBeams();
doUpdate();
}
// draw Spot Signal
$(spotSignalStage.getContainer())
.addClass('advancedMode')
.addClass('note_colored')
.attr('box_label', '(Integrated) Spot Signal');
var spotSignalBeam = beam.clone();
var updateSpotSignal = drawSpotSignal(spotContentStage, spotSignalStage, spotSignalBeam);
// draw Spot Layout
$(probeLayoutStage.getContainer()).attr('box_label', 'Spot Layout');
var layoutBeam = beam.clone();
var updateProbeLayout = drawProbeLayout(probeLayoutStage, subregionImage, spotScaling, layoutBeam);
// draw Sampled Subregion
// compute resampled image
$(layoutSampledStage.getContainer())
.addClass('advancedMode')
.attr('box_label', 'Sampled Subregion');
var layoutSampledBeam = beam.clone();
var updateProbeLayoutSamplingPreview = drawProbeLayoutSampling(
layoutSampledStage,
subregionImage,
spotScaling,
layoutSampledBeam
);
// draw Resulting Subregion
$(resampledStage.getContainer())
.attr('box_label', 'Resulting Subregion')
.css('border-color', 'lime');
var resampledBeam = beam.clone();
var updateResampled = drawResampled(
layoutSampledStage,
resampledStage,
subregionImage,
spotScaling,
resampledBeam
);
var updateResamplingSteps = function(){
updateProbeLayout();
if ($(layoutSampledStage.getContainer()).is(':visible')) {
updateProbeLayoutSamplingPreview();
}
if ($(resampledStage.getContainer()).is(':visible')) {
updateResampled();
}
Utils.updateVSEM_ModeWarning(resampledStage);
};
G_UpdateResampled = updateResamplingSteps;
// draw Sample Ground Truth
$(groundtruthMapStage.getContainer()).attr('box_label', 'Sample Ground Truth');
var groundtruthMap = drawGroundtruthImage(groundtruthMapStage, eImg, subregionImage, G_BOX_SIZE, doUpdate);
var updateGroundtruthMap = groundtruthMap.updateFunc;
G_Update_GroundTruth = updateGroundtruthMap;
// add ruler on Ground truth stage
var rulerLayer = Utils.getLayerByName(groundtruthMapStage, 'myRuler');
if (rulerLayer != null) {
// make sure we don't add a duplicate layer, to avoid memory leaks
// mainly when a new image is loaded.
rulerLayer.destroy();
}
rulerLayer = new Konva.Layer({visible: false, name: 'myRuler'});
groundtruthMapStage.add(rulerLayer);
var ruler = Utils.CreateRuler(rulerLayer, eImg,
// Default is a ruler that is 2/3rd width of the stage and vertically in middle
groundtruthMapStage.width()*(1/3), groundtruthMapStage.height()/2,
groundtruthMapStage.width()*(2/3), groundtruthMapStage.height()/2
);
ruler.element.on('dblclick', function(){
var um = ruler.getLengthNm() / 1E3;
var pixelWidth = prompt("Please enter the length of the ruler in micrometers (μm)."
+ "\n\nTIP! Try holding the [Shift] key for horizontal lines or "
+ "[Ctrl] for vertical lines.", um, 0);
if (pixelWidth > 0) {
var pixelSize = ruler.getPixelSize(pixelWidth * 1E3);
// TODO: support non-square pixel...
// currently only support it in x-direction;
Utils.setPixelSizeNmInput(pixelSize.x);
ruler.doUpdate();
}
});
G_UpdateRuler = function(){
var show = Utils.getShowRulerInput();
ruler.doUpdate(); // update the ruler
rulerLayer.visible(show); // update visibility
};
// update ruler once immediately
G_UpdateRuler();
// draw Resulting Image
$(virtualSEMStage.getContainer()).attr('box_label', 'Resulting Image');
var vitualSEMBeam = beam.clone();
var updateVirtualSEM_Config = drawVirtualSEM(
virtualSEMStage,
vitualSEMBeam,
groundtruthMap.subregionRect,
groundtruthMapStage,
eImg,
spotScaling
);
G_UpdateVirtualSEMConfig = updateVirtualSEM_Config;
/** propagate changes to the spot-profile (beam) to the beams in the other stages */
function updateBeams(){
spotContentBeam.scale(beam.scale());
spotContentBeam.rotation(beam.rotation());
spotSignalBeam.scale(beam.scale());
spotSignalBeam.rotation(beam.rotation());
// keep the shape of the ellipse, not the actual size of it...
var maxScale = Math.max(beam.scaleX(), beam.scaleY());
layoutBeam.size({
width: beam.width() * (beam.scaleX() / maxScale),
height: beam.height() * (beam.scaleY() / maxScale),
});
layoutBeam.rotation(beam.rotation());
layoutSampledBeam.size(layoutBeam.size());
layoutSampledBeam.rotation(layoutBeam.rotation());
resampledBeam.size(layoutBeam.size());
resampledBeam.rotation(layoutBeam.rotation());
vitualSEMBeam.size(layoutBeam.size());
vitualSEMBeam.rotation(layoutBeam.rotation());
}
// update beams
beam.off('transform'); // prevent "eventHandler doubling" from subsequent calls
beam.on('transform', function(){
updateBeams();
doUpdate();
});
function updateFilters(){
var doBC = Utils.getGlobalBCInput();
if (doBC) {
// stages that we want to apply filters to...
var fStages = [
groundtruthMapStage, baseImageStage,
spotContentStage, probeLayoutStage,
layoutSampledStage
];
// apply the filters
const brightness = Utils.getBrightnessInput();
const contrast = Utils.getContrastInput();
for (let i = 0; i < fStages.length; i++) {
const fStage = fStages[i];
let image = Utils.getFirstImageFromStage(fStage);
Utils.applyBrightnessContrast(image, brightness, contrast);
}
// for the resulting images, the sampling function, Utils.ComputeProbeValue_gs(),
// is made B/C aware and using Konva's built-in filters directly.
}
// call global visual update
doUpdate();
}
// update filters once immediately
updateFilters();
G_UpdateFilters = updateFilters;
doUpdate();
Utils.updateAdvancedMode();
// update stage related settings
G_UpdateStageSettings = function(){
// update image smoothing for all stages
var smooth = Utils.getImageSmoothing();
for (let i = 0; i < stages.length; i++) {
const stage = stages[i];
Utils.setStageImageSmoothing(stage, smooth);
}
};
// update once immediately to stay in sync on start up
G_UpdateStageSettings();
}
/** Draws the full resample image given the parameters in the GUI and logs
* the progress in the webconsole. Very heavy and slow. Could may be optimized
* with an offscreenCanvas and webworkers...
* @todo Maybe no longer needed and can be removed?
*/
function ResampleFullImage() {
var image = G_MAIN_IMAGE_OBJ;
if (image == null) {
alert('image is not loaded yet. Please wait and try again in a few moments.');
return;
}
var eStatus = document.querySelector('#status');
var StartTime = Date.now();
let msg = 'ResampleFullImage Start: '+ (new Date(StartTime)).toString();
console.log(msg);
eStatus.innerHTML = msg;
var iw = image.naturalWidth, ih = image.naturalHeight;
// calculate grid layout
var pixelSizeX = Utils.getCellWInput(); // px
var pixelSizeY = Utils.getCellHInput(); // px
var cols = Math.floor(iw / pixelSizeX);
var rows = Math.floor(ih / pixelSizeY);
// cell half width/height
var cell_half_W = pixelSizeX / 2;
var cell_half_H = pixelSizeY / 2;
// spot size ratio
var spot_rX = Utils.getSpotXInput(); // %
var spot_rY = Utils.getSpotYInput(); // %
// probe radii
var probe_rX = (pixelSizeX/2) * (spot_rX / 100);
var probe_rY = (pixelSizeY/2) * (spot_rY / 100);
var probe_rotationRad = Utils.toRadians(Utils.getSpotAngleInput());
// prep result canvas, if not already there
var cv = document.querySelector('#finalCanvas');
if (cv == null) {
cv = document.createElement('canvas');
cv.id = 'finalCanvas';
cv.width = cols; cv.height = rows;
var cc = $('<div/>').addClass('box final').appendTo(G_MAIN_CONTAINER); cc.append(cv);
}
cv.width = cols;
cv.height = rows;
// get context
var ctx = cv.getContext('2d');
// clear the canvas
ctx.clearRect(0, 0, cv.width, cv.height);
// row pixels container array
// number of pixels (rows, cols) * 4 (components: RGBA)
var pixels = new Uint8ClampedArray(rows * cols * 4);
var count = 0;
// process and compute each pixel grid cell
for (let i = 0; i < rows; i++) {
// compute pixel value and push to matrix
for (let j = 0; j < cols; j++) {
const probe = {
centerX: (j * pixelSizeX) + cell_half_W,
centerY: (i * pixelSizeY) + cell_half_H,
radiusX: probe_rX,
radiusY: probe_rY,
rotationRad: probe_rotationRad
};
// compute pixel value - greyscale
const pixel = Utils.ComputeProbeValue_gs(image, probe);
// console.info(pixel);
// push pixel to array - RGBA values
pixels[count+0] = pixel;
pixels[count+1] = pixel;
pixels[count+2] = pixel;
pixels[count+3] = 255;
count += 4;
}
let msg = 'computed row: '+(i+1)+' / '+rows;
console.log(msg);
// eStatus.innerHTML = msg;
// drawing the image row by row
if (G_DEBUG)
console.log('ResampleFullImage drew row: '+(i+1)+' / '+rows);
/*
// free memory
pixels = null;
imageData = null;
*/
}
let imageData = new ImageData(pixels, cols); // rows/height is auto-calculated
ctx.putImageData(imageData, 0, 0);
var Elapsed = Date.now() - StartTime;
const second = 1000; //ms
msg = 'ResampleFullImage End: took '+ Math.floor(Elapsed / second).toString()+" seconds.";
console.log(msg);
eStatus.innerHTML = msg;
}