diff --git a/docs/releases/v2.3.0.md b/docs/releases/v2.3.0.md index 708c1f6a0519980a66747372a34c883db3936d47..d66a43f06b15a5a2573a5c45310cdc7a3917a276 100644 --- a/docs/releases/v2.3.0.md +++ b/docs/releases/v2.3.0.md @@ -2,4 +2,7 @@ ## New feature -- update dataset preview functionality, allow the previewing of png \ No newline at end of file +- update dataset preview functionality, allow the previewing of png +- improved the previewing of maps + - parse min and max, if these metadata are provided + - allowing for color maps other than jet diff --git a/src/glue.ts b/src/glue.ts index 34e9525e79880ef4a1093fa16d8ef2750ede4424..7be1c6b576e6cd9fa9df0354567110892428b251 100644 --- a/src/glue.ts +++ b/src/glue.ts @@ -11,6 +11,8 @@ import { HttpClient } from "@angular/common/http" import { DS_PREVIEW_URL, getShader, PMAP_DEFAULT_CONFIG } from 'src/util/constants' import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, INgLayerInterface } from "./services/state/ngViewerState.store.helper" import { ARIA_LABELS } from 'common/constants' +import { NgLayersService } from "src/ui/layerbrowser/ngLayerService.service" +import { EnumColorMapName } from "./util/colorMaps" const PREVIEW_FILE_TYPES_NO_UI = [ EnumPreviewFileTypes.NIFTI, @@ -165,6 +167,7 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ constructor( private store$: Store<any>, private http: HttpClient, + private layersService: NgLayersService, @Optional() @Inject(ACTION_TO_WIDGET_TOKEN) private actionOnWidget: TypeActionToWidget<any> ){ if (!this.actionOnWidget) console.warn(`actionOnWidget not provided in DatasetPreviewGlue. Did you forget to provide it?`) @@ -227,31 +230,47 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ ).subscribe(([ { prvToShow, prvToDismiss }, templateSelected ]) => { // TODO consider where to check validity of previewed nifti file for (const prv of prvToShow) { - const { url, filename, volumeMetadata = {} } = prv - const { min, max } = volumeMetadata || {} + + const { url, filename, name, volumeMetadata = {} } = prv + const { min, max, colormap = EnumColorMapName.VIRIDIS } = volumeMetadata || {} + const previewFileId = DatasetPreviewGlue.GetDatasetPreviewId(prv) + + const shaderObj = { + ...PMAP_DEFAULT_CONFIG, + ...{ colormap }, + ...( typeof min !== 'undefined' ? { lowThreshold: min } : {} ), + ...( max ? { highThreshold: max } : { highThreshold: 1 } ) + } + const layer = { - name: filename, + // name: filename, + name: name || filename, id: previewFileId, source : `nifti://${url}`, mixability : 'nonmixable', - shader : getShader({ - ...PMAP_DEFAULT_CONFIG, - ...( typeof min !== 'undefined' ? { lowThreshold: min } : {} ), - ...( max ? { highThreshold: max } : {} ) - }), + shader : getShader(shaderObj), annotation: `${DATASET_PREVIEW_ANNOTATION} ${filename}` } + + const { name: layerName } = layer + const { colormap: cmap, lowThreshold, highThreshold, removeBg } = shaderObj + + this.layersService.highThresholdMap.set(layerName, highThreshold) + this.layersService.lowThresholdMap.set(layerName, lowThreshold) + this.layersService.colorMapMap.set(layerName, cmap) + this.layersService.removeBgMap.set(layerName, removeBg) + this.store$.dispatch( ngViewerActionAddNgLayer({ layer }) ) } for (const prv of prvToDismiss) { - const { url, filename } = prv + const { url, filename, name } = prv const previewFileId = DatasetPreviewGlue.GetDatasetPreviewId(prv) const layer = { - name: filename, + name: name || filename, id: previewFileId, source : `nifti://${url}`, mixability : 'nonmixable', diff --git a/src/ui/layerbrowser/layerDetail/layerDetail.component.ts b/src/ui/layerbrowser/layerDetail/layerDetail.component.ts index ce525450d66af85854ae715723c14ff6ad6ffd35..414bab57e2f2ef11e8232a7405e8b206d5707cda 100644 --- a/src/ui/layerbrowser/layerDetail/layerDetail.component.ts +++ b/src/ui/layerbrowser/layerDetail/layerDetail.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnChanges, ChangeDetectionStrategy, Optional, Inject import { NgLayersService } from "../ngLayerService.service"; import { MatSliderChange } from "@angular/material/slider"; import { MatSlideToggleChange } from "@angular/material/slide-toggle"; -import { COLORMAP_IS_JET, getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants"; +import { getShader } from "src/util/constants"; export const VIEWER_INJECTION_TOKEN = `VIEWER_INJECTION_TOKEN` @@ -28,19 +28,12 @@ export class LayerDetailComponent implements OnChanges{ ngOnChanges(){ if (!this.layerName) return - const isPmap = (this.fragmentMain.value as string).includes(COLORMAP_IS_JET) - const { colormap, lowThreshold, removeBg } = PMAP_DEFAULT_CONFIG - if (isPmap) { - this.colormap = colormap - this.lowThreshold = lowThreshold - this.removeBg = removeBg - } - this.lowThreshold = this.layersService.lowThresholdMap.get(this.layerName) || this.lowThreshold this.highThreshold = this.layersService.highThresholdMap.get(this.layerName) || this.highThreshold this.brightness = this.layersService.brightnessMap.get(this.layerName) || this.brightness this.contrast = this.layersService.contrastMap.get(this.layerName) || this.contrast this.removeBg = this.layersService.removeBgMap.get(this.layerName) || this.removeBg + this.colormap = this.layersService.colorMapMap.get(this.layerName) || this.colormap } public lowThreshold: number = 0 diff --git a/src/ui/layerbrowser/ngLayerService.service.ts b/src/ui/layerbrowser/ngLayerService.service.ts index baf59a9a4191c27ea5ddffb72629148cdb1a1ab9..35bd949ba6885423862692dacdac94e35dc96a7b 100644 --- a/src/ui/layerbrowser/ngLayerService.service.ts +++ b/src/ui/layerbrowser/ngLayerService.service.ts @@ -1,4 +1,5 @@ import { Injectable } from "@angular/core"; +import { EnumColorMapName } from "src/util/colorMaps"; @Injectable({ providedIn: 'root' @@ -10,4 +11,5 @@ export class NgLayersService{ public brightnessMap: Map<string, number> = new Map() public contrastMap: Map<string, number> = new Map() public removeBgMap: Map<string, boolean> = new Map() + public colorMapMap: Map<string, EnumColorMapName> = new Map() } diff --git a/src/ui/searchSideNav/searchSideNav.template.html b/src/ui/searchSideNav/searchSideNav.template.html index d9f0f5b8aa3d25548cde01e6a735243afbc2e747..eb1ae006e424599bda673f2aa471b4e5a8ab93ec 100644 --- a/src/ui/searchSideNav/searchSideNav.template.html +++ b/src/ui/searchSideNav/searchSideNav.template.html @@ -69,7 +69,7 @@ </div> -<div [hidden]> +<div class="d-none"> <layer-browser (nonBaseLayersChanged)="handleNonbaseLayerEvent($event)" #layerBrowser> diff --git a/src/util/colorMaps.ts b/src/util/colorMaps.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad3ab450b293f301dc90182328e1bd2b06002a7a --- /dev/null +++ b/src/util/colorMaps.ts @@ -0,0 +1,204 @@ +export const COLORMAP_IS_DEFAULT = `// iav-colormap-default` + +export const COLORMAP_IS_JET = `// iav-colormap-is-jet` + +export const COLORMAP_IS_VIRIDIS = `// iav-colormap-is-viridis` +export const COLORMAP_IS_MAGMA = `// iav-colormap-is-magma` +export const COLORMAP_IS_PLASMA = `// iav-colormap-is-plasma` +export const COLORMAP_IS_INFERNO = `// iav-colormap-is-inferno` + +export const COLORMAP_IS_GREYSCALE = `// iav-colormap-is-greyscale` + +export enum EnumColorMapName{ + JET='jet', + + VIRIDIS='viridis', + PLASMA='plasma', + MAGMA='magma', + INFERNO='inferno', + + GREYSCALE='greyscale', +} + +interface IColorMap{ + /** + * header + */ + header: string + /** + * appended before void main() {} block + */ + premain: string + /** + * appended in void main(){} block + * + * input: + * + * float x; + * + * populate: + * + * vec3 rgb; + */ + main: string +} + +export const mapKeyColorMap = new Map<EnumColorMapName, IColorMap>([ + [ EnumColorMapName.JET, { + header: COLORMAP_IS_JET, + /** + * The MIT License (MIT) + + Copyright (c) 2015 kbinani + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + * <https://github.com/kbinani/colormap-shaders/blob/master/shaders/glsl/MATLAB_jet.frag> + */ + premain: ` + float colormap_red(float x) { + if (x < 0.7) { + return 4.0 * x - 1.5; + } else { + return -4.0 * x + 4.5; + } + } + + float colormap_green(float x) { + if (x < 0.5) { + return 4.0 * x - 0.5; + } else { + return -4.0 * x + 3.5; + } + } + + float colormap_blue(float x) { + if (x < 0.3) { + return 4.0 * x + 0.5; + } else { + return -4.0 * x + 2.5; + } + } + `, + main: ` + float r = clamp(colormap_red(x), 0.0, 1.0); + float g = clamp(colormap_green(x), 0.0, 1.0); + float b = clamp(colormap_blue(x), 0.0, 1.0); + rgb=vec3(r,g,b); + ` + } ], + + [ EnumColorMapName.VIRIDIS, { + header: COLORMAP_IS_VIRIDIS, + /** + * created by mattz CC/0 + * https://www.shadertoy.com/view/WlfXRN + */ + premain: ` + vec3 viridis(float t) { + + const vec3 c0 = vec3(0.2777273272234177, 0.005407344544966578, 0.3340998053353061); + const vec3 c1 = vec3(0.1050930431085774, 1.404613529898575, 1.384590162594685); + const vec3 c2 = vec3(-0.3308618287255563, 0.214847559468213, 0.09509516302823659); + const vec3 c3 = vec3(-4.634230498983486, -5.799100973351585, -19.33244095627987); + const vec3 c4 = vec3(6.228269936347081, 14.17993336680509, 56.69055260068105); + const vec3 c5 = vec3(4.776384997670288, -13.74514537774601, -65.35303263337234); + const vec3 c6 = vec3(-5.435455855934631, 4.645852612178535, 26.3124352495832); + + return c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6))))); + } + `, + main: 'rgb=viridis(x);' + } ], + [ EnumColorMapName.PLASMA, { + header: COLORMAP_IS_PLASMA, + /** + * created by mattz CC/0 + * https://www.shadertoy.com/view/WlfXRN + */ + premain: ` + vec3 plasma(float t) { + + const vec3 c0 = vec3(0.05873234392399702, 0.02333670892565664, 0.5433401826748754); + const vec3 c1 = vec3(2.176514634195958, 0.2383834171260182, 0.7539604599784036); + const vec3 c2 = vec3(-2.689460476458034, -7.455851135738909, 3.110799939717086); + const vec3 c3 = vec3(6.130348345893603, 42.3461881477227, -28.51885465332158); + const vec3 c4 = vec3(-11.10743619062271, -82.66631109428045, 60.13984767418263); + const vec3 c5 = vec3(10.02306557647065, 71.41361770095349, -54.07218655560067); + const vec3 c6 = vec3(-3.658713842777788, -22.93153465461149, 18.19190778539828); + + return c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6))))); + + } + `, + main: 'rgb=plasma(x);' + } ], + [ EnumColorMapName.MAGMA, { + header: COLORMAP_IS_MAGMA, + /** + * created by mattz CC/0 + * https://www.shadertoy.com/view/WlfXRN + */ + premain: ` + vec3 magma(float t) { + + const vec3 c0 = vec3(-0.002136485053939582, -0.000749655052795221, -0.005386127855323933); + const vec3 c1 = vec3(0.2516605407371642, 0.6775232436837668, 2.494026599312351); + const vec3 c2 = vec3(8.353717279216625, -3.577719514958484, 0.3144679030132573); + const vec3 c3 = vec3(-27.66873308576866, 14.26473078096533, -13.64921318813922); + const vec3 c4 = vec3(52.17613981234068, -27.94360607168351, 12.94416944238394); + const vec3 c5 = vec3(-50.76852536473588, 29.04658282127291, 4.23415299384598); + const vec3 c6 = vec3(18.65570506591883, -11.48977351997711, -5.601961508734096); + + return c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6))))); + + } + `, + main: 'rgb=magma(x);' + } ], + [ EnumColorMapName.INFERNO, { + header: '', + /** + * created by mattz CC/0 + * https://www.shadertoy.com/view/WlfXRN + */ + premain: ` + vec3 inferno(float t) { + + const vec3 c0 = vec3(0.0002189403691192265, 0.001651004631001012, -0.01948089843709184); + const vec3 c1 = vec3(0.1065134194856116, 0.5639564367884091, 3.932712388889277); + const vec3 c2 = vec3(11.60249308247187, -3.972853965665698, -15.9423941062914); + const vec3 c3 = vec3(-41.70399613139459, 17.43639888205313, 44.35414519872813); + const vec3 c4 = vec3(77.162935699427, -33.40235894210092, -81.80730925738993); + const vec3 c5 = vec3(-71.31942824499214, 32.62606426397723, 73.20951985803202); + const vec3 c6 = vec3(25.13112622477341, -12.24266895238567, -23.07032500287172); + + return c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6))))); + + } + `, + main: 'rgb=inferno(x);' + } ], + + [ EnumColorMapName.GREYSCALE, { + header: COLORMAP_IS_GREYSCALE, + premain: '', + main: 'rgb=vec3(x, x, x);' + } ] +]) diff --git a/src/util/constants.ts b/src/util/constants.ts index 549c483a9435b38ca31e9181a552dfdf087b6540..0a0b993c6cfa54b69687f37479bf4ea704ace688 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -68,40 +68,41 @@ export const getHttpHeader: () => HttpHeaders = () => { return header } -const CM_MATLAB_JET = `float r;if( x < 0.7 ){r = 4.0 * x - 1.5;} else {r = -4.0 * x + 4.5;}float g;if (x < 0.5) {g = 4.0 * x - 0.5;} else {g = -4.0 * x + 3.5;}float b;if (x < 0.3) {b = 4.0 * x + 0.5;} else {b = -4.0 * x + 2.5;}float a = 1.0;` -const CM_DEFAULT = `float r = x; float g = x; float b = x;` export const COLORMAP_IS_JET = `// iav-colormap-is-jet` -export const COLORMAP_IS_DEFAULT = `// iav-colormap-default` +import { EnumColorMapName, mapKeyColorMap } from './colorMaps' export const getShader = ({ - colormap = null, + colormap = EnumColorMapName.GREYSCALE, lowThreshold = 0, highThreshold = 1, brightness = 0, contrast = 0, removeBg = false } = {}): string => { - const header = colormap === 'jet' ? COLORMAP_IS_JET : COLORMAP_IS_DEFAULT - const colormapGlsl = colormap === 'jet' ? CM_MATLAB_JET : CM_DEFAULT + const { header, main, premain } = mapKeyColorMap.get(colormap) || (() => { + console.warn(`colormap ${colormap} not found. Using default colormap instead`) + return mapKeyColorMap.get(EnumColorMapName.GREYSCALE) + })() // so that if lowthreshold is defined to be 0, at least some background removal will be done const _lowThreshold = lowThreshold + 1e-10 return `${header} +${premain} void main() { float raw_x = toNormalized(getDataValue()); float x = (raw_x - ${_lowThreshold.toFixed(10)}) / (${highThreshold - _lowThreshold}) ${ brightness > 0 ? '+' : '-' } ${Math.abs(brightness).toFixed(10)}; ${ removeBg ? 'if(x>1.0){emitTransparent();}else if(x<0.0){emitTransparent();}else{' : '' } - ${colormapGlsl} - - emitRGB(vec3(r, g, b)*exp(${contrast.toFixed(10)})); + vec3 rgb; + ${main} + emitRGB(rgb*exp(${contrast.toFixed(10)})); ${ removeBg ? '}' : '' } } ` } export const PMAP_DEFAULT_CONFIG = { - colormap: 'jet', + colormap: EnumColorMapName.VIRIDIS, lowThreshold: 0.05, removeBg: true }