Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
LTakhyunKim committed Nov 1, 2024
2 parents 33be243 + 8256e7c commit d4ed84c
Show file tree
Hide file tree
Showing 19 changed files with 2,376 additions and 1,817 deletions.
6 changes: 4 additions & 2 deletions apps/insight-viewer-dev-e2e/src/e2e/interaction.custom.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,14 @@ describe(

it('click after panning', { scrollBehavior: false }, () => {
cy.get('.primary-drag-pan').click() // for dragging
cy.get('.primary-click').click()

cy.get('.cornerstone-canvas-wrapper').dragCanvas({
x: 140,
y: 30,
button: 0,
})
cy.get('.primary-click').click()

cy.get('.cornerstone-canvas-wrapper').mouseclick({
x: CLIENT_X,
y: CLIENT_Y,
Expand Down Expand Up @@ -196,13 +197,14 @@ describe(

it('click after panning', { scrollBehavior: false }, () => {
cy.get('.primary-drag-pan').click() // for dragging
cy.get('.primary-click').click()

cy.get('.cornerstone-canvas-wrapper').dragCanvas({
x: -140,
y: -60,
button: 0,
})
cy.get('.primary-click').click()

cy.get('.cornerstone-canvas-wrapper').mouseclick({
x: CLIENT_X,
y: CLIENT_Y,
Expand Down
2 changes: 1 addition & 1 deletion apps/insight-viewer-dev/components/Wrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function ViewerWrapper({
className,
}: WithChildren<{ flexible?: boolean; className?: string }>): JSX.Element {
return (
<Box bg="black" w="100%" d="flex" justifyContent="center">
<Box style={{ display: 'flex', justifyContent: 'center' }} bg="black" w="100%">
<Box w={500} h={500} className={className ?? ''}>
{children}
</Box>
Expand Down
13 changes: 0 additions & 13 deletions apps/insight-viewer-dev/containers/Basic/DicomImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,6 @@ export default function DicomImageViewer(): JSX.Element {
const { ImageSelect, selected } = useImageSelect()
const { loadingState, image } = useImage({
wadouri: selected,
loaderOptions: {
webWorkerManagerOptions: {
webWorkerTaskPaths: [
`${window.location.origin}/workers/610.bundle.min.worker.js`,
`${window.location.origin}/workers/888.bundle.min.worker.js`,
],
taskConfiguration: {
decodeTask: {
initializeCodecsOnStartup: false,
},
},
},
},
})

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useRef } from 'react'
import { Box, Stack, Switch, Text } from '@chakra-ui/react'
import { Resizable } from 're-resizable'
import InsightViewer, { useImage, useInteraction, HeatmapViewer } from '@lunit/insight-viewer'
import InsightViewer, { useImage, useInteraction, HeatmapViewer, CXR4HeatmapViewer } from '@lunit/insight-viewer'
import { useViewport } from '@lunit/insight-viewer/viewport'
import { IMAGES } from '@insight-viewer-library/fixtures'
import OverlayLayer from '../../../components/OverlayLayer'
Expand Down Expand Up @@ -71,25 +71,49 @@ function HeatmapContainer(): JSX.Element {
</Box>
</Stack>
</Box>
<Box data-cy-loaded={loadingState}>
<Resizable
style={style}
defaultSize={{
width: 500,
height: 500,
}}
>
<InsightViewer
viewerRef={viewerRef}
image={image}
viewport={viewport}
onViewportChange={setViewport}
interaction={interaction}
<Box style={{ display: 'flex', gap: 12, width: '100%', height: '100%' }}>
<Box data-cy-loaded={loadingState} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<p style={{ fontSize: 24, fontWeight: 'bold' }}>Heatmap</p>
<Resizable
style={style}
defaultSize={{
width: 500,
height: 500,
}}
>
{loadingState === 'success' && <HeatmapViewer posMap={posMap} threshold={0.15} />}
<OverlayLayer viewport={viewport} />
</InsightViewer>
</Resizable>
<InsightViewer
viewerRef={viewerRef}
image={image}
viewport={viewport}
onViewportChange={setViewport}
interaction={interaction}
>
{loadingState === 'success' && <HeatmapViewer posMap={posMap} threshold={0.15} />}
<OverlayLayer viewport={viewport} />
</InsightViewer>
</Resizable>
</Box>
<Box data-cy-loaded={loadingState} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<p style={{ fontSize: 24, fontWeight: 'bold' }}>CXR4 Heatmap</p>
<Resizable
style={style}
defaultSize={{
width: 500,
height: 500,
}}
>
<InsightViewer
viewerRef={viewerRef}
image={image}
viewport={viewport}
onViewportChange={setViewport}
interaction={interaction}
>
{loadingState === 'success' && <CXR4HeatmapViewer posMap={posMap} threshold={0.15} />}
<OverlayLayer viewport={viewport} />
</InsightViewer>
</Resizable>
</Box>
</Box>
</>
)
Expand Down
1 change: 0 additions & 1 deletion apps/insight-viewer-dev/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@emotion/react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
Expand Down
4 changes: 2 additions & 2 deletions libs/insight-viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"polylabel": "^1.1.0"
},
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CSSProperties } from 'react'

export const style: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
filter: 'blur(3.5px)',
} as const
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { OverlayContext } from '../../contexts'

export interface HeatmapViewerProps {
/** Heatmap data format resulting from AI */
posMap: number[][]

/** Threshold value (CXR = 0.15, MMG = 0.1) */
threshold: number
}

export interface HeatmapDrawProps extends Pick<OverlayContext, 'setToPixelCoordinateSystem' | 'enabledElement'> {
baseCanvas: HTMLCanvasElement | null
heatmapData: ImageData | undefined
}
12 changes: 12 additions & 0 deletions libs/insight-viewer/src/Viewer/CXR4HeatmapViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React, { ReactElement } from 'react'

import useHeatmapDrawing from './useHeatmapDrawing'
import { style } from './HeatmapViewer.styles'

import type { HeatmapViewerProps } from './HeatmapViewer.types'

export function CXR4HeatmapViewer(props: HeatmapViewerProps): ReactElement<HTMLCanvasElement> {
const [canvasRef] = useHeatmapDrawing(props)

return <canvas data-cy-name="heatmap-canvas" ref={canvasRef} style={style} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable no-param-reassign */
import { useEffect, useMemo, useRef, RefObject } from 'react'

import { HeatmapViewerProps } from './HeatmapViewer.types'
import { useOverlayContext } from '../../contexts'

import drawHeatmap from '../../utils/CXR4HeatmapViewer/drawHeatmap'
import getHeatmapImageData from '../../utils/CXR4HeatmapViewer/getHeatmapImageData'

function useHeatmapDrawing({ posMap, threshold }: HeatmapViewerProps): [RefObject<HTMLCanvasElement>] {
const canvasRef = useRef<HTMLCanvasElement>(null)
const baseCanvas = canvasRef?.current
const { enabledElement, setToPixelCoordinateSystem } = useOverlayContext()
const { heatmapData, heatmapCanvas } = useMemo(
() =>
getHeatmapImageData({
posMap,
threshold,
canvas: baseCanvas,
}),
[posMap, threshold, baseCanvas]
)

useEffect(() => {
drawHeatmap({
baseCanvas,
heatmapData,
heatmapCanvas,
enabledElement,
setToPixelCoordinateSystem,
})
}, [baseCanvas, heatmapCanvas, heatmapData, setToPixelCoordinateSystem, enabledElement])

return [canvasRef]
}

export default useHeatmapDrawing
7 changes: 6 additions & 1 deletion libs/insight-viewer/src/components/ViewerWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ const Forwarded = forwardRef<
}, [width, height, resizeRef, imageEnabled, onViewportChange])

return (
<div ref={resizeRef} style={style} className="cornerstone-canvas-wrapper" data-cy="cornerstone-canvas-wrapper">
/**
* react 18 적용 및 react-resize-detector 버전을 올리면서
* 기존 resizeRef 를 할당하는 방식은 무한 렌더링을 유발하여 ref 를 할당
* TODO: resize 관련 이슈 대응 시, 추가 작업 필요
*/
<div ref={ref} style={style} className="cornerstone-canvas-wrapper" data-cy="cornerstone-canvas-wrapper">
{Progress && <LoadingProgress Progress={Progress} />}
<canvas className="cornerstone-canvas" />
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import { useResizeDetector } from 'react-resize-detector'
import { resize } from '../../utils/cornerstoneHelper'
import { Element } from '../../types'

export default function useResize(ref: React.ForwardedRef<HTMLDivElement>): {
resizeRef: React.RefObject<HTMLDivElement>
width: number | undefined
height: number | undefined
} {
export default function useResize(ref: React.ForwardedRef<HTMLDivElement>) {
const targetRef = <React.MutableRefObject<Element>>ref
const element = targetRef?.current

Expand Down
1 change: 1 addition & 0 deletions libs/insight-viewer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './const'
export { InsightViewer as default } from './Viewer'
export { CXR4HeatmapViewer } from './Viewer/CXR4HeatmapViewer'
export { HeatmapViewer } from './Viewer/HeatmapViewer'
export { useMultipleImages } from './hooks/useMultipleImages'
export { useInteraction } from './hooks/useInteraction'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
interface HeatmapClearProps {
canvas: HTMLCanvasElement
context: CanvasRenderingContext2D
}

export default function clearHeatmap({ canvas, context }: HeatmapClearProps): void {
context.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight)
}
55 changes: 55 additions & 0 deletions libs/insight-viewer/src/utils/CXR4HeatmapViewer/drawHeatmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable no-param-reassign */

import { OverlayContext } from '../../contexts'
import clearHeatmap from './clearHeatmap'

interface DrawHeatmapProps extends Pick<OverlayContext, 'setToPixelCoordinateSystem' | 'enabledElement'> {
baseCanvas: HTMLCanvasElement | null
heatmapData: ImageData | null
heatmapCanvas: HTMLCanvasElement | null
}

function drawHeatmap({
baseCanvas,
heatmapData,
heatmapCanvas,
enabledElement,
setToPixelCoordinateSystem,
}: DrawHeatmapProps): void {
if (!heatmapData || !baseCanvas || !heatmapCanvas || !enabledElement) return

const baseCanvasContext = baseCanvas.getContext('2d')
const heatmapCanvasContext = heatmapCanvas.getContext('2d')

if (!baseCanvasContext || !heatmapCanvasContext) return

clearHeatmap({ canvas: baseCanvas, context: baseCanvasContext })

const { offsetWidth, offsetHeight } = baseCanvas

baseCanvas.width = offsetWidth
baseCanvas.height = offsetHeight
heatmapCanvas.width = heatmapData.width
heatmapCanvas.height = heatmapData.height

baseCanvasContext.save()
heatmapCanvasContext.putImageData(heatmapData, 0, 0)

setToPixelCoordinateSystem(baseCanvasContext)

baseCanvasContext.drawImage(
heatmapCanvas,
0,
0,
heatmapCanvas.width,
heatmapCanvas.height,
0,
0,
enabledElement.image?.width ?? 0,
enabledElement.image?.height ?? 0
)

baseCanvasContext.restore()
}

export default drawHeatmap
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { getRGBArray } from './getRGBArray'

interface GetHeatmapImageDataProps {
canvas: HTMLCanvasElement | null
posMap: number[][]
threshold: number
}

interface GetHeatmapImageDataReturn {
heatmapData: ImageData | null
heatmapCanvas: HTMLCanvasElement | null
}

const MAX_ALPHA = 255
const HEATMAP_OPACITY = 0.35

export default function getHeatmapImageData({
canvas,
posMap,
threshold,
}: GetHeatmapImageDataProps): GetHeatmapImageDataReturn {
if (!canvas) {
return { heatmapData: null, heatmapCanvas: null }
}

const heatmapWidth = posMap[0].length ?? 0
const heatmapHeight = posMap.length ?? 0

canvas.width = heatmapWidth
canvas.height = heatmapHeight

const heatmapCanvas = document.createElement('canvas')
const heatmapImageData = canvas.getContext('2d')?.createImageData(heatmapWidth, heatmapHeight)
const pixels = heatmapImageData?.data

if (!heatmapImageData || !pixels) {
return { heatmapData: null, heatmapCanvas: null }
}

// convert prob_map into a heatmap on the canvas
for (let i = 0; i < heatmapHeight; i += 1) {
for (let j = 0; j < heatmapWidth; j += 1) {
const offset = (i * heatmapWidth + j) * 4
const posProb = posMap[i][j]

const thPosProb = (posProb - threshold) / (1 - threshold)
if (posProb < threshold) {
pixels[offset + 3] = 0
} else {
const pixVal = getRGBArray(thPosProb)

pixels[offset] = pixVal[0]
pixels[offset + 1] = pixVal[1]
pixels[offset + 2] = pixVal[2]
pixels[offset + 3] = Math.round(MAX_ALPHA * HEATMAP_OPACITY)
}
}
}

return { heatmapData: heatmapImageData, heatmapCanvas }
}
Loading

0 comments on commit d4ed84c

Please sign in to comment.