我目前正在维护一个强大的开源创意画板。这款画板集成了很多有趣的画笔和辅助绘图功能,可以让用户体验到全新的绘图效果。无论是在移动端还是PC端,都可以享受到更好的交互体验和效果展示。
在这篇文章中,我将详细讲解如何结合 Transformers.js 实现背景去除和图像标记分割。结果如下
链接:https://songlh.top/paint-board/
Github:https://github.com/LHRUN/paint-board 欢迎Star ⭐️
Transformers.js 是一个基于 Hugging Face 的 Transformers 的强大 JavaScript 库,可以直接在浏览器中运行,无需依赖服务器端计算。这意味着您可以在本地运行模型,从而提高效率并降低部署和维护成本。
目前 Transformers.js 在 Hugging Face 上提供了 1000 个模型,覆盖各个领域,可以满足你的大部分需求,比如图像处理、文本生成、翻译、情感分析等任务处理,你都可以通过 Transformers 轻松实现.js。按如下方式搜索型号。
目前 Transformers.js 的主要版本已更新为 V3,增加了很多很棒的功能,详细信息:Transformers.js v3:WebGPU 支持、新模型和任务以及更多......
我在这篇文章中添加的两个功能都使用了 WebGpu 支持,该支持仅在 V3 中可用,并且大大提高了处理速度,现在解析速度为毫秒级。不过需要注意的是,支持WebGPU的浏览器并不多,建议使用最新版本的Google进行访问。
为了删除背景,我使用 Xenova/modnet 模型,如下所示
处理逻辑可以分为三步
代码逻辑如下,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
图像标记分割是使用 Xenova/slimsam-77-uniform 模型实现的。效果如下,图片加载完成后点击即可,根据你点击的坐标生成分割
处理逻辑可以分为五个步骤
代码逻辑如下,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中文网其他相关文章!