LogoSignature Detection API

Extract signature crops from bounding boxes

Extract signature crops from bounding boxes

Signature Detection Team

Signature Detection Team

@Signature Detection Team

The detection response does not include a PNG of the signature. It includes a rectangle. You already have the image, because you sent it, so the crop is a local image operation.

A detection looks like this (the numbers are the sample published in the API docs):

{
  "bbox": {
    "x1": 321.23,
    "y1": 373.39,
    "x2": 647.21,
    "y2": 501.68
  },
  "confidence": 0.192,
  "class": 0,
  "class_name": "signature"
}

Those values are not a 0–1 ratio. A ratio would not be 321 pixels across. Treat them as pixels of the image you submitted. x1, y1 is the top-left. x2, y2 is the bottom-right. The origin is the top-left of the image, x to the right, y down. That matches how Pillow and sharp address pixels.

The server only returns boxes at or above result.confidenceThreshold. That value defaults to 0.1. On an image request you can send threshold from 0 to 1 and the response will use it. PDF pages always use 0.1. Crop every detection that comes back, or skip ones under a cutoff you choose after the response.

Pillow

Run detection first, then open the same file you sent. If you sent imageUrl, download those bytes. Cropping a different resolution, or a re-compressed preview, will miss the mark.

import base64
import requests
from PIL import Image
 
image_path = "application.png"
 
with open(image_path, "rb") as handle:
    image_base64 = base64.b64encode(handle.read()).decode("ascii")
 
response = requests.post(
    "https://signature-detector.com/api/detect",
    headers={
        "Content-Type": "application/json",
        "x-api-key": "your_api_key_here",
    },
    json={"imageBase64": image_base64},
    timeout=60,
)
response.raise_for_status()
data = response.json()
 
image = Image.open(image_path)
width, height = image.size
 
for index, detection in enumerate(data["result"]["detections"], start=1):
    box = detection["bbox"]
    left = max(0, int(box["x1"]))
    top = max(0, int(box["y1"]))
    right = min(width, int(box["x2"]))
    bottom = min(height, int(box["y2"]))
    if right <= left or bottom <= top:
        continue
    image.crop((left, top, right, bottom)).save(f"signature-{index}.png")

Image.crop takes (left, upper, right, lower), which is (x1, y1, x2, y2) after you clamp to the file. int() truncates toward zero. That is fine for positive coordinates. If you would rather keep the fractional pixel, pass the floats. Recent Pillow accepts them.

Install Pillow in your own environment (pip install pillow). It is not part of this API.

sharp

import { readFile } from "node:fs/promises";
import sharp from "sharp";
 
const imagePath = "application.png";
const bytes = await readFile(imagePath);
 
const response = await fetch("https://signature-detector.com/api/detect", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.SIGNATURE_API_KEY,
  },
  body: JSON.stringify({
    imageBase64: bytes.toString("base64"),
  }),
});
const data = await response.json();
if (!response.ok) {
  throw new Error(data.error || `HTTP ${response.status}`);
}
 
const source = sharp(bytes);
const meta = await source.metadata();
const imageWidth = meta.width ?? 0;
const imageHeight = meta.height ?? 0;
 
for (const [index, detection] of data.result.detections.entries()) {
  const { x1, y1, x2, y2 } = detection.bbox;
  const left = Math.max(0, Math.floor(x1));
  const top = Math.max(0, Math.floor(y1));
  const right = Math.min(imageWidth, Math.ceil(x2));
  const bottom = Math.min(imageHeight, Math.ceil(y2));
  const width = right - left;
  const height = bottom - top;
  if (width < 1 || height < 1) continue;
 
  await sharp(bytes)
    .extract({ left, top, width, height })
    .toFile(`signature-${index + 1}.png`);
}

sharp.extract wants integer left, top, width, and height, and it throws if the rectangle leaves the image. Clamping avoids that. Install sharp in the project that crops. Do not add it to the browser.

PDF pages

A PDF response puts pages at the top level. Each page includes width and height. The box is in that raster's pixel space, not the original PDF's point size, and the detect call still does not return the rendered bitmap. Pages are rendered at 150 DPI with the long edge capped at 2,000 pixels. A US Letter page is 1275 by 1650. To crop, render the page at that size, then apply bbox. If you do not know that pixel size, send the page image you rendered (imageBase64) so the coordinates and the file are the same thing. Guessing a scale from PDF points will shift the crop.

The PDF request fields are pdfUrl and pdfBase64. creditsUsed is 1 per page, or 0 when the same file is cached. See PDF signature detection.

Why bother cropping

A reviewer can judge a 200-pixel-tall mark faster than a full contract scan. Store the crop next to detectionId if your retention rules allow the image. The detection record in this product keeps the JSON and a hash of the image, not the crop. You produce the crop. You decide where it lives.

Related: Detect signatures with Node.js, signature detection API, contracts.

Ready to detect signatures?

Extract signature crops from bounding boxes | Signature Detection API