cornerstone 등 OPT 기능을 구현하기 위한 여러 graphics layer를 구현한다.
Install
npm install @lunit/insight-viewer
{
"scripts": {
"app:build": "zeroconfig-webapp-scripts build app --static-file-packages @lunit/insight-viewer",
"app:start": "zeroconfig-webapp-scripts start app --static-file-packages @lunit/insight-viewer"
}
}
Package 내부에 public/cornerstoneWADOImageLoaderCodecs.min.js
파일과 public/cornerstoneWADOImageLoaderWebWorker.min.js
파일을 가지고 있고, 이를 App 빌드에 적용하기 위해서는 위와 같이 --static-file-packages
옵션을 추가해줘야 한다.
API
Basic Types
import { CornerstoneEventData, Viewport } from 'cornerstone-core';
export type CornerstoneRenderData = Required<Pick<CornerstoneEventData, 'canvasContext' | 'element' | 'enabledElement' | 'image' | 'renderTimeInMs' | 'viewport'>>;
export type Point = [number, number];
export interface Contour {
id: number;
polygon: Point[];
label?: ((contour: this) => string) | string;
dataAttrs?: {[attr: string]: string};
}
export interface ViewportTransformParams {
element: HTMLElement;
minScale: number;
maxScale: number;
currentViewport: Viewport | null;
}
export type ViewportTransform = (params: ViewportTransformParams) => Partial<Viewport> | undefined;
import { Image } from 'cornerstone-core';
import { Observable } from 'rxjs';
export interface CornerstoneImage {
readonly image: Observable<Image | null>;
readonly progress: Observable<number>;
destroy: () => void;
}
export interface CornerstoneBulkImage extends CornerstoneImage {
length: () => number;
readonly index: Observable<number>;
next: (delta?: number) => void;
prev: (delta?: number) => void;
goto: (index: number) => void;
getIndex: () => number;
}
export interface ProgressEventDetail {
url: string;
imageId: string;
loaded: number;
total: number;
percentComplete: number;
}
export function getProgressEventDetail(event: Event): ProgressEventDetail | undefined {
const detail: object | undefined = event['detail'];
if (
detail
&& typeof detail['url'] === 'string'
&& typeof detail['imageId'] === 'string'
&& typeof detail['loaded'] === 'number'
&& typeof detail['total'] === 'number'
&& typeof detail['percentComplete'] === 'number'
) {
return detail as ProgressEventDetail;
}
return undefined;
}
export interface LoadImageParams {
imageId: string;
options?: object;
}
export interface ImageLoader {
loadImage: (params: LoadImageParams) => Promise<Image>;
}
Images
new CornerstoneSingleImage(imageId: string, options: { unload: () => void })
new CornerstoneSeriesImage(imageIds: string[], options: { unload: () => void })
Components
<InsightViewer width={number}
height={number}
image={CornerstoneImage}
resetTime={number}
pan={boolean | HTMLElement | null}
adjust={boolean | HTMLElement | null}
zoom={boolean | HTMLElement | null}
invert={boolean}
flip={boolean}
updateCornerstoneRenderData={(renderData: CornerstoneRenderData) => void} />
interface InsightViewerContainerProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
<InsightViewerContainer {...divProps}
width={number}
height={number} />
<MachineHeatmapViewer width={number}
height={number}
posMap={number[][]}
threshold={number}
cornerstoneRenderData={CornerstoneRenderData | null}/>
<UserContourViewer width={number}
height={number}
contours={Contour[]}
focusedContour={Contour | null}
cornerstoneRenderData={CornerstoneRenderData | null}
canvasStrokeLineWidth?={number}
canvasStrokeStyle?={string}
canvasFillStyle?={string}
canvasFontStyle?={string}
canvasFocusedStrokeLineWidth?={number}
canvasFocusedStrokeStyle?={string}
canvasFocusedFillStyle?={string}
canvasFocusedFontStyle?={string} />
<UserContourDrawer width={number}
height={number}
contours={Contour[]}
draw={boolean | HTMLElement | null}
onFocus={(contour: Contour | null) => void}
onAdd={(polygon: Point[], event: MouseEvent) => void}
onRemove={(contour: Contour) => void}
cornerstoneRenderData={CornerstoneRenderData | null}
canvasStokeLineWidght?={number}
canvasStokeStyle?={string}
canvasFillStyle?={string}/>
<ProgressViewer width={number}
height={number}
inProgress?={boolean}
image?={CornerstoneImage | null | undefined} />
<ProgressCollector>
{children}
</ProgressCollector>
function useProgrssViewersActivity(): boolean
function useContainerStyleOfProgressViewersInactivity(inactivityStyle: CSSProperties): CSSProperties
Hooks
useBulkImageScroll({
image: CornerstoneBulkImage,
element: HTMLElement | null,
enabled?: boolean,
})
useImageProgress(image: CornerstoneImage | null | undefined): number | null
useInsightViewerSync(): {
cornerstoneRenderData: CornerstoneRenderData | null,
updateCornerstoneRenderData: (renderData: CornerstoneRenderData) => void,
}
useUserContour(): {
contours: Contour[],
focusedContour: Contour | null,
addContour: (polygon: Point[], confidenceLevel: number) => Contour | null,
focusContour: (contour: Contour | null) => void,
updateContour: (contour: Contour, patch: Partial<Contour>) => void,
removeContour: (contour: Contour) => void,
removeAllContours: () => void,
}
useViewportMirroring(...destinations: (InsightViewer | RefObject<InsightViewer>)[]): {
updateMasterRenderData: (renderData: CornerstoneRenderData) => void,
}
WADO Image Loader
installWADOImageLoader()
unloadWADOImage(imageId: string | string[] | null)
Sample Codes
Storybook
Stories
__stories__/DCMViewer.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
installWADOImageLoader,
unloadWADOImage,
} from '@lunit/insight-viewer';
import { DCMImage } from '@lunit/insight-viewer/components/DCMImage';
import { storiesOf } from '@storybook/react';
import React from 'react';
installWADOImageLoader();
const image1: CornerstoneImage = new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage});
const image2: CornerstoneImage = new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000011.dcm`, {unload: unloadWADOImage});
const image3: CornerstoneImage = new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000012.dcm`, {unload: unloadWADOImage});
const image4: CornerstoneImage = new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000013.dcm`, {unload: unloadWADOImage});
storiesOf('insight-viewer', module)
.add('<DCMImage>', () => (
<ul>
{
[image1, image2, image3, image4].map((image, i) => (
<li key={'image' + i}>
<DCMImage cornerstoneImage={image} width={120} height={150}/>
</li>
))
}
</ul>
));
__stories__/InsightViewer.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
installWADOImageLoader,
unloadWADOImage,
useInsightViewerSync,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo } from 'react';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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-viewer', module)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pan', ['none', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<InsightViewer>', () => <Sample/>);
__stories__/MachineHeatmapViewer.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
MachineHeatmapViewer,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
import data from './posMap.sample.json';
const {engine_result: {engine_result: {pos_map: posMap}}} = data;
installWADOImageLoader();
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.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
import {
CornerstoneImage,
CornerstoneSeriesImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressCollector,
ProgressViewer,
unloadWADOImage,
useContainerStyleOfProgressViewersInactivity,
useInsightViewerSync,
useProgressViewersActivity,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { CSSProperties, useMemo } from 'react';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';
installWADOImageLoader();
function Sample() {
return (
<ProgressCollector>
<Container/>
</ProgressCollector>
);
}
function Container() {
const image1: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
const image2: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000020.dcm`, {unload: unloadWADOImage}), []);
const image3: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000030.dcm`, {unload: unloadWADOImage}), []);
const image4: CornerstoneImage = useMemo(() => new CornerstoneSeriesImage(series.map(p => `wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/${p}`), {unload: unloadWADOImage}), []);
const progressActivity: boolean = useProgressViewersActivity();
const containerDisabledStyle: CSSProperties = useContainerStyleOfProgressViewersInactivity({pointerEvents: 'none'});
return (
<div style={containerDisabledStyle}>
<div style={{display: 'flex'}}>
<Component image={image1}/>
<Component image={image2}/>
<Component image={image3}/>
<Component image={image4}/>
</div>
<p>
{
progressActivity
? '<ProgressViewer>가 하나 이상 작동 중입니다!!!'
: '동작중인 <ProgressViewer>가 없습니다!!!'
}
</p>
</div>
);
}
function Component({image}: {image: CornerstoneImage}) {
const resetTime: number = useMemo(() => Date.now(), []);
const {
width,
height,
control,
wheel,
invert,
flip,
} = useController();
const {updateCornerstoneRenderData} = useInsightViewerSync();
return (
<InsightViewerContainer width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan={control === 'pan'}
adjust={control === 'adjust'}
zoom={wheel === 'zoom'}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{/* 이 <ProgressViewer>가 <ProgressCollector>에 수집된다 */}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.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('<ProgressCollector>', () => <Sample/>);
__stories__/SeriesImage.stories.tsx
import {
CornerstoneBulkImage,
CornerstoneSeriesImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useBulkImageScroll,
useImageProgress,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';
installWADOImageLoader();
function Component() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneBulkImage = useMemo(() => new CornerstoneSeriesImage(series.map(p => `wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/${p}`), {unload: unloadWADOImage}), []);
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();
useBulkImageScroll({
image,
element: interactionElement,
enabled: wheel === 'scroll',
});
const imageProgress = useImageProgress(image);
return (
<div>
<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)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
{
typeof imageProgress === 'number' &&
<button onClick={() => image.destroy()}>
Destroy Image (= Cancel Loading)
</button>
}
</div>
);
}
storiesOf('insight-viewer', module)
.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,
}))
.add('Series Image', () => <Component/>);
__stories__/UserCircleViewer.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserCircleDrawer,
UserCircleViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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({
mode: 'circle',
});
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}/>
{
// 사용자가 그린 Annotation을 보여준다
// contours가 있는 경우에만 출력
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserCircleViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
// Annotation을 그리고, 지우게 해준다
// control === 'pen' 인 경우에만 출력
contours &&
cornerstoneRenderData &&
control === 'pen' &&
<UserCircleDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pen', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserCircleViewer>', () => <Sample/>);
__stories__/UserContourViewer.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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}/>
{
// 사용자가 그린 Annotation을 보여준다
// contours가 있는 경우에만 출력
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={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)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pen', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourViewer>', () => <Sample/>);
__stories__/UserContourViewerCanvasStyle.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourCanvasDrawer,
UserContourCanvasViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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을 변경할 수 있다
<UserContourCanvasViewer 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을 변경할 수 있다
<UserContourCanvasDrawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}
canvasStokeLineWidth={8}
canvasStokeStyle="purple"
canvasFillStyle="rgba(255, 255, 255, 0.4)"/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pen', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourCanvasViewer canvasStokeStyle="{}">', () => <Sample/>);
import {
Contour,
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import styled, { css } from 'styled-components';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
function labelFunction(contour: Contour): string {
return `CONTOUR(${contour.id})`;
}
const initialContours: Omit<Contour, 'id'>[] = [
{
polygon: [[365.2266666666667, 40.959999999999994], [360.96000000000004, 43.519999999999996], [356.6933333333333, 46.93333333333334], [353.28000000000003, 50.346666666666664], [349.8666666666667, 53.760000000000005], [348.16, 58.879999999999995], [346.4533333333334, 64.85333333333334], [345.6, 70.82666666666667], [345.6, 77.65333333333334], [349.0133333333334, 85.33333333333333], [358.40000000000003, 93.01333333333334], [371.20000000000005, 98.13333333333334], [390.8266666666667, 102.39999999999999], [412.16, 103.25333333333334], [432.64000000000004, 101.54666666666667], [444.5866666666667, 98.13333333333334], [453.12, 94.72000000000001], [458.24, 91.30666666666666], [460.8, 86.18666666666668], [461.65333333333336, 82.77333333333334], [457.3866666666667, 77.65333333333334], [452.2666666666667, 70.82666666666667], [446.29333333333335, 63.146666666666675], [443.73333333333335, 58.02666666666667], [441.17333333333335, 52.906666666666666], [439.4666666666667, 49.49333333333334], [437.76000000000005, 47.78666666666666]],
label: labelFunction,
},
{
polygon: [[260.2666666666667, 180.9066666666667], [260.2666666666667, 181.76], [256, 186.0266666666667], [253.44000000000003, 188.5866666666667], [250.88000000000002, 192], [248.32000000000002, 197.97333333333336], [245.76000000000002, 204.8], [244.9066666666667, 212.48000000000002], [244.9066666666667, 224.42666666666668], [248.32000000000002, 235.51999999999998], [257.7066666666667, 246.61333333333334], [271.36, 256.85333333333335], [298.6666666666667, 265.38666666666666], [308.9066666666667, 266.24], [331.9466666666667, 264.53333333333336], [343.04, 258.56], [349.8666666666667, 253.44], [354.1333333333334, 248.32], [356.6933333333333, 242.3466666666667], [357.5466666666667, 236.37333333333333], [357.5466666666667, 228.69333333333333], [357.5466666666667, 220.16000000000003], [354.9866666666667, 211.62666666666667], [349.8666666666667, 201.38666666666666], [343.8933333333334, 193.70666666666665], [337.06666666666666, 189.44], [328.53333333333336, 186.0266666666667], [320.85333333333335, 186.0266666666667], [313.17333333333335, 186.88]],
label: labelFunction,
},
{
polygon: [[157.01333333333335, 369.49333333333334], [157.01333333333335, 369.49333333333334], [151.89333333333335, 376.32], [148.48000000000002, 382.29333333333335], [144.21333333333334, 389.97333333333336], [138.24, 405.33333333333337], [134.82666666666668, 416.4266666666667], [133.12, 431.7866666666667], [132.26666666666668, 444.5866666666667], [133.12, 454.82666666666665], [136.53333333333333, 462.50666666666666], [145.06666666666666, 470.18666666666667], [155.30666666666667, 474.4533333333333], [169.81333333333333, 477.0133333333334], [184.32000000000002, 476.16], [195.41333333333336, 472.7466666666667], [205.65333333333334, 467.62666666666667], [211.6266666666667, 463.36], [219.30666666666667, 456.53333333333336], [221.86666666666667, 451.41333333333336], [222.72000000000003, 446.29333333333335], [222.72000000000003, 439.4666666666667], [221.01333333333335, 430.08], [216.74666666666667, 418.9866666666667], [212.48000000000002, 409.6], [207.36, 401.06666666666666], [200.53333333333336, 394.24], [193.70666666666668, 389.12], [187.73333333333335, 385.70666666666665]],
label: labelFunction,
},
{
polygon: [[104.96000000000001, 89.60000000000001], [104.10666666666667, 89.60000000000001], [97.28, 91.30666666666666], [91.30666666666667, 93.01333333333334], [86.18666666666667, 94.72000000000001], [79.36, 98.13333333333334], [71.68, 103.25333333333334], [65.70666666666668, 109.22666666666667], [61.440000000000005, 113.49333333333333], [58.88, 121.17333333333333], [58.02666666666667, 129.70666666666665], [60.58666666666667, 145.06666666666666], [64, 151.04000000000002], [73.38666666666667, 162.98666666666668], [96.42666666666668, 179.2], [115.2, 186.88], [134.82666666666668, 191.1466666666667], [155.30666666666667, 191.1466666666667], [168.10666666666668, 188.5866666666667], [178.34666666666666, 186.0266666666667], [186.88000000000002, 180.9066666666667], [191.14666666666668, 175.7866666666667], [193.70666666666668, 169.81333333333333], [194.56, 163.84000000000003], [194.56, 158.72000000000003], [194.56, 151.89333333333332], [193.70666666666668, 146.77333333333337], [192, 140.8], [192, 139.09333333333336], [190.29333333333335, 133.97333333333336]],
label: labelFunction,
},
{
polygon: [[249.17333333333335, -17.06666666666667], [246.61333333333334, -17.06666666666667], [232.10666666666668, -12.800000000000004], [221.01333333333335, -9.38666666666667], [208.21333333333334, -5.1200000000000045], [198.82666666666668, 0], [192, 5.119999999999997], [187.73333333333335, 10.240000000000002], [185.17333333333335, 15.36], [183.46666666666667, 23.040000000000006], [183.46666666666667, 34.13333333333334], [183.46666666666667, 46.93333333333334], [186.02666666666667, 64.85333333333334], [190.29333333333335, 76.8], [197.97333333333336, 86.18666666666668], [207.36, 93.01333333333334], [221.86666666666667, 97.28000000000002], [238.08, 98.98666666666666], [256.85333333333335, 98.13333333333334], [271.36, 93.86666666666666], [284.16, 87.89333333333333], [296.96000000000004, 80.21333333333334], [306.3466666666667, 72.53333333333333], [310.61333333333334, 67.41333333333334], [313.17333333333335, 60.58666666666667], [314.0266666666667, 54.61333333333333], [314.0266666666667, 44.373333333333335], [312.32, 35.84], [308.9066666666667, 25.599999999999994], [306.3466666666667, 17.066666666666663], [304.64000000000004, 7.68], [303.7866666666667, 1.7066666666666634], [302.93333333333334, -2.5600000000000023], [302.08000000000004, -5.973333333333336]],
label: labelFunction,
},
{
polygon: [[320, 363.52], [318.29333333333335, 363.52], [314.0266666666667, 365.2266666666667], [308.05333333333334, 367.7866666666667], [300.37333333333333, 374.61333333333334], [291.84000000000003, 381.44], [287.5733333333333, 389.12], [285.0133333333334, 395.94666666666666], [284.16, 401.92], [284.16, 407.04], [287.5733333333333, 414.72], [295.25333333333333, 423.25333333333333], [305.49333333333334, 432.64], [318.29333333333335, 441.17333333333335], [329.3866666666667, 446.29333333333335], [344.74666666666667, 449.7066666666667], [353.28000000000003, 450.56], [360.96000000000004, 450.56], [366.93333333333334, 448], [374.61333333333334, 444.5866666666667], [380.5866666666667, 441.17333333333335], [384.85333333333335, 437.76], [389.12, 433.49333333333334], [391.68, 426.6666666666667], [392.53333333333336, 416.4266666666667], [393.3866666666667, 401.92], [393.3866666666667, 391.68], [390.8266666666667, 382.29333333333335], [388.2666666666667, 377.17333333333335], [384.85333333333335, 372.9066666666667], [379.73333333333335, 371.2], [374.61333333333334, 370.3466666666667], [367.7866666666667, 370.3466666666667], [358.40000000000003, 370.3466666666667], [354.9866666666667, 370.3466666666667]],
label: labelFunction,
},
{
polygon: [[410.4533333333334, 273.06666666666666], [407.04, 273.92], [401.92, 275.62666666666667], [397.65333333333336, 276.48], [395.09333333333336, 278.18666666666667], [392.53333333333336, 279.04], [389.97333333333336, 281.6], [388.2666666666667, 284.16], [386.56, 289.28000000000003], [384.85333333333335, 296.1066666666667], [383.1466666666667, 306.3466666666667], [381.44, 318.29333333333335], [380.5866666666667, 327.68], [381.44, 333.65333333333336], [385.7066666666667, 337.92], [392.53333333333336, 342.18666666666667], [401.92, 345.6], [414.72, 349.0133333333333], [427.52000000000004, 349.8666666666667], [438.61333333333334, 349.8666666666667], [446.29333333333335, 347.3066666666667], [453.12, 344.74666666666667], [456.53333333333336, 341.3333333333333], [459.9466666666667, 337.06666666666666], [461.65333333333336, 332.8], [463.36, 327.68], [464.21333333333337, 321.70666666666665], [465.0666666666667, 314.88], [465.92, 307.2], [465.92, 302.08], [465.92, 296.96], [465.92, 292.6933333333333], [465.0666666666667, 289.28000000000003], [463.36, 285.0133333333333], [461.65333333333336, 281.6], [458.24, 277.3333333333333], [454.8266666666667, 274.7733333333333], [450.56, 271.36], [447.1466666666667, 269.6533333333333], [444.5866666666667, 267.94666666666666], [442.88000000000005, 267.09333333333336], [440.32000000000005, 267.09333333333336]],
label: labelFunction,
},
{
polygon: [[95.57333333333334, 251.73333333333335], [89.60000000000001, 255.14666666666665], [82.77333333333334, 261.12], [75.94666666666667, 267.94666666666666], [72.53333333333333, 273.92], [69.97333333333334, 279.8933333333333], [69.12, 285.0133333333333], [69.12, 290.9866666666667], [69.97333333333334, 298.6666666666667], [75.94666666666667, 308.05333333333334], [82.77333333333334, 315.73333333333335], [93.01333333333334, 323.41333333333336], [106.66666666666667, 330.24], [120.32000000000001, 332.8], [134.82666666666668, 333.65333333333336], [157.86666666666667, 330.24], [174.08, 323.41333333333336], [183.46666666666667, 317.44], [193.70666666666668, 310.61333333333334], [201.38666666666668, 303.7866666666667], [205.65333333333334, 297.81333333333333], [207.36, 291.84], [208.21333333333334, 283.3066666666667], [205.65333333333334, 273.92], [197.97333333333336, 262.82666666666665], [188.58666666666667, 251.73333333333335], [180.05333333333334, 244.90666666666664], [169.81333333333333, 238.07999999999998], [161.28, 233.81333333333333], [154.45333333333335, 231.25333333333333], [149.33333333333334, 230.39999999999998], [145.06666666666666, 230.39999999999998]],
label: labelFunction,
},
{
polygon: [[31.573333333333334, 365.2266666666667], [30.720000000000002, 365.2266666666667], [27.30666666666667, 368.64], [24.74666666666667, 372.9066666666667], [19.62666666666667, 380.5866666666667], [15.360000000000001, 387.41333333333336], [11.946666666666667, 395.09333333333336], [10.24, 403.62666666666667], [9.386666666666667, 410.4533333333333], [9.386666666666667, 417.28000000000003], [11.093333333333334, 422.40000000000003], [17.92, 427.52000000000004], [30.720000000000002, 433.49333333333334], [45.22666666666667, 435.2], [55.46666666666667, 435.2], [65.70666666666668, 434.3466666666667], [74.24000000000001, 430.08], [81.92, 424.96000000000004], [87.04, 419.84000000000003], [90.45333333333333, 414.72], [92.16000000000001, 409.6], [93.01333333333334, 403.62666666666667], [93.01333333333334, 395.94666666666666], [90.45333333333333, 388.26666666666665], [86.18666666666667, 381.44], [81.92, 375.4666666666667], [75.09333333333333, 370.3466666666667], [63.14666666666667, 364.37333333333333], [53.760000000000005, 362.6666666666667], [48.64, 362.6666666666667], [42.66666666666667, 363.52], [40.96, 364.37333333333333], [36.693333333333335, 366.08]],
label: labelFunction,
},
];
const contourStyle = (id: number, color: string) => css`
> polygon[data-id="${id}"] {
stroke: ${color};
fill: ${color};
fill-opacity: 0.3;
}
> text[data-id="${id}"] {
fill: ${color};
opacity: 0.8;
&[data-focused] {
opacity: 1;
}
}
`;
const colors = [
'#3366cc',
'#dc3912',
'#ff9900',
'#109618',
'#990099',
'#0099c6',
'#dd4477',
'#66aa00',
'#b82e2e',
'#316395',
'#994499',
'#22aa99',
'#aaaa11',
'#6633cc',
'#e67300',
'#8b0707',
'#651067',
'#329262',
'#5574a6',
'#3b3eac',
];
const Viewer = styled(UserContourViewer)`
${colors.map((color, i) => contourStyle(i, color))}
`;
const Drawer = styled(UserContourDrawer)`
> polyline {
stroke: purple;
stroke-width: 5px;
fill: rgba(255, 255, 255, 0.4);
}
`;
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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({initialContours});
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을 변경할 수 있다
<Viewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
// Canvas Style을 변경할 수 있다
<Drawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, {label: labelFunction})}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pen', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourViewer className={colors}>', () => <Sample/>);
__stories__/UserContourViewerColors2.stories.tsx
import {
Contour,
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import styled, { css } from 'styled-components';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
function labelFunction(contour: Contour): string {
return `[${contour.id}] ${contour.dataAttrs!['data-category']}`;
}
const initialContours: Omit<Contour, 'id'>[] = [
{
polygon: [[365.2266666666667, 40.959999999999994], [360.96000000000004, 43.519999999999996], [356.6933333333333, 46.93333333333334], [353.28000000000003, 50.346666666666664], [349.8666666666667, 53.760000000000005], [348.16, 58.879999999999995], [346.4533333333334, 64.85333333333334], [345.6, 70.82666666666667], [345.6, 77.65333333333334], [349.0133333333334, 85.33333333333333], [358.40000000000003, 93.01333333333334], [371.20000000000005, 98.13333333333334], [390.8266666666667, 102.39999999999999], [412.16, 103.25333333333334], [432.64000000000004, 101.54666666666667], [444.5866666666667, 98.13333333333334], [453.12, 94.72000000000001], [458.24, 91.30666666666666], [460.8, 86.18666666666668], [461.65333333333336, 82.77333333333334], [457.3866666666667, 77.65333333333334], [452.2666666666667, 70.82666666666667], [446.29333333333335, 63.146666666666675], [443.73333333333335, 58.02666666666667], [441.17333333333335, 52.906666666666666], [439.4666666666667, 49.49333333333334], [437.76000000000005, 47.78666666666666]],
label: labelFunction,
dataAttrs: {
'data-category': 'normal',
},
},
{
polygon: [[260.2666666666667, 180.9066666666667], [260.2666666666667, 181.76], [256, 186.0266666666667], [253.44000000000003, 188.5866666666667], [250.88000000000002, 192], [248.32000000000002, 197.97333333333336], [245.76000000000002, 204.8], [244.9066666666667, 212.48000000000002], [244.9066666666667, 224.42666666666668], [248.32000000000002, 235.51999999999998], [257.7066666666667, 246.61333333333334], [271.36, 256.85333333333335], [298.6666666666667, 265.38666666666666], [308.9066666666667, 266.24], [331.9466666666667, 264.53333333333336], [343.04, 258.56], [349.8666666666667, 253.44], [354.1333333333334, 248.32], [356.6933333333333, 242.3466666666667], [357.5466666666667, 236.37333333333333], [357.5466666666667, 228.69333333333333], [357.5466666666667, 220.16000000000003], [354.9866666666667, 211.62666666666667], [349.8666666666667, 201.38666666666666], [343.8933333333334, 193.70666666666665], [337.06666666666666, 189.44], [328.53333333333336, 186.0266666666667], [320.85333333333335, 186.0266666666667], [313.17333333333335, 186.88]],
label: labelFunction,
dataAttrs: {
'data-category': 'abnormal',
},
},
{
polygon: [[157.01333333333335, 369.49333333333334], [157.01333333333335, 369.49333333333334], [151.89333333333335, 376.32], [148.48000000000002, 382.29333333333335], [144.21333333333334, 389.97333333333336], [138.24, 405.33333333333337], [134.82666666666668, 416.4266666666667], [133.12, 431.7866666666667], [132.26666666666668, 444.5866666666667], [133.12, 454.82666666666665], [136.53333333333333, 462.50666666666666], [145.06666666666666, 470.18666666666667], [155.30666666666667, 474.4533333333333], [169.81333333333333, 477.0133333333334], [184.32000000000002, 476.16], [195.41333333333336, 472.7466666666667], [205.65333333333334, 467.62666666666667], [211.6266666666667, 463.36], [219.30666666666667, 456.53333333333336], [221.86666666666667, 451.41333333333336], [222.72000000000003, 446.29333333333335], [222.72000000000003, 439.4666666666667], [221.01333333333335, 430.08], [216.74666666666667, 418.9866666666667], [212.48000000000002, 409.6], [207.36, 401.06666666666666], [200.53333333333336, 394.24], [193.70666666666668, 389.12], [187.73333333333335, 385.70666666666665]],
label: labelFunction,
dataAttrs: {
'data-category': 'normal',
},
},
{
polygon: [[104.96000000000001, 89.60000000000001], [104.10666666666667, 89.60000000000001], [97.28, 91.30666666666666], [91.30666666666667, 93.01333333333334], [86.18666666666667, 94.72000000000001], [79.36, 98.13333333333334], [71.68, 103.25333333333334], [65.70666666666668, 109.22666666666667], [61.440000000000005, 113.49333333333333], [58.88, 121.17333333333333], [58.02666666666667, 129.70666666666665], [60.58666666666667, 145.06666666666666], [64, 151.04000000000002], [73.38666666666667, 162.98666666666668], [96.42666666666668, 179.2], [115.2, 186.88], [134.82666666666668, 191.1466666666667], [155.30666666666667, 191.1466666666667], [168.10666666666668, 188.5866666666667], [178.34666666666666, 186.0266666666667], [186.88000000000002, 180.9066666666667], [191.14666666666668, 175.7866666666667], [193.70666666666668, 169.81333333333333], [194.56, 163.84000000000003], [194.56, 158.72000000000003], [194.56, 151.89333333333332], [193.70666666666668, 146.77333333333337], [192, 140.8], [192, 139.09333333333336], [190.29333333333335, 133.97333333333336]],
label: labelFunction,
dataAttrs: {
'data-category': 'normal',
},
},
{
polygon: [[249.17333333333335, -17.06666666666667], [246.61333333333334, -17.06666666666667], [232.10666666666668, -12.800000000000004], [221.01333333333335, -9.38666666666667], [208.21333333333334, -5.1200000000000045], [198.82666666666668, 0], [192, 5.119999999999997], [187.73333333333335, 10.240000000000002], [185.17333333333335, 15.36], [183.46666666666667, 23.040000000000006], [183.46666666666667, 34.13333333333334], [183.46666666666667, 46.93333333333334], [186.02666666666667, 64.85333333333334], [190.29333333333335, 76.8], [197.97333333333336, 86.18666666666668], [207.36, 93.01333333333334], [221.86666666666667, 97.28000000000002], [238.08, 98.98666666666666], [256.85333333333335, 98.13333333333334], [271.36, 93.86666666666666], [284.16, 87.89333333333333], [296.96000000000004, 80.21333333333334], [306.3466666666667, 72.53333333333333], [310.61333333333334, 67.41333333333334], [313.17333333333335, 60.58666666666667], [314.0266666666667, 54.61333333333333], [314.0266666666667, 44.373333333333335], [312.32, 35.84], [308.9066666666667, 25.599999999999994], [306.3466666666667, 17.066666666666663], [304.64000000000004, 7.68], [303.7866666666667, 1.7066666666666634], [302.93333333333334, -2.5600000000000023], [302.08000000000004, -5.973333333333336]],
label: labelFunction,
dataAttrs: {
'data-category': 'abnormal',
},
},
{
polygon: [[320, 363.52], [318.29333333333335, 363.52], [314.0266666666667, 365.2266666666667], [308.05333333333334, 367.7866666666667], [300.37333333333333, 374.61333333333334], [291.84000000000003, 381.44], [287.5733333333333, 389.12], [285.0133333333334, 395.94666666666666], [284.16, 401.92], [284.16, 407.04], [287.5733333333333, 414.72], [295.25333333333333, 423.25333333333333], [305.49333333333334, 432.64], [318.29333333333335, 441.17333333333335], [329.3866666666667, 446.29333333333335], [344.74666666666667, 449.7066666666667], [353.28000000000003, 450.56], [360.96000000000004, 450.56], [366.93333333333334, 448], [374.61333333333334, 444.5866666666667], [380.5866666666667, 441.17333333333335], [384.85333333333335, 437.76], [389.12, 433.49333333333334], [391.68, 426.6666666666667], [392.53333333333336, 416.4266666666667], [393.3866666666667, 401.92], [393.3866666666667, 391.68], [390.8266666666667, 382.29333333333335], [388.2666666666667, 377.17333333333335], [384.85333333333335, 372.9066666666667], [379.73333333333335, 371.2], [374.61333333333334, 370.3466666666667], [367.7866666666667, 370.3466666666667], [358.40000000000003, 370.3466666666667], [354.9866666666667, 370.3466666666667]],
label: labelFunction,
dataAttrs: {
'data-category': 'normal',
},
},
{
polygon: [[410.4533333333334, 273.06666666666666], [407.04, 273.92], [401.92, 275.62666666666667], [397.65333333333336, 276.48], [395.09333333333336, 278.18666666666667], [392.53333333333336, 279.04], [389.97333333333336, 281.6], [388.2666666666667, 284.16], [386.56, 289.28000000000003], [384.85333333333335, 296.1066666666667], [383.1466666666667, 306.3466666666667], [381.44, 318.29333333333335], [380.5866666666667, 327.68], [381.44, 333.65333333333336], [385.7066666666667, 337.92], [392.53333333333336, 342.18666666666667], [401.92, 345.6], [414.72, 349.0133333333333], [427.52000000000004, 349.8666666666667], [438.61333333333334, 349.8666666666667], [446.29333333333335, 347.3066666666667], [453.12, 344.74666666666667], [456.53333333333336, 341.3333333333333], [459.9466666666667, 337.06666666666666], [461.65333333333336, 332.8], [463.36, 327.68], [464.21333333333337, 321.70666666666665], [465.0666666666667, 314.88], [465.92, 307.2], [465.92, 302.08], [465.92, 296.96], [465.92, 292.6933333333333], [465.0666666666667, 289.28000000000003], [463.36, 285.0133333333333], [461.65333333333336, 281.6], [458.24, 277.3333333333333], [454.8266666666667, 274.7733333333333], [450.56, 271.36], [447.1466666666667, 269.6533333333333], [444.5866666666667, 267.94666666666666], [442.88000000000005, 267.09333333333336], [440.32000000000005, 267.09333333333336]],
label: labelFunction,
dataAttrs: {
'data-category': 'abnormal',
},
},
{
polygon: [[95.57333333333334, 251.73333333333335], [89.60000000000001, 255.14666666666665], [82.77333333333334, 261.12], [75.94666666666667, 267.94666666666666], [72.53333333333333, 273.92], [69.97333333333334, 279.8933333333333], [69.12, 285.0133333333333], [69.12, 290.9866666666667], [69.97333333333334, 298.6666666666667], [75.94666666666667, 308.05333333333334], [82.77333333333334, 315.73333333333335], [93.01333333333334, 323.41333333333336], [106.66666666666667, 330.24], [120.32000000000001, 332.8], [134.82666666666668, 333.65333333333336], [157.86666666666667, 330.24], [174.08, 323.41333333333336], [183.46666666666667, 317.44], [193.70666666666668, 310.61333333333334], [201.38666666666668, 303.7866666666667], [205.65333333333334, 297.81333333333333], [207.36, 291.84], [208.21333333333334, 283.3066666666667], [205.65333333333334, 273.92], [197.97333333333336, 262.82666666666665], [188.58666666666667, 251.73333333333335], [180.05333333333334, 244.90666666666664], [169.81333333333333, 238.07999999999998], [161.28, 233.81333333333333], [154.45333333333335, 231.25333333333333], [149.33333333333334, 230.39999999999998], [145.06666666666666, 230.39999999999998]],
label: labelFunction,
dataAttrs: {
'data-category': 'normal',
},
},
{
polygon: [[31.573333333333334, 365.2266666666667], [30.720000000000002, 365.2266666666667], [27.30666666666667, 368.64], [24.74666666666667, 372.9066666666667], [19.62666666666667, 380.5866666666667], [15.360000000000001, 387.41333333333336], [11.946666666666667, 395.09333333333336], [10.24, 403.62666666666667], [9.386666666666667, 410.4533333333333], [9.386666666666667, 417.28000000000003], [11.093333333333334, 422.40000000000003], [17.92, 427.52000000000004], [30.720000000000002, 433.49333333333334], [45.22666666666667, 435.2], [55.46666666666667, 435.2], [65.70666666666668, 434.3466666666667], [74.24000000000001, 430.08], [81.92, 424.96000000000004], [87.04, 419.84000000000003], [90.45333333333333, 414.72], [92.16000000000001, 409.6], [93.01333333333334, 403.62666666666667], [93.01333333333334, 395.94666666666666], [90.45333333333333, 388.26666666666665], [86.18666666666667, 381.44], [81.92, 375.4666666666667], [75.09333333333333, 370.3466666666667], [63.14666666666667, 364.37333333333333], [53.760000000000005, 362.6666666666667], [48.64, 362.6666666666667], [42.66666666666667, 363.52], [40.96, 364.37333333333333], [36.693333333333335, 366.08]],
label: labelFunction,
dataAttrs: {
'data-category': 'abnormal',
},
},
];
const contourStyle = (value: string, color: string) => css`
> polygon[data-category="${value}"] {
stroke: ${color};
fill: ${color};
fill-opacity: 0.3;
}
> text[data-category="${value}"] {
fill: ${color};
opacity: 0.8;
&[data-focused] {
opacity: 1;
}
}
`;
const colors = {
normal: '#3366cc',
abnormal: '#dc3912',
};
const Viewer = styled(UserContourViewer)`
${Object.keys(colors).map(value => contourStyle(value, colors[value]))};
`;
const Drawer = styled(UserContourDrawer)`
> polyline {
stroke: purple;
stroke-width: 5px;
fill: rgba(255, 255, 255, 0.4);
}
`;
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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({initialContours});
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을 변경할 수 있다
<Viewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
// Canvas Style을 변경할 수 있다
<Drawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour, {label: labelFunction})}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pen', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourViewer className={categoryColors}>', () => <Sample/>);
__stories__/UserContourViewerStyle.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
const Viewer = styled(UserContourViewer)`
> polygon {
stroke-width: 10px;
stroke: blue;
fill: rgba(0, 0, 255, 0.3);
&[data-focused] {
stroke-width: 20px;
stroke: red;
fill: rgba(255, 0, 0, 0.3);
}
}
> text {
fill: blue;
&[data-focused] {
fill: red;
}
}
`;
const Drawer = styled(UserContourDrawer)`
> polyline {
stroke: purple;
stroke-width: 8px;
fill: rgba(255, 255, 255, 0.4);
}
`;
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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을 변경할 수 있다
<Viewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
control === 'pen' &&
// Canvas Style을 변경할 수 있다
<Drawer width={width}
height={height}
contours={contours}
draw={control === 'pen' && interactionElement}
onFocus={focusContour}
onAdd={contour => addContour(contour)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pen', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserContourViewer className="">', () => <Sample/>);
__stories__/useResizeObserver.stories.tsx
import {
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
unloadWADOImage,
useInsightViewerSync,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo } from 'react';
import useResizeObserver from 'use-resize-observer';
installWADOImageLoader();
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
const {
updateCornerstoneRenderData,
} = useInsightViewerSync();
const [resizeRef, width, height] = useResizeObserver();
return (
<div ref={resizeRef} style={{width: '50vw', height: '80vh'}}>
<InsightViewerContainer width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={false}
flip={false}
pan
adjust={false}
zoom={false}
resetTime={resetTime}
image={image}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
</InsightViewerContainer>
</div>
);
}
storiesOf('insight-viewer', module)
.add('useResizeObserver', () => <Sample/>);
__stories__/UserPointViewer.stories.tsx
import {
Contour,
CornerstoneImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
ProgressViewer,
unloadWADOImage,
useInsightViewerSync,
UserPointViewer,
useUserContour,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import { useController, withTestController } from './decorators/withTestController';
installWADOImageLoader();
function labelFunction({id}: Contour): string {
return 'p' + id;
}
const initialContours: Omit<Contour, 'id'>[] = [
{
label: labelFunction,
polygon: [[100, 200]],
},
{
label: labelFunction,
polygon: [[200, 200]],
},
];
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
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({
mode: 'point',
initialContours,
});
return (
<Div>
<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 &&
cornerstoneRenderData &&
<UserPointViewer width={width}
height={height}
contours={contours}
interact={control === 'pen'}
focusedContour={focusedContour}
onFocus={focusContour}
onAdd={polygon => addContour(polygon, {label: labelFunction})}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
<ProgressViewer image={image}
width={width}
height={height}/>
</InsightViewerContainer>
<div>
<pre>
{JSON.stringify(focusedContour, null, 2)}
</pre>
</div>
</Div>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withTestController({
width: [600, 400, 1000],
height: [700, 400, 1000],
control: ['pen', ['none', 'pen', 'pan', 'adjust']],
wheel: ['zoom', ['none', 'zoom']],
flip: false,
invert: false,
}))
.add('<UserPointViewer>', () => <Sample/>);
const Div = styled.div`
display: flex;
`;
__stories__/useViewportMirroring.stories.tsx
import {
CornerstoneBulkImage,
CornerstoneImage,
CornerstoneSeriesImage,
CornerstoneSingleImage,
InsightViewer,
InsightViewerContainer,
installWADOImageLoader,
unloadWADOImage,
useBulkImageScroll,
useInsightViewerSync,
UserContourDrawer,
UserContourViewer,
useUserContour,
useViewportMirroring,
} from '@lunit/insight-viewer';
import { storiesOf } from '@storybook/react';
import React, { RefObject, useMemo, useRef, useState } from 'react';
import { useController, withTestController } from './decorators/withTestController';
import series from './series.json';
installWADOImageLoader();
function Sample() {
const resetTime: number = useMemo(() => Date.now(), []);
const image1: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000010.dcm`, {unload: unloadWADOImage}), []);
const image2: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000020.dcm`, {unload: unloadWADOImage}), []);
const image3: CornerstoneImage = useMemo(() => new CornerstoneSingleImage(`wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/CT000030.dcm`, {unload: unloadWADOImage}), []);
const image4: CornerstoneBulkImage = useMemo(() => new CornerstoneSeriesImage(series.map(p => `wadouri:https://lunit-io.github.io/frontend-fixtures/dcm-files/series/${p}`), {unload: unloadWADOImage}), []);
const {
width,
height,
invert,
flip,
} = useController();
const viewer2: RefObject<InsightViewer> = useRef(null);
const viewer3: RefObject<InsightViewer> = useRef(null);
const viewer4: RefObject<InsightViewer> = useRef(null);
const {updateMasterRenderData} = useViewportMirroring(viewer2, viewer3, viewer4);
const [interactionElement3, setInteractionElement3] = useState<HTMLElement | null>(null);
const {
cornerstoneRenderData,
updateCornerstoneRenderData,
} = useInsightViewerSync();
const {
contours,
focusedContour,
addContour,
removeContour,
focusContour,
} = useUserContour();
const [interactionElement4, setInteractionElement4] = useState<HTMLElement | null>(null);
useBulkImageScroll({
image: image4,
element: interactionElement4,
enabled: true,
});
return (
<div style={{display: 'flex'}}>
<InsightViewerContainer width={width} height={height}>
<InsightViewer width={width}
height={height}
invert={invert}
flip={flip}
pan
adjust={false}
zoom
resetTime={resetTime}
image={image1}
updateCornerstoneRenderData={updateMasterRenderData}/>
</InsightViewerContainer>
<InsightViewerContainer width={width} height={height}>
<InsightViewer ref={viewer2}
width={width}
height={height}
invert={invert}
flip={flip}
pan={false}
adjust
zoom={false}
resetTime={resetTime}
image={image2}
updateCornerstoneRenderData={renderData => console.log('#2', renderData)}/>
</InsightViewerContainer>
<InsightViewerContainer ref={setInteractionElement3} width={width} height={height}>
<InsightViewer ref={viewer3}
width={width}
height={height}
invert={invert}
flip={flip}
pan={false}
adjust={false}
zoom={false}
resetTime={resetTime}
image={image3}
updateCornerstoneRenderData={updateCornerstoneRenderData}/>
{
contours &&
contours.length > 0 &&
cornerstoneRenderData &&
<UserContourViewer width={width}
height={height}
contours={contours}
focusedContour={focusedContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
{
contours &&
cornerstoneRenderData &&
<UserContourDrawer width={width}
height={height}
contours={contours}
draw={interactionElement3}
onFocus={focusContour}
onAdd={contour => addContour(contour)}
onRemove={removeContour}
cornerstoneRenderData={cornerstoneRenderData}/>
}
</InsightViewerContainer>
<InsightViewerContainer ref={setInteractionElement4} width={width} height={height}>
<InsightViewer ref={viewer4}
width={width}
height={height}
invert={invert}
flip={flip}
pan={false}
adjust={false}
zoom={false}
resetTime={resetTime}
image={image4}
updateCornerstoneRenderData={renderData => console.log('#4', renderData)}/>
</InsightViewerContainer>
</div>
);
}
storiesOf('insight-viewer', module)
.addDecorator(withTestController({
width: [300, 200, 500],
height: [400, 300, 600],
control: ['none', ['none']],
wheel: ['none', ['none']],
flip: false,
invert: false,
}))
.add('useViewportMirroring()', () => <Sample/>);
Tests
Changelog
3.0.0
Added
- Zoom, Adjust 단축키 구현을 위한
updateViewport()
, zoomMiddleLeft()
, zoomMiddleRight()
, zoomMiddleCenter()
, adjustWindowCenter()
, adjustWindowWidth()
추가 <InsightViewer defaultViewportTransforms={} />
추가useImageLoadedTime()
추가
Fixed
- Zoom 속도 보정 (
deltaY * 0.03
→ (deltaY > 0 ? 1 : -1) * 0.03
) - Cornerstone
loadImage()
실패를 위한 Retry 추가 (5000ms)
Breaking Changes
ContourInfo
Type이 삭제됨Contour
를 사용하는 모든 @lunit/insight-viewer
Component들이 <T extends Contour>
형태로 변경됨Contour
Type에 의한 문제는 JS의 경우 애초에 Type에 의한 제한이 없기 때문에 큰 문제가 없고, TS의 경우 Compile Error가 발생할 여지가 있음useUserContour()
hook 의 addContour()
arguments 가 addContour: (polygon: Point[], contourInfo?: Omit<T, 'id' | 'polygon'>) => T | null;
로 변경됨- 기존
addContour(polygon, 0, label...)
과 같이 사용하던 것을 addContour(polygon, {label, dataAttrs...})
와 같이 변경해 줘야함 addContour()
를 사용하지 않은 경우, 또는 2번째 이후의 인자를 넘기지 않는 형태로 사용한 경우 영향이 없음
2.6.1
Fixed
<DCMImage>
에서 WebGL 사용하지 않도록 변경 (WebGL Context 수량 제한 회피)
2.6.0
Added
2.5.0
Added
2.4.1
Fixed
- Fix ESLint warnings
<UserPointViewer>
의 기본 label 표시를 contour.id
사용
2.4.0
Added
2.3.0
Added
2.2.0
Fixed
CornerstoneImage.destroy()
시에 모든 dcm image loading을 취소시킴
2.1.9
Fixed
useViewportMirroring
에서 오작동이 일어날 수 있는 <InsightViewer>.updateViewport()
를 분할
2.1.8
Added
Contour.label
추가 (입력된 경우 Contour.id
대신 출력됨)const {addContours} = useUserContour()
추가 (다량의 Contour 추가 가능)useUserContour({initialContours})
추가 (초기화)
2.1.7
Fixed
UserContourDrawer
의 componentWillUnmount()
에 의한 Event deactivation이 mouseup
Event에 의해 취소되는 Event 순서 문제 해결UserContourDrawer
에서 null
가능성 배제 코드 제거
2.1.4
Fixed
CornerstoneSeriesImage
의 이미지 순서가 뒤섞이는 문제 해결
2.1.2
Added
useUserContour({ nextId?: number | RefObject<number> })
옵션 추가
2.1.1
Added
<UserContourViewer className="">
, <UserContourDrawer className="">
Custom Style
2.1.0
Added
- 기존
<UserContourViewer>
와 <UserContourDrawer>
는 <UserContourCanvasViewer>
와 <UserContourCanvasDrawer>
로 변경 - 신규
<UserContourViewer>
와 <UserContourDrawer>
는 SVG 기반으로 변경 (Viewport와 무관하게 일정한 Style을 유지함)
2.0.3
Fixed
useViewportMirroring()
는 위치에 관련된 일부 속성만을 미러링 (hflip
, vflip
, translation
, scale
)