cornerstone 등 OPT 기능을 구현하기 위한 여러 graphics layer를 구현한다.
Install
npm install @lunit/insight-viewer2
Sample Codes
Storybook
Stories
__stories__/InsightViewer.stories.tsx
```tsx
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
import { useController, withTestController } from './decorators/withTestController';
// cornerstoneWADOImageLoader 초기화
installWADOImageLoader();
// 을 변경하면 Viewport 등 cornerstone-core 관련 속성들이 초기화 된다
const resetTime: number = Date.now();
// unload 옵션은 위에 선언된 installWADOImageLoader()와 함께 동작한다
// CornerstoneImage 객체를 unload 할때 wado image loader의 unload 동작을 하게 된다
const image: CornerstoneImage = new CornerstoneSingleImage(wadouri:samples/series/CT000010.dcm
, {unload: unloadWADOImage});
function Sample() {
// addDecorator(withTestController())의 값을 받는다
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const {
updateCornerstoneRenderData,
} = useInsightViewerSync();
return (
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan'}
adjust={control === 'adjust'}
zoom={wheel === 'zoom'}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
);
}
storiesOf('insight-viewer2', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('', () => );
<h3>__stories__/MachineHeatmapViewer.stories.tsx</h3>
```tsx
import { MachineHeatmapViewer } from '@lunit/insight-viewer2';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import { ProgressViewer } from '../components/ProgressViewer';
import { UserContourDrawer } from '../components/UserContourDrawer';
import { UserContourViewer } from '../components/UserContourViewer';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { useUserContour } from '../hooks/useUserContour';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
import { useController, withTestController } from './decorators/withTestController';
import data from './posMap.sample.json';
const {engine_result: {engine_result: {pos_map: posMap}}} = data;
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
function Sample() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
return (
<InsightViewerContainer ref={setInteractionElement} width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{/*
engineResult.posMap을 그리는 Layer
현재 Heatmap Spec (number[][])에 맞춰서 개발되었기 때문에
Spec이 다르다면 새로운 Viewer를 만들어야 한다
*/}
<MachineHeatmapViewer width={width}
height={height}
posMap={posMap}
threshold={0.1}
cornerstoneRenderData={cornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer2', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<MachineHeatmapViewer>', () => <Sample/>);
__stories__/ProgressCollector.stories.tsx
```tsx
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { CSSProperties } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import {
ProgressCollector,
ProgressViewer,
useContainerStyleOfProgressViewersInactivity,
useProgressViewersActivity,
} from '../components/ProgressViewer';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { CornerstoneSeriesImage } from '../image/CornerstoneSeriesImage';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';
installWADOImageLoader();
const resetTime: number = Date.now();
const image1: CornerstoneImage = new CornerstoneSingleImage(wadouri:samples/series/CT000010.dcm
, {unload: unloadWADOImage});
const image2: CornerstoneImage = new CornerstoneSingleImage(wadouri:samples/series/CT000020.dcm
, {unload: unloadWADOImage});
const image3: CornerstoneImage = new CornerstoneSingleImage(wadouri:samples/series/CT000030.dcm
, {unload: unloadWADOImage});
const image4: CornerstoneImage = new CornerstoneSeriesImage(series.map(p => wadouri:samples/series/${p}
), {unload: unloadWADOImage});
function Sample() {
// 는 하위의 들의 상태를 수집한다
return (
);
}
function Container() {
// 에서 수집한 정보를 얻을 수 있다
const progressActivity: boolean = useProgressViewersActivity();
// 혹은 가 동작중일때 비활성을 처리할 Style을 만들 수 있다
const containerDisabledStyle: CSSProperties = useContainerStyleOfProgressViewersInactivity({pointerEvents: 'none'});
return (
<div style={{display: 'flex'}}>
{
progressActivity
? '가 하나 이상 작동 중입니다!!!'
: '동작중인 가 없습니다!!!'
}
);
}
function Component({image}: {image: CornerstoneImage}) {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const {updateCornerstoneRenderData} = useInsightViewerSync();
return (
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan'}
adjust={control === 'adjust'}
zoom={wheel === 'zoom'}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{/* 이 가 에 수집된다 */}
);
}
storiesOf('insight-viewer2', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [300, 200, 500],
height: [400, 300, 600],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('', () => );
<h3>__stories__/SeriesImage.stories.tsx</h3>
```tsx
import { UserContourDrawer, UserContourViewer, useUserContour } from '@lunit/insight-viewer2';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import { ProgressViewer } from '../components/ProgressViewer';
import { useBulkImageScroll } from '../hooks/useBulkImageScroll';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { CornerstoneSeriesImage } from '../image/CornerstoneSeriesImage';
import { CornerstoneBulkImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
import { withSeriesImageController } from './decorators/withSeriesImageController';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';
installWADOImageLoader();
const resetTime: number = Date.now();
// CornerstoneSeriesImage는 여러장의 dcm 이미지를 받는다
const image: CornerstoneBulkImage = new CornerstoneSeriesImage(series.map(p => `wadouri:samples/series/${p}`), {unload: unloadWADOImage});
function Component() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
// CornerstoneBulkImage를 Wheel과 연결 시킨다
useBulkImageScroll({
image,
element: interactionElement,
enabled: wheel === 'scroll',
});
return (
<InsightViewerContainer ref={setInteractionElement} width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer2', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['scroll', ['none', 'zoom', 'scroll']],
flip: false,
invert: false,
}))
.addDecorator(withSeriesImageController(image))
.add('Series Image', () => <Component/>);
__stories__/UserContourViewer.stories.tsx
```tsx
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import { ProgressViewer } from '../components/ProgressViewer';
import { UserContourDrawer } from '../components/UserContourDrawer';
import { UserContourViewer } from '../components/UserContourViewer';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { useUserContour } from '../hooks/useUserContour';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(wadouri:samples/series/CT000010.dcm
, {unload: unloadWADOImage});
function Sample() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
// pan, adjust, zoom은
// pan={true}로 설정하면 내부 Element를 사용해서 MouseEvent를 처리하게 되고,
// pan={HTMLElement}로 설정하면 해당 Element를 사용해서 MouseEvent를 처리하게 된다.
// MouseEvent를 처리하는 Layer가 여러개 중첩될 때, 하위 Layer의 MouseEvent가 막히는 현상을 해결해준다.
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
// 내부의 Canvas Render를 외부의 다른 Component들에 동기화 시키기 위한 Hook
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
// Annotation (사용자 Contour)를 다루기 위한 Hook
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
return (
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
// 사용자가 그린 Annotation을 보여준다
// contours가 있는 경우에만 출력
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
}
{
// Annotation을 그리고, 지우게 해준다
// control === 'pen' 인 경우에만 출력
contours &&
cornerstoneRenderData &&
control === 'pen' &&
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
);
}
storiesOf('insight-viewer2', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('', () => );
<h3>__stories__/UserContourViewerCanvasStyle.stories.tsx</h3>
```tsx
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { InsightViewer } from '../components/InsightViewer';
import { InsightViewerContainer } from '../components/InsightViewerContainer';
import { ProgressViewer } from '../components/ProgressViewer';
import { UserContourDrawer } from '../components/UserContourDrawer';
import { UserContourViewer } from '../components/UserContourViewer';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { useUserContour } from '../hooks/useUserContour';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(`wadouri:samples/series/CT000010.dcm`, {unload: unloadWADOImage});
function Sample() {
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const [interactionElement, setInteractionElement] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
return (
<InsightViewerContainer ref={setInteractionElement} width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan' && interactionElement}
adjust={control === 'adjust' && interactionElement}
zoom={wheel === 'zoom' && interactionElement}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
// Canvas Style을 변경할 수 있다
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}
canvasStrokeLineWidth={10}
canvasStrokeStyle="blue"
canvasFillStyle="rgba(0, 0, 255, 0.3)"
canvasFontStyle="normal normal 600 40px proximanova"
canvasFocusedStrokeLineWidth={20}
canvasFocusedStrokeStyle="red"
canvasFocusedFillStyle="rgba(255, 0, 0, 0.3)"
canvasFocusedFontStyle="normal normal 600 50px proximanova"/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
// Canvas Style을 변경할 수 있다
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, 0)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}
canvasStokeLineWidght={8}
canvasStokeStyle="purple"
canvasFillStyle="rgba(255, 255, 255, 0.4)"/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer2', module)
.addDecorator(withOptTheme)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourViewer canvasStokeStyle="{}">', () => <Sample/>);
__stories__/useResizeObserver.stories.tsx
```tsx
import { InsightViewerContainer } from '@lunit/insight-viewer2';
import { withOptTheme } from '@lunit/opt-theme';
import { storiesOf } from '@storybook/react';
import React from 'react';
import useResizeObserver from 'use-resize-observer';
import { InsightViewer } from '../components/InsightViewer';
import { useInsightViewerSync } from '../hooks/useInsightViewerSync';
import { CornerstoneSingleImage } from '../image/CornerstoneSingleImage';
import { CornerstoneImage } from '../image/types';
import { installWADOImageLoader, unloadWADOImage } from '../installWADO';
installWADOImageLoader();
const resetTime: number = Date.now();
const image: CornerstoneImage = new CornerstoneSingleImage(wadouri:samples/series/CT000010.dcm
, {unload: unloadWADOImage});
function Sample() {
const {
updateCornerstoneRenderData,
} = useInsightViewerSync();
// 특정 Element의 width, height를 지속적으로 감지한다
// flex 등 layout으로 처리된
Element의 width, height를 useResizeObserver()로 받아서
// 로 넘길 수 있다
const [resizeRef, width, height] = useResizeObserver();
console.log('useResizeObserver.stories.tsx..Sample()', width, height);
return (
<div ref={resizeRef} style={{width: '50vw', height: '80vh'}}>
);
}
storiesOf('insight-viewer2', module)
.addDecorator(withOptTheme)
.add('useResizeObserver', () => );
<!-- importend -->
## Tests
<!-- import __tests__/*.{ts,tsx} --title-tag h3 -->
<!-- importend -->