diff --git a/backend/app/routers/tools.py b/backend/app/routers/tools.py index b455fa6..61ddf19 100644 --- a/backend/app/routers/tools.py +++ b/backend/app/routers/tools.py @@ -33,6 +33,10 @@ class InpaintRequest(BaseModel): guidance_scale: Optional[float] = 7.5 +class RemoveBackgroundRequest(BaseModel): + image: str # Base64 encoded image + + @router.post("/smart-select-base64") async def smart_select_base64(request: SmartSelectRequest): """ @@ -124,6 +128,46 @@ async def inpaint_base64(request: InpaintRequest): raise HTTPException(status_code=500, detail=str(e)) +@router.post("/remove-background-base64") +async def remove_background_base64(request: RemoveBackgroundRequest): + """ + Remove background from a base64 encoded image using rembg. + Returns base64 encoded PNG with transparent background. + Used by miniPaint frontend. + """ + try: + from rembg import remove + except ImportError: + raise HTTPException( + status_code=500, + detail="rembg not installed. Run: pip install rembg" + ) + + try: + # Decode base64 image + image_bytes = base64.b64decode(request.image) + + # Remove background + result_bytes = remove(image_bytes) + + # Convert result to base64 + result_b64 = base64.b64encode(result_bytes).decode('utf-8') + + # Get dimensions + img = Image.open(BytesIO(result_bytes)) + + return { + "result": result_b64, + "width": img.width, + "height": img.height + } + + except Exception as e: + import traceback + traceback.print_exc() + raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/remove-background") async def remove_background( project_id: Optional[int] = Form(None), diff --git a/frontend/src/js/config-menu.js b/frontend/src/js/config-menu.js index a5e6b3f..eb76ec6 100644 --- a/frontend/src/js/config-menu.js +++ b/frontend/src/js/config-menu.js @@ -283,6 +283,14 @@ const menuDefinition = [ name: 'Histogram', ellipsis: true, target: 'image/histogram.histogram' + }, + { + divider: true + }, + { + name: 'Remove Background (AI)', + ellipsis: true, + target: 'image/remove_background.remove_background' } ] }, diff --git a/frontend/src/js/modules/image/remove_background.js b/frontend/src/js/modules/image/remove_background.js new file mode 100644 index 0000000..64e3050 --- /dev/null +++ b/frontend/src/js/modules/image/remove_background.js @@ -0,0 +1,145 @@ +/** + * Remove Background Module - Uses AI to remove background and create transparent layer + */ + +import app from './../../app.js'; +import config from './../../config.js'; +import Base_layers_class from './../../core/base-layers.js'; +import Dialog_class from './../../libs/popup.js'; +import Helper_class from './../../libs/helpers.js'; +import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js'; +import apiService from './../../services/api.js'; + +var instance = null; + +class Image_remove_background_class { + + constructor() { + // Singleton + if (instance) { + return instance; + } + instance = this; + + this.Base_layers = new Base_layers_class(); + this.Helper = new Helper_class(); + this.Dialog = new Dialog_class(); + this.isProcessing = false; + } + + /** + * Remove background from current layer + * Creates a new layer with transparent background + */ + async remove_background() { + var _this = this; + + if (this.isProcessing) { + alertify.warning('Already processing... please wait'); + return; + } + + // Check if current layer is an image + if (config.layer.type != 'image') { + alertify.error('Current layer must be an image'); + return; + } + + var settings = { + title: 'Remove Background', + params: [ + { name: "info", title: "AI will detect the main subject and remove the background.", type: "label" }, + { name: "new_layer", title: "Create as new layer:", value: true }, + { name: "trim_result", title: "Trim transparent edges:", value: false }, + ], + on_finish: async function (params) { + await _this.do_remove_background(params); + }, + }; + this.Dialog.show(settings); + } + + async do_remove_background(params) { + this.isProcessing = true; + alertify.message('AI is removing background... this may take a moment'); + + try { + // Get current layer image as base64 + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + canvas.width = config.layer.width_original; + canvas.height = config.layer.height_original; + ctx.drawImage(config.layer.link, 0, 0); + + var imageData = canvas.toDataURL('image/png').split(',')[1]; + + // Call backend API + var result = await apiService.removeBackground(imageData); + + // Create image from result + var resultImage = new Image(); + resultImage.onload = () => { + if (params.new_layer) { + // Create as new layer + var layerParams = { + x: config.layer.x, + y: config.layer.y, + width: resultImage.width, + height: resultImage.height, + width_original: resultImage.width, + height_original: resultImage.height, + type: 'image', + name: config.layer.name + ' (No BG)', + data: 'data:image/png;base64,' + result.result + }; + + app.State.do_action( + new app.Actions.Bundle_action('remove_background', 'Remove Background', [ + new app.Actions.Insert_layer_action(layerParams) + ]) + ); + + alertify.success('Background removed! New layer created.'); + } else { + // Replace current layer + var newCanvas = document.createElement('canvas'); + newCanvas.width = resultImage.width; + newCanvas.height = resultImage.height; + var newCtx = newCanvas.getContext('2d'); + newCtx.drawImage(resultImage, 0, 0); + + app.State.do_action( + new app.Actions.Bundle_action('remove_background', 'Remove Background', [ + new app.Actions.Update_layer_image_action(newCanvas, config.layer.id) + ]) + ); + + alertify.success('Background removed!'); + } + + // Enable transparency if not already + if (config.TRANSPARENCY == false) { + config.TRANSPARENCY = true; + this.Base_layers.render(); + alertify.message('Transparency enabled to show removed background'); + } + + this.isProcessing = false; + }; + + resultImage.onerror = () => { + alertify.error('Failed to load result image'); + this.isProcessing = false; + }; + + resultImage.src = 'data:image/png;base64,' + result.result; + + } catch (error) { + console.error('Remove background error:', error); + alertify.error('Failed to remove background: ' + error.message); + this.isProcessing = false; + } + } +} + +export default Image_remove_background_class; diff --git a/frontend/src/js/services/api.js b/frontend/src/js/services/api.js index 554eacb..ee4abb3 100644 --- a/frontend/src/js/services/api.js +++ b/frontend/src/js/services/api.js @@ -71,6 +71,30 @@ class ApiService { return response.json(); } + /** + * Remove background from image using AI (rembg) + * @param {string} imageData - Base64 encoded image data + * @returns {Promise<{result: string, width: number, height: number}>} - Base64 encoded result with transparency + */ + async removeBackground(imageData) { + const response = await fetch(`${this.baseUrl}/tools/remove-background-base64`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + image: imageData, + }), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: 'Unknown error' })); + throw new Error(error.detail || `Remove background request failed: ${response.status}`); + } + + return response.json(); + } + /** * Health check for the backend * @returns {Promise}