首页 > web前端 > js教程 > 正文

探索Canvas系列:结合Transformers.js实现智能图像处理

Susan Sarandon
发布: 2024-11-26 21:26:14
原创
115 人浏览过

介绍

我目前正在维护一个强大的开源创意画板。这款画板集成了很多有趣的画笔和辅助绘图功能,可以让用户体验到全新的绘图效果。无论是在移动端还是PC端,都可以享受到更好的交互体验和效果展示。

在这篇文章中,我将详细讲解如何结合 Transformers.js 实现背景去除和图像标记分割。结果如下

Exploring the Canvas Series: combined with Transformers.js to achieve intelligent image processing

链接:https://songlh.top/paint-board/

Github:https://github.com/LHRUN/paint-board 欢迎Star ⭐️

Transformers.js

Transformers.js 是一个基于 Hugging Face 的 Transformers 的强大 JavaScript 库,可以直接在浏览器中运行,无需依赖服务器端计算。这意味着您可以在本地运行模型,从而提高效率并降低部署和维护成本。

目前 Transformers.js 在 Hugging Face 上提供了 1000 个模型,覆盖各个领域,可以满足你的大部分需求,比如图像处理、文本生成、翻译、情感分析等任务处理,你都可以通过 Transformers 轻松实现.js。按如下方式搜索型号。

Exploring the Canvas Series: combined with Transformers.js to achieve intelligent image processing

目前 Transformers.js 的主要版本已更新为 V3,增加了很多很棒的功能,详细信息:Transformers.js v3:WebGPU 支持、新模型和任务以及更多......

我在这篇文章中添加的两个功能都使用了 WebGpu 支持,该支持仅在 V3 中可用,并且大大提高了处理速度,现在解析速度为毫秒级。不过需要注意的是,支持WebGPU的浏览器并不多,建议使用最新版本的Google进行访问。

功能一:去除背景

为了删除背景,我使用 Xenova/modnet 模型,如下所示

Exploring the Canvas Series: combined with Transformers.js to achieve intelligent image processing

处理逻辑可以分为三步

  1. 初始化状态,并加载模型和处理器。
  2. 界面的显示,这是你自己设计的,不是我的。
  3. 展示效果,这是你自己设计的,不是我的。现在比较流行的是用边框线来动态展示去除背景前后的对比效果。

代码逻辑如下,React TS ,具体参见我的项目源码,源码位于 src/components/boardOperation/uploadImage/index.tsx

import { useState, FC, useRef, useEffect, useMemo } from 'react'
import {
  env,
  AutoModel,
  AutoProcessor,
  RawImage,
  PreTrainedModel,
  Processor
} from '@huggingface/transformers'

const REMOVE_BACKGROUND_STATUS = {
  LOADING: 0,
  NO_SUPPORT_WEBGPU: 1,
  LOAD_ERROR: 2,
  LOAD_SUCCESS: 3,
  PROCESSING: 4,
  PROCESSING_SUCCESS: 5
}

type RemoveBackgroundStatusType =
  (typeof REMOVE_BACKGROUND_STATUS)[keyof typeof REMOVE_BACKGROUND_STATUS]

