Asyira / project_a_output_progress /project_a_overview.md
AsyiraFitri
output update
71870f0
|
Raw
History Blame Contribute Delete
15.5 kB

Project A β€” Synthetic Signature Dataset: Technical Overview

What the Project Does

Project A generates a synthetic dataset of signed documents for training and testing a signature verification model. Real signature images are taken from public datasets, cleaned up, and pasted onto document templates to produce labelled images β€” each one recording whether the signature on it is genuine or forged, and who signed where.

The project has two main components:

  • prepare_signatures.py β€” loads signature images from datasets, removes backgrounds, and applies augmentation
  • synthesize_documents.py β€” pastes prepared signatures onto document templates and builds the labelled dataset
  • generate_test_cases.py β€” generates a structured 50-case test suite with ground truth labels across 5 failure scenarios

Datasets Used

Dataset What it contains How it is used
CEDAR Genuine and forged signatures in flat folders (full_org, full_forg) Loaded as a flat list of file paths
SigNet Genuine and forged signatures organised per writer (001/, 001_forg/, etc.) Loaded per writer so writer identity is preserved
GPDS Small sample only Not actively used yet β€” reserved for future work

File 1: prepare_signatures.py

This file handles everything to do with loading and preparing signature images before they are pasted onto documents.

Loading Functions

list_signature_files(folder)

Scans a folder and returns all image file paths with valid extensions (.png, .jpg, .jpeg, .tif, .bmp). Used internally by other loading functions.

load_cedar_signatures(genuine, limit)

Loads CEDAR signature file paths. Pass genuine=True for real signatures, False for forged ones. The limit parameter allows loading a small sample for quick testing.

list_signet_writer_ids(split)

Lists all writer IDs available in SigNet for a given split ("train" or "test"). A writer is only included if both their genuine folder and their forged folder exist, ensuring no incomplete writers are returned.

load_signet_signatures(writer_id, split, genuine)

