添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
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';
    // serverPath: 资源代理路径,使用 webpack devServer 进行代理
    import { serverPath } from '@utils/CommonVars';
    interface IFile {
      folderName: string, // 服务器上文件夹名称
      cellSize: string, // 每张切片边长,即之前 xml 文件中的 TileSize 字段
      width: string, // 原始图片宽度,即之前 xml 文件中的 width 字段
      height: string // 原始图片高度,即之前 xml 文件中的  height 字段
    const OnlyImage = () => {
      const section: IFile = {
        folderName: 'DSI0',
        cellSize: '512',
        width: '46511',
        height: '49974'
      useEffect(() => {
        initOpenSeaDragon();
      }, []);
      // 初始化 openSeadragon
      const initOpenSeaDragon = () => {
        if (section) {
          OpenSeadragon({
            id: 'openSeaDragon',
            // 装有各种按钮名称的文件夹 images 地址,即库文件中的 images 文件夹
            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
            // 至少 20% 显示在可视区域内
            visibilityRatio: 0.2,
            // 开启调试模式
            // debugMode : true,
            // 是否允许水平拖动
            panHorizontal: true,
            // 初始化默认放大倍数,按home键也返回该层
            // defaultZoomLevel: 5,
            // 最小允许放大倍数
            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 = () => {
      // 创建 canvas 画布容器
      canvasDiv = document.createElement('div');
      canvasDiv.style.position = 'absolute';
      canvasDiv.style.left = canvasDiv.style.top = '0';
      canvasDiv.style.width = canvasDiv.style.height = '100%';
      // 将容器放进 openSeadragon 中
      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 事件监听
      openSeadragon.addHandler('update-viewport', resize);
      openSeadragon.addHandler('open', resize);
      // 设置 fabric 事件监听
      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) {
        // remove 仅将目前移除,clear 清除上一残留,只剩当前
        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) {
        // 写入 Canvas
        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…

    有问题或者探讨可以评论!欢迎讨论。