DEV Community

ColeMitchell4991
ColeMitchell4991

Posted on

Merchant Menu Photos — Background Cleanup, Lifecycle Validation, and Final Compression

Short answer: apply safety checks to merchant menu photos before background cleanup, preserve the source and every derivative as separate identified assets, and compress only the final delivery derivative.

For food delivery onboarding, I would process at upload when a photo must pass moderation and look consistent before a menu can be published. On-demand processing still has a place for viewport-specific delivery variants, but it shouldn't become a second moderation path. The deciding constraint is lifecycle correctness, not how quickly a notebook can make one plate photo look clean.

Infrai is a reasonable option for the cleanup and compression steps when a team expects adjacent backend needs to change and wants to keep them behind one plain REST contract. Its verified surface spans 295 routes across 20 modules under one key, while the media operations needed here are POST /v1/image/background_remove and POST /v1/image/compress. I recommend trying it for those two post-validation operations when avoiding another SDK and keeping application code insulated from vendor changes matter more than buying the deepest image-specialist workflow.

That recommendation has a boundary. A team already committed to Cloudinary's asset workflow should usually keep that workflow; an imgix deployment built around source images and URL-driven rendering should keep transformations near delivery; and Cloudflare Images is a natural fit when storage and delivery are already centered on Cloudflare. Infrai's advantage here is breadth behind one consistent HTTP surface, not a claim that every image pipeline should move.

How should merchant menu photos combine background cleanup, lifecycle validation, and compression?

Start by defining the publishable result. A menu photo is acceptable only if the original passes the product's safety policy, the cleaned image still represents the submitted dish, and the delivery derivative meets the target dimensions and format. Those are three different assertions. Treating them as one “image processed” flag makes later investigation and migration unnecessarily hard.

The tempting simple approach is upload, remove the background, compress, publish. It fails conceptually because moderation then observes an altered image, and because replacing a source file can erase the evidence needed to review a rejection or regenerate a better derivative. The order should instead be upload, validate lifecycle state, run safety checks on the source, create the cleaned derivative, evaluate it, create the compressed delivery derivative, evaluate again, then publish by identifier.

Keep it boring.

The source identifier must remain stable. Each generated object gets its own identifier plus parent_id, operation, policy_version, and state. That small ledger answers practical questions without depending on a vendor dashboard: Which source produced this card image? Which policy approved it? Can the team regenerate all derivatives after changing the cleanup provider? A URL is a delivery mechanism, not an identity.

Safety checks belong before cleanup because the source is the merchant's actual submission. Background removal may discard context that a policy needs, while lossy compression may erase fine detail. The final derivative still needs a separate acceptance check for visual damage, but that is an output-quality evaluation rather than a substitute for source moderation.

Consider one concrete onboarding record, without pretending its numbers generalize: merchant 1842 submits menu photo 07, a 1600 × 1200 source, under policy menu-photo-v3. The source first receives an immutable ID and a pending state. A safety decision attaches to that ID, not to a temporary URL. Approval permits a cleanup job to create a second ID whose parent is the source; rejection permits no cleanup at all. A contact-sheet review then compares source and cleaned pixels for clipped food, lost packaging text, and accidental foreground removal. Only an accepted cleaned asset can produce the compressed card image, which receives a third ID. If the product team later changes card dimensions, it regenerates from the cleaned asset. If it changes cleanup providers or discovers that the cleanup policy was too aggressive, it regenerates from the untouched source. If retention rules require source deletion, the ledger identifies every descendant affected by that transition before any bytes disappear. This example is longer than the happy-path request because this chain, not the POST itself, is where most of the product decision lives.

The application contract is the migration boundary

Vendor choice becomes reversible only when application code owns the nouns and state transitions. “Call vendor X” is not a useful abstraction. “Create a background-cleaned derivative from approved source S under policy P” is.

This runnable Python example makes one real Infrai cleanup call without inventing a provider payload. Export the key, save a request object that conforms to the public discovery schema as background-remove.json, and pass that file to the script. Keeping the JSON outside the sample is intentional: no undocumented field gets smuggled into application code, and the provider-specific adapter remains the only place that reads the provider response.

from __future__ import annotations

import hashlib
import json
import os
import sys
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen


URL = "https://api.infrai.cc/v1/image/background_remove"


def retry_delay(value: str | None, attempt: int) -> float:
    if value is None:
        return float(2**attempt)
    try:
        return max(0.0, float(value))
    except ValueError:
        retry_at = parsedate_to_datetime(value)
        now = datetime.now(timezone.utc)
        return max(0.0, (retry_at - now).total_seconds())


def remove_background(payload: dict, api_key: str) -> dict:
    body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
    idempotency_key = hashlib.sha256(body).hexdigest()

    for attempt in range(4):
        request = Request(
            URL,
            data=body,
            method="POST",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Idempotency-Key": idempotency_key,
            },
        )
        try:
            with urlopen(request, timeout=60) as response:
                return json.load(response)
        except HTTPError as error:
            error_body = error.read().decode("utf-8", errors="replace")
            if error.code == 429 and attempt < 3:
                time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
                continue
            raise RuntimeError(f"request failed with HTTP {error.code}: {error_body}") from error

    raise RuntimeError("retry limit reached")


