import { Layer } from './Layer.js' import { Raster } from './Raster.js' import { ShaderMultispectral } from './ShaderMultispectral.js' import { Util } from './Util.js' import { Transform } from './Transform.js' /** * @typedef {Object} LayerMultispectralOptions * @property {string} url - URL to multispectral info.json file (required) * @property {string} layout - Layout type: 'image', 'deepzoom', 'google', 'iiif', 'zoomify', 'tarzoom', 'itarzoom' * @property {string} [defaultMode='single_band'] - Initial visualization mode ('rgb' or 'single_band') * @property {string} [server] - IIP server URL (for IIP layout) * @property {boolean} [linearRaster=true] - Whether to use linear color space for rasters (recommended for scientific accuracy) * @property {string|Object} presets - Path to presets JSON file or presets object containing CTW configurations * @extends LayerOptions */ /** * LayerMultispectral - Advanced multispectral imagery visualization layer * * This layer provides specialized handling of multispectral image data with configurable * visualization modes and interactive spectral analysis capabilities through Color Twist * Weights (CTW). It supports scientific visualization workflows for remote sensing, art analysis, * medical imaging, and other multispectral applications. * * Features: * - Multiple visualization modes (RGB, single band) * - UBO-optimized Color Twist Weights implementation for real-time spectral transformations * - Preset system for common visualization configurations (false color, etc.) * - Support for multiple image layouts and tiling schemes * - Compatible with both single images and tile-based formats (DeepZoom, etc.) * * Technical implementation: * - Uses WebGL2 features for efficient processing * - Implements shader-based visualization pipeline * - Supports multiple image layouts and tiling schemes * * @extends Layer * * @example * // Create multispectral layer with deepzoom layout * const msLayer = new OpenLIME.Layer({ * type: 'multispectral', * url: 'path/to/info.json', * layout: 'deepzoom', * defaultMode: 'rgb', * presets: 'path/to/presets.json' * }); * * // Add to viewer * viewer.addLayer('ms', msLayer); * * // Apply a preset CTW * msLayer.applyPreset('falseColor'); */ class LayerMultispectral extends Layer { /** * Creates a new LayerMultispectral instance * @param {LayerMultispectralOptions} options - Configuration options * @throws {Error} If rasters options is not empty (rasters are created automatically) * @throws {Error} If url to info.json is not provided * @throws {Error} If presets option is not provided */ constructor(options) { super(options); if (Object.keys(this.rasters).length != 0) throw new Error("Rasters options should be empty!"); if (!this.url) throw new Error("Url option is required"); if (!this.presets) { throw new Error("Presets option is required"); } this.loadPresets(); // Set default options this.linearRaster = true; this.defaultMode = this.defaultMode || 'single_band'; // Create shader this.shaders['multispectral'] = new ShaderMultispectral(); this.setShader('multispectral'); // Set current CTW arrays this._currentCTW = { red: null, green: null, blue: null }; // Load configuration this.info = null; this.loadInfo(this.url); } /** * Constructs URL for image resources based on layout type * * Handles different image layout conventions including deepzoom, google maps tiles, * zoomify, and specialized formats like tarzoom. * * @param {string} url - Base URL * @param {string} filename - Base filename without extension * @returns {string} Complete URL for the resource * @private */ imageUrl(url, filename) { let path = this.url.substring(0, this.url.lastIndexOf('/') + 1); switch (this.layout.type) { case 'image': return path + filename + '.jpg'; case 'google': return path + filename; case 'deepzoom': // Special handling for multispectral deepzoom return path + filename + '.dzi'; case 'tarzoom': return path + filename + '.tzi'; case 'itarzoom': return path + filename + '.tzi'; case 'zoomify': return path + filename + '/ImageProperties.xml'; case 'iip': return url; case 'iiif': throw new Error("Unimplemented"); default: throw new Error("Unknown layout: " + this.layout.type); } } /** * Loads and processes multispectral configuration * * Fetches the info.json file containing wavelength, basename, and other * configuration parameters, then sets up the rasters and shader accordingly. * * @param {string} url - URL to info.json * @private * @async */ async loadInfo(url) { try { let infoUrl = url; // Need to handle embedded info.json when using IIP and TIFF image stacks if (this.layout.type == "iip") infoUrl = (this.server ? this.server + '?FIF=' : '') + url + "&obj=description"; this.info = await Util.loadJSON(infoUrl); console.log("Multispectral info loaded:", this.info); // Check if basename is present if (!this.info.basename) { this.status = "Error: 'basename' is required in the multispectral configuration file"; console.error(this.status); return; } // Update layout image format and pixelSize if provided in info.json if (this.info.format) this.layout.suffix = this.info.format; if (this.info.pixelSizeInMM) this.pixelSize = this.info.pixelSizeInMM; // Initialize shader with info this.shader.init(this.info); // Set texture size if available if (this.info.width && this.info.height) { this.width = this.info.width; this.height = this.info.height; } // Get basename from info const baseName = this.info.basename; console.log("Using basename:", baseName); // Create rasters and URLs array for each image const urls = []; // Handle special case for itarzoom (all planes in one file) if (this.layout.type === 'itarzoom') { // Create a single raster for all planes let raster = new Raster({ format: 'vec3', isLinear: this.linearRaster }); this.rasters.push(raster); // Add a single URL for all planes urls.push(this.imageUrl(url, baseName)); } else { // Standard case: one file per image for (let p = 0; p < this.shader.nimg; p++) { // Create raster with linear color space let raster = new Raster({ format: 'vec3', isLinear: this.linearRaster }); this.rasters.push(raster); // Format index with leading zeros (e.g., 00, 01, 02) const indexStr = p.toString().padStart(2, '0'); // Generate URL for this image const imgUrl = this.imageUrl(url, `${baseName}_${indexStr}`); urls.push(imgUrl); console.log(`Plane ${p} URL: ${imgUrl}`); } } // Set URLs for layout if (urls.length > 0) { this.layout.setUrls(urls); } // Set up the shader this.setMode(this.defaultMode); this.initDefault(); } catch (e) { console.error("Error loading multispectral info:", e); this.status = e; } } /** * Loads preset definitions for Color Twist Weights * * Can load presets from a URL or use directly provided preset object. * Presets define predefined CTW configurations for common visualization needs. * * @private * @async */ async loadPresets() { if (typeof this.presets === 'string' && this.presets.trim() !== '') { this.presets = await Util.loadJSON(this.presets); } if (typeof this.presets !== 'object') { throw new Error("presets not well formed"); } } /** * Gets info * * @returns {Object|null} Object with info on multispectral dataset or null if not found */ info() { return this.info; } /** * Initializes default CTW based on default mode * * Creates initial CTW arrays with zeros and applies default * visualization settings. * * @private */ initDefault() { // Create default CTW arrays const nplanes = this.shader.nplanes; if (!nplanes) return; // Not yet initialized let redCTW = new Float32Array(nplanes).fill(0); let greenCTW = new Float32Array(nplanes).fill(0); let blueCTW = new Float32Array(nplanes).fill(0); // Update current CTW and shader this._currentCTW.red = redCTW; this._currentCTW.green = greenCTW; this._currentCTW.blue = blueCTW; if (this.defaultMode === 'single-band') { this.setSingleBand(0, 0); } } /** * Sets the visualization mode * * Changes how multispectral data is visualized: * - 'rgb': Uses CTW coefficients to create RGB visualization * - 'single_band': Shows a single spectral band * * @param {string} mode - Mode name ('rgb', 'single_band') */ setMode(mode) { if (this.shader) { this.shader.setMode(mode); this.emit('update'); } } /** * Sets single band visualization * * Displays a single spectral band on a specific output channel. * * @param {number} bandIndex - Index of band to visualize * @param {number} [channel=0] - Output channel (0=all/gray, 1=R, 2=G, 3=B) */ setSingleBand(bandIndex, channel = 0) { if (this.shader) { this.shader.setSingleBand(bandIndex, channel); this.emit('update'); } } /** * Sets Color Twist Weights coefficients manually * * CTW coefficients define how spectral bands are combined to create * RGB visualization. Each array contains weights for each spectral band. * * @param {Float32Array} redCTW - Red channel coefficients * @param {Float32Array} greenCTW - Green channel coefficients * @param {Float32Array} blueCTW - Blue channel coefficients * @throws {Error} If arrays have incorrect length */ setCTW(redCTW, greenCTW, blueCTW) { if (!this.shader || !this.gl) return; // Validate array lengths const nplanes = this.shader.nplanes; if (redCTW.length !== nplanes || greenCTW.length !== nplanes || blueCTW.length !== nplanes) { throw new Error(`CTW arrays must be of length ${nplanes}`); } // Update current CTW this._currentCTW.red = redCTW; this._currentCTW.green = greenCTW; this._currentCTW.blue = blueCTW; // Update shader CTW this.shader.setupCTW(this.gl, redCTW, greenCTW, blueCTW); // Set to rgb mode this.setMode('rgb'); } /** * Gets a preset CTW configuration by name * * Retrieves the preset's red, green, and blue CTW arrays from * the presets collection. * * @param {string} presetName - Name of the preset * @returns {Object|null} Object with red, green, blue arrays or null if not found */ getPreset(presetName) { if (presetName in this.presets) { const { red, green, blue } = this.presets[presetName]; return { red, green, blue }; } else { console.warn(`Preset "${presetName}" not found.`); return null; } } /** * Applies a preset CTW from the presets library * * Loads and applies a predefined set of CTW coefficients for * specialized visualization (e.g., false color, vegetation analysis). * * @param {string} presetName - Name of the preset * @throws {Error} If preset doesn't exist */ applyPreset(presetName) { if (!this.shader) return; // Get preset from the preset manager const preset = this.getPreset(presetName); if (!preset) { throw new Error(`Preset '${presetName}' not found`); } // Apply the preset this.setCTW(preset.red, preset.green, preset.blue); } /** * Gets the wavelength array for spectral bands * * Returns the wavelength values (in nm) for each spectral band. * * @returns {number[]} Array of wavelengths */ getWavelengths() { return this.shader ? this.shader.wavelength : []; } /** * Gets the number of spectral bands * * Returns the count of spectral planes in the multispectral dataset. * * @returns {number} Number of bands */ getBandCount() { return this.shader ? this.shader.nplanes : 0; } /** * Gets available presets * * Returns the names of all available preset CTW configurations. * * @returns {string[]} Array of preset names */ getAvailablePresets() { return this.presets ? Object.keys(this.presets) : []; } /** * Prepares WebGL resources including UBO for CTW * * Sets up WebGL context and ensures CTW arrays are uploaded to GPU. * * @override * @private */ prepareWebGL() { // Call parent implementation super.prepareWebGL(); // Setup CTW if needed if (this._currentCTW.red && this.shader && this.gl) { this.shader.setupCTW( this.gl, this._currentCTW.red, this._currentCTW.green, this._currentCTW.blue ); } } /** * Gets spectrum data for a specific pixel * * For tiled formats, this method finds the appropriate tiles * and reads the spectral values. * * @param {number} x - X coordinate in image space * @param {number} y - Y coordinate in image space * @returns {number[]} Array of spectral values (0-100) */ getSpectrum(x, y) { // For tiled formats, we need a special approach if (this.isTiledFormat()) { return this.getTiledSpectrum(x, y); } // For standard formats, use the original approach const pixelData = this.getPixelValues(x, y); const spectrum = []; if (!pixelData || pixelData.length === 0) { return new Array(this.shader.nplanes).fill(0); } for (let i = 0; i < this.info.nplanes; i++) { const idx = Math.floor(i / 3); if (idx < pixelData.length) { const px = pixelData[idx]; const pxIdx = i % 3; if (px && pxIdx < 3) { spectrum.push(px[pxIdx] / 255.0 * 100); } else { spectrum.push(0); } } else { spectrum.push(0); } } return spectrum; } /** * Checks if current layout is a tiled format * @private * @returns {boolean} True if using a tiled format */ isTiledFormat() { const tiledFormats = ['deepzoom', 'deepzoom1px', 'google', 'zoomify', 'iiif', 'tarzoom']; return tiledFormats.includes(this.layout.type); } /** * Gets spectrum data for a specific pixel * * Uses the improved getPixelValues method from the base Layer class * to obtain spectral values across all bands. * * @param {number} x - X coordinate in image space * @param {number} y - Y coordinate in image space * @returns {number[]} Array of spectral values (0-100) */ getSpectrum(x, y) { // Get pixel data from base Layer class method const pixelData = this.getPixelValues(x, y); // Create spectrum array for all planes const spectrum = new Array(this.shader.nplanes).fill(0); // If no data was returned, return empty spectrum if (!pixelData || pixelData.length === 0) { return spectrum; } // Convert pixel data to spectrum values for (let i = 0; i < this.shader.nplanes; i++) { const idx = Math.floor(i / 3); // Which texture contains this band const pxIdx = i % 3; // Which channel (R, G, B) in the texture if (idx < pixelData.length) { const px = pixelData[idx]; if (px && pxIdx < 3) { // Convert to percentage (0-100) spectrum[i] = px[pxIdx] / 255.0 * 100; } } } return spectrum; } } /** * Register this layer type with the Layer factory * @type {Function} * @private */ Layer.prototype.types['multispectral'] = (options) => { return new LayerMultispectral(options); } export { LayerMultispectral }