Je gère actuellement une puissante planche à dessin créative open source. Cette planche à dessin intègre de nombreux pinceaux intéressants et fonctions de dessin auxiliaires, ce qui permet aux utilisateurs de découvrir un nouvel effet de dessin. Que ce soit sur mobile ou PC, vous pouvez profiter d'une meilleure expérience interactive et d'un meilleur affichage des effets.
Dans cet article, j'expliquerai en détail comment combiner Transformers.js pour réaliser la suppression de l'arrière-plan et la segmentation du marquage d'image. Le résultat est le suivant
Lien : https://songlh.top/paint-board/
Github : https://github.com/LHRUN/paint-board Bienvenue sur Star ⭐️
Transformers.js est une puissante bibliothèque JavaScript basée sur les Transformers de Hugging Face qui peut être exécutée directement dans le navigateur sans recourir au calcul côté serveur. Cela signifie que vous pouvez exécuter vos modèles localement, augmentant ainsi l'efficacité et réduisant les coûts de déploiement et de maintenance.
Actuellement, Transformers.js a fourni 1 000 modèles sur Hugging Face, couvrant divers domaines, qui peuvent satisfaire la plupart de vos besoins, tels que le traitement d'images, la génération de texte, la traduction, l'analyse des sentiments et d'autres tâches de traitement que vous pouvez facilement réaliser grâce à Transformers. .js. Recherchez des modèles comme suit.
La version majeure actuelle de Transformers.js a été mise à jour vers la V3, qui ajoute de nombreuses fonctionnalités intéressantes, des détails : Transformers.js v3 : prise en charge WebGPU, nouveaux modèles et tâches, et plus encore….
Les deux fonctionnalités que j'ai ajoutées à cet article utilisent le support WebGpu, qui n'est disponible que dans la V3, et ont considérablement amélioré la vitesse de traitement, avec une analyse désormais en millisecondes. Cependant, il convient de noter qu'il n'existe pas beaucoup de navigateurs prenant en charge WebGPU, il est donc recommandé d'utiliser la dernière version de Google pour visiter.
Pour supprimer l'arrière-plan j'utilise le modèle Xenova/modnet, qui ressemble à ceci
La logique de traitement peut être divisée en trois étapes
La logique du code est la suivante, React TS , voir le code source de mon projet pour plus de détails, le code source se trouve dans 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
La segmentation des marqueurs d'image est implémentée à l'aide du modèle Xenova/slimsam-77-uniform. L'effet est le suivant, vous pouvez cliquer sur l'image après son chargement, et la segmentation est générée en fonction des coordonnées de votre clic.
La logique de traitement peut être divisée en cinq étapes
La logique du code est la suivante, React TS , voir le code source de mon projet pour plus de détails, le code source se trouve dans src/components/boardOperation/uploadImage/imageSegmentation.tsx
importer { useState, useRef, useEffect, useMemo, MouseEvent, FC } depuis 'react' importer { SamModèle, Autoprocesseur, Image brute, Modèle pré-entraîné, Processeur, Tenseur, SamImageProcessorResult } de '@huggingface/transformers' importer LoadingIcon depuis '@/components/icons/loading.svg?react' importer PositiveIcon depuis '@/components/icons/boardOperation/image-segmentation-positive.svg?react' importer NegativeIcon depuis '@/components/icons/boardOperation/image-segmentation-negative.svg?react' interface MarkPoint { poste : numéro[] étiquette : numéro } const SEGMENTATION_STATUS = { CHARGEMENT : 0, NO_SUPPORT_WEBGPU : 1, LOAD_ERROR : 2, CHARGE_SUCCÈS : 3, TRAITEMENT: 4, PROCESSING_SUCCESS : 5 } tapez SegmentationStatusType = (type de SEGMENTATION_STATUS)[clé de type de SEGMENTATION_STATUS] const ImageSegmentation : FC<{ url : string }> = ({url}) => { const [markPoints, setMarkPoints] = useState<MarkPoint[]>([]) const [segmentationStatus, setSegmentationStatus] = useState<SegmentationStatusType>() const [pointStatus, setPointStatus] = useState<boolean>(true) const maskCanvasRef = useRef<HTMLCanvasElement>(null) // Masque de segmentation const modelRef = useRef<PreTrainedModel>() // modèle const processeurRef = useRef<Processor>() // processeur const imageInputRef = useRef<RawImage>() // image originale const imageProcessed = useRef<SamImageProcessorResult>() // Image traitée const imageEmbeddings = useRef<Tensor>() // Incorporation de données const segmentationTip = useMemo(() => { commutateur (segmentationStatus) { cas SEGMENTATION_STATUS.LOADING : return 'Fonction de segmentation d'image Chargement' cas SEGMENTATION_STATUS.NO_SUPPORT_WEBGPU : return 'WebGPU n'est pas pris en charge dans ce navigateur, pour utiliser la fonction de segmentation d'image, veuillez utiliser la dernière version de Google Chrome.' cas SEGMENTATION_STATUS.LOAD_ERROR : return 'La fonction de segmentation d'image n'a pas pu être chargée' cas SEGMENTATION_STATUS.LOAD_SUCCESS : return 'Fonction de segmentation d'image chargée avec succès' cas SEGMENTATION_STATUS.PROCESSING : return 'Traitement d'image...' cas SEGMENTATION_STATUS.PROCESSING_SUCCESS : return 'L'image a été traitée avec succès, vous pouvez cliquer sur l'image pour la marquer, la zone du masque vert est la zone de segmentation.' défaut: retour '' } }, [état de segmentation]) // 1. charger le modèle et le processeur useEffect(() => { ;(async () => { essayer { si (segmentationStatus === SEGMENTATION_STATUS.LOADING) { retour } setSegmentationStatus(SEGMENTATION_STATUS.LOADING) si (!navigateur?.gpu) { setSegmentationStatus(SEGMENTATION_STATUS.NO_SUPPORT_WEBGPU) retour }const model_id = 'Xenova/slimsam-77-uniform' modelRef.current ??= attendre SamModel.from_pretrained(model_id, { dtype : 'fp16', // ou "fp32" périphérique : 'webgpu' }) processeurRef.current ??= attendre AutoProcessor.from_pretrained(model_id) setSegmentationStatus(SEGMENTATION_STATUS.LOAD_SUCCESS) } attraper (erreur) { console.log('erreur', erreur) setSegmentationStatus(SEGMENTATION_STATUS.LOAD_ERROR) } })() }, []) // 2. image de processus useEffect(() => { ;(async () => { essayer { si ( !modelRef.current || !processorRef.current || !URL || segmentationStatus === SEGMENTATION_STATUS.PROCESSING ) { retour } setSegmentationStatus(SEGMENTATION_STATUS.PROCESSING) points clairs() imageInputRef.current = attendre RawImage.fromURL(url) imageProcessed.current = attendre processeurRef.current ( imageInputRef.current ) imageEmbeddings.current = attendre ( modelRef.current comme n'importe quel autre ).get_image_embeddings(imageProcessed.current) setSegmentationStatus(SEGMENTATION_STATUS.PROCESSING_SUCCESS) } attraper (erreur) { console.log('erreur', erreur) } })() }, [url, modelRef.current, processeurRef.current]) // Mise à jour de l'effet masque function updateMaskOverlay (masque : RawImage, scores : Float32Array) { const masqueCanvas = masqueCanvasRef.current si (!maskCanvas) { retour } const maskContext = maskCanvas.getContext('2d') comme CanvasRenderingContext2D // Mettre à jour les dimensions du canevas (si différentes) if (maskCanvas.width !== masque.width || maskCanvas.height !== masque.height) { masqueCanvas.width = masque.largeur masqueCanvas.hauteur = masque.hauteur } // Allouer un tampon pour les données de pixels const imageData = masqueContext.createImageData( masqueCanvas.largeur, masqueCanvas.hauteur ) // Sélectionnez le meilleur masque const numMasks = scores.length // 3 laissez bestIndex = 0 pour (soit i = 1 ; i < numMasks ; i) { si (scores[i] > scores[bestIndex]) { meilleurIndex = je } } // Remplir le masque de couleur const pixelData = imageData.data pour (soit i = 0; i < pixelData.length; i) { if (mask.data[numMasks * i bestIndex] === 1) { décalage const = 4 * je pixelData[offset] = 101 // r pixelData[offset 1] = 204 // g pixelData[offset 2] = 138 // b pixelData[offset 3] = 255 // un } } // Dessine les données de l'image dans le contexte masqueContext.putImageData(imageData, 0, 0) } // 3. Décodage basé sur les données de clic const decode = async (markPoints : MarkPoint[]) => { si ( !modelRef.current || !imageEmbeddings.current || !processorRef.current || !imageProcessed.current ) { retour }// Aucun clic sur les données efface directement l'effet de segmentation if (!markPoints.length && maskCanvasRef.current) { const masqueContext = masqueCanvasRef.current.getContext( '2j' ) comme CanvasRenderingContext2D masqueContext.clearRect( 0, 0, masqueCanvasRef.current.width, masqueCanvasRef.current.height ) retour } // Préparer les entrées pour le décodage const reshaped = imageProcessed.current.reshape_input_sizes[0] const points = markPoints .map((x) => [x.position[0] * remodelé[1], x.position[1] * remodelé[0]]) .flat(Infini) const labels = markPoints.map((x) => BigInt(x.label)).flat(Infinity) const num_points = markPoints.longueur const input_points = new Tensor('float32', points, [1, 1, num_points, 2]) const input_labels = new Tensor('int64', labels, [1, 1, num_points]) // Génère le masque const { pred_masks, iou_scores } = attendre modelRef.current ({ ...imageEmbeddings.current, points d'entrée, étiquettes_d'entrée }) // Post-traiter le masque const masques = attendre (processorRef.current comme n'importe quel).post_process_masks( pred_masks, imageProcessed.current.original_sizes, imageProcessed.current.reshape_input_sizes ) updateMaskOverlay(RawImage.fromTensor(masks[0][0]), iou_scores.data) } const clamp = (x : nombre, min = 0, max = 1) => { retourner Math.max(Math.min(x, max), min) } const clickImage = (e: MouseEvent) => { si (segmentationStatus !== SEGMENTATION_STATUS.PROCESSING_SUCCESS) { retour } const { clientX, clientY, currentTarget } = e const { gauche, haut } = currentTarget.getBoundingClientRect() const x = pince ( (clientX - gauche currentTarget.scrollLeft) / currentTarget.scrollWidth ) const y = pince ( (clientY - haut currentTarget.scrollTop) / currentTarget.scrollHeight ) const existantPointIndex = markPoints.findIndex( (point) => Math.abs(point.position[0] - x) < 0,01 && Math.abs(point.position[1] - y) < 0,01 && point.label === (pointStatus ? 1 : 0) ) const nouveauxPoints = [...markPoints] si (existingPointIndex !== -1) { // S'il y a un marqueur dans la zone actuellement cliquée, il est supprimé. newPoints.splice (existingPointIndex, 1) } autre { nouveauxPoints.push({ position : [x, y], étiquette : pointStatus ? 1 : 0 }) } setMarkPoints (nouveaux Points) décoder (nouveaux points) } const clearPoints = () => { setMarkPoints([]) décoder([]) } retour ( <div className="card shadow-xl overflow-auto"> <div className="flex items-center gap-x-3"> <button className="btn btn-primary btn-sm" onClick={clearPoints}> Effacer les points </bouton> <bouton className="btn btn-primaire btn-sm" onClick={() => setPointStatus(vrai)} > {statutpoint ? 'Positif' : 'Négatif'} </bouton> </div> <div className="text-xs text-base-content mt-2">{segmentationTip}</div> <div > <h2> Conclusion </h2> <p>Merci d'avoir lu. C'est tout le contenu de cet article, j'espère que cet article vous sera utile, bienvenue pour aimer et mettre en favori. Si vous avez des questions, n'hésitez pas à en discuter dans la section commentaires !</p>
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!