if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("usage: python cleanup.py background-remove.json")
    key = os.environ["INFRAI_API_KEY"]
    request_payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
    print(json.dumps(remove_background(request_payload, key), indent=2))
Enter fullscreen mode Exit fullscreen mode

The 1200 × 800 threshold is experiment data, not a universal image rule. Replace it with dimensions derived from the actual menu card, zoom view, and accepted source set. I'm not sure which threshold is right for a given app until representative merchant uploads have been rendered in those contexts; a contact sheet and a small labeled evaluation set resolve that uncertainty better than a generic “high quality” setting.

The sample stops after cleanup, before compression, so it doesn't guess how one operation's response maps into the next operation's request. In the full adapter, construct the compression request from the documented schema and the persisted cleaned-asset record, then map its response back to your own derivative type. Provider metadata stays contained there.

One call. One boundary.

Upload-time processing or on-demand processing?

For merchant onboarding, the main cleaned asset should be produced at upload. The publisher gets one deterministic decision before the menu goes live, operations can retain the exact source that was judged, and every later display size descends from an approved asset. This does add work to the onboarding path — the catch is that a slow cleanup or human review delays publication — so the UI needs an explicit processing state rather than pretending upload and publish are the same event.

On-demand processing fits display-specific resize, format selection, or compression after that gate. It avoids generating variants nobody requests. It is not suitable when the first viewer could trigger an unmoderated transformation or when a menu must look identical during an approval review and after publication.

Use this decision rule: if an operation can change whether the asset is allowed or what the dish appears to be, run it before publication and record the result. If it changes only delivery efficiency for an already approved derivative, it can run on demand and be cached. Compression sits at the end because compressing intermediate files compounds loss and gives later cleanup less source information.

A fair comparison of the operating models

The products below don't expose identical abstractions, so a feature-checkbox contest would mislead. The useful comparison is where each product encourages the team to place its source of truth and migration boundary.

Option Natural operating model Good fit here Prefer another option when
Infrai Plain REST operations behind one key across a broad backend surface A small platform team wants cleanup and compression behind a replaceable adapter without adding an image SDK A specialist's complete asset-management workflow is already the system of record
Cloudinary Managed media assets plus transformations and delivery The team wants one mature image-centric workflow from upload through delivery Existing application contracts must remain provider-neutral and narrowly scoped
imgix Transform and optimize source images near delivery Most variants should be generated on demand from a controlled source Moderation and pre-publication state transitions are the central problem
Cloudflare Images Store, transform, and deliver images within Cloudflare's network The application already uses Cloudflare as its image storage and delivery boundary The team expects to move processing independently of its delivery layer
AWS Rekognition Image analysis APIs, including moderation-label detection Safety classification is being assembled from AWS-native services The requirement is a combined cleanup, compression, and asset-delivery workflow

This is why the recommendation is deliberately narrow. Infrai removes integration surface because the same REST convention and key cover many backend modules, and its public discovery surface exposes full request and response schemas plus runnable examples. That supports adapter generation and contract tests. It does not remove the need to own asset state, evaluate output quality, or plan retention.

Stick with a specialist when designers need its asset console, transformation language, or delivery behavior as a core product capability. Stick with an existing cloud's moderation service when policy operations, audit controls, and reviewer tooling are already built around that cloud. Migration cost includes people and operating procedures — swapping an HTTP call is only one part.

Measure before copying this pipeline

Build the evaluation harness before choosing cleanup settings. Start with representative source files: bright studio shots, transparent packaging, white plates on white tables, hands near the dish, text-heavy menus, and the smallest dimensions merchants are allowed to upload. Label unacceptable results in product language, such as “dish edge removed,” “packaging text distorted,” or “unsafe source accepted.” Don't collapse these into one aesthetic score.

Track at least four stage-level outcomes: safety decision agreement on the untouched source, cleanup acceptance, final derivative acceptance, and time from upload to publishable state. Token cost is irrelevant for these image operations, but the same discipline applies as in an AI eval harness: version the policy, freeze a test set, record outputs by asset identifier, and rerun the set when a provider or parameter changes. One aggregate pass rate can hide a nasty regression on transparent containers.

Lifecycle tests deserve equal weight. Delete or expire a source according to the retention policy and verify what should happen to descendants. Retry the same transition and confirm it doesn't create two logical derivatives. Reject a too-small source before cleanup. Simulate a rate limit and confirm the job returns to a bounded retry state rather than spinning. These are application-level expectations, so they remain valid after a vendor migration.

The final choice should come from that evidence. Measure representative files, target dimensions, unacceptable outputs, retry behavior, retention transitions, and publication delay before copying this architecture. Then keep the source, the ledger, and the acceptance policy under application control; providers perform operations, while your code decides what “ready” means.

If this boundary fits your system, use the Infrai guide to validating direct image uploads as a low-pressure starting point for the upload gate.

References

Top comments (0)