Skip to content
pokemontcgapi.com

Blog

Card centering from a photo: measure first, confirm the lines second

Build a two-step card centering flow: photo to ratios and geometry, person-reviewed lines to centering-only ceilings, with TypeScript, Python and Go.

Published sdk

Measure a card's centering from a photo. The API returns the L/R and T/B ratios with their uncertainty and the detected corners and border lines; a person reviews the lines in your app and confirms them, and the API returns the centering-only grade ceilings for PSA, BGS, CGC and SGC. Growth and higher; a verified trial includes 5 measure and 5 confirm calls. Measure 25 credits, confirm 5, grade 1, standards 1.

This is a ceiling from centering alone, not a prediction of the final grade.

Why there are two steps

A phone photo contains a card, a background, shadows and sometimes a sleeve. The outer cut and the printed border are different lines, and an automatic detection can place either one on a highlight or a dark edge. A ratio can look plausible even when the line that produced it is wrong. Returning a grade without someone looking at those lines would turn a detection into a promise it cannot support.

On real photos, measure almost always answers HTTP 200 with status: needs-review and ceilings: null. That is a result to review, not a failed HTTP request. It supplies the L/R and T/B ratios, their uncertainty and, when detection permits, geometry for an editor. The application shows the evidence before asking the person to accept it. Only then does confirm return the photo-based ceilings. This is a ceiling from centering alone, not a prediction of the final grade.

Keep the distinction between an HTTP error, status: failed and needs-review. A failed detection needs a better photo or corrected corners. If geometry is absent, offer a retake rather than a confirm button. If it is present, uncertainty and warnings belong beside the overlay, where the person can act on them.

Photo, geometry, review, confirm

  1. Send a photo of one face to POST /v1/centering/measure, with face: front or back and the actual card_size. For a borderless front, use the back.
  2. Show the ratios and uncertainty and draw the detected corners and cut and print lines. If the corners are wrong, collect corrected corners and call measure again.
  3. Let a person inspect or adjust the cut and print lines. Preserve the full geometry from measure, including dimensions, rotations, corner_offset, warnings and positioning_sigma.
  4. Only after that person explicitly accepts the lines, send POST /v1/centering/confirm with the reviewed geometry, the same face and card_size, and confirmed_by: person.
Photo of a Japanese Magikarp card on dark cloth with the detected corners, cut lines and print lines drawn over it
Japanese Koiking / Magikarp Art Rare (sv1a 080/073), photographed with a phone on dark cloth and cropped around the card before upload. Measure returned L/R 50/50 ±15.1 pp and T/B 51/49 ±17.3 pp. After a person reviewed the lines, confirm returned L/R 50/50 ±1.7 pp, T/B 51/49 ±1.7 pp and ceilings of PSA 10 (official), BGS 10 (interpreted), CGC 10 (interpreted) and SGC 10 (approximate). This is a ceiling from centering alone, not a prediction of the final grade.
Photo of the back of a Japanese Pokémon card on dark cloth with the detected corners, cut lines and print lines drawn over it
The back of the same card: the full photo was uploaded without cropping, with the card small in the frame; this image is cropped only for presentation. Measure returned status: needs-review, code: HIGH_UNCERTAINTY, L/R 52/48 ±23.4 pp and T/B 52/48 ±25.7 pp. After a person reviewed the lines, confirm returned L/R 52/48 ±1.6 pp, T/B 52/48 ±1.6 pp and ceilings of PSA 10, BGS 10 and CGC 10; SGC was null because it has no published back table. Grade with both faces returned 10 for PSA, BGS, CGC and SGC. This is a ceiling from centering alone, not a prediction of the final grade.

The geometry guide matters when you build the overlay: corners use EXIF-oriented image pixels after reduction, while lines use the rectified coordinate system including padding. A projective transform connects them; drawing rectified line coordinates straight onto the original camera image will put the review in the wrong place. The line editor guide provides the formulas and a tested JavaScript implementation.

Confirm returns ratios and ceilings with basis: client-confirmed-lines. That basis records your application’s assertion that a person reviewed the lines. It does not return another geometry, so keep the geometry the person accepted in your app. This is a ceiling from centering alone, not a prediction of the final grade.

The same flow in three SDKs

These examples deliberately stop at the editor boundary. The callback belongs to the explicit acceptance action in your interface; a script calling it immediately after measure would skip the review the API requires. Use a server-side key and keep it out of browser code.

centering.ts
import { readFile } from "node:fs/promises";
import { PokemonTcgApi, type CenteringGeometry } from "@pokemontcgapi/sdk";

const client = new PokemonTcgApi({ apiKey: process.env.PTCG_API_KEY });
const measurement = await client.centering.measure(await readFile("card.jpg"), {
  face: "front", card_size: "standard",
});
const measured = measurement.data;
if (!measured.geometry) throw new Error("Retake the photo before line review");
console.log(measured.status, measured.lr, measured.tb, measured.warnings);
// Render measured.geometry and let a person inspect or adjust its lines.

// Connect only to the person's explicit acceptance in your line editor.
async function onPersonAccepted(reviewedGeometry: CenteringGeometry) {
  const { data } = await client.centering.confirm({
    face: measured.face, card_size: measured.card_size,
    geometry: reviewedGeometry, confirmed_by: "person",
  });
  console.log(data.ceilings, data.basis);
  console.log("This is a ceiling from centering alone, not a prediction of the final grade.");
}
centering.py
from pokemontcgapi import PokemonTcgApi
from pokemontcgapi.types import CenteringGeometry

