2,108
在公司的一个病理图像标注的过程中,需要实现超过 2G 大小的图像的加载,以及标注功能。以下代码均使用 React Hook 书写,如有其他框架需求,也可进行参考。
主要技术:
Fabric.js 用于标注。
OpenSeadragon 用于金字塔图像加载。
OpenSeadragon
一种基于 Web 的开源查看器,用于高分辨率可缩放图像,在纯 JavaScript 中实现,用于桌面和移动设备。
具体使用可参考官网:
openseadragon.github.io/
Fabric.js
有写过一个关于 Fabric.js 官网整体介绍的文档,就不挪过来,可自行选择是否进行查看。
官网介绍:
note.youdao.com/s/XqLk2oSs
使用介绍(vite 版本):
github.com/obf1313/fab…
库文件准备
打开 OpenSeadragon 官网:
openseadragon.github.io/
找到该文件,下载并解压:
可使用微软提供的 DeepZoomComposer,因为官网的下载链接我已经找不到了,这里提供一下下载地址。
链接:
Deep Zoom Composer
提取码: 6trz
安装完成后新建项目,拖入图片,点击 Export:
在导出的文件夹中找到 dzc_output_images 文件夹,里面的 图片名_files 就是我们所需的文件储存格式,图片名.xml 则是我们所属的图片信息,之后编程会用到。
实例一:单独 OpenSeadragon 实现大图查看
import React, { useEffect } from 'react';
import OpenSeadragon from 'openseadragon';
import { serverPath } from '@utils/CommonVars';
interface IFile {
folderName: string,
cellSize: string,
width: string,
height: string
const OnlyImage = () => {
const section: IFile = {
folderName: 'DSI0',
cellSize: '512',
width: '46511',
height: '49974'
useEffect(() => {
initOpenSeaDragon();
}, []);
const initOpenSeaDragon = () => {
if (section) {
OpenSeadragon({
id: 'openSeaDragon',
prefixUrl: serverPath + '/images/',
showNavigator: true,
navigatorAutoFade: false,
navigatorPosition: 'ABSOLUTE',
navigatorTop: 0,
navigatorLeft: 0,
navigatorHeight: '90px',
navigatorWidth: '200px',
navigatorBackground: '#fefefe',
navigatorBorderColor: '#191970',
navigatorDisplayRegionColor: '#FF0000',
tileSources: {
Image: {
xmlns: 'http://schemas.microsoft.com/deepzoom/2009',
Url: serverPath + section.folderName + '/',
Overlap: '1',
TileSize: section.cellSize,
Format: 'jpg',
Size: {
Width: section.width,
Height: section.height
visibilityRatio: 0.2,
panHorizontal: true,
minZoomLevel: 0.4,
maxZoomLevel: 40,
zoomInButton: 'zoom-in',
zoomOutButton: 'zoom-out',
gestureSettingsMouse: {
clickToZoom: false
return (
<div id="openSeaDragon" style={{ width: '100%', height: 'calc(100vh - 60px)' }} />
export default OnlyImage;
实现效果如下:
实例二:OpenSeadragon 结合 Fabric.js 完成图像标注
使用 openSeadragon 上一个实例已经说了,这个实例主要讲讲 fabric.js 结合使用做标注的功能。
初始化画布
const initCanvas = () => {
canvasDiv = document.createElement('div');
canvasDiv.style.position = 'absolute';
canvasDiv.style.left = canvasDiv.style.top = '0';
canvasDiv.style.width = canvasDiv.style.height = '100%';
openSeadragon.canvas.appendChild(canvasDiv);
myCanvas = document.createElement('canvas');
myCanvas.setAttribute('id', 'canvas');
canvasDiv.appendChild(myCanvas);
resize();
fabricCanvas = new fabric.Canvas('canvas', { selection: false });
fabricCanvas.freeDrawingBrush.color = pencilColor;
fabricCanvas.freeDrawingBrush.width = pencilWidth;
openSeadragon.addHandler('update-viewport', resize);
openSeadragon.addHandler('open', resize);
mouseDown();
mouseMove();
mouseUp();
onSelectObject();
getAnnotate();
画布改变监听
const resize = () => {
let width = openSeadragon.container.clientWidth;
let height = openSeadragon.container.clientHeight;
setCanvasShape([width, height]);
canvasDiv.setAttribute('width', width);
myCanvas.setAttribute('width', width);
canvasDiv.setAttribute('height', height);
myCanvas.setAttribute('height', height);
fabric 事件监听
const mouseDown = () => {
fabricCanvas.on('mouse:down', (options: any) => {
if (!annotationView && !ifSelectObj) {
let offsetX = fabricCanvas.calcOffset().viewportTransform[4];
let offsetY = fabricCanvas.calcOffset().viewportTransform[5];
const x: number = Math.round(options.e.offsetX - offsetX);
const y: number = Math.round(options.e.offsetY - offsetY);
mouseFrom.x = x;
mouseFrom.y = y;
doDrawing = true;
const mouseMove = () => {
fabricCanvas.on('mouse:move', (options: any) => {
if (selectPencil && doDrawing && !ifSelectObj && !annotationView) {
if (moveCount % 2 && !doDrawing) {
return;
moveCount++;
let offsetX = fabricCanvas.calcOffset().viewportTransform[4];
let offsetY = fabricCanvas.calcOffset().viewportTransform[5];
const x = Math.round(options.e.offsetX - offsetX);
const y = Math.round(options.e.offsetY - offsetY);
mouseTo.x = x;
mouseTo.y = y;
drawing(x, y);
const mouseUp = () => {
fabricCanvas.on('mouse:up', (options: any) => {
let offsetX = fabricCanvas.calcOffset().viewportTransform[4];
let offsetY = fabricCanvas.calcOffset().viewportTransform[5];
mouseTo.x = Math.round(options.e.offsetX - offsetX);
mouseTo.y = Math.round(options.e.offsetY - offsetY);
if (currCanvasObject) {
if (Math.abs(currCanvasObject.width) <= 1) {
fabricCanvas.remove(currCanvasObject).renderAll();
message.error('标注范围太小,请重新标注!');
resetCanvasOption();
return;
} else if (currCanvasObject) {
setAnnotationView(true);
const drawing = (offsetX: number, offsetY: number) => {
if (currCanvasObject) {
fabricCanvas.remove(currCanvasObject);
const zoom: any = openSeadragon.viewport.getZoom(true);
let canZoom: any = openSeadragon.viewport.viewportToImageZoom(zoom);
let canvasObject: any = null;
let left: number = mouseFrom.x;
let top: number = mouseFrom.y;
const radius = Math.sqrt((mouseTo.x - left) * (mouseTo.x - left) + (mouseTo.y - top) * (mouseTo.y - top)) / canZoom;
const commonParams = {
stroke: pencilColor,
strokeWidth: pencilWidth,
selectionBackgroundColor: 'rgba(0, 0, 0, 0.25)',
fill: 'rgba(255, 255, 255, 0)'
switch (selectPencil) {
case EPencilType.circle:
canvasObject = new fabric.Circle({
left: left / canZoom,
top: top / canZoom,
originX: 'center',
originY: 'center',
radius: radius,
hasControls: true,
...commonParams
break;
case EPencilType.rectangle:
canvasObject = new fabric.Rect({
top: mouseFrom.y / canZoom,
left: mouseFrom.x / canZoom,
width: (mouseTo.x - mouseFrom.x) / canZoom,
height: (mouseTo.y - mouseFrom.y) / canZoom,
...commonParams
break;
case EPencilType.polygon:
lineList.push({
x: offsetX / canZoom,
y: offsetY / canZoom
canvasObject = new fabric.Polygon(lineList, {
...commonParams
break;
default:
break;
if (canvasObject) {
currCanvasObject = canvasObject;
selectObj = currCanvasObject;
fabricCanvas.add(currCanvasObject);
const onSelectObject = () => {
fabricCanvas.on('selection:created', (options: any) => {
if (options.target) {
selectObj = options.target;
ifSelectObj = true;
resetCanvasOption();
数据保存,复现
const getAnnotate = () => {
const note = localStorage.getItem('markData');
if (note) {
fabricCanvas.loadFromJSON(JSON.parse(note), () => {
fabricCanvas.renderAll();
const addAnnotate = () => {
currCanvasObject.id = new Date().valueOf();
fabricCanvas.renderAll();
localStorage.setItem('markData', JSON.stringify(fabricCanvas.toJSON(['id'])));
setAnnotationView(false);
resetCanvasOption();
具体代码实现可参考 Git 项目:
github.com/obf1313/ima…
有问题或者探讨可以评论!欢迎讨论。