const UploadImage: FC<{ url: string }> = ({ url }) => {
  const [removeBackgroundStatus, setRemoveBackgroundStatus] =
    useState<RemoveBackgroundStatusType>()
  const [processedImage, setProcessedImage] = useState('')

  const modelRef = useRef<PreTrainedModel>()
  const processorRef = useRef<Processor>()

  const removeBackgroundBtnTip = useMemo(() => {
    switch (removeBackgroundStatus) {
      case REMOVE_BACKGROUND_STATUS.LOADING:
        return 'Remove background function loading'
      case REMOVE_BACKGROUND_STATUS.NO_SUPPORT_WEBGPU:
        return 'WebGPU is not supported in this browser, to use the remove background function, please use the latest version of Google Chrome'
      case REMOVE_BACKGROUND_STATUS.LOAD_ERROR:
        return 'Remove background function failed to load'
      case REMOVE_BACKGROUND_STATUS.LOAD_SUCCESS:
        return 'Remove background function loaded successfully'
      case REMOVE_BACKGROUND_STATUS.PROCESSING:
        return 'Remove Background Processing'
      case REMOVE_BACKGROUND_STATUS.PROCESSING_SUCCESS:
        return 'Remove Background Processing Success'
      default:
        return ''
    }
  }, [removeBackgroundStatus])

  useEffect(() => {
    ;(async () => {
      try {
        if (removeBackgroundStatus === REMOVE_BACKGROUND_STATUS.LOADING) {
          return
        }
        setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.LOADING)

        // Checking WebGPU Support
        if (!navigator?.gpu) {
          setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.NO_SUPPORT_WEBGPU)
          return
        }
        const model_id = 'Xenova/modnet'
        if (env.backends.onnx.wasm) {
          env.backends.onnx.wasm.proxy = false
        }

        // Load model and processor
        modelRef.current ??= await AutoModel.from_pretrained(model_id, {
          device: 'webgpu'
        })
        processorRef.current ??= await AutoProcessor.from_pretrained(model_id)
        setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.LOAD_SUCCESS)
      } catch (err) {
        console.log('err', err)
        setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.LOAD_ERROR)
      }
    })()
  }, [])

  const processImages = async () => {
    const model = modelRef.current
    const processor = processorRef.current

    if (!model || !processor) {
      return
    }

    setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.PROCESSING)

    // load image
    const img = await RawImage.fromURL(url)

    // Pre-processed image
    const { pixel_values } = await processor(img)

    // Generate image mask
    const { output } = await model({ input: pixel_values })
    const maskData = (
      await RawImage.fromTensor(output[0].mul(255).to('uint8')).resize(
        img.width,
        img.height
      )
    ).data

    // Create a new canvas
    const canvas = document.createElement('canvas')
    canvas.width = img.width
    canvas.height = img.height
    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D

    // Draw the original image
    ctx.drawImage(img.toCanvas(), 0, 0)

    // Updating the mask area
    const pixelData = ctx.getImageData(0, 0, img.width, img.height)
    for (let i = 0; i < maskData.length; ++i) {
      pixelData.data[4 * i + 3] = maskData[i]
    }
    ctx.putImageData(pixelData, 0, 0)

    // Save new image
    setProcessedImage(canvas.toDataURL('image/png'))
    setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.PROCESSING_SUCCESS)
  }

  return (
    <div className="card shadow-xl">
      <button
        className={`btn btn-primary btn-sm ${
          ![
            REMOVE_BACKGROUND_STATUS.LOAD_SUCCESS,
            REMOVE_BACKGROUND_STATUS.PROCESSING_SUCCESS,
            undefined
          ].includes(removeBackgroundStatus)
            ? 'btn-disabled'
            : ''
        }`}
        onClick={processImages}
      >
        Remove background
      </button>
      <div className="text-xs text-base-content mt-2 flex">
        {removeBackgroundBtnTip}
      </div>
      <div className="relative mt-4 border border-base-content border-dashed rounded-lg overflow-hidden">
        <img
          className={`w-[50vw] max-w-[400px] h-[50vh] max-h-[400px] object-contain`}
          src={url}
        />
        {processedImage && (
          <img
            className={`w-full h-full absolute top-0 left-0 z-[2] object-contain`}
            src={processedImage}
          />
        )}
      </div>
    </div>
  )
}

export default UploadImage
登录后复制

功能2:图像标记分割

图像标记分割是使用 Xenova/slimsam-77-uniform 模型实现的。效果如下,图片加载完成后点击即可,根据你点击的坐标生成分割

Exploring the Canvas Series: combined with Transformers.js to achieve intelligent image processing

处理逻辑可以分为五个步骤

  1. 初始化状态,并加载模型和处理器
  2. 获取图像并加载,然后保存图像加载数据和嵌入数据。
  3. 监听图像点击事件,记录点击数据,分为正标记和负标记,每次点击后根据点击数据解码生成mask数据,然后根据mask数据绘制分割效果.
  4. 界面展示,这个要自己设计任意发挥,不是我的为准
  5. 点击保存图像,根据mask像素数据,匹配原始图像数据,然后通过canvas绘图导出

代码逻辑如下,React TS ,具体参见我的项目源码,源码位于 src/components/boardOperation/uploadImage/imageSegmentation.tsx

从 'react' 导入 { useState, useRef, useEffect, useMemo, MouseEvent, FC }
进口 {
  萨姆模型,
  自动处理器,
  原始图像,
  预训练模型,
  处理器,
  张量,
  SamImageProcessor结果
} 来自 '@huggingface/transformers'