client = PokemonTcgApi()  # reads PTCG_API_KEY
measurement = client.centering.measure("card.jpg", face="front", card_size="standard")
measured = measurement["data"]
if not measured.get("geometry"):
    raise ValueError("Retake the photo before line review")
print(measured["status"], measured["lr"], measured["tb"], measured["warnings"])
# Render measured["geometry"] and let a person inspect or adjust its lines.

# Connect only to the person's explicit acceptance in your line editor.
def on_person_accepted(reviewed_geometry: CenteringGeometry):
    result = client.centering.confirm({
        "face": measured["face"], "card_size": measured["card_size"],
        "geometry": reviewed_geometry, "confirmed_by": "person",
    })
    print(result["data"]["ceilings"], result["data"]["basis"])
    print("This is a ceiling from centering alone, not a prediction of the final grade.")
centering.go
package main

import (
    "context"
    "fmt"
    "os"

    "github.com/pokemontcgapi/sdk-go"
)

// Your app calls this after identification and keeps the returned callback
// for the person's explicit acceptance in its line editor.
func startReview(ctx context.Context, c *pokemontcgapi.Client) (func(context.Context, pokemontcgapi.CenteringGeometry) error, error) {
    photo, err := os.Open("card.jpg")
    if err != nil { return nil, err }
    defer photo.Close()
    measurement, err := c.Centering.Measure(ctx, photo, pokemontcgapi.CenteringMeasureOptions{
        Face: "front", CardSize: "standard",
    })
    if err != nil { return nil, err }
    measured := measurement.Data
    if measured.Geometry == nil { return nil, fmt.Errorf("retake the photo before line review") }
    fmt.Println(measured.Status, measured.LR, measured.TB, measured.Warnings)
    // Render measured.Geometry; do not call onPersonAccepted automatically.
    onPersonAccepted := func(ctx context.Context, reviewedGeometry pokemontcgapi.CenteringGeometry) error {
        result, err := c.Centering.Confirm(ctx, pokemontcgapi.CenteringConfirmInput{
            Face: measured.Face, CardSize: measured.CardSize,
            Geometry: reviewedGeometry, ConfirmedBy: "person",
        })
        if err != nil { return err }
        fmt.Println(result.Data.Ceilings, result.Data.Basis)
        fmt.Println("This is a ceiling from centering alone, not a prediction of the final grade.")
        return nil
    }
    return onPersonAccepted, nil
}

Python also exposes the same four methods through AsyncPokemonTcgApi: await client.centering.measure, confirm, grade and standards. The SDK method table lists the signatures for all three clients. The REST equivalents and error shapes are in the centering reference.

What a ceiling says

Confirm returns ceilings for PSA, BGS, CGC and SGC after the person’s review. Show each house’s maximum_grade together with source_status and borderline. The source status can be official, interpreted or approximate: preserve that distinction rather than turning every threshold into a company promise. The source dates describe when thresholds were reviewed, not a review of the card in the photo. This is a ceiling from centering alone, not a prediction of the final grade.

The other face or a missing axis can lower the ceiling. For known ratios, grade reports incomplete, missing_faces and missing_axes; do not fill gaps by guessing. SGC has no published back table, and an unconstrained result can have maximum_grade: null. A null is something to explain, not a reason to invent a number.

Use GET /v1/centering/standards for threshold bands, source URLs and rounding rules. Use POST /v1/centering/grade when the ratios are already known. Grade is a calculation from supplied ratios; it is not a substitute for the person reviewing the lines of a photo before confirm. This is a ceiling from centering alone, not a prediction of the final grade.

Costs, plans and the trial

Centering route costs
RouteCredits
POST /v1/centering/measure25
POST /v1/centering/confirm5
POST /v1/centering/grade1
GET /v1/centering/standards1

Ongoing access is included on Growth, Professional, Enterprise. Developer is excluded with 403 PLAN_REQUIRED. Every centering route requires vision:read, which also covers photo recognition. After email confirmation, the Free trial includes 5 measure and 5 confirm calls independently of each other and Vision. Grade and standards spend trial credits without separate call counters. These calls still use the trial’s 800 credits; the trial lasts 30 days from signup.

Before email confirmation, PLAN_REQUIRED asks the account owner to confirm the email. Read details.next_step when present, and stop on a plan refusal rather than retrying. Busy processing returns 503 PROVIDER_UNAVAILABLE with Retry-After: 2; credits and trial reservations are refunded. Back off with jitter instead of sending a burst of retries.

An assistant can measure, but cannot attest to your review

MCP exposes ptcg_measure_centering, ptcg_grade_centering and ptcg_get_centering_standards. There is deliberately no confirm tool: an assistant cannot assert that a person inspected the lines. Build that action into the application that shows the photo and collects the person’s acceptance, then call confirm there for the photo-based ceilings. This is a ceiling from centering alone, not a prediction of the final grade.

Build the editor before the grade display

Start with the two-step guide, then wire its geometry into your review interface. Keep ratios, uncertainty, source status and the limitation visible wherever you display a ceiling. This is a ceiling from centering alone, not a prediction of the final grade.

Keep reading

view this article as markdown