/* globals
LayoutManager
Utils
G_INPUT_IMAGE
*/
// 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_Update_Stages_Global
G_AUTO_PREVIEW_LIMIT
G_VSEM_PAUSED
G_VSEM_STAGE
G_SHOW_SUBREGION_OVERLAY
G_IMG_METRIC_ENABLED
G_APP_NAME
G_IMG_METRICS
*/
/** 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 = 18 ** 2;
/** 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 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;
/** global reference to call main update routine (used for most stages, updates stage if visible) */
var G_Update_Stages_Global = null;
/** global reference to the resulting image stage */
var G_VSEM_STAGE = null;
/////////////////////
// initialize, do calculations, create the stages, and setup UI
LayoutManager.Initialize();
/**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, LayoutManager.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 groundtruthMapStage = stages[0];
var baseImageStage = stages[1];
var spotProfileStage = stages[2];
var virtualSEMStage = stages[3];
var resampledStage = stages[4];
var probeLayoutStage = stages[5];
var spotContentStage = stages[6];
var spotSignalStage = stages[7];
var layoutSampledStage = stages[8];
/* eslint-enable no-magic-numbers */
// Set dialog titles accordingly
LayoutManager.SetDialogTitle(spotProfileStage, 'Spot Profile');
LayoutManager.SetDialogTitle(baseImageStage, 'Subregion View / FOV');
LayoutManager.SetDialogTitle(spotContentStage, 'Spot Content');
LayoutManager.SetDialogTitle(spotSignalStage, 'Spot Signal (Integrated)');
LayoutManager.SetDialogTitle(probeLayoutStage, 'Spot Layout');
LayoutManager.SetDialogTitle(layoutSampledStage, 'Sampled Subregion');
LayoutManager.SetDialogTitle(resampledStage, 'Resulting Subregion');
LayoutManager.SetDialogTitle(groundtruthMapStage, 'Sample Ground Truth');
LayoutManager.SetDialogTitle(virtualSEMStage, 'Resulting Image');
// Update global reference to this stage, so that the GUI's image export button works...
G_VSEM_STAGE = virtualSEMStage;
/** called when a change occurs in the spot profile, subregion, or spot content */
function doUpdate(){
// don't update spot signal if not shown
if (LayoutManager.IsStageVisible(spotSignalStage)) {
updateSpotSignal();
}
updateResamplingSteps();
if (LayoutManager.IsStageVisible(groundtruthMapStage)) {
updateGroundtruthMap();
}
updateVirtualSEM_Config();
updateInfoDisplays();
}
G_Update_Stages_Global = doUpdate;
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) {
// we can use 'beam' or 'spotContentBeam' here, they both work...
// TODO: maybe just use 'beam' here or a clone?
Utils._SetSpotWidth(spotWidth, spotContentBeam, spotScaling, function(){
updateBeams();
doUpdate();
});
}
}
// -----------------------------------------------------------
// draw Spot Profile
// -----------------------------------------------------------
$(spotProfileStage.getContainer())
.attr('note', 'Press [R] to reset shape\nScroll to change size');
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('note', 'Pan & Zoom: Drag and Scroll\nPress [R] to reset');
var subregionImage = drawSubregionImage(baseImageStage, eImg, doUpdate);
// -----------------------------------------------------------
// draw Spot Content
// -----------------------------------------------------------
$(spotContentStage.getContainer())
.addClass('grabCursor')
.attr('note', 'Scroll to adjust spot size\nHold [Shift] for half rate');
LayoutManager.DialogAddClass(spotContentStage, 'advancedMode');
var spotContentBeam = beam.clone();
// make a clone without copying over the event bindings
var imageCopy = subregionImage.clone().off();
drawSpotContent(spotContentStage, imageCopy, spotContentBeam, doUpdate);
// -----------------------------------------------------------
// draw Spot Signal
// -----------------------------------------------------------
$(spotSignalStage.getContainer()).addClass('note_colored');
LayoutManager.DialogAddClass(spotSignalStage, 'advancedMode');
var spotSignalBeam = beam.clone();
var updateSpotSignal = drawSpotSignal(spotContentStage, spotSignalStage, spotSignalBeam);
// -----------------------------------------------------------
// draw Spot Layout
// -----------------------------------------------------------
var layoutBeam = beam.clone();
var updateProbeLayout = drawProbeLayout(probeLayoutStage, subregionImage, spotScaling, layoutBeam);
// -----------------------------------------------------------
// draw Sampled Subregion
// -----------------------------------------------------------
// compute resampled image
LayoutManager.DialogAddClass(layoutSampledStage, 'advancedMode');
var layoutSampledBeam = beam.clone();
var updateProbeLayoutSamplingPreview = drawProbeLayoutSampling(
layoutSampledStage,
subregionImage,
spotScaling,
layoutSampledBeam
);
// -----------------------------------------------------------
// draw Resulting Subregion
// -----------------------------------------------------------
var resampledBeam = beam.clone();
var updateResampled = drawResampled(
layoutSampledStage,
resampledStage,
subregionImage,
spotScaling,
resampledBeam
);
var updateResamplingSteps = function(){
if (LayoutManager.IsStageVisible(probeLayoutStage)) {
updateProbeLayout();
}
if (LayoutManager.IsStageVisible(layoutSampledStage)) {
updateProbeLayoutSamplingPreview();
}
if (LayoutManager.IsStageVisible(resampledStage)) {
updateResampled();
}
Utils.updateVSEM_ModeWarning(resampledStage);
};
G_UpdateResampled = updateResamplingSteps;
// -----------------------------------------------------------
// draw Sample Ground Truth
// -----------------------------------------------------------
var groundtruthMap = drawGroundtruthImage(groundtruthMapStage, eImg, subregionImage, 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 lengthUm = 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 (lengthUm > 0) {
var pixelSize = ruler.getPixelSize(lengthUm * 1E3);
Utils.setPixelSizeNmInput(pixelSize);
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
// -----------------------------------------------------------
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();
});
// TODO: can this go into Utils?
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("#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;
}