Loads signature file paths for a single writer in SigNet. This is per-writer (unlike CEDAR's flat load) because SigNet's folder structure is organised by writer, and knowing which writer a signature belongs to is needed for hard-negative sampling in later work (e.g. deliberately picking visually similar writers as tricky negatives).

load_signet_all(split, limit_writers)

Convenience wrapper that calls load_signet_signatures() for every available writer and returns one flat list. Each entry is a dictionary with the fields:

{
    "path":      "/path/to/sig.png",
    "writer_id": "001",
    "label":     "genuine" or "forged",
    "source":    "signet"
}

This is the function synthesize_documents.py calls when building the signature pool.


Background Removal

remove_background(image_path, threshold=200)

Loads a signature image and removes its white or light-coloured background using OpenCV grayscale thresholding:

  1. Convert the image to grayscale
  2. Any pixel brighter than the threshold (default 200) is treated as background
  3. Build an alpha channel from that mask so background pixels become transparent
  4. Return a BGRA (OpenCV) image with a transparent background

This is a simple, no-ML approach that works well for offline signature scans which typically have a white paper background.

crop_to_content(rgba_image, padding=5)

Finds the bounding box of all non-transparent pixels in the BGRA image and crops tightly around the signature ink, with a small padding margin. This removes excess whitespace so the signature fits neatly inside a document's signature box.


Augmentation

Augmentation adds variation to the signatures so the model does not overfit to a fixed set of images. Three individual transforms are available, and one combined function applies them randomly.

augment_rotate(rgba_image, max_angle=10)

Randomly rotates the signature between βˆ’10Β° and +10Β° using cv2.getRotationMatrix2D and warpAffine. The border fill is transparent so no white rectangle is introduced.

augment_scale(rgba_image, scale_range=(0.85, 1.15))

Randomly resizes the signature to between 85% and 115% of its original size using cv2.resize.

augment_noise(rgba_image, amount=6)

Adds small random pixel noise (Β±6 per channel) to the RGB channels only, leaving the alpha channel unchanged. This simulates slight pen pressure variation or scan noise.

augment_signature(rgba_image)

Applies the three transforms above in combination with random probabilities:

  • Rotation: 70% chance
  • Scaling: 70% chance
  • Noise: 50% chance

This is the function synthesize_documents.py calls when preparing each signature for pasting.


Combined Pipeline

prepare_signature(image_path, threshold=200, padding=5, augment=False)

The full single-signature preparation pipeline in one call:

cv2.imread() β†’ remove_background() β†’ crop_to_content() β†’ [augment_signature()]

Returns a BGRA numpy array ready to be pasted onto a document. Returns None if the image cannot be read. This is the main function called by the generation loop.


File 2: synthesize_documents.py

This file handles pasting prepared signatures onto document templates and assembling the final labelled dataset.

Document Templates

The project uses 15 document templates across 5 groups, stored as PNG files in data/templates/. The templates are real document layouts converted from PDF:

Group Documents
Banking forms Bank account opening, corporate account application, wealth management
Corporate governance Board resolution, banking authorisation, shareholder resolution
Chinese documents Business registration, employee application, service application
Tamil documents Public service, trade licence, residence assessment
Cheques Continental Trust, Global Merchant, Standard Clearing

Signature Box Coordinates

For each template, the pixel coordinates (x1, y1, x2, y2) of every signature field are hardcoded in the DOCUMENT_TYPES dictionary. These coordinates were measured once using pdfplumber on the original born-digital PDFs, then visually verified by cropping and inspecting each box. They are not detected at runtime β€” the pipeline reads the stored values directly.

This approach works reliably because the templates are fixed PNGs converted from those same PDFs at a consistent resolution (1241Γ—1754 pixels at 150 dpi). If a template changes or is a scanned image at a different scale, the coordinates would need to be re-measured.


Core Functions

remove_white_background(sig_pil)

A second, PIL-based background removal pass applied just before pasting. It converts the signature image to RGBA and sets any pixel where R > 200 AND G > 200 to fully transparent. This catches white and near-white paper while preserving ink β€” including blue ballpoint pen ink, which has a low green channel value and is not affected by this condition.

paste_signature(doc_pil, sig_pil, box)

The core pasting function. For each signature box it:

  1. Calls remove_white_background() to clean the signature
  2. Resizes it to fit inside the box using thumbnail() which preserves aspect ratio
  3. Applies a small random offset of Β±5 pixels so signatures do not always land at the exact centre
  4. Converts the document to RGBA and pastes the signature using its alpha channel as a mask, so only ink pixels are blended in
  5. Applies a slight Gaussian blur (radius 0.3) to the whole document so the ink looks like it belongs on the paper rather than sitting on top of it
  6. Returns the final image as RGB

build_signature_pool()

Calls load_cedar_signatures() and load_signet_all() to combine all available signatures from both datasets into one flat pool. Each entry carries the file path, writer ID, label (genuine or forged), and source dataset. This pool is what the generation loop samples from randomly.

generate_synthetic_documents(num_documents=200)

The main generation loop. For each of the 200 documents it:

  1. Randomly picks a document type from DOCUMENT_TYPES
  2. Loads the corresponding template PNG
  3. For each signature box on that template, randomly picks a signature from the pool, runs it through prepare_signature() with augmentation, converts it from OpenCV BGRA to PIL RGBA, and calls paste_signature()
  4. Optional signature boxes (such as the officer rows in banking_authorization) have a 70% chance of being filled, simulating real documents that list one to three officers
  5. Labels the whole document as "forged" if any one signature on it is forged, otherwise "genuine"
  6. Saves the output PNG and appends a metadata row

write_dataset_csv(rows)

Writes metadata for all generated documents to data/synthetic_dataset.csv. Columns include filename, document type, group, overall label, number of signatures, and per-role details in the format role:label:source.


What Gets Saved to Disk

Item Saved? Location
Raw signature images Never written β€” read-only inputs data/raw/
Cleaned and augmented signatures No β€” RAM only, discarded after pasting β€”
Final document PNGs Yes data/synthetic/doc_001_…_genuine.png
Dataset metadata Yes data/synthetic_dataset.csv

Dataset Label Distribution

There is no fixed genuine-to-forged ratio. Because each document's overall label is "forged" if any signature on it is forged, documents with multiple signature boxes are more likely to be labelled forged β€” even with a 50/50 signature pool. For a document with three signature boxes, the probability of all three being genuine is only 12.5%, so the dataset will have significantly more forged documents than genuine ones by default.

To check the actual ratio after generation:

import pandas as pd
df = pd.read_csv("data/synthetic_dataset.csv")
print(df["label"].value_counts())

File 3: generate_test_cases.py

This file generates a structured 50-case test suite for evaluating a signature verification system. Each case is saved to its own folder and includes a generated document image, reference signature images, and a ground_truth.json file.

Test Case Types

Type Count Description Expected outcome Rejection code
PASS 20 Correct person signs the correct field ACCEPTED β€”
NO_SIGNATURE 5 Signature fields left completely blank REJECTED NO_SIGNATURE_FOUND
WRONG_PERSON 10 An impostor's signature is pasted instead REJECTED IDENTITY_MISMATCH
WRONG_ROLE 10 Correct person but fields swapped REJECTED WRONG_ROLE
REQUIRED_PERSON_MISSING 5 One required field left blank on a multi-sig doc REJECTED REQUIRED_SIGNATORY_MISSING

Persons

Ten named persons are defined, each mapped to a SigNet writer ID (001–010). Each person has 2–3 reference signature images copied from their genuine SigNet folder into the case directory as reference_1.png, reference_2.png, etc.

Output Structure

data/test_cases/
    case_001/
        document.png          ← generated document with signatures pasted
        reference_1.png       ← known-good reference signature
        reference_2.png
        reference_3.png
        ground_truth.json     ← case metadata and expected result
    case_002/
        ...
    test_cases_summary.csv    ← one row per case, all cases

ground_truth.json Format

{
  "case_id": "case_003",
  "type": "WRONG_PERSON",
  "document_type": "board_resolution",
  "expected_outcome": "REJECTED",
  "expected_rejection_code": "IDENTITY_MISMATCH",
  "notes": "Signed by Jane Smith instead of John Doe.",
  "authorized_signatory": {
    "name": "John Doe",
    "role": "director",
    "ref_paths": ["reference_1.png", "reference_2.png", "reference_3.png"]
  },
  "signed_roles": [
    {
      "role": "director_1",
      "signed_by": "Jane Smith",
      "status": "filled_wrong_person"
    },
    {
      "role": "director_2",
      "signed_by": "Jane Smith",
      "status": "filled_wrong_person"
    }
  ]
}

How Each Case Type is Generated

PASS β€” loads the authorized person's genuine SigNet signatures and pastes one into each required field with augmentation applied.

NO_SIGNATURE β€” loads the template but pastes nothing. All signature boxes are left as blank fields on the printed template.

WRONG_PERSON β€” loads a different (unauthorized) person's genuine signatures and pastes them into all fields. The reference signatures in the folder still belong to the authorized person, making the mismatch detectable.

WRONG_ROLE β€” loads the authorized person's genuine signatures but pastes them into the wrong fields by reversing the order of signature boxes. For example, the person who should sign the applicant box signs the officer box and vice versa.

REQUIRED_PERSON_MISSING β€” fills only the first signature box correctly and leaves all remaining required boxes blank. Only used with document types that have two or more signature boxes.

Summary CSV

test_cases_summary.csv contains one row per case with these columns:

Column Description
case_id e.g. case_003
type One of the 5 scenario types
document_type Which template was used
expected_outcome ACCEPTED or REJECTED
expected_rejection_code Rejection reason, or empty if accepted
authorized_name Name of the authorized signatory
authorized_role Their role on the document
num_ref_signatures How many reference images are in the folder
ref_paths Semicolon-separated list of reference filenames
notes Human-readable description of what is wrong
case_folder Relative path to the case folder

Full Data Flow Summary

Raw datasets (disk)
    CEDAR full_org / full_forg
    SigNet train/ test/ per-writer folders
            β”‚
            β–Ό
    build_signature_pool()
    Combined pool: {path, writer_id, label, source}
    Everything in RAM β€” nothing saved yet
            β”‚
            β–Ό (Γ—200 documents)
    For each document:
        1. Pick random doc type β†’ load template PNG from disk
        2. For each signature box:
               Pick random signature from pool
               prepare_signature()
                   β†’ remove_background()     OpenCV threshold β†’ transparent bg
                   β†’ crop_to_content()       tight crop around ink
                   β†’ augment_signature()     rotate / scale / noise
               Convert BGRA β†’ PIL RGBA
               paste_signature()
                   β†’ remove_white_background()   PIL-based final bg removal
                   β†’ thumbnail()                 resize to fit box
                   β†’ paste() with alpha mask     blend ink onto document
                   β†’ GaussianBlur(0.3)           soften ink into paper
        3. Label document: "forged" if any sig is forged
        4. doc_image.save() β†’ data/synthetic/doc_NNN_…_label.png
        5. Append metadata row
            β”‚
            β–Ό
    write_dataset_csv()
    β†’ data/synthetic_dataset.csv