mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
♻️ 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:
parent
3dd97a0e81
commit
ed1dfd7472
2 changed files with 10 additions and 76 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue