From 89ced8e6b6121f7fad95e8ad61360a118726808f Mon Sep 17 00:00:00 2001 From: "adrian.sebuliba" Date: Tue, 8 Oct 2024 14:29:05 +0300 Subject: [PATCH 1/4] Fix issue with wrong file extension check --- package-lock.json | 29 ++++ package.json | 1 + src/components/VolumeViewer.js | 246 ++++++++++++++++++--------------- 3 files changed, 164 insertions(+), 112 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07c1b0a..686752c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "antd": "^5.20.0", + "axios": "^1.7.7", "dat.gui": "^0.7.9", "react": "^18.3.1", "react-color": "^2.19.3", @@ -5770,6 +5771,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -15078,6 +15102,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index 6ad23ef..8c3d917 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "antd": "^5.20.0", + "axios": "^1.7.7", "dat.gui": "^0.7.9", "react": "^18.3.1", "react-color": "^2.19.3", diff --git a/src/components/VolumeViewer.js b/src/components/VolumeViewer.js index 2a1d068..27f3aff 100644 --- a/src/components/VolumeViewer.js +++ b/src/components/VolumeViewer.js @@ -12,15 +12,13 @@ import { Lut } from "@aics/volume-viewer"; import * as THREE from 'three'; -import { TEST_DATA, loaderContext, PREFETCH_DISTANCE, MAX_PREFETCH_CHUNKS, myState } from "./appConfig"; +import { loaderContext, PREFETCH_DISTANCE, MAX_PREFETCH_CHUNKS, myState } from "./appConfig"; import { useConstructor } from './useConstructor'; -import { Slider, Switch, InputNumber, Row, Col, Collapse, Layout, Button, Select, Input, Tooltip } from 'antd'; - -const { Sider, Content } = Layout; -const { Panel } = Collapse; -const { Option } = Select; -const { Vector3 } = THREE; +import { Slider, Switch, InputNumber, Row, Col, Collapse, Layout, Button, Select, Input, Tooltip, Spin } from 'antd'; +import axios from 'axios'; +import { API_URL } from '../config'; // Importing API_URL from your config +// Utility function to concatenate arrays const concatenateArrays = (arrays) => { const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0); const result = new Uint8Array(totalLength); @@ -30,70 +28,23 @@ const concatenateArrays = (arrays) => { offset += arr.length; } return result; -}; - -const createTestVolume = () => { - const sizeX = 64; - const sizeY = 64; - const sizeZ = 64; - const imgData = { - name: "AICS-10_5_5", - sizeX, - sizeY, - sizeZ, - sizeC: 3, - physicalPixelSize: [1, 1, 1], - spatialUnit: "", - channelNames: ["DRAQ5", "EGFP", "SEG_Memb"], - }; +} - const channelVolumes = [ - VolumeMaker.createSphere(sizeX, sizeY, sizeZ, 24), - VolumeMaker.createTorus(sizeX, sizeY, sizeZ, 24, 8), - VolumeMaker.createCone(sizeX, sizeY, sizeZ, 24, 24), - ]; - const alldata = concatenateArrays(channelVolumes); - return { - metadata: imgData, - data: { - dtype: "uint8", - shape: [channelVolumes.length, sizeZ, sizeY, sizeX], - buffer: new DataView(alldata.buffer), - }, - }; -}; - -const createLoader = async (data, loadContext) => { - console.log("Creating loader context..."); - await loadContext.onOpen(); - console.log("Loader context opened."); - - const options = {}; - let path = data.url; - if (data.type === VolumeFileFormat.JSON) { - path = []; - const times = data.times || 0; - for (let t = 0; t <= times; t++) { - path.push(data.url.replace("%%", t.toString())); - } - } else if (data.type === VolumeFileFormat.DATA) { - const volumeInfo = createTestVolume(); - options.fileType = VolumeFileFormat.DATA; - options.rawArrayOptions = { data: volumeInfo.data, metadata: volumeInfo.metadata }; - } - const loader = await loadContext.createLoader(path, { - ...options, - fetchOptions: { maxPrefetchDistance: PREFETCH_DISTANCE, maxPrefetchChunks: MAX_PREFETCH_CHUNKS }, - }); - console.log("Loader created."); - return loader; -}; +const { Sider, Content } = Layout; +const { Panel } = Collapse; +const { Option } = Select; +const { Vector3 } = THREE; -function App() { +const VolumeViewer = () => { const viewerRef = useRef(null); const view3D = useConstructor(() => new View3d({ parentElement: viewerRef.current })); const loadContext = useConstructor(() => loaderContext); + const [loader, setLoader] = useState(null); + const [fileData, setFileData] = useState({}); + const [selectedBodyPart, setSelectedBodyPart] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + const [currentVolume, setCurrentVolume] = useState(null); const [density, setDensity] = useState(myState.density); const [exposure, setExposure] = useState(myState.exposure); const [lights, setLights] = useState([ @@ -102,7 +53,6 @@ function App() { ]); const [isPT, setIsPT] = useState(myState.isPT); const [channels, setChannels] = useState([]); - const [currentVolume, setCurrentVolume] = useState(null); const [cameraMode, setCameraMode] = useState('3D'); const [isTurntable, setIsTurntable] = useState(false); const [showAxis, setShowAxis] = useState(false); @@ -126,6 +76,7 @@ function App() { const [currentFrame, setCurrentFrame] = useState(0); const [totalFrames, setTotalFrames] = useState(0); const [timerId, setTimerId] = useState(null); + const [isLoading, setIsLoading] = useState(false); const densitySliderToView3D = (density) => density / 50.0; @@ -162,49 +113,101 @@ function App() { await loader.loadVolumeData(volume); }; - const loadTestData = async (testdata) => { - const loader = await createLoader(testdata, loadContext); - setLoader(loader); - const loadSpec = new LoadSpec(); - myState.totalFrames = testdata.times; - setTotalFrames(testdata.times); - await loadVolume(loadSpec, loader); + const loadVolumeFromServer = async (url) => { + setIsLoading(true); + try { + const loadSpec = new LoadSpec(); + const fileExtension = url.split('.').pop(); + console.log(fileExtension) + const volumeFileType = (fileExtension === 'tiff' || fileExtension === 'tif' || fileExtension === 'ome.tiff' || fileExtension === 'ome.tif') ? VolumeFileFormat.TIFF : VolumeFileFormat.ZARR; + console.log(volumeFileType) + const loader = await loadContext.createLoader(url, { + fileType: volumeFileType, + fetchOptions: { maxPrefetchDistance: PREFETCH_DISTANCE, maxPrefetchChunks: MAX_PREFETCH_CHUNKS }, + }); + + setLoader(loader); + await loadVolume(loadSpec, loader); + } catch (error) { + console.error('Error loading volume:', error); + } finally { + setIsLoading(false); + } + }; + + + const createTestVolume = () => { + const sizeX = 64; + const sizeY = 64; + const sizeZ = 64; + const imgData = { + name: "AICS-10_5_5", + sizeX, + sizeY, + sizeZ, + sizeC: 3, + physicalPixelSize: [1, 1, 1], + spatialUnit: "", + channelNames: ["DRAQ5", "EGFP", "SEG_Memb"], + }; + + const channelVolumes = [ + VolumeMaker.createSphere(sizeX, sizeY, sizeZ, 24), + VolumeMaker.createTorus(sizeX, sizeY, sizeZ, 24, 8), + VolumeMaker.createCone(sizeX, sizeY, sizeZ, 24, 24), + ]; + + const alldata = concatenateArrays(channelVolumes); + return { + metadata: imgData, + data: { + dtype: "uint8", + shape: [channelVolumes.length, sizeZ, sizeY, sizeX], + buffer: new DataView(alldata.buffer), + }, + }; + }; + + const fetchFiles = async () => { + try { + const response = await axios.get(`${API_URL}/files`); + setFileData(response.data); + } catch (error) { + console.error('Error fetching files:', error); + } }; + const handleFileSelect = async (bodyPart, file) => { + setSelectedBodyPart(bodyPart); + setSelectedFile(file); + await loadVolumeFromServer(`${API_URL}/${bodyPart}/${file}`); + }; + + useEffect(() => { + fetchFiles(); + }, []); + useEffect(() => { if (viewerRef.current) { - (async () => { - try { - await loadTestData(TEST_DATA['timeSeries']); - - const container = viewerRef.current; - container.appendChild(view3D.getDOMElement()); - - const handleResize = () => { - view3D.resize(); - }; - - window.addEventListener("resize", handleResize); - - // Force a resize event after a slight delay - setTimeout(() => { - handleResize(); - }, 100); - - return () => { - window.removeEventListener("resize", handleResize); - if (view3D.getDOMElement().parentNode) { - view3D.getDOMElement().parentNode.removeChild(view3D.getDOMElement()); - } - view3D.removeAllVolumes(); - }; - } catch (error) { - console.error("Error during initialization:", error); + const container = viewerRef.current; + container.appendChild(view3D.getDOMElement()); + + const handleResize = () => view3D.resize(); + window.addEventListener("resize", handleResize); + + view3D.resize(); + + return () => { + window.removeEventListener("resize", handleResize); + if (view3D.getDOMElement().parentNode) { + view3D.getDOMElement().parentNode.removeChild(view3D.getDOMElement()); } - })(); + view3D.removeAllVolumes(); + }; } }, [viewerRef, view3D]); + // Various useEffect hooks for updating the volume based on user controls useEffect(() => { if (currentVolume) { view3D.updateDensity(currentVolume, densitySliderToView3D(density)); @@ -217,17 +220,17 @@ function App() { view3D.updateExposure(exposure); view3D.redraw(); } - }, [exposure]); + }, [currentVolume, exposure, view3D]); useEffect(() => { view3D.setVolumeRenderMode(isPT ? RENDERMODE_PATHTRACE : RENDERMODE_RAYMARCH); view3D.redraw(); - }, [isPT]); + }, [isPT, view3D]); useEffect(() => { view3D.updateLights(lights); view3D.redraw(); - }, [lights]); + }, [lights, view3D]); useEffect(() => { if (currentVolume) { @@ -243,25 +246,25 @@ function App() { useEffect(() => { view3D.setAutoRotate(isTurntable); - }, [isTurntable]); + }, [isTurntable, view3D]); useEffect(() => { view3D.setShowAxis(showAxis); - }, [showAxis]); + }, [showAxis, view3D]); useEffect(() => { if (currentVolume) { view3D.setShowBoundingBox(currentVolume, showBoundingBox); } - }, [showBoundingBox]); + }, [currentVolume, showBoundingBox, view3D]); useEffect(() => { view3D.setShowScaleBar(showScaleBar); - }, [showScaleBar]); + }, [showScaleBar, view3D]); useEffect(() => { view3D.setBackgroundColor(backgroundColor); - }, [backgroundColor]); + }, [backgroundColor, view3D]); useEffect(() => { if (currentVolume) { @@ -788,12 +791,31 @@ function App() { +
+ + {Object.keys(fileData).map((bodyPart) => ( + + {fileData[bodyPart].map((file) => ( +
handleFileSelect(bodyPart, file)} + > + {file} +
+ ))} +
+ ))} +
+
- -
+ + +
+
); } -export default App; +export default VolumeViewer; \ No newline at end of file From a523606fad1d5ab859c0867eb20bf439c65fcaa6 Mon Sep 17 00:00:00 2001 From: "adrian.sebuliba" Date: Tue, 8 Oct 2024 14:36:19 +0300 Subject: [PATCH 2/4] Add configuration url --- src/config.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/config.js diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..4a0c2f3 --- /dev/null +++ b/src/config.js @@ -0,0 +1,2 @@ +// API URL +export const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api'; \ No newline at end of file From dd6444f3d9a3579b32e44ac3735b4d085fae3bec Mon Sep 17 00:00:00 2001 From: "adrian.sebuliba" Date: Tue, 15 Oct 2024 14:11:21 +0300 Subject: [PATCH 3/4] Add histogram controls for opacity, window, and level adjustments in volume rendering --- src/components/VolumeViewer.js | 91 +++++++++++++++++----------------- src/components/appConfig.js | 4 +- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/components/VolumeViewer.js b/src/components/VolumeViewer.js index 27f3aff..6eac595 100644 --- a/src/components/VolumeViewer.js +++ b/src/components/VolumeViewer.js @@ -94,9 +94,18 @@ const VolumeViewer = () => { }; const onVolumeCreated = (volume) => { + + volume.channelColorsDefault = volume.imageInfo.channelNames.map(() => DEFAULT_CHANNEL_COLOR); + setCurrentVolume(volume); view3D.removeAllVolumes(); view3D.addVolume(volume); + + + // Log the channel colors to verify the change + console.log("Channel Default Colors:", volume.channelColors); + + setInitialRenderMode(); showChannelUI(volume); view3D.updateActiveChannels(volume); @@ -304,11 +313,13 @@ const VolumeViewer = () => { view3D.setMaxProjectMode(currentVolume, false); }; - const showChannelUI = (volume) => { + const DEFAULT_CHANNEL_COLOR = [128, 128, 128]; // Medium gray + + const showChannelUI = (volume) => { const channelGui = volume.imageInfo.channelNames.map((name, index) => ({ name, enabled: index < 3, - colorD: volume.channelColorsDefault[index], + colorD: volume.channelColorsDefault[index] || DEFAULT_CHANNEL_COLOR, colorS: [0, 0, 0], colorE: [0, 0, 0], glossiness: 0, @@ -318,13 +329,18 @@ const VolumeViewer = () => { isosurface: false })); setChannels(channelGui); + + // Log channel colors for verification + channelGui.forEach((channel, index) => { + console.log(`Channel ${index} (${channel.name}) color:`, channel.colorD); + }); }; const updateChannel = (index, key, value) => { const updatedChannels = [...channels]; updatedChannels[index][key] = value; setChannels(updatedChannels); - + if (currentVolume) { if (key === 'enabled') { view3D.setVolumeChannelEnabled(currentVolume, index, value); @@ -352,6 +368,28 @@ const VolumeViewer = () => { } }; + + // Histogram-based LUT adjustments + const updateChannelLut = (index, type) => { + if (currentVolume) { + let lut; + if (type === 'autoIJ') { + const [hmin, hmax] = currentVolume.getHistogram(index).findAutoIJBins(); + lut = new Lut().createFromMinMax(hmin, hmax); + } else if (type === 'auto0') { + const [b, e] = currentVolume.getHistogram(index).findAutoMinMax(); + lut = new Lut().createFromMinMax(b, e); + } else if (type === 'bestFit') { + const [hmin, hmax] = currentVolume.getHistogram(index).findBestFitBins(); + lut = new Lut().createFromMinMax(hmin, hmax); + } + + currentVolume.setLut(index, lut); + view3D.updateLuts(currentVolume); + view3D.redraw(); + } + }; + const setCameraModeHandler = (mode) => { setCameraMode(mode); }; @@ -637,50 +675,11 @@ const VolumeViewer = () => { - Specular Color + Histogram Adjustments - c.toString(16).padStart(2, '0')).join('')}`} onChange={(e) => updateChannel(index, 'colorS', e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> - - - - Emissive Color - - c.toString(16).padStart(2, '0')).join('')}`} onChange={(e) => updateChannel(index, 'colorE', e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> - - - - Glossiness - - updateChannel(index, 'glossiness', value)} - /> - - - - Window - - updateChannel(index, 'window', value)} - /> - - - - Level - - updateChannel(index, 'level', value)} - /> + + + diff --git a/src/components/appConfig.js b/src/components/appConfig.js index 3e5b36c..f668120 100644 --- a/src/components/appConfig.js +++ b/src/components/appConfig.js @@ -177,4 +177,6 @@ export const getDefaultChannelState = () => ({ opacity: 1.0, color: [255, 255, 255], controlPoints: [], -}); \ No newline at end of file +}); + +export const DEFAULT_CHANNEL_COLOR = [128, 128, 128]; // Medium gray From 6fef2c493797e61bbb6f750f328c0077fc86a7cb Mon Sep 17 00:00:00 2001 From: Adrian Sebuliba Date: Wed, 16 Oct 2024 15:01:06 +0200 Subject: [PATCH 4/4] Ensure opacity can be controlled --- src/components/VolumeViewer.js | 502 +++++++++++++++++++++++++-------- src/components/constants.js | 128 +++++++++ src/config.js | 2 +- 3 files changed, 520 insertions(+), 112 deletions(-) create mode 100644 src/components/constants.js diff --git a/src/components/VolumeViewer.js b/src/components/VolumeViewer.js index 6eac595..5a34f53 100644 --- a/src/components/VolumeViewer.js +++ b/src/components/VolumeViewer.js @@ -16,7 +16,7 @@ import { loaderContext, PREFETCH_DISTANCE, MAX_PREFETCH_CHUNKS, myState } from " import { useConstructor } from './useConstructor'; import { Slider, Switch, InputNumber, Row, Col, Collapse, Layout, Button, Select, Input, Tooltip, Spin } from 'antd'; import axios from 'axios'; -import { API_URL } from '../config'; // Importing API_URL from your config +import { API_URL } from '../config'; // Importing API_URL from your config // Utility function to concatenate arrays const concatenateArrays = (arrays) => { @@ -77,6 +77,27 @@ const VolumeViewer = () => { const [totalFrames, setTotalFrames] = useState(0); const [timerId, setTimerId] = useState(null); const [isLoading, setIsLoading] = useState(false); + + const [maskAlpha, setMaskAlpha] = useState(myState.maskAlpha); + const [primaryRay, setPrimaryRay] = useState(myState.primaryRay); + const [secondaryRay, setSecondaryRay] = useState(myState.secondaryRay); + const [fov, setFov] = useState(myState.fov); + const [focalDistance, setFocalDistance] = useState(myState.focal_distance); + const [aperture, setAperture] = useState(myState.aperture); + const [samplingRate, setSamplingRate] = useState(myState.samplingRate); + + + + const [skyTopIntensity, setSkyTopIntensity] = useState(myState.skyTopIntensity); + const [skyMidIntensity, setSkyMidIntensity] = useState(myState.skyMidIntensity); + const [skyBotIntensity, setSkyBotIntensity] = useState(myState.skyBotIntensity); + const [skyTopColor, setSkyTopColor] = useState(myState.skyTopColor); + const [skyMidColor, setSkyMidColor] = useState(myState.skyMidColor); + const [skyBotColor, setSkyBotColor] = useState(myState.skyBotColor); + const [lightColor, setLightColor] = useState(myState.lightColor); + const [lightIntensity, setLightIntensity] = useState(myState.lightIntensity); + const [lightTheta, setLightTheta] = useState(myState.lightTheta); + const [lightPhi, setLightPhi] = useState(myState.lightPhi); const densitySliderToView3D = (density) => density / 50.0; @@ -95,7 +116,7 @@ const VolumeViewer = () => { const onVolumeCreated = (volume) => { - volume.channelColorsDefault = volume.imageInfo.channelNames.map(() => DEFAULT_CHANNEL_COLOR); + // volume.channelColorsDefault = volume.imageInfo.channelNames.map(() => DEFAULT_CHANNEL_COLOR); setCurrentVolume(volume); view3D.removeAllVolumes(); @@ -112,13 +133,21 @@ const VolumeViewer = () => { view3D.updateLuts(volume); view3D.updateLights(lights); view3D.updateDensity(volume, densitySliderToView3D(density)); + view3D.updateMaskAlpha(volume, maskAlpha); + view3D.setRayStepSizes(volume, primaryRay, secondaryRay); view3D.updateExposure(exposure); + view3D.updateCamera(fov, focalDistance, aperture); + // view3D.updatePixelSamplingRate(samplingRate); view3D.redraw(); }; const loadVolume = async (loadSpec, loader) => { const volume = await loader.createVolume(loadSpec, onChannelDataArrived); onVolumeCreated(volume); + + console.log(volume.imageInfo, volume.imageInfo.times) + // Set total frames based on the volume's metadata (assuming 'times' represents the number of frames) + setTotalFrames(volume.imageInfo.times || 1); await loader.loadVolumeData(volume); }; @@ -127,9 +156,7 @@ const VolumeViewer = () => { try { const loadSpec = new LoadSpec(); const fileExtension = url.split('.').pop(); - console.log(fileExtension) const volumeFileType = (fileExtension === 'tiff' || fileExtension === 'tif' || fileExtension === 'ome.tiff' || fileExtension === 'ome.tif') ? VolumeFileFormat.TIFF : VolumeFileFormat.ZARR; - console.log(volumeFileType) const loader = await loadContext.createLoader(url, { fileType: volumeFileType, fetchOptions: { maxPrefetchDistance: PREFETCH_DISTANCE, maxPrefetchChunks: MAX_PREFETCH_CHUNKS }, @@ -144,7 +171,6 @@ const VolumeViewer = () => { } }; - const createTestVolume = () => { const sizeX = 64; const sizeY = 64; @@ -216,7 +242,6 @@ const VolumeViewer = () => { } }, [viewerRef, view3D]); - // Various useEffect hooks for updating the volume based on user controls useEffect(() => { if (currentVolume) { view3D.updateDensity(currentVolume, densitySliderToView3D(density)); @@ -308,6 +333,70 @@ const VolumeViewer = () => { } }, [clipRegion]); + useEffect(() => { + if (currentVolume) { + view3D.updateCamera(fov, focalDistance, aperture); + view3D.redraw(); + } + }, [fov, focalDistance, aperture]); + + + useEffect(() => { + if (currentVolume) { + view3D.setRayStepSizes(currentVolume, primaryRay, secondaryRay); + view3D.redraw(); + } + }, [primaryRay, secondaryRay]); + + useEffect(() => { + if (currentVolume) { + view3D.updateMaskAlpha(currentVolume, maskAlpha); + view3D.redraw(); + } + }, [maskAlpha]) + + useEffect(() => { + if (view3D && lights[0]) { + const skyLight = lights[0]; + skyLight.mColorTop = new Vector3( + (skyTopColor[0] / 255.0) * skyTopIntensity, + (skyTopColor[1] / 255.0) * skyTopIntensity, + (skyTopColor[2] / 255.0) * skyTopIntensity + ); + skyLight.mColorMiddle = new Vector3( + (skyMidColor[0] / 255.0) * skyMidIntensity, + (skyMidColor[1] / 255.0) * skyMidIntensity, + (skyMidColor[2] / 255.0) * skyMidIntensity + ); + skyLight.mColorBottom = new Vector3( + (skyBotColor[0] / 255.0) * skyBotIntensity, + (skyBotColor[1] / 255.0) * skyBotIntensity, + (skyBotColor[2] / 255.0) * skyBotIntensity + ); + view3D.updateLights(lights); + // view3D.redraw(); + console.log([skyTopColor, skyTopIntensity, skyMidColor, skyMidIntensity, skyBotColor, skyBotIntensity]); + } + + }, [skyTopColor, skyTopIntensity, skyMidColor, skyMidIntensity, skyBotColor, skyBotIntensity]); + + // useEffect for area light + useEffect(() => { + if (view3D && lights[1]) { + const areaLight = lights[1]; + areaLight.mColor = new Vector3( + (lightColor[0] / 255.0) * lightIntensity, + (lightColor[1] / 255.0) * lightIntensity, + (lightColor[2] / 255.0) * lightIntensity + ); + areaLight.mTheta = (lightTheta * Math.PI) / 180.0; + areaLight.mPhi = (lightPhi * Math.PI) / 180.0; + view3D.updateLights(lights); + // view3D.redraw(); + } + console.log([lightColor, lightIntensity, lightTheta, lightPhi]); + }, [lightColor, lightIntensity, lightTheta, lightPhi]); + const setInitialRenderMode = () => { view3D.setVolumeRenderMode(isPT ? RENDERMODE_PATHTRACE : RENDERMODE_RAYMARCH); view3D.setMaxProjectMode(currentVolume, false); @@ -346,6 +435,11 @@ const VolumeViewer = () => { view3D.setVolumeChannelEnabled(currentVolume, index, value); } else if (key === 'isosurface') { view3D.setVolumeChannelOptions(currentVolume, index, { isosurfaceEnabled: value }); + if (value) { + view3D.createIsosurface(currentVolume, index, updatedChannels[index].isovalue, 1.0); + } else { + view3D.clearIsosurface(currentVolume, index); + } } else if (['colorD', 'colorS', 'colorE', 'glossiness'].includes(key)) { view3D.updateChannelMaterial( currentVolume, @@ -368,6 +462,12 @@ const VolumeViewer = () => { } }; + const updateIsovalue = (index, isovalue) => { + if (currentVolume) { + view3D.updateIsosurface(currentVolume, index, isovalue); + view3D.redraw(); + } + }; // Histogram-based LUT adjustments const updateChannelLut = (index, type) => { @@ -382,6 +482,10 @@ const VolumeViewer = () => { } else if (type === 'bestFit') { const [hmin, hmax] = currentVolume.getHistogram(index).findBestFitBins(); lut = new Lut().createFromMinMax(hmin, hmax); + } else if (type === 'pct50_98') { + const hmin = currentVolume.getHistogram(index).findBinOfPercentile(0.5); + const hmax = currentVolume.getHistogram(index).findBinOfPercentile(0.983); + lut = new Lut().createFromMinMax(hmin, hmax); } currentVolume.setLut(index, lut); @@ -476,6 +580,17 @@ const VolumeViewer = () => { const goToZSlice = (slice) => { if (currentVolume && view3D.setZSlice(currentVolume, slice)) { // Z slice updated successfully + const zSlider = document.getElementById("zSlider"); + const zInput = document.getElementById("zValue"); + + if (zInput) { + zInput.value = slice; + } + if (zSlider) { + zSlider.value = slice; + } + } else { + console.log('Failed to update Z slice'); } }; @@ -502,52 +617,165 @@ const VolumeViewer = () => { setIsPlaying(false); }; + const rgbToHex = (r, g, b) => { + const toHex = (component) => { + const hex = Math.round(component).toString(16); + return hex.length === 1 ? '0' + hex : hex; // Ensures two digits + }; + + // Ensure r, g, b are valid numbers and fall back to 0 if undefined or invalid + r = isNaN(r) ? 0 : r; + g = isNaN(g) ? 0 : g; + b = isNaN(b) ? 0 : b; + + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + }; + + const updatePixelSamplingRate = (rate) => { + setSamplingRate(rate); + view3D.updatePixelSamplingRate(rate); + view3D.redraw(); + }; + + + const updateSkyLight = (position, intensity, color) => { + if (position === 'top') { + setSkyTopIntensity(intensity); + setSkyTopColor(color); + } else if (position === 'mid') { + setSkyMidIntensity(intensity); + setSkyMidColor(color); + } else if (position === 'bot') { + setSkyBotIntensity(intensity); + setSkyBotColor(color); + } + updateLights(); + }; + + const updateAreaLight = (intensity, color, theta, phi) => { + setLightIntensity(intensity); + setLightColor(color); + setLightTheta(theta); + setLightPhi(phi); + updateLights(); + }; + + const updateLights = () => { + const updatedLights = [...lights]; + // Update sky light + updatedLights[0].mColorTop = new Vector3( + (skyTopColor[0] / 255.0) * skyTopIntensity, + (skyTopColor[1] / 255.0) * skyTopIntensity, + (skyTopColor[2] / 255.0) * skyTopIntensity + ); + updatedLights[0].mColorMiddle = new Vector3( + (skyMidColor[0] / 255.0) * skyMidIntensity, + (skyMidColor[1] / 255.0) * skyMidIntensity, + (skyMidColor[2] / 255.0) * skyMidIntensity + ); + updatedLights[0].mColorBottom = new Vector3( + (skyBotColor[0] / 255.0) * skyBotIntensity, + (skyBotColor[1] / 255.0) * skyBotIntensity, + (skyBotColor[2] / 255.0) * skyBotIntensity + ); + + // Update area light + updatedLights[1].mColor = new Vector3( + (lightColor[0] / 255.0) * lightIntensity, + (lightColor[1] / 255.0) * lightIntensity, + (lightColor[2] / 255.0) * lightIntensity + ); + updatedLights[1].mTheta = (lightTheta * Math.PI) / 180.0; + updatedLights[1].mPhi = (lightPhi * Math.PI) / 180.0; + + setLights(updatedLights); + view3D.updateLights(updatedLights); + view3D.redraw(); + }; + return ( + {/* Render Mode */} Path Trace - { - setIsPT(checked); - }} /> + setIsPT(checked)} /> + + {/* Density */} - { - setDensity(value); - }} - /> + + + + {/* Mask Alpha */} + + + + {/* Ray Step Sizes */} + + + + + + + + {/* Exposure */} - { - setExposure(value); - }} - /> + + + + {/* Camera Settings */} + + + FOV + + + + + + Focal Distance + + + + + + Aperture + + + + + + {/* Sampling Rate */} + + + Pixel Sampling Rate + + + + + + + {/* Lights */} {lights.map((light, index) => (
Intensity - { const updatedLights = [...lights]; updatedLights[index].mColor.setScalar(value / 255); @@ -559,10 +787,7 @@ const VolumeViewer = () => { Theta - { const updatedLights = [...lights]; updatedLights[index].mTheta = value * (Math.PI / 180); @@ -574,10 +799,7 @@ const VolumeViewer = () => { Phi - { const updatedLights = [...lights]; updatedLights[index].mPhi = value * (Math.PI / 180); @@ -589,6 +811,86 @@ const VolumeViewer = () => {
))}
+ + + + + + Top Intensity + + updateSkyLight('top', value, skyTopColor)} + /> + + + + Top Color + + updateSkyLight('top', skyTopIntensity, e.target.value.match(/[A-Za-z0-9]{2}/g).map(v => parseInt(v, 16)))} + /> + + + {/* Repeat for Mid and Bottom with appropriate state variables */} + + + + Intensity + + updateAreaLight(value, lightColor, lightTheta, lightPhi)} + /> + + + + Color + + updateAreaLight(lightIntensity, e.target.value.match(/[A-Za-z0-9]{2}/g).map(v => parseInt(v, 16)), lightTheta, lightPhi)} + /> + + + + Theta (deg) + + updateAreaLight(lightIntensity, lightColor, value, lightPhi)} + /> + + + + Phi (deg) + + updateAreaLight(lightIntensity, lightColor, lightTheta, value)} + /> + + + + + + + {/* Camera Mode */} + + {/* Controls */} @@ -605,54 +909,48 @@ const VolumeViewer = () => { Background Color - c.toString(16).padStart(2, '0')).join('')}`} onChange={(e) => updateBackgroundColor(e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> + c.toString(16).padStart(2, '0')).join('')}`} + onChange={(e) => updateBackgroundColor(e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> Bounding Box Color - c.toString(16).padStart(2, '0')).join('')}`} onChange={(e) => updateBoundingBoxColor(e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> + c.toString(16).padStart(2, '0')).join('')}`} + onChange={(e) => updateBoundingBoxColor(e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> + + {/* Gamma */} Min - updateGamma([value, gamma[1], gamma[2]])} - /> + updateGamma([value, gamma[1], gamma[2]])} /> Mid - updateGamma([gamma[0], value, gamma[2]])} - /> + updateGamma([gamma[0], value, gamma[2]])} /> Max - updateGamma([gamma[0], gamma[1], value])} - /> + updateGamma([gamma[0], gamma[1], value])} /> + + {/* Channels */} {channels.map((channel, index) => (
@@ -668,10 +966,18 @@ const VolumeViewer = () => { updateChannel(index, 'isosurface', checked)} /> + + Isovalue + + updateIsovalue(index, value)} /> + + Diffuse Color - c.toString(16).padStart(2, '0')).join('')}`} onChange={(e) => updateChannel(index, 'colorD', e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> + updateChannel(index, 'colorD', e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> @@ -680,85 +986,54 @@ const VolumeViewer = () => { +
))}
+ + {/* Clip Region */} X Min - updateClipRegion('xmin', value)} - /> + updateClipRegion('xmin', value)} /> X Max - updateClipRegion('xmax', value)} - /> + updateClipRegion('xmax', value)} /> Y Min - updateClipRegion('ymin', value)} - /> + updateClipRegion('ymin', value)} /> Y Max - updateClipRegion('ymax', value)} - /> + updateClipRegion('ymax', value)} /> Z Min - updateClipRegion('zmin', value)} - /> + updateClipRegion('zmin', value)} /> Z Max - updateClipRegion('zmax', value)} - /> + updateClipRegion('zmax', value)} /> + + {/* Playback */} @@ -769,18 +1044,14 @@ const VolumeViewer = () => { Frame - + Z Slice { /> + + +
+
{Object.keys(fileData).map((bodyPart) => ( {fileData[bodyPart].map((file) => ( -
handleFileSelect(bodyPart, file)} > {file} @@ -808,6 +1087,7 @@ const VolumeViewer = () => {
+
diff --git a/src/components/constants.js b/src/components/constants.js new file mode 100644 index 0000000..27e35d6 --- /dev/null +++ b/src/components/constants.js @@ -0,0 +1,128 @@ +// constants.js + +// URL search parameter keys +export const CELL_ID_QUERY = "cellId"; +export const FOV_ID_QUERY = "fovId"; +export const CELL_LINE_QUERY = "cellLine"; +export const IMAGE_NAME_QUERY = "name"; + +// View modes +export const YZ_MODE = "YZ"; +export const XZ_MODE = "XZ"; +export const XY_MODE = "XY"; +export const THREE_D_MODE = "3D"; + +// App state values +export const SEGMENTED_CELL = "segmented"; +export const FULL_FIELD_IMAGE = "full field"; + +// Channel setting keys +export const ISO_SURFACE_ENABLED = "isoSurfaceEnabled"; +export const VOLUME_ENABLED = "volumeEnabled"; + +// App State Keys +export const ALPHA_MASK_SLIDER_LEVEL = "alphaMaskSliderLevel"; +export const BRIGHTNESS_SLIDER_LEVEL = "brightnessSliderLevel"; +export const DENSITY_SLIDER_LEVEL = "densitySliderLevel"; +export const LEVELS_SLIDER = "levelsSlider"; +export const MODE = "mode"; +export const AUTO_ROTATE = "autorotate"; +export const MAX_PROJECT = "maxProject"; +export const VOLUMETRIC_RENDER = "volume"; +export const PATH_TRACE = "pathTrace"; +export const LUT_CONTROL_POINTS = "controlPoints"; +export const COLORIZE_ALPHA = "colorizeAlpha"; +export const COLORIZE_ENABLED = "colorizeEnabled"; + +// Volume viewer keys +export const ISO_VALUE = "isovalue"; +export const OPACITY = "opacity"; +export const COLOR = "color"; +export const SAVE_ISO_SURFACE = "saveIsoSurface"; + +// LUT percentiles for remapping intensity values +export const LUT_MIN_PERCENTILE = 0.5; +export const LUT_MAX_PERCENTILE = 0.983; + +// Opacity control for isosurfaces +export const ISOSURFACE_OPACITY_SLIDER_MAX = 255.0; + +// Default values for sliders +export const ALPHA_MASK_SLIDER_3D_DEFAULT = [50]; +export const ALPHA_MASK_SLIDER_2D_DEFAULT = [0]; +export const BRIGHTNESS_SLIDER_LEVEL_DEFAULT = [70]; +export const DENSITY_SLIDER_LEVEL_DEFAULT = [50]; +export const LEVELS_SLIDER_DEFAULT = [35.0, 140.0, 255.0]; + +// Channel group keys +export const OTHER_CHANNEL_KEY = "Other"; +export const SINGLE_GROUP_CHANNEL_KEY = "Channels"; + +// Special channel names +export const CELL_SEGMENTATION_CHANNEL_NAME = "SEG_Memb"; + +// Color presets for channels +export const PRESET_COLORS_1 = [ + [190, 68, 171, 255], + [189, 211, 75, 255], + [61, 155, 169, 255], + [128, 128, 128, 255], + [255, 255, 255, 255], + [239, 27, 45, 255], + [238, 77, 245, 255], + [96, 255, 255, 255] +]; + +export const PRESET_COLORS_2 = [ + [128, 0, 0, 255], + [0, 128, 0, 255], + [0, 0, 128, 255], + [32, 32, 32, 255], + [255, 255, 0, 255], + [255, 0, 255, 255], + [0, 255, 0, 255], + [0, 0, 255, 255] +]; + +export const PRESET_COLORS_3 = [ + [128, 0, 128, 255], + [128, 128, 128, 255], + [0, 128, 128, 255], + [128, 128, 0, 255], + [255, 255, 255, 255], + [255, 0, 0, 255], + [255, 0, 255, 255], + [0, 255, 255, 255] +]; + +// Map of preset color groups +export const PRESET_COLOR_MAP = Object.freeze([ + { + colors: PRESET_COLORS_1, + name: "Thumbnail colors", + key: 1, + }, + { + colors: PRESET_COLORS_2, + name: "RGB colors", + key: 2, + }, + { + colors: PRESET_COLORS_3, + name: "White structure", + key: 3, + } +]); + +// Application color scheme +export default { + primary1Color: '#0B9AAB', // bright blue + primary2Color: '#827AA3', // aics purple + primary3Color: '#d8e0e2', // light blue gray + accent1Color: '#B8D637', // aics lime green light + accent2Color: '#C1F448', // aics lime green bright + accent3Color: '#d8e0e2', // light blue gray + textColor: '#003057', // dark blue + disabledColor: '#D1D1D1', // dull gray + pickerHeaderColor: '#316773' // cool blue green +}; diff --git a/src/config.js b/src/config.js index 4a0c2f3..5dacc8e 100644 --- a/src/config.js +++ b/src/config.js @@ -1,2 +1,2 @@ // API URL -export const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api'; \ No newline at end of file +export const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000'; \ No newline at end of file