diff --git a/apps/figgos/CLAUDE.md b/apps/figgos/CLAUDE.md index 771b3e965..84e1a1e6b 100644 --- a/apps/figgos/CLAUDE.md +++ b/apps/figgos/CLAUDE.md @@ -72,7 +72,6 @@ S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin S3_BUCKET=figgos-storage CORS_ORIGINS=http://localhost:5196,http://localhost:8081 -BG_REMOVAL_METHOD=feathered # optional, see below ``` ### Mobile (.env) @@ -82,23 +81,9 @@ EXPO_PUBLIC_BACKEND_URL=http://localhost:3025 EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 ``` -### Background Removal (`BG_REMOVAL_METHOD`) +### Background Removal -Two methods available for removing white backgrounds from generated card images: - -| Method | Env Value | Speed | Quality | Dependencies | -|--------|-----------|-------|---------|--------------| -| **Feathered Threshold** | `feathered` (default) | ~77ms | Good for white/near-white backgrounds | `sharp` only | -| **RMBG-1.4 AI** | `rmbg` | ~1s (first run downloads model) | Better for complex backgrounds | `@huggingface/transformers` | - -- **Feathered** (default): Removes near-white pixels (threshold=240) with a soft 10px feather edge. Fast, no model download needed. Works well because Gemini generates cards on white backgrounds. -- **RMBG**: Uses the RMBG-1.4 segmentation model via Hugging Face Transformers. Model is lazy-loaded and cached after first use. Better quality but slower. - -Set in `.env`: -```env -BG_REMOVAL_METHOD=feathered # fast, sharp-based (default) -BG_REMOVAL_METHOD=rmbg # AI-based, higher quality -``` +Uses **RMBG-1.4** AI segmentation model (`@huggingface/transformers`) for background removal. The model is lazy-loaded and cached on first use (~1s first run to download). After removal, a white-threshold cleanup pass targets the top 12% of the card to remove peg-hole (hang tab) artifacts that the model sometimes preserves. ## Game Concept diff --git a/apps/figgos/apps/backend/src/generation/image-processing.service.ts b/apps/figgos/apps/backend/src/generation/image-processing.service.ts index 15e949158..8cca5c67f 100644 --- a/apps/figgos/apps/backend/src/generation/image-processing.service.ts +++ b/apps/figgos/apps/backend/src/generation/image-processing.service.ts @@ -1,68 +1,19 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import sharp from 'sharp'; -type BgRemovalMethod = 'feathered' | 'rmbg'; - @Injectable() export class ImageProcessingService implements OnModuleInit { private readonly logger = new Logger(ImageProcessingService.name); - private method: BgRemovalMethod; private segmenter: any = null; - constructor(private config: ConfigService) { - this.method = (this.config.get('BG_REMOVAL_METHOD') || 'feathered') as BgRemovalMethod; - } - onModuleInit() { - this.logger.log(`Background removal method: ${this.method}`); - if (this.method === 'rmbg') { - this.logger.log('RMBG-1.4 model will be lazy-loaded on first use'); - } + this.logger.log('RMBG-1.4 model will be lazy-loaded on first use'); } + /** + * Remove background using RMBG-1.4 AI model, trim, and clean peg-hole artifacts. + */ async removeBackground(pngBuffer: Buffer): Promise { - if (this.method === 'rmbg') { - return this.removeWithRmbg(pngBuffer); - } - return this.removeWithThreshold(pngBuffer); - } - - /** - * Feathered threshold background removal (~77ms). - * Removes near-white pixels with a soft edge transition. - * T=240, feather=10. Output: WebP quality 85. - */ - private async removeWithThreshold(inputBuffer: Buffer): Promise { - const { data, info } = await sharp(inputBuffer) - .ensureAlpha() - .raw() - .toBuffer({ resolveWithObject: true }); - - const T = 240; - const F = 10; - const low = T - F; - - for (let i = 0; i < data.length; i += 4) { - const m = Math.min(data[i], data[i + 1], data[i + 2]); - if (m > T) { - data[i + 3] = 0; - } else if (m > low) { - data[i + 3] = Math.min(data[i + 3], Math.round((255 * (T - m)) / F)); - } - } - - return sharp(data, { raw: { width: info.width, height: info.height, channels: 4 } }) - .trim() - .webp({ quality: 85 }) - .toBuffer(); - } - - /** - * RMBG-1.4 AI background removal (~1s). - * Uses @huggingface/transformers pipeline. Model is lazy-loaded and cached. - */ - private async removeWithRmbg(inputBuffer: Buffer): Promise { if (!this.segmenter) { this.logger.log('Loading RMBG-1.4 model (first use)...'); const { pipeline } = await import('@huggingface/transformers'); @@ -71,7 +22,6 @@ export class ImageProcessingService implements OnModuleInit { } // HF transformers pipeline needs a file path or URL, not a buffer - // Write to a temp file, process, then clean up const { writeFile, unlink } = await import('node:fs/promises'); const { join } = await import('node:path'); const { tmpdir } = await import('node:os'); @@ -79,7 +29,7 @@ export class ImageProcessingService implements OnModuleInit { const tmpPath = join(tmpdir(), `figgos-rmbg-${randomUUID()}.png`); try { - await writeFile(tmpPath, inputBuffer); + await writeFile(tmpPath, pngBuffer); const result = await this.segmenter(tmpPath); const img = Array.isArray(result) ? result[0] : result; @@ -92,8 +42,7 @@ export class ImageProcessingService implements OnModuleInit { .raw() .toBuffer({ resolveWithObject: true }); - // RMBG sometimes keeps the peg hole (hang tab) at the top of the card. - // Apply white-threshold cleanup to the top 12%, middle 50% of the trimmed image. + // Clean leftover white pixels in the peg-hole region (hang tab at top of card) this.cleanPegHole(trimmed.data, trimmed.info.width, trimmed.info.height); return sharp(trimmed.data, { @@ -108,7 +57,7 @@ export class ImageProcessingService implements OnModuleInit { /** * Remove leftover white pixels in the peg-hole region (top 12%, middle 50%). - * Same threshold logic as feathered method but scoped to that zone only. + * Uses white-threshold with feathered edge (T=240, feather=10). */ private cleanPegHole(data: Buffer, width: number, height: number): void { const T = 240; @@ -122,7 +71,7 @@ export class ImageProcessingService implements OnModuleInit { for (let y = 0; y < yEnd; y++) { for (let x = xStart; x < xEnd; x++) { const i = (y * width + x) * 4; - if (data[i + 3] === 0) continue; // already transparent + if (data[i + 3] === 0) continue; const m = Math.min(data[i], data[i + 1], data[i + 2]); if (m > T) { data[i + 3] = 0;