从 '@/components/icons/loading.svg?react' 导入 LoadingIcon
从 '@/components/icons/boardOperation/image-segmentation-positive.svg?react' 导入 PositiveIcon
从 '@/components/icons/boardOperation/image-segmentation-negative.svg?react' 导入 NegativeIcon

接口标记点{
  位置:数字[]
  标签: 数字
}

常量 SEGMENTATION_STATUS = {
  正在加载:0,
  NO_SUPPORT_WEBGPU:1,
  加载错误:2,
  加载成功:3,
  加工:4、
  处理成功:5
}

类型分段状态类型 =
  (SEGMENTATION_STATUS 类型)[SEGMENTATION_STATUS 类型键]

const ImageSegmentation: FC; = ({ url }) =>; {
  const [markPoints, setMarkPoints] = useState<markpoint>([])
  const [segmentationStatus, setSegmentationStatus] =
    useState<segmentationstatustype>()
  const [pointStatus, setPointStatus] = useState<boolean>(true)

  const maskCanvasRef = useRef<htmlcanvaselement>(null) // 分段掩码
  const modelRef = useRef<pretrainedmodel>() // 模型
  const handlerRef = useRef<processor>() // 处理器
  const imageInputRef = useRef<rawimage>() // 原始图像
  const imageProcessed = useRef<samimageprocessorresult>() // 处理后的图像
  const imageEmbeddings = useRef<tensor>() // 嵌入数据

  const分段提示 = useMemo(() => {
    开关(分段状态){
      案例 SEGMENTATION_STATUS.LOADING:
        return '图像分割函数加载中'
      案例 SEGMENTATION_STATUS.NO_SUPPORT_WEBGPU:
        return '该浏览器不支持WebGPU,要使用图像分割功能,请使用最新版本的Google Chrome。'
      案例 SEGMENTATION_STATUS.LOAD_ERROR:
        return '图像分割函数加载失败'
      案例SEGMENTATION_STATUS.LOAD_SUCCESS:
        return '图像分割函数加载成功'
      案例 SEGMENTATION_STATUS.PROCESSING:
        返回“图像处理...”
      案例SEGMENTATION_STATUS.PROCESSING_SUCCESS:
        return '图片处理成功,可以点击图片进行标记,绿色遮罩区域为分割区域。'
      默认:
        返回 '​​'
    }
  }, [分段状态])

  // 1. 加载模型和处理器
  useEffect(() => {
    ;(异步() => {
      尝试 {
        if (segmentationStatus === SEGMENTATION_STATUS.LOADING) {
          返回
        }

        setSegmentationStatus(SEGMENTATION_STATUS.LOADING)
        if (!navigator?.gpu) {
          setSegmentationStatus(SEGMENTATION_STATUS.NO_SUPPORT_WEBGPU)
          返回
        }const model_id = 'Xenova/slimsam-77-uniform'
        modelRef.current ??= 等待 SamModel.from_pretrained(model_id, {
          dtype: 'fp16', // 或 "fp32"
          设备:'webgpu'
        })
        handlerRef.current ??= 等待 AutoProcessor.from_pretrained(model_id)

        setSegmentationStatus(SEGMENTATION_STATUS.LOAD_SUCCESS)
      } 捕获(错误){
        console.log('错误', 错误)
        setSegmentationStatus(SEGMENTATION_STATUS.LOAD_ERROR)
      }
    })()
  }, [])

  // 2. 处理图像
  useEffect(() => {
    ;(异步() => {
      尝试 {
        如果 (
          !modelRef.current ||
          !processorRef.current ||
          !url ||
          分段状态 === SEGMENTATION_STATUS.PROCESSING
        ){
          返回
        }
        setSegmentationStatus(SEGMENTATION_STATUS.PROCESSING)
        清除点()

        imageInputRef.current = 等待 RawImage.fromURL(url)
        imageProcessed.current = 等待处理器Ref.current(
          imageInputRef.current
        )
        imageEmbeddings.current = 等待 (
          modelRef.current 为任意
        ).get_image_embeddings(imageProcessed.current)

        setSegmentationStatus(SEGMENTATION_STATUS.PROCESSING_SUCCESS)
      } 捕获(错误){
        console.log('错误', 错误)
      }
    })()
  },[url,modelRef.current,processorRef.current])

  // 更新遮罩效果
  函数 updateMaskOverlay(掩码:RawImage,分数:Float32Array) {
    const maskCanvas = maskCanvasRef.current
    如果(!maskCanvas){
      返回
    }
    const maskContext = maskCanvas.getContext('2d') as CanvasRenderingContext2D

    // 更新画布尺寸(如果不同)
    if (maskCanvas.width !== mask.width || maskCanvas.height !== mask.height) {
      maskCanvas.width = mask.width
      maskCanvas.height = mask.高度
    }

    // 为像素数据分配缓冲区
    const imageData = maskContext.createImageData(
      maskCanvas.宽度,
      maskCanvas.height
    )

    // 选择最佳掩码
    const numMasks = Scores.length // 3
    让最佳索引 = 0
    for (令 i = 1; i scores[bestIndex]) {
        最佳索引 = i
      }
    }

    // 用颜色填充蒙版
    const PixelData = imageData.data
    for (让 i = 0; i  {
    如果 (
      !modelRef.current ||
      !imageEmbeddings.current ||
      !processorRef.current ||
      !imageProcessed.current
    ){
      返回
    }// 没有点击数据直接清除分割效果
    if (!markPoints.length && maskCanvasRef.current) {
      const maskContext = maskCanvasRef.current.getContext(
        '2d'
      ) 作为 CanvasRenderingContext2D
      maskContext.clearRect(
        0,
        0,
        maskCanvasRef.current.width,
        maskCanvasRef.current.height
      )
      返回
    }

    // 准备解码输入
    const reshape = imageProcessed.current.reshape_input_sizes[0]
    常量点 = 标记点
      .map((x) => [x.position[0] * 重塑[1], x.position[1] * 重塑[0]])
      .flat(无穷大)
    const labels = markPoints.map((x) => BigInt(x.label)).flat(Infinity)

    const num_points = markPoints.length
    const input_points = new Tensor('float32', 点, [1, 1, num_points, 2])
    const input_labels = new Tensor('int64', labels, [1, 1, num_points])

    // 生成掩码
    const { pred_masks, iou_scores } = 等待 modelRef.current({
      ...imageEmbeddings.current,
      输入点,
      输入标签
    })

    // 对掩码进行后处理
    const mask = wait (processorRef.current as any).post_process_masks(
      pred_masks,
      imageProcessed.current.original_sizes,
      imageProcessed.current.reshape_input_sizes
    )

    updateMaskOverlay(RawImage.fromTensor(masks[0][0]), iou_scores.data)
  }

  const 钳位 = (x: 数字, 最小值 = 0, 最大值 = 1) => {
    返回 Math.max(Math.min(x, max), min)
  }

  const clickImage = (e: MouseEvent) =>; {
    if (segmentationStatus !== SEGMENTATION_STATUS.PROCESSING_SUCCESS) {
      返回
    }

    const { clientX, clientY, currentTarget } = e
    const { 左,上 } = currentTarget.getBoundingClientRect()

    常量 x = 钳位(
      (clientX - 左 currentTarget.scrollLeft) / currentTarget.scrollWidth
    )
    常量 y = 钳位(
      (clientY - 顶部 currentTarget.scrollTop) / currentTarget.scrollHeight
    )

    const现有PointIndex = markPoints.findIndex(
      (点)=>
        Math.abs(point.position[0] - x) ; {
    设置标记点([])
    解码([])
  }

  返回 (
    <div className="cardshadow-xloverflow-auto">
      <div className="flex items-center gap-x-3">
        <按钮 className="btn btn-primary btn-sm" onClick={clearPoints}>
          清除积分
        </按钮>

        ; setPointStatus(true)}
        >
          {点状态? “正”:“负”}
        </按钮>
      
      <div classname="text-xs text-base-content mt-2">{segmentationTip}</div>;
      <div>



<h2>
  
  
  结论
</h2>

<p>感谢您的阅读。这就是本文的全部内容,希望本文对您有所帮助,欢迎点赞收藏。如果有任何疑问,欢迎在评论区讨论!</p>


          </div>

            
        </tensor></samimageprocessorresult></rawimage></processor></pretrainedmodel></htmlcanvaselement></boolean></segmentationstatustype></markpoint>
登录后复制

以上是探索Canvas系列:结合Transformers.js实现智能图像处理的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板