Fork me on GitHub

Source: drawsteps.js

  1. /* globals
  2. * Utils,
  3. * G_BOX_SIZE, G_DEBUG, G_AUTO_PREVIEW_LIMIT,
  4. * G_VSEM_PAUSED, G_MATH_TOFIXED,
  5. * G_SHOW_SUBREGION_OVERLAY,
  6. * G_update_ImgMetrics
  7. */
  8. /* exported
  9. * drawSpotProfileEdit, drawSubregionImage, drawSpotContent, drawSpotSignal,
  10. * drawProbeLayout, drawProbeLayoutSampling, drawResampled, drawGroundtruthImage,
  11. * drawVirtualSEM
  12. */
  13. // used by "Resulting Image" box / drawVirtualSEM()
  14. // to reduce artifacts from drawing pixel-by-pixel in canvas
  15. var G_DRAW_WITH_OVERLAP = true;
  16. // overlap amount in pixels to all edges (top, left, right, bottom)
  17. var G_DRAW_OVERLAP_PIXELS = 1;
  18. // Optionally draw with overlap when above a certain pixel (cell) count
  19. // set to 0 to essentially ignore this threshold value...
  20. // eslint-disable-next-line no-magic-numbers
  21. var G_DRAW_OVERLAP_THRESHOLD = 10 * 10; // rows * cols
  22. // Optionally, to draw normally (w/o overlap) after a number of passes
  23. var G_DRAW_OVERLAP_PASSES = 1;
  24. // The minimum average pixel/signal value for an image to be considered "non-blank"
  25. var G_MIN_AVG_SIGNAL_VALUE = 2;
  26. // the pixel size of the spot used for the subregion render view, updated elsewhere
  27. var G_BEAMRADIUS_SUBREGION_PX = {x:1,y:1};
  28. const KEYCODE_R = 82;
  29. const KEYCODE_ESC = 27;
  30. const G_ZOOM_FACTOR_PER_TICK = 1.2;
  31. var G_VirtualSEM_animationFrameRequestId = null;
  32. var G_SubResampled_animationFrameRequestId = null;
  33. /**
  34. * Draws an node-editable ellipse shape on the given drawing stage.
  35. * @param {*} stage the stage to draw on.
  36. * @param {Function} updateCallback optional callback when a change occurs in spotSize
  37. * @returns the spot/beam (Ellipse) object
  38. */
  39. function drawSpotProfileEdit(stage, updateCallback = null) {
  40. var layer = stage.getLayers()[0];
  41. layer.destroyChildren(); // avoid memory leaks
  42. // default beam shape values
  43. var defaultRadius = {
  44. x: 70,
  45. y: 70
  46. };
  47. // create our shape
  48. var beam = new Konva.Ellipse({
  49. x: stage.width() / 2,
  50. y: stage.height() / 2,
  51. radius: defaultRadius,
  52. fill: 'white',
  53. strokeWidth: 0,
  54. });
  55. layer.add(beam);
  56. layer.draw();
  57. // make it editable
  58. var tr = new Konva.Transformer({
  59. nodes: [beam],
  60. centeredScaling: true,
  61. // style the transformer:
  62. // https://konvajs.org/docs/select_and_transform/Transformer_Styling.html
  63. anchorSize: 11,
  64. anchorCornerRadius: 3,
  65. borderDash: [3, 3],
  66. // eslint-disable-next-line no-magic-numbers
  67. rotationSnaps: [0, 45, 90, 135, 180],
  68. // resize limits
  69. // https://konvajs.org/docs/select_and_transform/Resize_Limits.html
  70. boundBoxFunc: function (oldBoundBox, newBoundBox) {
  71. // if the new bounding box is too large or small
  72. // small than the stage size, but more than 1 px.
  73. // then, we return the old bounding box
  74. if ( newBoundBox.width > stage.width() || newBoundBox.width < 1
  75. || newBoundBox.height > stage.height() || newBoundBox.height < 1) {
  76. return oldBoundBox;
  77. }
  78. return newBoundBox;
  79. }
  80. });
  81. layer.listening(true);
  82. layer.add(tr);
  83. // make it (de)selectable
  84. // based on https://konvajs.org/docs/select_and_transform/Basic_demo.html
  85. stage.off('click tap'); // prevent "eventHandler doubling" from subsequent calls
  86. stage.on('click tap', function (e) {
  87. // if click on empty area - remove all selections
  88. if (e.target === stage) {
  89. tr.nodes([]);
  90. return;
  91. }
  92. const isSelected = tr.nodes().indexOf(e.target) >= 0;
  93. if (!isSelected) {
  94. // was not already selected, so now we add it to the transformer
  95. // select just the one
  96. tr.nodes([e.target]);
  97. }
  98. });
  99. // keyboard events
  100. // based on https://konvajs.org/docs/events/Keyboard_Events.html
  101. var container = stage.container();
  102. // make it focusable
  103. container.tabIndex = 2;
  104. // Avoiding using addEventListener, since we only ever want one handler at time.
  105. // This way, we easily replace it when a new image is loaded.
  106. container.onkeydown = function(e) {
  107. // don't handle meta-key'd events for now...
  108. const metaPressed = e.shiftKey || e.ctrlKey || e.metaKey;
  109. if (metaPressed)
  110. return;
  111. switch (e.keyCode) {
  112. case KEYCODE_R: // 'r' key, reset beam shape
  113. beam.rotation(0);
  114. beam.scale({x:1, y:1});
  115. // update other beams based on this one
  116. // https://konvajs.org/docs/events/Fire_Events.html
  117. beam.fire('transform');
  118. break;
  119. case KEYCODE_ESC: // 'esc' key, deselect all
  120. tr.nodes([]);
  121. break;
  122. default: break;
  123. }
  124. e.preventDefault();
  125. };
  126. // Replacing userScaledImage / userImage
  127. // invisible shape to get scale X/Y as a control for spot size/mag
  128. // by mouse scroll or values being set on a konva object to call .ScaleX/Y()
  129. var rectW = stage.width();
  130. var magRect = new Konva.Rect({
  131. width: rectW,
  132. height: rectW,
  133. // fill: 'red',
  134. fillEnabled: false,
  135. // strokeWidth: 0,
  136. strokeWidth: 1,
  137. stroke: 'lime',
  138. // strokeEnabled: false,
  139. strokeScaleEnabled: false,
  140. listening: false,
  141. visible: false,
  142. perfectDrawEnabled: false,
  143. });
  144. // "pre-zoom" a bit, and start with center position
  145. // zoom/scale so that the spot size starts at 100%
  146. var _tempCellWidth = magRect.width() / Utils.getColsInput();
  147. var initialSpotScale = beam.width() / _tempCellWidth;
  148. Utils.scaleOnCenter(stage, magRect, 1, initialSpotScale);
  149. // optional event callback
  150. var doUpdate = function(){
  151. if (typeof updateCallback == 'function')
  152. return updateCallback();
  153. };
  154. stage.off('wheel').on('wheel', function(e){
  155. e.evt.preventDefault(); // stop default scrolling
  156. var scaleBy = G_ZOOM_FACTOR_PER_TICK;
  157. // Do half rate scaling, if shift is pressed
  158. if (e.evt.shiftKey) {
  159. scaleBy = 1 +((scaleBy-1) / 2);
  160. }
  161. // how to scale? Zoom in? Or zoom out?
  162. let direction = e.evt.deltaY > 0 ? -1 : 1;
  163. var oldScale = magRect.scaleX();
  164. var newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
  165. // limit the max zoom from scrolling, to prevent blank pixel data
  166. // because of too small of a spot size...
  167. const tolerance = -0.1;
  168. if (G_BEAMRADIUS_SUBREGION_PX.x + tolerance < 1
  169. || G_BEAMRADIUS_SUBREGION_PX.y + tolerance < 1) {
  170. newScale = Math.min(oldScale, newScale);
  171. }
  172. Utils.centeredScale(magRect, newScale);
  173. doUpdate();
  174. });
  175. layer.add(magRect);
  176. return {beam: beam, spotSize: magRect};
  177. }
  178. /**
  179. * Draws the subregion image display.
  180. * @param {*} stage The stage to draw it on.
  181. * @param {*} oImg The ground truth image.
  182. * @param {Number} size (to be removed) The max size (width or height) of the image to draw.
  183. * @param {Function} updateCallback
  184. * @returns a reference to the subregion image object that can be panned and zoomed by the user.
  185. *
  186. * @todo remove 'size' ... confusing and not useful.
  187. */
  188. function drawSubregionImage(stage, oImg, size, updateCallback = null) {
  189. var max = size;
  190. var imageSize = { w: oImg.naturalWidth, h: oImg.naturalHeight, };
  191. if (G_DEBUG)
  192. console.log("img natural size:", oImg.naturalWidth, oImg.naturalHeight);
  193. // get image ratios to "fit" in canvas
  194. var fillMode = Utils.getImageFillMode();
  195. var fitSize = Utils.fitImageProportions(oImg.naturalWidth, oImg.naturalHeight, max, fillMode);
  196. var fitScale = {
  197. x: fitSize.w / oImg.naturalWidth,
  198. y: fitSize.h / oImg.naturalHeight,
  199. };
  200. // force the image to be square by compressing it to the smallest dimension (w or h),
  201. // if we have the 'squish' fill mode.
  202. if (fillMode == 'squish') {
  203. let maxScale = Math.max(fitScale.x, fitScale.y);
  204. fitScale = { x: maxScale, y: maxScale };
  205. let minDim = Math.min(oImg.naturalWidth, oImg.naturalHeight);
  206. imageSize = { w: minDim, h: minDim };
  207. }
  208. // TODO: this should be in a helper likely,
  209. // since part of it is very similar to drawGroundtruthImage()
  210. var kImage = new Konva.Image({
  211. image: oImg,
  212. width: imageSize.w,
  213. height: imageSize.h,
  214. scale: {
  215. x: fitScale.x,
  216. y: fitScale.y,
  217. },
  218. draggable: true,
  219. });
  220. var layer = stage.getLayers()[0];
  221. layer.destroyChildren(); // avoid memory leaks
  222. var constrainBounds = function(){
  223. var scaleX = kImage.scaleX(), scaleY = kImage.scaleY();
  224. var x = kImage.x(), y = kImage.y();
  225. var w = kImage.width() * scaleX, h = kImage.height() * scaleY;
  226. var sx = stage.x(), sw = stage.width();
  227. var sy = stage.y(), sh = stage.height();
  228. if (x > sx) { kImage.x(sx); }
  229. if (x < (sx - w + sw) ) { kImage.x(sx - w + sw); }
  230. if (y > sy) { kImage.y(sy); }
  231. if (y < (sy - h + sh) ) { kImage.y(sy - h + sh); }
  232. stage.draw();
  233. };
  234. // optional event callback
  235. var doUpdate = function(){
  236. if (typeof updateCallback == 'function')
  237. return updateCallback();
  238. };
  239. // Enable drag and interaction events
  240. layer.listening(true);
  241. kImage.on('mouseup', function() { doUpdate(); });
  242. kImage.on('dragmove', function() {
  243. // set bounds on object, by overriding position here
  244. constrainBounds();
  245. doUpdate();
  246. });
  247. kImage.on('wheel', Utils.MakeZoomHandler(stage, kImage, function(){
  248. // bounds check for zooming out
  249. constrainBounds();
  250. // callback here, e.g. doUpdate();
  251. doUpdate();
  252. }, G_ZOOM_FACTOR_PER_TICK, fitScale.x));
  253. layer.add(kImage);
  254. stage.draw();
  255. // keyboard events
  256. // TODO: similar or duplicate from drawSpotProfileEdit() or
  257. // "Spot Profile" keyboard event code
  258. var container = stage.container();
  259. // make it focusable
  260. container.tabIndex = 1;
  261. // Avoiding using addEventListener, since we only ever want one handler at time.
  262. // This way, we easily replace it when a new image is loaded.
  263. container.onkeydown = function(e) {
  264. // don't handle meta-key'd events for now...
  265. const metaPressed = e.shiftKey || e.ctrlKey || e.metaKey;
  266. if (metaPressed)
  267. return;
  268. switch (e.keyCode) {
  269. case KEYCODE_R: // 'r' key, reset scale & position
  270. kImage.setAttrs({
  271. scaleX: fitScale.x,
  272. scaleY: fitScale.y,
  273. x:0, y:0
  274. });
  275. doUpdate();
  276. break;
  277. default: break;
  278. }
  279. e.preventDefault();
  280. };
  281. return kImage;
  282. }
  283. /**
  284. * Draws the spot content on the given drawing stage.
  285. * The given image is draggable (pan) and zoomable (scroll).
  286. * @param {*} stage the drawing stage.
  287. * @param {*} sImage the subregion image (will be cloned for the image object displayed).
  288. * @param {*} sBeam the beam/spot shape (used by reference).
  289. * @param {function} updateCallback a function to call when a change occurs such as pan-and-zoom.
  290. * @returns a reference to the image object being scaled by the user => "userScaledImage".
  291. */
  292. function drawSpotContent(stage, sImage, sBeam, updateCallback = null) {
  293. var layer = stage.getLayers()[0];
  294. layer.destroyChildren(); // avoid memory leaks
  295. layer.listening(true);
  296. // Give yellow box border to indicate interactive
  297. $(stage.getContainer()).css('border-color','yellow');
  298. var image = sImage.clone();
  299. image.draggable(true);
  300. image.globalCompositeOperation('source-in');
  301. layer.add(sBeam);
  302. layer.add(image);
  303. // "pre-zoom" a bit, and start with center position
  304. // zoom/scale so that the spot size starts at 100%
  305. var _tempCellSize = Utils.computeCellSize(sImage);
  306. var initialSpotScale = sBeam.width() / _tempCellSize.w;
  307. // get image proportions once scaled and fitted in the stages
  308. var max = G_BOX_SIZE, oImg = sImage.image(), fillMode = Utils.getImageFillMode();
  309. var fitSize = Utils.fitImageProportions(oImg.naturalWidth, oImg.naturalHeight, max, fillMode);
  310. var minScaleX = fitSize.w / oImg.naturalWidth;
  311. // center the image copy based on the calculated center and initial scales
  312. Utils.scaleOnCenter(stage, image, minScaleX, initialSpotScale);
  313. layer.draw();
  314. var doUpdate = function(){
  315. if (typeof updateCallback == 'function')
  316. return updateCallback();
  317. };
  318. // Events
  319. image.on('mouseup', function() { doUpdate(); });
  320. image.on('dragmove', function() { stage.draw(); });
  321. image.on('wheel', Utils.MakeZoomHandler(stage, image, function(){
  322. doUpdate();
  323. }, G_ZOOM_FACTOR_PER_TICK, 0, function(oldScale,newScale){
  324. // limit the max zoom from scrolling, to prevent blank pixel data
  325. // because of too small of a spot size...
  326. const tolerance = -0.1;
  327. if (G_BEAMRADIUS_SUBREGION_PX.x + tolerance < 1
  328. || G_BEAMRADIUS_SUBREGION_PX.y + tolerance < 1) {
  329. return Math.min(oldScale, newScale);
  330. }
  331. return newScale;
  332. }));
  333. return image;
  334. }
  335. /**
  336. * Draws the Spot Signal - previews the averaged signal for a given spot.
  337. * @param {*} sourceStage the source stage to sample from.
  338. * @param {*} destStage the stage to draw on.
  339. * @param {*} sBeam the beam to sample (or "stencil") to with.
  340. * @returns an update function to call when a redraw is needed.
  341. */
  342. function drawSpotSignal(sourceStage, destStage, sBeam) {
  343. var sourceLayer = sourceStage.getLayers()[0];
  344. var destLayer = destStage.getLayers()[0];
  345. destLayer.destroyChildren(); // avoid memory leaks
  346. var beam = sBeam; //.clone();
  347. var doUpdateAvgSpot = function(){
  348. var pCtx = sourceLayer.getContext();
  349. var allPx = pCtx.getImageData(0, 0, pCtx.canvas.width, pCtx.canvas.height);
  350. // var avgPx = Utils.get_avg_pixel_rgba(allPx);
  351. var avgPx = Utils.get_avg_pixel_gs(allPx); avgPx = [avgPx,avgPx,avgPx,1];
  352. var avgSpot = null;
  353. if (destLayer.getChildren().length <= 0){
  354. avgSpot = beam;
  355. destLayer.add(avgSpot);
  356. } else {
  357. avgSpot = destLayer.getChildren()[0];
  358. }
  359. var avgColor = "rgba("+ avgPx.join(',') +")";
  360. avgSpot.stroke(avgColor);
  361. avgSpot.fill(avgColor);
  362. destStage.getContainer().setAttribute('note', avgColor);
  363. destLayer.draw();
  364. };
  365. // run once immediately
  366. doUpdateAvgSpot();
  367. return doUpdateAvgSpot;
  368. }
  369. /**
  370. * Draws the probe layout.
  371. * @param {*} drawStage The stage to draw on.
  372. * @param {*} baseImage The subregion image to draw with (cloned).
  373. * @param {*} spotScale an object to query for the spot scale X/Y.
  374. * @param {*} beam the beam/spot shape to draw with (cloned and scaled).
  375. * @returns an update function to call when a redraw is needed.
  376. */
  377. function drawProbeLayout(drawStage, baseImage, spotScale, beam) {
  378. // draws probe layout
  379. var layers = drawStage.getLayers();
  380. var baseLayer = layers[0];
  381. baseLayer.destroyChildren(); // avoid memory leaks
  382. // The subregion area is based on what is "visible" in the subregion view.
  383. var baseGridRect = Utils.getRectFromKonvaObject(baseImage.getStage());
  384. var imageCopy = baseImage.clone();
  385. baseLayer.add(imageCopy);
  386. baseLayer.draw();
  387. // setup "last" values to help optimize for draw performance
  388. // no need to redraw the grid lines and spot outlines if the
  389. // no. rows, columns, spot radii, or rotation didn't change...
  390. var _last = {
  391. rows: -1,
  392. cols: -1,
  393. radiusX: -1,
  394. radiusY: -1,
  395. rotation: 0,
  396. beamOpacity: Utils.getSpotLayoutOpacityInput(),
  397. };
  398. var updateProbeLayout = function(){
  399. // get the over-layer, create if not already added
  400. var gridLayer = null;
  401. var gridDrawn = false;
  402. if (layers.length < 2) {
  403. gridLayer = new Konva.Layer();
  404. drawStage.add(gridLayer);
  405. } else {
  406. gridLayer = layers[1];
  407. gridDrawn = true; // assume we drew it already
  408. }
  409. // get probe layer, make a new if not already there
  410. var probesLayer = null;
  411. if (layers.length < 3) {
  412. probesLayer = new Konva.Layer();
  413. drawStage.add(probesLayer);
  414. } else {
  415. probesLayer = layers[2];
  416. }
  417. ///////////////////////////////
  418. // Do drawing work ...
  419. // update image based on user subregion
  420. imageCopy.x(baseImage.x());
  421. imageCopy.y(baseImage.y());
  422. imageCopy.scaleX(baseImage.scaleX());
  423. imageCopy.scaleY(baseImage.scaleY());
  424. var tRows = Utils.getRowsInput();
  425. var tCols = Utils.getColsInput();
  426. var radiusX = (beam.width() / spotScale.scaleX()) / 2; //(cell.width/2) * .8
  427. var radiusY = (beam.height() / spotScale.scaleY()) / 2; //(cell.height/2) * .8
  428. var beamRotation = beam.rotation();
  429. // preview spot display opacity
  430. var beamOpacity = Utils.getSpotLayoutOpacityInput();
  431. // only redraw grid lines and spot outlines if they would change
  432. if (_last.rows != tRows || _last.cols != tCols
  433. || _last.radiusX != radiusX || _last.radiusY != radiusY
  434. || _last.rotation != beamRotation
  435. || _last.beamOpacity != beamOpacity){
  436. // record for next change detect
  437. _last.rows = tRows, _last.cols = tCols;
  438. _last.radiusX = radiusX, _last.radiusY = radiusY,
  439. _last.rotation = beamRotation;
  440. _last.beamOpacity = beamOpacity;
  441. // comment to draw grid only once
  442. gridDrawn = false; gridLayer.destroyChildren();
  443. // draw grid, based on rect
  444. if (!gridDrawn)
  445. Utils.drawGrid(gridLayer, baseGridRect, tRows, tCols);
  446. // clear the probe layer
  447. probesLayer.destroyChildren();
  448. var probe = new Konva.Ellipse({
  449. radius : {
  450. x : radiusX,
  451. y : radiusY,
  452. },
  453. rotation: beamRotation,
  454. fill: 'rgba(255,0,0,'+beamOpacity+')',
  455. strokeWidth: 1,
  456. stroke: 'red'
  457. });
  458. Utils.repeatDrawOnGrid(probesLayer, baseGridRect, probe, tRows, tCols);
  459. }
  460. };
  461. // run once immediately
  462. updateProbeLayout();
  463. return updateProbeLayout;
  464. }
  465. /**
  466. * Draws the spot layout sampled image content. The image stenciled by the spot shape over a grid.
  467. * @param {*} drawStage The stage to draw on
  468. * @param {*} originalImage The image to "stencil" / "clip" or sample.
  469. * @param {*} spotScale an object to query for the spot scale X/Y.
  470. * @param {*} sBeam the spot/beam shape to use (cloned and scaled)
  471. * @returns an update function to call when a redraw is needed.
  472. */
  473. function drawProbeLayoutSampling(drawStage, originalImage, spotScale, sBeam) {
  474. var imageCopy = originalImage.clone();
  475. var beam = sBeam; //.clone();
  476. var drawLayer = drawStage.getLayers()[0];
  477. drawLayer.destroyChildren(); // avoid memory leaks
  478. // The subregion area is based on what is "visible" in the subregion view.
  479. var baseGridRect = Utils.getRectFromKonvaObject(originalImage.getStage());
  480. // setup "last" values to help optimize for draw performance
  481. // similar reason as for drawProbeLayout()
  482. var _last = {
  483. rows: -1,
  484. cols: -1,
  485. radiusX: -1,
  486. radiusY: -1,
  487. rotation: 0,
  488. };
  489. var updateProbeLayoutSampling = function(){
  490. var rows = Utils.getRowsInput();
  491. var cols = Utils.getColsInput();
  492. var radiusX = (beam.width() / spotScale.scaleX()) / 2;
  493. var radiusY = (beam.height() / spotScale.scaleY()) / 2;
  494. var beamRotation = beam.rotation();
  495. // only redraw as necessary: if the spots would change...
  496. if (_last.rows != rows || _last.cols != cols
  497. || _last.radiusX != radiusX || _last.radiusY != radiusY
  498. || _last.rotation != beamRotation){
  499. // record for next change detect
  500. _last.rows = rows, _last.cols = cols;
  501. _last.radiusX = radiusX, _last.radiusY = radiusY,
  502. _last.rotation = beamRotation;
  503. // clear the layer before we draw
  504. drawLayer.destroyChildren();
  505. var probe = new Konva.Ellipse({
  506. radius : {
  507. x : radiusX,
  508. y : radiusY,
  509. },
  510. rotation: beamRotation,
  511. fill: 'white',
  512. listening: false,
  513. });
  514. //Utils.computeResampledPreview(drawStage, imageCopy, probe, rows, cols, baseGridRect);
  515. Utils.repeatDrawOnGrid(drawLayer, baseGridRect, probe, rows, cols);
  516. imageCopy.globalCompositeOperation('source-in');
  517. drawLayer.add(imageCopy);
  518. // Not needed
  519. // drawStage.draw();
  520. }
  521. else
  522. {
  523. // otherwise only the image needs to updated by size & position
  524. imageCopy.x(originalImage.x());
  525. imageCopy.y(originalImage.y());
  526. imageCopy.scaleX(originalImage.scaleX());
  527. imageCopy.scaleY(originalImage.scaleY());
  528. }
  529. };
  530. // run once immediately
  531. updateProbeLayoutSampling();
  532. return updateProbeLayoutSampling;
  533. }
  534. /**
  535. * Draws the sampled subregion.
  536. * @param {*} sourceStage The source stage to sample from
  537. * @param {*} destStage The stage to draw on
  538. * @param {*} originalImage the subregion image to sample or 'stencil' over.
  539. * @param {*} spotScale an object to query for the spot scale X/Y.
  540. * @param {*} sBeam the beam to sample with (cloned and scaled)
  541. * @returns an update function to call when a redraw is needed
  542. */
  543. function drawResampled(sourceStage, destStage, originalImage, spotScale, sBeam) {
  544. var baseImage = originalImage; //.clone();
  545. var beam = sBeam; //.clone();
  546. // The subregion area is based on what is "visible" in the subregion view.
  547. var baseGridRect = Utils.getRectFromKonvaObject(baseImage.getStage());
  548. var rows = 0, cols = 0;
  549. var probe = null;
  550. var changeCount = 0, lastChange = 0;
  551. var updateConfigValues = function(){
  552. rows = Utils.getRowsInput();
  553. cols = Utils.getColsInput();
  554. probe = new Konva.Ellipse({
  555. radius : {
  556. x : (beam.width() / spotScale.scaleX()) / 2,
  557. y : (beam.height() / spotScale.scaleY()) / 2
  558. },
  559. rotation: beam.rotation(),
  560. fill: 'white',
  561. listening: false,
  562. });
  563. // update it globally, so we can limit zoom in Spot Content, based on this
  564. G_BEAMRADIUS_SUBREGION_PX = {x:probe.radiusX()*2, y:probe.radiusY()*2};
  565. // manage layers to allow for multilayer draw as needed for row-by-row drawing
  566. // guarantee at least one layer
  567. var layers = destStage.getLayers();
  568. if (layers.length < 1) { destStage.add(new Konva.Layer({ listening: false })); }
  569. changeCount++;
  570. };
  571. var currentRow = 0;
  572. function doUpdate(){
  573. var layers = destStage.getLayers();
  574. if (rows*cols > G_AUTO_PREVIEW_LIMIT) {
  575. // do row-by-row draw
  576. // otherwise, the app gets unresponsive since a frame may take too long to draw.
  577. if (!G_VSEM_PAUSED) {
  578. var row = currentRow++;
  579. if (currentRow >= rows) {
  580. currentRow = 0;
  581. var layer = new Konva.Layer({ listening: false });
  582. destStage.add(layer);
  583. }
  584. if (layers.length > 2) { layers[0].destroy(); }
  585. Utils.computeResampledSlow(sourceStage, destStage, baseImage, probe, rows, cols, baseGridRect
  586. ,row,row+1,0,-1,false,true);
  587. }
  588. } else {
  589. // do frame-by-frame draw
  590. if (changeCount > lastChange) {
  591. // reset change counts instead of recording it,
  592. // a trick to avoid an integer rollover, yet still functional.
  593. lastChange = changeCount = 0;
  594. // ensure we only use one layer
  595. // prevents any left over partially draw layers on top coming from the row-by-row draw
  596. if (layers.length > 1) {
  597. destStage.destroyChildren();
  598. destStage.add(new Konva.Layer({ listening: false }));
  599. }
  600. // Utils.computeResampledFast(sourceStage, destStage, baseImage, probe, rows, cols);
  601. // Utils.computeResampledSlow(sourceStage, destStage, baseImage, probe, rows, cols, baseGridRect);
  602. Utils.computeResampledSlow(sourceStage, destStage, baseImage, probe, rows, cols, baseGridRect
  603. ,0,-1,0,-1,true,false);
  604. }
  605. }
  606. // not needed
  607. // destStage.draw()
  608. G_SubResampled_animationFrameRequestId = requestAnimationFrame(doUpdate);
  609. }
  610. // run once immediately
  611. updateConfigValues();
  612. cancelAnimationFrame(G_SubResampled_animationFrameRequestId);
  613. G_SubResampled_animationFrameRequestId = requestAnimationFrame(doUpdate);
  614. return updateConfigValues;
  615. }
  616. /**
  617. * Draws the ground truth image and the subregion bounds overlay.
  618. * @param {*} stage the stage to draw on.
  619. * @param {*} imageObj the original/full-size image to draw
  620. * @param {*} subregionImage the subregion image (to get the bounds from)
  621. * @param {number} maxSize (to be removed) the maximum size (width or height) of the stage to fit the image?
  622. * @param {Function} updateCallback called when a change is made to the subregion
  623. * @returns an object with an update function to call for needed redraws and the subregion bounds.
  624. * @todo remove maxSize if possible?
  625. * @todo do we really need to return the subregionRect as well?
  626. */
  627. function drawGroundtruthImage(stage, imageObj, subregionImage, maxSize=G_BOX_SIZE, updateCallback = null){
  628. var fillMode = Utils.getImageFillMode();
  629. var fit = Utils.fitImageProportions(imageObj.naturalWidth, imageObj.naturalHeight, maxSize, fillMode);
  630. var layer = stage.getLayers()[0];
  631. layer.destroyChildren(); // avoid memory leaks
  632. // TODO: this shouldnt be need or it at least duplicate with
  633. // part of drawSubregionImage()
  634. var image = new Konva.Image({
  635. x: (maxSize - fit.w)/2,
  636. y: (maxSize - fit.h)/2,
  637. image: imageObj,
  638. width: fit.w,
  639. height: fit.h,
  640. listening: false,
  641. });
  642. var rect = new Konva.Rect({
  643. x: image.x(),
  644. y: image.y(),
  645. width: image.width(),
  646. height: image.height(),
  647. fill: "rgba(0,255,255,0.4)",
  648. stroke: "#00FFFF",
  649. strokeWidth: 1,
  650. // listening: false,
  651. draggable: true,
  652. strokeScaleEnabled: false,
  653. });
  654. var imagePixelScaling = Utils.imagePixelScaling(image, imageObj);
  655. if (fillMode == 'squish') {
  656. var maxScale = Math.max(imagePixelScaling.x, imagePixelScaling.y);
  657. imagePixelScaling = { x: maxScale, y: maxScale };
  658. }
  659. // Draggable nav-rect
  660. // https://github.com/joedf/ImgBeamer/issues/41
  661. rect.on('dragmove', function(){
  662. constrainRect();
  663. applyChangesFromNavRect();
  664. });
  665. var constrainRect = function(){
  666. var rw = rect.width();
  667. var rh = rect.height();
  668. var ss = stage.size();
  669. // top left corner limit
  670. if (rect.x() < 0) { rect.x(0); }
  671. if (rect.y() < 0) { rect.y(0); }
  672. // bottom right limit
  673. if (rect.x() > ss.width - rw) { rect.x(ss.width - rw); }
  674. if (rect.y() > ss.height - rh) { rect.y(ss.height - rh); }
  675. };
  676. stage.off('wheel'); // prevent "eventHandler doubling" from subsequent calls
  677. stage.on('wheel', function(e) {
  678. // code is based on Utils.MakeZoomHandler()
  679. e.evt.preventDefault(); // stop default scrolling
  680. const scaleFactor = G_ZOOM_FACTOR_PER_TICK;
  681. var scaleBy = scaleFactor;
  682. // Do half rate scaling, if shift is pressed
  683. if (e.evt.shiftKey) {
  684. scaleBy = 1 +((scaleBy-1) / 2);
  685. }
  686. // calculate scale with direction
  687. let direction = e.evt.deltaY > 0 ? -1 : 1;
  688. var scale = direction > 0 ? 1.0 / scaleBy : 1.0 * scaleBy;
  689. // calculate new size
  690. var rs = rect.size();
  691. var newWidth = rs.width * scale;
  692. var newHeigth = rs.height * scale;
  693. // constrain size
  694. var limitW = Math.min(Math.max(newWidth, 1), stage.width());
  695. var limitH = Math.min(Math.max(newHeigth, 1), stage.height());
  696. // get rect center point delta
  697. var dx = rect.width() - limitW;
  698. var dy = rect.height() - limitH;
  699. // apply new size
  700. rect.size({ width: limitW, height: limitH });
  701. // center rect based on new size
  702. rect.position({
  703. x: rect.x() + dx/2,
  704. y: rect.y() + dy/2
  705. });
  706. constrainRect();
  707. applyChangesFromNavRect();
  708. });
  709. var applyChangesFromNavRect = function(){
  710. // update the subregion view to the new position and zoom based on changes
  711. // to the nav-rect by the user
  712. var si = subregionImage;
  713. si.scale({
  714. x: (stage.width() / rect.width()) * imagePixelScaling.x,
  715. y: (stage.height() / rect.height()) * imagePixelScaling.y,
  716. });
  717. si.position({
  718. x: ((image.x() - rect.x()) * si.scaleX()) / imagePixelScaling.x,
  719. y: ((image.y() - rect.y()) * si.scaleY()) / imagePixelScaling.y,
  720. });
  721. // this propagates the changes to the subregion to the rest of the app
  722. if (typeof updateCallback == 'function')
  723. return updateCallback();
  724. };
  725. // Grab cursor for nav-rectangle overlay
  726. // https://konvajs.org/docs/styling/Mouse_Cursor.html
  727. layer.listening(true);
  728. rect.on('mouseenter', function () {
  729. stage.container().style.cursor = 'grab';
  730. }).on('mouseleave', function () {
  731. stage.container().style.cursor = 'default';
  732. });
  733. layer.add(image);
  734. layer.add(rect);
  735. var update = function(){
  736. // calc location rect from subregionImage
  737. // and update bounds drawn rectangle
  738. var si = subregionImage;
  739. rect.position({
  740. x: ((image.x() - si.x()) / si.scaleX()) * imagePixelScaling.x,
  741. y: ((image.y() - si.y()) / si.scaleY()) * imagePixelScaling.y,
  742. });
  743. rect.size({
  744. width: (stage.width() / si.scaleX()) * imagePixelScaling.x,
  745. height: (stage.height() / si.scaleY()) * imagePixelScaling.y,
  746. });
  747. // subregion overlay visibility
  748. rect.visible(G_SHOW_SUBREGION_OVERLAY);
  749. stage.draw();
  750. // center of the rect coords
  751. var center = {
  752. x: rect.x() + rect.width()/2,
  753. y: rect.y() + rect.height()/2,
  754. };
  755. // transform to unit square coords
  756. var unitCoords = Utils.stageToUnitCoordinates(center.x, center.y, stage);
  757. // scale to original image pixel size
  758. var pxImgCoords = Utils.unitToImagePixelCoordinates(unitCoords.x, unitCoords.y, imageObj);
  759. var pxSizeNm = Utils.getPixelSizeNmInput();
  760. // scale as real physical units
  761. var middle = Utils.imagePixelToRealCoordinates(pxImgCoords.x, pxImgCoords.y, pxSizeNm);
  762. // get as optimal displayUnit
  763. var fmtMiddle = Utils.formatUnitNm(middle.x, middle.y);
  764. var sizeFOV = {
  765. w: (rect.width() / stage.width()) * imageObj.naturalWidth * pxSizeNm,
  766. };
  767. var fmtSizeFOV = Utils.formatUnitNm(sizeFOV.w);
  768. // display coords & FOV size
  769. Utils.updateExtraInfo(stage, '('
  770. + fmtMiddle.value.toFixed(G_MATH_TOFIXED.SHORT) + ', '
  771. + fmtMiddle.value2.toFixed(G_MATH_TOFIXED.SHORT) + ')'
  772. + ' ' + fmtMiddle.unit
  773. + '<br>FOV width: ' + fmtSizeFOV.value.toFixed(G_MATH_TOFIXED.SHORT)
  774. + ' ' + fmtSizeFOV.unit
  775. );
  776. };
  777. update();
  778. stage.draw();
  779. return {
  780. updateFunc: update,
  781. subregionRect: rect
  782. };
  783. }
  784. /**
  785. * Draws the resulting image continously row-by-row.
  786. * @param {*} stage the stage to draw on
  787. * @param {*} beam the beam to sample with
  788. * @param {*} subregionRect the bounds on the subregion
  789. * @param {*} subregionRectStage the stage for the gorund truth
  790. * @param {*} originalImageObj the ground truth image
  791. * @param {*} spotScale an object to query for the spot scale X/Y.
  792. * @returns an update function to call when the spot profile or the cell/pixel size changes.
  793. * @todo do we really need both subregionRect and subregionRectStage as
  794. * separate parameters? maybe the info needed can be obtained with less
  795. * or more cleanly?
  796. * @todo rename confusing subregionRectStage to groundTruthStage?
  797. * @todo can we get rid userScaleImage / userImage throughout the source if possible, cleaner?
  798. */
  799. function drawVirtualSEM(stage, beam, subregionRect, subregionRectStage, originalImageObj, spotScale){
  800. var rows = 0, cols = 0;
  801. var cellW = 0, cellH = 0;
  802. var currentRow = 0;
  803. const indicatorWidth = 20;
  804. const indicatorHeight = 3;
  805. var pixelCount = 0;
  806. var currentDrawPass = 0;
  807. // use the canvas API directly in a konva stage
  808. // https://konvajs.org/docs/sandbox/Free_Drawing.html
  809. var layer = stage.getLayers()[0];
  810. layer.destroyChildren(); // avoid memory leaks
  811. var canvas = document.createElement('canvas');
  812. canvas.width = stage.width();
  813. canvas.height = stage.height();
  814. // the canvas is added to the layer as a "Konva.Image" element
  815. var image = new Konva.Image({
  816. image: canvas,
  817. x: 0,
  818. y: 0,
  819. });
  820. layer.add(image);
  821. // draw an indicator to show which row was last drawn
  822. var indicator = new Konva.Rect({
  823. x: stage.width() - indicatorWidth, y: 0,
  824. width: indicatorWidth,
  825. height: indicatorHeight,
  826. fill: 'red',
  827. });
  828. layer.add(indicator);
  829. var context = canvas.getContext('2d');
  830. context.imageSmoothingEnabled = false;
  831. var beamRadius = {x : 0, y: 0};
  832. var superScale = 1;
  833. // original image size
  834. var iw = originalImageObj.naturalWidth, ih = originalImageObj.naturalHeight;
  835. // get scale factor for full image size
  836. var irw = (iw / stage.width()), irh = (ih / stage.height());
  837. var updateConfigValues = function(){
  838. var ratioX = subregionRectStage.width() / subregionRect.width();
  839. var ratioY = subregionRectStage.height() / subregionRect.height();
  840. // multiply by the ratio, since we should have more cells on the full image
  841. rows = Math.round(Utils.getRowsInput() * ratioY);
  842. cols = Math.round(Utils.getColsInput() * ratioX);
  843. // the total number of "pixels" (cells) that will drawn
  844. pixelCount = rows * cols;
  845. // save last value, to detect significant change
  846. var lastCellW = cellW, lastCellH = cellH;
  847. cellW = stage.width() / cols;
  848. cellH = stage.height() / rows;
  849. var significantChange = (cellW != lastCellW) && (cellH != lastCellH);
  850. // get beam size based on user-scaled image
  851. beamRadius = {
  852. // divide by the ratio, since the spot should be smaller when mapped onto
  853. // the full image which is scaled down to the same stage size...
  854. x : (beam.width() / spotScale.scaleX()) / 2 / ratioX,
  855. y : (beam.height() / spotScale.scaleY()) / 2 / ratioY
  856. };
  857. // check if we need to scale up the image for sampling...
  858. // if the avg spot radius is less than 1, scale up at 1 / x (inversely proportional)
  859. var radiusAvg = (beamRadius.x+beamRadius.y)/2;
  860. superScale = (radiusAvg < 1) ? 1 / radiusAvg : 1;
  861. // we can clear the screen here, if we want to avoid lines from previous configs...
  862. if (significantChange) { // if it affects the drawing
  863. context.clearRect(0, 0, canvas.width, canvas.height);
  864. currentRow = 0; // restart drawing from the top
  865. currentDrawPass = 0;
  866. }
  867. // display image size / pixel counts and the pixel size
  868. var fullImgWidthNm = iw * Utils.getPixelSizeNmInput();
  869. var fmtPxSize = Utils.formatUnitNm(
  870. fullImgWidthNm / cols,
  871. fullImgWidthNm / rows,
  872. );
  873. Utils.updateExtraInfo(stage, cols + ' x ' + rows
  874. + ' px<br>' + fmtPxSize.value.toFixed(G_MATH_TOFIXED.SHORT)
  875. + " x " + fmtPxSize.value2.toFixed(G_MATH_TOFIXED.SHORT)
  876. + " " + fmtPxSize.unit + "/px");
  877. };
  878. updateConfigValues();
  879. // var colors = ['blue', 'yellow', 'red', 'green', 'cyan', 'pink'];
  880. var colors = ['#DDDDDD','#EEEEEE','#CCCCCC','#999999','#666666','#333333','#B6B6B6','#1A1A1A'];
  881. var color = colors[Utils.getRandomInt(colors.length)];
  882. var doUpdate = function(){
  883. if (!G_VSEM_PAUSED) {
  884. // track time to draw the row
  885. var timeRowStart = Date.now();
  886. var row = currentRow++;
  887. var ctx = context;
  888. if (currentRow >= rows) {
  889. currentRow = 0;
  890. currentDrawPass += 1;
  891. }
  892. var rowIntensitySum = 0;
  893. // interate over X
  894. for (let i = 0; i < cols; i++) {
  895. const cellX = i * cellW;
  896. const cellY = row * cellH;
  897. // TODO: check these values and the Utils.ComputeProbeValue_gs again
  898. // since the final image seems to differ...
  899. // map/transform values to full resolution image coordinates
  900. const scaledProbe = {
  901. centerX: (cellX + cellW/2) * irw,
  902. centerY: (cellY + cellH/2) * irh,
  903. rotationRad: Utils.toRadians(beam.rotation()),
  904. radiusX: beamRadius.x * irw,
  905. radiusY: beamRadius.y * irh,
  906. };
  907. // compute the pixel value, for the given spot/probe profile
  908. var gsValue = Utils.ComputeProbeValue_gs(originalImageObj, scaledProbe, superScale);
  909. color = 'rgba('+[gsValue,gsValue,gsValue].join(',')+',1)';
  910. ctx.fillStyle = color;
  911. rowIntensitySum += gsValue;
  912. // optionally, draw with overlap to reduce visual artifacts
  913. if ((currentDrawPass < G_DRAW_OVERLAP_PASSES)
  914. && (G_DRAW_WITH_OVERLAP && pixelCount >= G_DRAW_OVERLAP_THRESHOLD)) {
  915. ctx.fillRect(
  916. cellX -G_DRAW_OVERLAP_PIXELS,
  917. cellY -G_DRAW_OVERLAP_PIXELS,
  918. cellW +G_DRAW_OVERLAP_PIXELS,
  919. cellH +G_DRAW_OVERLAP_PIXELS
  920. );
  921. } else {
  922. ctx.fillRect(cellX, cellY, cellW, cellH);
  923. }
  924. }
  925. // if the last drawn was essentially completely black
  926. // assume the spot size was too small or no signal
  927. // for 1-2 overlapped-draw passes...
  928. var rowIntensityAvg = rowIntensitySum / cols;
  929. if (rowIntensityAvg <= G_MIN_AVG_SIGNAL_VALUE) { // out of 255
  930. currentDrawPass = -1;
  931. }
  932. // move/update the indicator
  933. indicator.y((row+1) * cellH - indicator.height());
  934. layer.batchDraw();
  935. // use this for debugging, less heavy, draw random color rows
  936. // color = colors[Utils.getRandomInt(colors.length)];
  937. // updateConfigValues();
  938. var timeDrawTotal = Date.now() - timeRowStart;
  939. stage.getContainer().setAttribute('note', timeDrawTotal + " ms/Row");
  940. // update the image metric automatically a few times
  941. // so the score updates as parts of the images changes
  942. if (typeof G_update_ImgMetrics == "function") {
  943. // update it every quarter of the rows drawn
  944. let imgMetricUpdateTick = (currentRow % Math.floor(rows / 4) == 0) && (currentRow > 1);
  945. // or update if the draw-rate is fast (less than 50 ms/row)
  946. const rowTime = 50; //ms
  947. // and not an SSIM-based algorithm, since they are comparatively slow...
  948. var isSSIM = (Utils.getImageMetricAlgorithm()).indexOf('SSIM') >= 0;
  949. if ( (timeDrawTotal < rowTime && !isSSIM) || imgMetricUpdateTick) {
  950. G_update_ImgMetrics();
  951. }
  952. }
  953. }
  954. // see comment on using this instead of setInterval below
  955. G_VirtualSEM_animationFrameRequestId = requestAnimationFrame(doUpdate);
  956. };
  957. // a warning is logged with slow setTimeout or requestAnimationFrame callbacks
  958. // for each frame taking longer than ~60+ ms... resulting in hundreds/thousands,
  959. // possibly slowing down the browser over time...
  960. // ... read next comment block first, then come back to this one ...
  961. // This cancel is needed, otherwise subsequent calls will multiple rogue update functions
  962. // (going out of scope) running forever, but never allow itself to be garbage collected
  963. // because the execution never ends... A similar case would likely happen with timers
  964. // as well, e.g. self-calling setTimeout or setInterval...
  965. cancelAnimationFrame(G_VirtualSEM_animationFrameRequestId);
  966. // ... but we use requestAnimationFrame to let the browser determine what the
  967. // fastest possible ideal speed is.
  968. G_VirtualSEM_animationFrameRequestId = requestAnimationFrame(doUpdate);
  969. return updateConfigValues;
  970. }

↑ Top