Fork me on GitHub

Source: main.js

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

↑ Top