Fork me on GitHub

Source: main.js

/* 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;
}

↑ Top