diff options
-rw-r--r-- | src/.DS_Store | bin | 10244 -> 10244 bytes | |||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 6 | ||||
-rw-r--r-- | src/client/util/Import & Export/ImageUtils.ts | 46 | ||||
-rw-r--r-- | src/client/views/global/globalScripts.ts | 10 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 49 | ||||
-rw-r--r-- | src/workers/image.worker.ts | 41 |
6 files changed, 119 insertions, 33 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex a3c57385c..ffaca7e1b 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 9fbc82bef..79942d7ee 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -834,8 +834,10 @@ pie title Minerals in my tap water } static imageTools() { return [ - { title: "Pixels",toolTip: "Set Native Pixel Size", btnType: ButtonType.ClickButton, icon: "portrait", scripts: { onClick: 'imageSetPixelSize();' }}, - { title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }}, + { title: "Pixels", toolTip: "Set Native Pixel Size", btnType: ButtonType.ClickButton, icon: "portrait", scripts: { onClick: 'imageSetPixelSize();' }}, + { title: "Rotate", toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }}, + { title: "NoBkgd", toolTip: "Remove Background", btnType: ButtonType.ClickButton, icon: "portrait", scripts: { onClick: 'imageRemoveBackground();' }}, + { title: "MaskFgd",toolTip: "Mask Foreground", btnType: ButtonType.ClickButton, icon: "portrait", scripts: { onClick: 'imageMaskForeground();' }}, ]; } static contextMenuTools():Button[] { diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 23102e051..72c1b468a 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -42,14 +42,14 @@ export namespace ImageUtils { reader.readAsDataURL(blob as Blob); }); } - export function createImageDocFromBlob(blob: Blob | undefined, options: DocumentOptions & { _nativeWidth: number; _nativeHeight: number }, filename: string): Promise<Doc> { + export function createImageDocFromBlob(blob: Blob | undefined, options: DocumentOptions & { _nativeWidth: number; _nativeHeight: number }, filename: string, overwriteDoc?: Doc): Promise<Doc> { return new Promise((resolve, reject) => { if (!blob) return reject('No image blob provided'); convertImgBlobToDataURL(blob) .then(durl => { ClientUtils.convertDataUri(durl as string, filename) .then(url => { - const imageSnapshot = Docs.Create.ImageDocument(url, options); + const imageSnapshot = Docs.Create.ImageDocument(url, options, overwriteDoc); Doc.SetNativeWidth(imageSnapshot[DocData], options._nativeWidth); Doc.SetNativeHeight(imageSnapshot[DocData], options._nativeHeight); resolve(imageSnapshot); @@ -71,9 +71,14 @@ export namespace ImageUtils { workerCallbackMap.delete(docId); // worker.terminate(); }; backgroundRemovalWorker.onmessage = async (event: MessageEvent) => { - const map = workerCallbackMap.get(event.data.docId); - event.data.success ? map?.res(event.data.result) : map?.rej(); - workerCallbackMap.delete(event.data.docId); // worker.terminate(); + if (event.data.type === 'progress') { + // Handle progress updates if needed + console.log(`Progress for docId ${event.data.docId}: ${event.data.progress}`); + } else { + const map = workerCallbackMap.get(event.data.docId); + event.data.success ? map?.res(event.data.result) : map?.rej(); + workerCallbackMap.delete(event.data.docId); // worker.terminate(); + } }; return backgroundRemovalWorker; } @@ -81,7 +86,36 @@ export namespace ImageUtils { return new Promise<Blob | undefined>((res, rej) => { if (!imagePath) return rej('No image path provided'); workerCallbackMap.set(docId, { res, rej }); // Store the callback by docId (or use a unique requestId) - getBackgroundRemovalWorker().postMessage({ imagePath, docId }); + getBackgroundRemovalWorker().postMessage({ + imagePath, + docId, + config: { + output: { + quality: 0.8, // The quality. (Default: 0.8) + }, + }, + }); + }); + } + export function maskForeground(docId: string, imagePath: string) { + return new Promise<Blob | undefined>((res, rej) => { + if (!imagePath) return rej('No image path provided'); + workerCallbackMap.set(docId, { res, rej }); // Store the callback by docId (or use a unique requestId) + getBackgroundRemovalWorker().postMessage({ + imagePath, + docId, + config: { + //publicPath: string; // The public path used for model and wasm files. Default: 'https://staticimgly.com/${PACKAGE_NAME}-data/${PACKAGE_VERSION}/dist/' + //debug: bool; // enable or disable useful console.log outputs + //device: 'gpu', // 'cpu' | 'gpu'; // choose the execution device. gpu will use webgpu if available + //model: 'isnet' | 'isnet_fp16' | 'isnet_quint8'; // The model to use. (Default "isnet_fp16") + output: { + //format: 'image/png' | 'image/jpeg' | 'image/webp'; // The output format. (Default "image/png") + quality: 0.8, // The quality. (Default: 0.8) + type: 'mask', // 'foreground' | 'background' | 'mask'; // The output type. (Default "foreground") + }, + }, + }); }); } } diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index e098d50d8..981c4d111 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -553,6 +553,16 @@ ScriptingGlobals.add(function videoSnapshot() { }); // eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function imageMaskForeground() { + const selected = DocumentView.Selected().lastElement()?.ComponentView as ImageBox; + selected?.maskForeground(); +}); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function imageRemoveBackground() { + const selected = DocumentView.Selected().lastElement()?.ComponentView as ImageBox; + selected?.removeBackground(); +}); +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function imageSetPixelSize() { const selected = DocumentView.Selected().lastElement()?.ComponentView as ImageBox; selected?.setNativeSize(); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index e62718ec4..66ab858e2 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -389,27 +389,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._props.bringToFront?.(cropping); return cropping; }; - removeBackground = () => { - const batch = UndoManager.StartBatch('remove image background'); - ImageUtils.removeImgBackground(this.Document[Id], this.choosePath(ImageCast(this.dataDoc[this.fieldKey])?.url)).then(imgBlob => - ImageUtils.createImageDocFromBlob( - imgBlob, - { - _nativeWidth: Doc.NativeWidth(this.Document), - _nativeHeight: Doc.NativeHeight(this.Document), - x: NumCast(this.Document.x) + NumCast(this.Document._width), - y: NumCast(this.Document.y), - _width: NumCast(this.Document._width), - _height: (NumCast(this.Document._height) / (NumCast(this.Document._width) || 1)) * NumCast(this.Document._width), - title: 'bgdRemoved:' + this.Document.title, - }, - this.Document[Id] + '_noBgd' - ).then(imageSnapshot => { - this._props.addDocument?.(imageSnapshot); - batch.end(); - }) - ); + createLoadingDoc = () => { + const loading = Docs.Create.LoadingDocument('background removed', { + x: NumCast(this.Document.x) + NumCast(this.Document._width), + y: NumCast(this.Document.y), + backgroundColor: 'transparent', + _width: NumCast(this.Document._width), + _height: (NumCast(this.Document._height) / (NumCast(this.Document._width) || 1)) * NumCast(this.Document._width), + title: 'bgdRemoved:' + this.Document.title, + }); + Doc.addCurrentlyLoading(loading); + this._props.addDocument?.(loading); + return loading; }; + replaceImage = (imgBlob: Blob | undefined, loading: Doc) => { + const batch2 = UndoManager.StartBatch('remove mask background'); + ImageUtils.createImageDocFromBlob(imgBlob, { _nativeWidth: Doc.NativeWidth(this.Document), _nativeHeight: Doc.NativeHeight(this.Document) }, this.Document[Id] + '_fgdMask', loading).then(() => { + Doc.removeCurrentlyLoading(loading); + batch2.end(); + }); + }; + removeBackground = undoable(() => { + const loading = this.createLoadingDoc(); + ImageUtils.removeImgBackground(this.Document[Id], this.choosePath(ImageCast(this.dataDoc[this.fieldKey])?.url)).then(imageBlob => this.replaceImage(imageBlob, loading)); + }, 'create image background placeholder'); + + maskForeground = undoable(() => { + const loading = this.createLoadingDoc(); + ImageUtils.maskForeground(this.Document[Id], this.choosePath(ImageCast(this.dataDoc[this.fieldKey])?.url)).then(imageBlob => this.replaceImage(imageBlob, loading)); + }, 'create image mask placeholder'); docEditorView = () => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); @@ -659,6 +667,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' }); funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' }); funcs.push({ description: 'Remove Background', event: this.removeBackground, icon: 'pencil-alt' }); + funcs.push({ description: 'Mask Foreground', event: this.maskForeground, icon: 'pencil-alt' }); this.layoutDoc.ai && funcs.push({ description: 'Regenerate AI Image', diff --git a/src/workers/image.worker.ts b/src/workers/image.worker.ts index 48b4e8585..eb878e336 100644 --- a/src/workers/image.worker.ts +++ b/src/workers/image.worker.ts @@ -1,14 +1,45 @@ -import { removeBackground } from '@imgly/background-removal'; +import { removeBackground, segmentForeground, preload, Config } from '@imgly/background-removal'; + +// Preload the model when the worker starts +let modelLoaded = false; +preload() + .then(() => { + modelLoaded = true; + console.log('Background removal model preloaded successfully.'); + }) + .catch(error => { + console.error('Failed to preload background removal model:', error); + }); self.onmessage = async (event: MessageEvent) => { - const { imagePath, options, nativeWidth, nativeHeight, docId } = event.data; + const { imagePath, config, docId } = event.data; + const configWithDefaults: Config = { + ...config, + debug: true, + // You can set default config values here if needed + onProgress: (progress: number) => { + console.log('Progress: ' + progress); + // Send progress updates to the main thread + self.postMessage({ type: 'progress', docId, progress }); + }, + }; try { - // Perform the background removal - const result = await removeBackground(imagePath); + // Ensure the model is preloaded before processing + if (!modelLoaded) { + await preload(configWithDefaults); + modelLoaded = true; + } + // Simulate progress updates (if the library doesn't provide them natively) + self.postMessage({ type: 'progress', docId, progress: 0 }); + + const resultProm = + config.output.type === 'mask' + ? segmentForeground(imagePath, configWithDefaults) // + : removeBackground(imagePath, configWithDefaults); // Send the result back to the main thread - self.postMessage({ success: true, result, options, nativeWidth, nativeHeight, docId }); + self.postMessage({ success: true, result: await resultProm, docId }); } catch (error) { // Send the error back to the main thread // eslint-disable-next-line @typescript-eslint/no-explicit-any |