♻️ refactor(figgos): remove feathered bg removal, RMBG-1.4 only

Drop the threshold-based method and BG_REMOVAL_METHOD config toggle.
RMBG-1.4 is now the sole background removal pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chr1st1anG 2026-02-12 13:58:13 +01:00
parent 3dd97a0e81
commit ed1dfd7472
2 changed files with 10 additions and 76 deletions

View file

@ -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

View file

@ -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<string>('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<Buffer> {
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<Buffer> {
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<Buffer> {
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;