我目前正在維護一個強大的開源創意畫板。這款畫板整合了許多有趣的畫筆和輔助繪圖功能,可以讓使用者體驗到全新的繪圖效果。無論是在行動端還是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"> 清除積分 按鈕> ; setPointStatus(true)} > {點狀態? 「正」:「負」} 按鈕> </div> <div classname="text-xs text-base-content mt-2">{segmentationTip}</div>; <div> <h2> 結論 </h2> <p>感謝您的閱讀。這就是本文的全部內容,希望本文對您有所幫助,歡迎點讚收藏。如有任何疑問,歡迎在留言區討論! </p> </div> </div></tensor></samimageprocessorresult></rawimage></processor></pretrainedmodel></htmlcanvaselement></boolean></segmentationstatustype></markpoint>
以上是探索Canvas系列:結合Transformers.js實現智慧型影像處理的詳細內容。更多資訊請關注PHP中文網其他相關文章!