Fork me on GitHub

Source: main.js

  1. /* globals Utils, GetOptimalBoxWidth */
  2. // global functions from drawsteps.js
  3. /* globals
  4. drawSpotProfileEdit, drawSubregionImage, drawSpotContent, drawSpotSignal,
  5. drawProbeLayout, drawProbeLayoutSampling, drawResampled, drawGroundtruthImage,
  6. drawVirtualSEM
  7. */
  8. /* exported
  9. G_UpdateResampled,
  10. G_UpdateVirtualSEMConfig,
  11. ResampleFullImage,
  12. G_Update_GroundTruth
  13. G_Update_InfoDisplays
  14. G_update_ImgMetrics
  15. G_UpdateRuler
  16. G_UpdateFilters
  17. G_UpdateStageSettings
  18. G_AUTO_PREVIEW_LIMIT
  19. G_VSEM_PAUSED
  20. G_SHOW_SUBREGION_OVERLAY
  21. G_IMG_METRIC_ENABLED
  22. G_APP_NAME
  23. G_INPUT_IMAGE
  24. G_PRELOADED_IMAGES
  25. G_PRELOADED_IMAGES_ROOT
  26. G_IMG_METRICS
  27. G_STAGES
  28. */
  29. /** Name of the application */
  30. const G_APP_NAME = "ImgBeamer";
  31. var G_DEBUG = false;
  32. Konva.autoDrawEnabled = true;
  33. /** The number of cells in the raster grid at which auto-preview stops, for responsiveness */
  34. // eslint-disable-next-line no-magic-numbers
  35. var G_AUTO_PREVIEW_LIMIT = 16 * 16;
  36. /** Toggle value to pause the continously draw the Resulting Image / Virtual SEM view */
  37. var G_VSEM_PAUSED = false;
  38. /** Toggle value to show/hide the subregion overlay on the Sample Ground Truth stage. */
  39. var G_SHOW_SUBREGION_OVERLAY = true;
  40. /** Toggle value to pause/hide the image quality metric calculation of the Resulting Image / Virtual SEM view */
  41. var G_IMG_METRIC_ENABLED = true;
  42. /** The root folder for all the preloaded images specified by {@link G_PRELOADED_IMAGES}. */
  43. const G_PRELOADED_IMAGES_ROOT = "src/testimages/";
  44. /**
  45. * The list of default/preloaded images to use with the application.
  46. * @see G_PRELOADED_IMAGES_ROOT
  47. */
  48. const G_PRELOADED_IMAGES = [
  49. 'grains1.png',
  50. 'grains2full.png',
  51. 'grains2tl.png',
  52. 'grains2nc.png',
  53. 'APT_needle.png',
  54. 'tephra_448nm.png',
  55. 'tephra_200nm.png',
  56. ];
  57. /** global variable to set the input ground truth image */
  58. // var G_INPUT_IMAGE = Utils.getGroundtruthImage();
  59. var G_INPUT_IMAGE = G_PRELOADED_IMAGES_ROOT + 'grains2tl.png';
  60. // Preload the larger image files in the background
  61. // without blocking the UI for improved responsiveness
  62. window.addEventListener('load', function(){
  63. // https://stackoverflow.com/a/59861857/883015
  64. // var images = G_PRELOADED_IMAGES;
  65. var images = ['grains2full.png', 'APT_needle.png', 'tephra_448nm.png', 'tephra_200nm.png'];
  66. var preload = '';
  67. for(let i = 0; i < images.length; i++) {
  68. preload += '<link rel="preload" href="' + G_PRELOADED_IMAGES_ROOT
  69. + images[i] + '" as="image">\n';
  70. }
  71. $('head').append(preload);
  72. });
  73. /** The list of image quality metrics supported by the application. */
  74. const G_IMG_METRICS = [
  75. 'SSIM',
  76. 'MS-SSIM',
  77. 'MSE',
  78. 'PSNR',
  79. 'iNRMSE',
  80. 'iNMSE',
  81. ];
  82. /** global reference to update the resampling steps (spot layout,
  83. * sampled subregion, resulting subregion) displays,
  84. * This is mainly to be called when the auto-preview is disabled/off */
  85. var G_UpdateResampled = null;
  86. /** global reference to update the beam values for the Virtual SEM view */
  87. var G_UpdateVirtualSEMConfig = null;
  88. /** global reference to update the Groundtruth view */
  89. var G_Update_GroundTruth = null;
  90. /** global reference to update the Information displays */
  91. var G_Update_InfoDisplays = null;
  92. /** global reference to update the just the Image Metrics information display */
  93. var G_update_ImgMetrics = null;
  94. /** global reference to update the ruler */
  95. var G_UpdateRuler = null;
  96. /** global reference to update/apply image filters */
  97. var G_UpdateFilters = null;
  98. /** global reference to update stage related settings */
  99. var G_UpdateStageSettings = null;
  100. /** a global reference to the main body container that holds the boxes/stages.
  101. * @todo do we still need this? Maybe remove... */
  102. var G_MAIN_CONTAINER = $('#main-container');
  103. /** The calculated size of each box/stage */
  104. var G_BOX_SIZE = GetOptimalBoxWidth();
  105. /** The number of stages to create */
  106. const nStages = 9;
  107. /** The array/list of all the stages. */
  108. var G_STAGES = [];
  109. // first create the stages
  110. for (let i = 0; i < nStages; i++) {
  111. let stage = Utils.newStageTemplate(G_MAIN_CONTAINER, G_BOX_SIZE, G_BOX_SIZE);
  112. G_STAGES.push(stage);
  113. }
  114. /////////////////////
  115. /**Currently only used by {@link ResampleFullImage}
  116. * @todo Possibly, to be removed along with it. */
  117. var G_MAIN_IMAGE_OBJ = null;
  118. // call once on App start
  119. UpdateBaseImage();
  120. // update event for ground truth image change
  121. $(document.body).on('OnGroundtruthImageChange', UpdateBaseImage);
  122. /** Updates everything needed assuming that {@link G_INPUT_IMAGE} has changed,
  123. * updates/draws all the stages/boxes once. */
  124. function UpdateBaseImage(){
  125. // load image and wait for when ready
  126. Utils.loadImage(G_INPUT_IMAGE, function(event){
  127. var imageObj = event.target;
  128. G_MAIN_IMAGE_OBJ = imageObj;
  129. OnImageLoaded(imageObj, G_STAGES);
  130. });
  131. }
  132. /**
  133. * Called by {@link UpdateBaseImage} once the image data has been loaded,
  134. * Draws and manages all the drawing stages with each their event handlers.
  135. * @param {*} eImg The Element/Object of the loaded image.
  136. * @param {*} stages The array of stages to use for drawing.
  137. */
  138. function OnImageLoaded(eImg, stages){
  139. /* eslint-disable no-magic-numbers */
  140. // Edit these numbers to change the display order
  141. var baseImageStage = stages[1];
  142. var spotProfileStage = stages[2];
  143. var spotContentStage = stages[3];
  144. var spotSignalStage = stages[4];
  145. var probeLayoutStage = stages[5];
  146. var layoutSampledStage = stages[6];
  147. var resampledStage = stages[7];
  148. var groundtruthMapStage = stages[0];
  149. var virtualSEMStage = stages[8];
  150. /* eslint-enable no-magic-numbers */
  151. /** called when a change occurs in the spot profile, subregion, or spot content */
  152. function doUpdate(){
  153. // don't update spot signal if not shown
  154. if ($(spotSignalStage.getContainer()).is(':visible')) {
  155. updateSpotSignal();
  156. }
  157. updateProbeLayout();
  158. updateResamplingSteps();
  159. updateGroundtruthMap();
  160. updateVirtualSEM_Config();
  161. updateInfoDisplays();
  162. }
  163. var updateImgMetrics = function(){
  164. Utils.updateImageMetricsInfo(groundtruthMapStage, virtualSEMStage);
  165. };
  166. G_update_ImgMetrics = updateImgMetrics;
  167. var updateInfoDisplays = function(){
  168. // update spot/beam info: size, rotation, shape
  169. var cellSize = Utils.computeCellSize(subregionImage.getStage());
  170. Utils.updateDisplayBeamParams(spotProfileStage, layoutBeam, cellSize, spotScaling, promptForSpotWidth);
  171. Utils.updateMagInfo(baseImageStage, subregionImage);
  172. Utils.updateSubregionPixelSize(resampledStage, subregionImage, eImg);
  173. updateImgMetrics();
  174. };
  175. G_Update_InfoDisplays = updateInfoDisplays;
  176. /** prompts the user for the spot width % */
  177. function promptForSpotWidth(){
  178. var spotWidth = prompt("Spot width (%) - Default is 100%", 100);
  179. if (spotWidth > 0) {
  180. Utils_SetSpotWidth(spotWidth);
  181. }
  182. }
  183. // draw Spot Profile
  184. $(spotProfileStage.getContainer())
  185. .attr('box_label', 'Spot Profile')
  186. .attr('note', 'Press [R] to reset shape\nScroll to change size')
  187. .css('border-color', 'red');
  188. var _spotProfileInfo = drawSpotProfileEdit(spotProfileStage, doUpdate);
  189. var beam = _spotProfileInfo.beam;
  190. var spotScaling = _spotProfileInfo.spotSize;
  191. // Subregion View
  192. // draw base image (can pan & zoom)
  193. $(baseImageStage.getContainer())
  194. .addClass('grabCursor')
  195. .attr('box_label', 'Subregion View / FOV')
  196. .attr('note', 'Pan & Zoom: Drag and Scroll\nPress [R] to reset')
  197. .css('border-color', 'blue');
  198. var subregionImage = drawSubregionImage(baseImageStage, eImg, G_BOX_SIZE, doUpdate);
  199. // draw Spot Content
  200. $(spotContentStage.getContainer())
  201. .addClass('advancedMode')
  202. .addClass('grabCursor')
  203. .attr('box_label', 'Spot Content')
  204. .attr('note', 'Scroll to adjust spot size\nHold [Shift] for half rate');
  205. var spotContentBeam = beam.clone();
  206. // make a clone without copying over the event bindings
  207. var imageCopy = subregionImage.clone().off();
  208. drawSpotContent(spotContentStage, imageCopy, spotContentBeam, doUpdate);
  209. /**(temporary) publicly exposed function to set the spot width
  210. * @param {number} spotWidth the spot width in percent (%), ex. use 130 for 130%.
  211. * @todo move into separate file if possible */
  212. function Utils_SetSpotWidth(spotWidth=100){
  213. var beam = spotContentBeam;
  214. var spotScaler = spotScaling;
  215. // calculate the new scale for spot-content image, based on the given spot width
  216. var cellSize = Utils.computeCellSize(spotScaler);
  217. var maxScale = Math.max(beam.scaleX(), beam.scaleY());
  218. var eccScaled = beam.scaleX() / maxScale;
  219. var newScale = ((beam.width() * eccScaled) / (spotWidth/100)) / cellSize.w;
  220. Utils.centeredScale(spotScaler, newScale);
  221. // propagate changes and update stages
  222. updateBeams();
  223. doUpdate();
  224. }
  225. // draw Spot Signal
  226. $(spotSignalStage.getContainer())
  227. .addClass('advancedMode')
  228. .addClass('note_colored')
  229. .attr('box_label', '(Integrated) Spot Signal');
  230. var spotSignalBeam = beam.clone();
  231. var updateSpotSignal = drawSpotSignal(spotContentStage, spotSignalStage, spotSignalBeam);
  232. // draw Spot Layout
  233. $(probeLayoutStage.getContainer()).attr('box_label', 'Spot Layout');
  234. var layoutBeam = beam.clone();
  235. var updateProbeLayout = drawProbeLayout(probeLayoutStage, subregionImage, spotScaling, layoutBeam);
  236. // draw Sampled Subregion
  237. // compute resampled image
  238. $(layoutSampledStage.getContainer())
  239. .addClass('advancedMode')
  240. .attr('box_label', 'Sampled Subregion');
  241. var layoutSampledBeam = beam.clone();
  242. var updateProbeLayoutSamplingPreview = drawProbeLayoutSampling(
  243. layoutSampledStage,
  244. subregionImage,
  245. spotScaling,
  246. layoutSampledBeam
  247. );
  248. // draw Resulting Subregion
  249. $(resampledStage.getContainer())
  250. .attr('box_label', 'Resulting Subregion')
  251. .css('border-color', 'lime');
  252. var resampledBeam = beam.clone();
  253. var updateResampled = drawResampled(
  254. layoutSampledStage,
  255. resampledStage,
  256. subregionImage,
  257. spotScaling,
  258. resampledBeam
  259. );
  260. var updateResamplingSteps = function(){
  261. updateProbeLayout();
  262. if ($(layoutSampledStage.getContainer()).is(':visible')) {
  263. updateProbeLayoutSamplingPreview();
  264. }
  265. if ($(resampledStage.getContainer()).is(':visible')) {
  266. updateResampled();
  267. }
  268. Utils.updateVSEM_ModeWarning(resampledStage);
  269. };
  270. G_UpdateResampled = updateResamplingSteps;
  271. // draw Sample Ground Truth
  272. $(groundtruthMapStage.getContainer()).attr('box_label', 'Sample Ground Truth');
  273. var groundtruthMap = drawGroundtruthImage(groundtruthMapStage, eImg, subregionImage, G_BOX_SIZE, doUpdate);
  274. var updateGroundtruthMap = groundtruthMap.updateFunc;
  275. G_Update_GroundTruth = updateGroundtruthMap;
  276. // add ruler on Ground truth stage
  277. var rulerLayer = Utils.getLayerByName(groundtruthMapStage, 'myRuler');
  278. if (rulerLayer != null) {
  279. // make sure we don't add a duplicate layer, to avoid memory leaks
  280. // mainly when a new image is loaded.
  281. rulerLayer.destroy();
  282. }
  283. rulerLayer = new Konva.Layer({visible: false, name: 'myRuler'});
  284. groundtruthMapStage.add(rulerLayer);
  285. var ruler = Utils.CreateRuler(rulerLayer, eImg,
  286. // Default is a ruler that is 2/3rd width of the stage and vertically in middle
  287. groundtruthMapStage.width()*(1/3), groundtruthMapStage.height()/2,
  288. groundtruthMapStage.width()*(2/3), groundtruthMapStage.height()/2
  289. );
  290. ruler.element.on('dblclick', function(){
  291. var um = ruler.getLengthNm() / 1E3;
  292. var pixelWidth = prompt("Please enter the length of the ruler in micrometers (μm)."
  293. + "\n\nTIP! Try holding the [Shift] key for horizontal lines or "
  294. + "[Ctrl] for vertical lines.", um, 0);
  295. if (pixelWidth > 0) {
  296. var pixelSize = ruler.getPixelSize(pixelWidth * 1E3);
  297. // TODO: support non-square pixel...
  298. // currently only support it in x-direction;
  299. Utils.setPixelSizeNmInput(pixelSize.x);
  300. ruler.doUpdate();
  301. }
  302. });
  303. G_UpdateRuler = function(){
  304. var show = Utils.getShowRulerInput();
  305. ruler.doUpdate(); // update the ruler
  306. rulerLayer.visible(show); // update visibility
  307. };
  308. // update ruler once immediately
  309. G_UpdateRuler();
  310. // draw Resulting Image
  311. $(virtualSEMStage.getContainer()).attr('box_label', 'Resulting Image');
  312. var vitualSEMBeam = beam.clone();
  313. var updateVirtualSEM_Config = drawVirtualSEM(
  314. virtualSEMStage,
  315. vitualSEMBeam,
  316. groundtruthMap.subregionRect,
  317. groundtruthMapStage,
  318. eImg,
  319. spotScaling
  320. );
  321. G_UpdateVirtualSEMConfig = updateVirtualSEM_Config;
  322. /** propagate changes to the spot-profile (beam) to the beams in the other stages */
  323. function updateBeams(){
  324. spotContentBeam.scale(beam.scale());
  325. spotContentBeam.rotation(beam.rotation());
  326. spotSignalBeam.scale(beam.scale());
  327. spotSignalBeam.rotation(beam.rotation());
  328. // keep the shape of the ellipse, not the actual size of it...
  329. var maxScale = Math.max(beam.scaleX(), beam.scaleY());
  330. layoutBeam.size({
  331. width: beam.width() * (beam.scaleX() / maxScale),
  332. height: beam.height() * (beam.scaleY() / maxScale),
  333. });
  334. layoutBeam.rotation(beam.rotation());
  335. layoutSampledBeam.size(layoutBeam.size());
  336. layoutSampledBeam.rotation(layoutBeam.rotation());
  337. resampledBeam.size(layoutBeam.size());
  338. resampledBeam.rotation(layoutBeam.rotation());
  339. vitualSEMBeam.size(layoutBeam.size());
  340. vitualSEMBeam.rotation(layoutBeam.rotation());
  341. }
  342. // update beams
  343. beam.off('transform'); // prevent "eventHandler doubling" from subsequent calls
  344. beam.on('transform', function(){
  345. updateBeams();
  346. doUpdate();
  347. });
  348. function updateFilters(){
  349. var doBC = Utils.getGlobalBCInput();
  350. if (doBC) {
  351. // stages that we want to apply filters to...
  352. var fStages = [
  353. groundtruthMapStage, baseImageStage,
  354. spotContentStage, probeLayoutStage,
  355. layoutSampledStage
  356. ];
  357. // apply the filters
  358. const brightness = Utils.getBrightnessInput();
  359. const contrast = Utils.getContrastInput();
  360. for (let i = 0; i < fStages.length; i++) {
  361. const fStage = fStages[i];
  362. let image = Utils.getFirstImageFromStage(fStage);
  363. Utils.applyBrightnessContrast(image, brightness, contrast);
  364. }
  365. // for the resulting images, the sampling function, Utils.ComputeProbeValue_gs(),
  366. // is made B/C aware and using Konva's built-in filters directly.
  367. }
  368. // call global visual update
  369. doUpdate();
  370. }
  371. // update filters once immediately
  372. updateFilters();
  373. G_UpdateFilters = updateFilters;
  374. doUpdate();
  375. Utils.updateAdvancedMode();
  376. // update stage related settings
  377. G_UpdateStageSettings = function(){
  378. // update image smoothing for all stages
  379. var smooth = Utils.getImageSmoothing();
  380. for (let i = 0; i < stages.length; i++) {
  381. const stage = stages[i];
  382. Utils.setStageImageSmoothing(stage, smooth);
  383. }
  384. };
  385. // update once immediately to stay in sync on start up
  386. G_UpdateStageSettings();
  387. }
  388. /** Draws the full resample image given the parameters in the GUI and logs
  389. * the progress in the webconsole. Very heavy and slow. Could may be optimized
  390. * with an offscreenCanvas and webworkers...
  391. * @todo Maybe no longer needed and can be removed?
  392. */
  393. function ResampleFullImage() {
  394. var image = G_MAIN_IMAGE_OBJ;
  395. if (image == null) {
  396. alert('image is not loaded yet. Please wait and try again in a few moments.');
  397. return;
  398. }
  399. var eStatus = document.querySelector('#status');
  400. var StartTime = Date.now();
  401. let msg = 'ResampleFullImage Start: '+ (new Date(StartTime)).toString();
  402. console.log(msg);
  403. eStatus.innerHTML = msg;
  404. var iw = image.naturalWidth, ih = image.naturalHeight;
  405. // calculate grid layout
  406. var pixelSizeX = Utils.getCellWInput(); // px
  407. var pixelSizeY = Utils.getCellHInput(); // px
  408. var cols = Math.floor(iw / pixelSizeX);
  409. var rows = Math.floor(ih / pixelSizeY);
  410. // cell half width/height
  411. var cell_half_W = pixelSizeX / 2;
  412. var cell_half_H = pixelSizeY / 2;
  413. // spot size ratio
  414. var spot_rX = Utils.getSpotXInput(); // %
  415. var spot_rY = Utils.getSpotYInput(); // %
  416. // probe radii
  417. var probe_rX = (pixelSizeX/2) * (spot_rX / 100);
  418. var probe_rY = (pixelSizeY/2) * (spot_rY / 100);
  419. var probe_rotationRad = Utils.toRadians(Utils.getSpotAngleInput());
  420. // prep result canvas, if not already there
  421. var cv = document.querySelector('#finalCanvas');
  422. if (cv == null) {
  423. cv = document.createElement('canvas');
  424. cv.id = 'finalCanvas';
  425. cv.width = cols; cv.height = rows;
  426. var cc = $('<div/>').addClass('box final').appendTo(G_MAIN_CONTAINER); cc.append(cv);
  427. }
  428. cv.width = cols;
  429. cv.height = rows;
  430. // get context
  431. var ctx = cv.getContext('2d');
  432. // clear the canvas
  433. ctx.clearRect(0, 0, cv.width, cv.height);
  434. // row pixels container array
  435. // number of pixels (rows, cols) * 4 (components: RGBA)
  436. var pixels = new Uint8ClampedArray(rows * cols * 4);
  437. var count = 0;
  438. // process and compute each pixel grid cell
  439. for (let i = 0; i < rows; i++) {
  440. // compute pixel value and push to matrix
  441. for (let j = 0; j < cols; j++) {
  442. const probe = {
  443. centerX: (j * pixelSizeX) + cell_half_W,
  444. centerY: (i * pixelSizeY) + cell_half_H,
  445. radiusX: probe_rX,
  446. radiusY: probe_rY,
  447. rotationRad: probe_rotationRad
  448. };
  449. // compute pixel value - greyscale
  450. const pixel = Utils.ComputeProbeValue_gs(image, probe);
  451. // console.info(pixel);
  452. // push pixel to array - RGBA values
  453. pixels[count+0] = pixel;
  454. pixels[count+1] = pixel;
  455. pixels[count+2] = pixel;
  456. pixels[count+3] = 255;
  457. count += 4;
  458. }
  459. let msg = 'computed row: '+(i+1)+' / '+rows;
  460. console.log(msg);
  461. // eStatus.innerHTML = msg;
  462. // drawing the image row by row
  463. if (G_DEBUG)
  464. console.log('ResampleFullImage drew row: '+(i+1)+' / '+rows);
  465. /*
  466. // free memory
  467. pixels = null;
  468. imageData = null;
  469. */
  470. }
  471. let imageData = new ImageData(pixels, cols); // rows/height is auto-calculated
  472. ctx.putImageData(imageData, 0, 0);
  473. var Elapsed = Date.now() - StartTime;
  474. const second = 1000; //ms
  475. msg = 'ResampleFullImage End: took '+ Math.floor(Elapsed / second).toString()+" seconds.";
  476. console.log(msg);
  477. eStatus.innerHTML = msg;
  478. }

↑ Top