Compare commits

...
Author SHA1 Message Date
Brandon Thomas be184d740c remove background: frontend UX 2025-03-20 01:40:17 -04:00
12 changed files with 206 additions and 55 deletions
@@ -32,6 +32,7 @@ pub async fn infer_image(
app_data_root: State<'_, AppDataRoot>,
app: AppHandle,
) -> Result<String, String> {
info!("infer_image endpoint called.");
let prompt = get_prompt_or_fallback(prompt);
@@ -6,7 +6,7 @@ use crate::state::app_dir::AppDataRoot;
use crate::utils::image::decode_base64_image::decode_base64_image;
use crate::utils::image::encode_dynamic_image_base64_png::encode_dynamic_image_base64_png;
use image::{imageops, DynamicImage};
use log::error;
use log::{error, info};
use ndarray::{Array, CowArray};
use ort::Value;
use tauri::{AppHandle, State};
@@ -19,6 +19,7 @@ pub async fn remove_background(
app_data_root: State<'_, AppDataRoot>,
app: AppHandle,
) -> Result<String, String> {
info!("remove_background endpoint called.");
let image = decode_base64_image(image)
.map_err(|err| format!("Couldn't hydrate image from base64: {}", err))?;
+72 -19
View File
@@ -1,35 +1,36 @@
import Konva from "konva";
import { uiAccess, uiEvents } from "~/signals";
import { ShapeNode } from "./Nodes";
import { PaintNode } from "./Nodes/PaintNode";
import { PreviewCopyNode } from "./Nodes/PreviewCopy";
import { UndoStackManager } from "./UndoRedo";
import { CommandManager, MatteBox, SceneManager } from "./EngineUtitlities";
import {uiAccess, uiEvents} from "~/signals";
import {ImageNode, ShapeNode, ShapeType, TextNode, VideoNode} from "./Nodes";
import {PaintNode} from "./Nodes/PaintNode";
import {PreviewCopyNode} from "./Nodes/PreviewCopy";
import {UndoStackManager} from "./UndoRedo";
import {CommandManager, MatteBox, SceneManager} from "./EngineUtitlities";
import {
NodesManager,
NodeIsolator,
NodeTransformer,
NodesManager,
NodesTranslationEventDetails,
NodeTransformationEventDetails,
NodeTransformer,
SelectionManager,
SelectionManagerEvents,
SelectorSquare,
} from "./NodesManagers";
import { ImageNode, VideoNode, TextNode } from "./Nodes";
import { EngineOptions, TextNodeData, VideoNodeData } from "./types";
import {EngineOptions, TextNodeData, VideoNodeData} from "./types";
import { AppModes, VideoResolutions } from "./constants";
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
import {AppModes, VideoResolutions} from "./constants";
import {ToolbarMainButtonNames} from "~/components/features/ToolbarMain/enum";
import { ToolbarNodeButtonNames } from "~/components/features/ToolbarNode/enums";
import { NavigateFunction } from "react-router-dom";
import { LoadingVideosProvider } from "./EngineUtitlities/LoadingVideosProvider";
import {ToolbarNodeButtonNames} from "~/components/features/ToolbarNode/enums";
import {NavigateFunction} from "react-router-dom";
import {LoadingVideosProvider} from "./EngineUtitlities/LoadingVideosProvider";
import { VideoExtractionHandler } from "./EngineUtitlities/VideoExtractionHandler/VideoExtractionHandler";
import { RealTimeDrawEngine } from "./RenderingPrimitives/RealTimeDrawEngine";
import { NodeColor } from "~/signals/uiEvents/toolbarNode";
import { ShapeType } from "./Nodes";
import {VideoExtractionHandler} from "./EngineUtitlities/VideoExtractionHandler/VideoExtractionHandler";
import {RealTimeDrawEngine} from "./RenderingPrimitives/RealTimeDrawEngine";
import {NodeColor} from "~/signals/uiEvents/toolbarNode";
import {invoke} from "@tauri-apps/api/core";
import {DecodeBase64ToImage} from "~/utilities/DecodeBase64ToImage.ts";
import {EncodeImageBitmapToBase64} from "~/utilities/EncodeImageBitmapToBase64.ts";
export interface RenderingOptions {
@@ -326,6 +327,9 @@ export class Engine {
uiEvents.toolbarNode.MOVE_LAYER_UP.onClick(() =>
this.commandManager.moveNodesUp(),
);
uiEvents.toolbarNode.REMOVE_BACKGROUND.onClick(() =>
this.removeBackground(),
);
// Listen to Toolbar Main
uiEvents.toolbarMain.UNDO.onClick(() => {
@@ -445,6 +449,38 @@ export class Engine {
});
}
removeBackground() {
const selectedNodeSet = this.selectionManager.getSelectedNodes();
if (selectedNodeSet.size !== 1) {
return; // Can only remove background from a single image at a time.
}
const [ selectedNode ] = selectedNodeSet;
const removeBgInvocation
= async (selectedNode: ImageNode, callback: (existingImageNode: ImageNode, createdImage: ImageBitmap) => void) => {
const image : HTMLImageElement = selectedNode.kNode.image();
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const base64BitmapResponse = await invoke("remove_background", {
image: base64Bitmap,
});
const createdImage = await DecodeBase64ToImage(base64BitmapResponse as string);
callback(selectedNode, createdImage);
};
const _promise = removeBgInvocation(selectedNode, (selectedNode: ImageNode, createdImage: ImageBitmap) => {
if (!this.nodesManager.hasNode(selectedNode)) {
return;
}
this.commandManager.deleteSpecificNodes([selectedNode]);
this.addImageFromImageBitmap(createdImage);
})
}
disableAllButtons() {
const buttonNames = Object.values(ToolbarNodeButtonNames);
for (const name of buttonNames) {
@@ -624,6 +660,23 @@ export class Engine {
this.commandManager.createNode(imageNode);
}
public addImageFromImageBitmap(imageBitmap: ImageBitmap) {
const imageNode = new ImageNode({
mediaLayerRef: this.mediaLayer,
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
imageBitmap: imageBitmap,
selectionManagerRef: this.selectionManager,
loaded: async () => {
await this.realTimeDrawEngine.render();
this.setAppMode(AppModes.SELECT);
},
});
this.commandManager.createNode(imageNode);
}
public addPaintNode(
canvas: HTMLCanvasElement,
lineBounds: {
@@ -78,6 +78,20 @@ export class CommandManager {
this.undoStackManagerRef.executeCommand(command);
this.renderEngineRef.render();
}
deleteSpecificNodes(nodes: MediaNode[]) {
const command = new DeleteCommand({
nodes: nodes,
mediaLayerRef: this.mediaLayerRef,
nodesManagerRef: this.nodesManagerRef,
nodeTransformerRef: this.nodeTransformerRef,
selectionManagerRef: this.selectionManagerRef,
renderEngineRef: this.renderEngineRef,
});
this.undoStackManagerRef.executeCommand(command);
this.renderEngineRef.render();
}
toggleLockNodes() {
const nodes = this.selectionManagerRef.getSelectedNodes();
const node = nodes.values().next().value;
@@ -12,6 +12,7 @@ interface ImageNodeContructor {
canvasPosition: Position;
canvasSize: Size;
imageFile?: File;
imageBitmap?: ImageBitmap;
mediaFileToken?: string;
mediaFileUrl?: string;
transform?: TransformationData;
@@ -34,6 +35,7 @@ export class ImageNode extends NetworkedNode {
mediaLayerRef,
selectionManagerRef,
loaded,
imageBitmap,
}: ImageNodeContructor) {
// kNodes need to be created first to guaruntee
// that it is not undefined in parent's context
@@ -73,6 +75,14 @@ export class ImageNode extends NetworkedNode {
this.loadImageFromUrl(mediaFileUrl);
return;
}
if (imageBitmap) {
this.loadImageFromImageBitmap({
imageBitmap: imageBitmap,
maxSize: canvasSize,
refPosition: canvasPosition,
});
return;
}
console.log("image node creation is fucked");
}
private loadImageFromFile({
@@ -136,6 +146,40 @@ export class ImageNode extends NetworkedNode {
this.listenToBaseKNode();
}
private loadImageFromImageBitmap({
imageBitmap,
maxSize,
refPosition,
}: {
imageBitmap: ImageBitmap;
maxSize: Size;
refPosition: Position;
}) {
this.imageSize = {
width: imageBitmap.width,
height: imageBitmap.height,
};
const adjustedSize = NodeUtilities.adjustNodeSizeToCanvas({
componentSize: this.imageSize,
maxSize: maxSize, // TODO
});
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
canvasOffset: refPosition, // TODO
componentSize: adjustedSize, // TODO
maxSize: maxSize, // TODO
});
this.kNode.image(imageBitmap);
this.kNode.setSize(adjustedSize);
this.kNode.setPosition(centerPosition);
this.kNode.moveToTop();
this.kNode.fill(transparent);
this.listenToBaseKNode();
this.mediaLayerRef.draw();
//this.uploadImage(imageFile);
this.loaded();
this.listenToBaseKNode();
}
private async uploadImage(imageFile: File) {
//this.setProgress({ progress: 10, status: UploadStatus.UPLOADING });
@@ -44,6 +44,10 @@ export class NodesManager {
// this.printNodesAndZIndices();
}
public hasNode(node: MediaNode): boolean {
return this.allNodes.has(node);
}
/*************************
* For Debugging use
*/
@@ -94,7 +94,8 @@ export function getTextNodeButtonStates(
buttonName === ButtonNames.AI_STYLIZE ||
buttonName === ButtonNames.SEGMENTATION ||
buttonName === ButtonNames.DOWNLOAD ||
buttonName === ButtonNames.CHROMA
buttonName === ButtonNames.CHROMA ||
buttonName === ButtonNames.REMOVE_BACKGROUND
) {
buttonStates[buttonName] = {
disabled: true,
@@ -132,7 +133,8 @@ export function getVideoNodeButtonStates(
}
// soon to come feature is disabled
if (buttonName === ButtonNames.AI_STYLIZE ||
buttonName === ButtonNames.COLOR
buttonName === ButtonNames.COLOR ||
buttonName === ButtonNames.REMOVE_BACKGROUND
) {
buttonStates[buttonName] = {
disabled: true,
@@ -174,7 +176,8 @@ export function getShapeNodeButtonStates(
buttonName === ButtonNames.AI_STYLIZE ||
buttonName === ButtonNames.SEGMENTATION ||
buttonName === ButtonNames.DOWNLOAD ||
buttonName === ButtonNames.CHROMA
buttonName === ButtonNames.CHROMA ||
buttonName === ButtonNames.REMOVE_BACKGROUND
) {
buttonStates[buttonName] = {
disabled: true,
@@ -218,7 +221,8 @@ export function getMultiSelectButtonStates(
buttonName === ButtonNames.SEGMENTATION ||
buttonName === ButtonNames.DOWNLOAD ||
buttonName === ButtonNames.CHROMA ||
buttonName === ButtonNames.COLOR
buttonName === ButtonNames.COLOR ||
buttonName === ButtonNames.REMOVE_BACKGROUND
) {
buttonStates[buttonName] = {
disabled: true,
@@ -25,6 +25,7 @@ import {
isLoadingVisible,
loadingProgress,
} from "~/signals/uiEvents/loadingIndicator";
import {DecodeBase64ToImage} from "~/utilities/DecodeBase64ToImage.ts";
interface ServerSettings {
model_path: string;
@@ -285,7 +286,7 @@ export class RealTimeDrawEngine {
this.client.onResult(async (response) => {
try {
// Convert the base64 image to ImageBitmap and update the preview
const bitmap = await this.base64ToImageBitmap(response.image);
const bitmap = await DecodeBase64ToImage(response.image);
this.outputBitmap = bitmap;
this.previewCanvas.image(bitmap);
this.mediaLayerRef.batchDraw();
@@ -895,35 +896,6 @@ export class RealTimeDrawEngine {
return base64String.split(",")[1];
}
private async base64ToImageBitmap(
base64String: string,
): Promise<ImageBitmap> {
// Create an image element
const img = document.createElement("img");
// Convert base64 to data URL if it doesn't include the prefix
const dataUrl = base64String.startsWith("data:")
? base64String
: `data:image/png;base64,${base64String}`;
// Create a promise to handle the image loading
return new Promise((resolve, reject) => {
img.onload = async () => {
try {
const bitmap = await createImageBitmap(img);
resolve(bitmap);
} catch (error) {
reject(error);
}
};
img.onerror = () => reject(new Error("Failed to load image"));
// Set the source to trigger loading
img.src = dataUrl;
});
}
public async saveOutput() {
if (!this.outputBitmap) {
console.error("No output bitmap available to save");
@@ -1007,7 +979,7 @@ export class RealTimeDrawEngine {
});
//console.log(base64BitmapResponse);
const decoded = await this.base64ToImageBitmap(
const decoded = await DecodeBase64ToImage(
base64BitmapResponse as string,
);
@@ -22,6 +22,11 @@ export const ToolbarNodeButtonData = [
icon: faPalette,
tooltip: "Color",
},
{
name: ButtonNames.REMOVE_BACKGROUND,
icon: faScalpelLineDashed,
tooltip: "Remove Background",
},
{
name: ButtonNames.CHROMA,
icon: faTransporter,
@@ -8,4 +8,5 @@ export enum ToolbarNodeButtonNames {
MOVE_LAYER_DOWN = "MOVE_LAYER_DOWN",
DOWNLOAD = "DOWNLOAD",
COLOR = "COLOR",
REMOVE_BACKGROUND = "REMOVE_BACKGROUND",
}
@@ -0,0 +1,30 @@
/**
* This decodes Base64 encoded PNG images into an image element.
* @param base64String a standard (not "web safe") base64-encoded string
*/
export const DecodeBase64ToImage = async (base64String: string) : Promise<ImageBitmap> => {
// Create an image element
const img = document.createElement("img");
// Convert base64 to data URL if it doesn't include the prefix
const dataUrl = base64String.startsWith("data:")
? base64String
: `data:image/png;base64,${base64String}`;
// Create a promise to handle the image loading
return new Promise((resolve, reject) => {
img.onload = async () => {
try {
const bitmap = await createImageBitmap(img);
resolve(bitmap);
} catch (error) {
reject(error);
}
};
img.onerror = () => reject(new Error("Failed to load image"));
// Set the source to trigger loading
img.src = dataUrl;
});
}
@@ -0,0 +1,22 @@
export const EncodeImageBitmapToBase64 = async (imageBitmap: ImageBitmap): Promise<string> => {
// Create a temporary canvas
const canvas = document.createElement("canvas");
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
// Draw the ImageBitmap onto the canvas
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Failed to get 2D context");
ctx.drawImage(imageBitmap, 0, 0);
// Convert to base64
const base64String = canvas.toDataURL("image/png");
// Clean up
canvas.remove();
// Remove the data:image/png;base64, prefix if you want just the base64 string
return base64String.split(",")[1];
}