CoolFace
Modelpublic

pablo-moreira/puzzle-piece-classifier

sourceHugging Facecc-by-4.0updated 25d agoView on Hugging Face
0likes34downloads
Model Card

Puzzle Piece Classifier

A computer-vision model for identifying the position and orientation of an individual jigsaw puzzle piece within a set of candidate puzzle-piece crops.

The model is part of the PuzzleMap project and is designed to operate after puzzle-piece detection.

Unlike a conventional image classifier, the model does not classify a piece into a fixed semantic category. Instead, it performs two related tasks:

  1. 1.Crop selection — identifies which candidate crop corresponds to the input puzzle piece.
  2. 2.Angle prediction — determines the orientation of the input piece relative to the selected crop.

The model uses DINOv2 Base as its visual backbone and adds custom neural heads for crop ranking and angle estimation.


Model Details

PropertyValue
ArchitectureCustom PuzzlePieceClassifier
Backbonefacebook/dinov2-base
FrameworkPyTorch
IntegrationHugging Face Transformers
TaskPuzzle-piece matching and orientation
Crop candidatesVariable number
Angle classes4
Supported angles0°, 90°, 180°, 270°
Training frameworkPyTorch Lightning
Model version20
Crop lossCross Entropy
Angle lossCross Entropy
Angle loss weight1.0
Phase 1 learning rate1e-3
Phase 2 learning rate5e-6
Phase 1 backboneFrozen
Phase 2 backboneFine-tuned
Patch interactionMulti-Head Cross-Attention
Attention heads8
Attention dropout0.1
Patch poolingLearned Attention Pooling

Intended Use

The model is intended to answer the following question:

Given an isolated puzzle piece and several candidate positions from a puzzle image, which candidate corresponds to the piece, and in which orientation should the piece be placed?

A typical PuzzleMap pipeline is:

text
Puzzle Image
     │
     ▼
Puzzle Piece Detection
     │
     ▼
Bounding Boxes
     │
     ▼
Individual Piece Crop
     │
     ▼
Puzzle Piece Classifier
     │
     ├─────────────────────┐
     │                     │
     ▼                     ▼
Crop Selection        Angle Prediction
     │                     │
     └──────────┬──────────┘
                ▼
        Piece Localization
        + Orientation
                │
                ▼
        Puzzle Assembly

The detector provides the initial piece localization, while this model determines which position in the puzzle corresponds to the extracted piece.


Architecture

The model is based on facebook/dinov2-base.

DINOv2 produces:

text
CLS token
+
Patch tokens

The model uses the DINOv2 patch tokens as the primary representation for both crop matching and angle prediction.

Unlike version 19, which calculated cosine similarity between corresponding patch representations, version 20 uses Multi-Head Cross-Attention to allow the patches from the rotated puzzle piece to attend to the patches from each candidate crop.

The architecture can be summarized as:

text
                           Input Piece
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       │
              Create 4 Rotations            │
                    │                       │
          ┌─────────┼─────────┬─────────┐   │
          │         │         │         │   │
          ▼         ▼         ▼         ▼   │
         0°        90°       180°      270° │
          │         │         │         │   │
          └─────────┴─────────┴─────────┘   │
                    │                       │
                    ▼                       │
              DINOv2 Backbone               │
                    │                       │
                    ▼                       │
               Patch Tokens                 │
                    │                       │
                    ▼                       │
              Patch Projection              │
                    │                       │
                    ▼                       │
              [B, 4, 256, D]                │
                                            │
                                            │
Candidate Crops ────────────────────────────┘
      │
      ▼
DINOv2 Backbone
      │
      ▼
Patch Tokens
      │
      ▼
Patch Projection
      │
      ▼
[B, Nc, 256, D]
      │
      │
      └──────────────────┐
                         │
                         ▼
              Pair every rotation
                 with every crop
                         │
                         ▼
               Multi-Head Cross
                   Attention
                         │
              ┌──────────┴──────────┐
              │                     │
        Query = Piece         Key = Crop
                              Value = Crop
              │                     │
              └──────────┬──────────┘
                         │
                         ▼
                  Residual +
                   LayerNorm
                         │
                         ▼
                 Attention Pooling
                         │
                         ▼
                  [B, Nc, 4, D]
                         │
                         ▼
                    Crop Head
                         │
                         ▼
                  [B, Nc, 4]
                         │
                ┌────────┴────────┐
                │                 │
                ▼                 ▼
          Max over 4          Winner Crop
                │                 │
                ▼                 ▼
         Crop Scores        Angle Logits
          [B, Nc]              [B, 4]

The important architectural change in version 20 is that each candidate crop is evaluated jointly with each of the four rotations of the input piece.

For every candidate, the model therefore produces four scores:

text
Candidate 0:
    rotation 0°
    rotation 90°
    rotation 180°
    rotation 270°

Candidate 1:
    rotation 0°
    rotation 90°
    rotation 180°
    rotation 270°
...

This produces an intermediate tensor:

text
[B, Nc, 4]

where:

text
B  = batch size
Nc = number of candidate crops
4  = possible rotations

Model Components

DINOv2 Backbone

The backbone is loaded from:

text
facebook/dinov2-base

The implementation uses AutoModel so that the backbone can be loaded from its Hugging Face configuration.

The hidden dimension is obtained dynamically from:

python
self.backbone.config.hidden_size

The model uses the DINOv2 patch tokens rather than the CLS token.

This allows the custom heads to be constructed according to the backbone configuration.

For the standard DINOv2 Base configuration, the output contains:

text
1 CLS token
+
256 patch tokens

The CLS token is removed:

python
patch_tokens = output[:, 1:]

resulting in:

text
[B, 256, hidden_size]

These patch representations are then passed through the shared patch_proj layer.


Patch Projection

The model applies the same learned projection to the patch tokens from both the puzzle piece and candidate crops.

The projection is:

text
Linear(hidden_size → hidden_size)
GELU
Linear(hidden_size → hidden_size)

implemented as:

python
self.patch_proj = nn.Sequential(
    nn.Linear(hidden_size, hidden_size),
    nn.GELU(),
    nn.Linear(hidden_size, hidden_size)
)

The projection is shared between:

text
Piece patches
Crop patches

After projection, the model normalizes the patch representations:

python
F.normalize(patch_tokens, dim=-1)

The resulting representations are used by the cross-attention layer.

Crop Ranking

The model evaluates every candidate crop against all four rotations of the input piece.

For each pair:

text
piece rotation × candidate crop

the model applies Multi-Head Cross-Attention.

The attention uses:

text
Query = rotated piece patch tokens
Key   = candidate crop patch tokens
Value = candidate crop patch tokens

The attention layer is:

python
self.crop_attention = nn.MultiheadAttention(
    embed_dim=hidden_size,
    num_heads=8,
    dropout=0.1,
    batch_first=True
)

This produces an attended representation of the piece conditioned on the candidate crop.

A residual connection followed by LayerNorm is then applied:

python
attention_output = self.attention_norm(
    piece_tokens + attention_output
)

The resulting representation is pooled using learned attention and passed through the crop head.

For every candidate crop, the model produces four scores:

text
[B, Nc, 4]

representing:

text
candidate 0 → [score 0°, score 90°, score 180°, score 270°]
candidate 1 → [score 0°, score 90°, score 180°, score 270°]
...

Cross Attention

The main architectural innovation introduced in version 20 is the use of Multi-Head Cross-Attention between the puzzle piece and each candidate crop.

For each candidate and each possible rotation:

text
Rotated Piece Patches
          │
          │ Query
          ▼
   ┌───────────────┐
   │Cross-Attention│
   └───────────────┘
          ▲
          │
          │ Key / Value
          │
    Crop Patches

The query consists of the patch tokens from the rotated puzzle piece.

The key and value consist of the patch tokens from the candidate crop.

This allows the model to learn interactions between regions of the piece and regions of the candidate rather than relying only on independent embeddings or patch-to-patch similarity.

The attention configuration is:

text
Embedding dimension: hidden_size
Number of heads:     8
Dropout:             0.1
Batch first:         True

The output has the same sequence length and hidden dimension as the piece representation:

text
[B × Nc × 4, 256, hidden_size]

A residual connection is applied:

python
attention_output = self.attention_norm(
    piece_tokens + attention_output
)

This provides the angle/crop head with a representation that incorporates information from both the piece and the candidate crop.


Crop Head

Version 20 uses a single neural head to score every:

text
candidate crop × rotation

combination.

The head is:

text
Linear(hidden_size → 512)
GELU
Dropout(0.2)

Linear(512 → 256)
GELU
Dropout(0.2)

Linear(256 → 1)

The input is:

text
[B, Nc, 4, hidden_size]

and the output is:

text
[B, Nc, 4]

The final dimension represents the four possible rotations:

text
0 →   0°
1 →  90°
2 → 180°
3 → 270°

The implementation is:

python
all_angle_logits = self.crop_head(
    pooled
).squeeze(-1)

The name all_angle_logits reflects the fact that the model initially calculates an angle-aware score for every candidate crop.


Crop Selection

For each candidate crop, the model has four scores:

text
[B, Nc, 4]

The crop score is defined as the best score among its four possible rotations:

python
crop_scores = all_angle_logits.max(dim=-1).values

This produces:

text
[B, Nc]

The best candidate is then selected using:

python
winner = crop_scores.argmax(dim=1)

Therefore:

text
all_angle_logits
        │
        ▼
max over rotations
        │
        ▼
crop_scores [B, Nc]
        │
        ▼
argmax
        │
        ▼
winner [B]

The use of the maximum means that a candidate crop is considered strong if at least one of its four possible orientations produces a high compatibility score.


Angle Prediction

Angle prediction is directly coupled to crop scoring.

The model first computes:

text
[B, Nc, 4]

scores for all candidate/rotation combinations.

After selecting the best candidate:

python
winner = crop_scores.argmax(dim=1)

the four scores corresponding to that candidate are extracted:

python
angle_logits = all_angle_logits[
    torch.arange(B, device=all_angle_logits.device),
    winner
]

The result is:

text
[B, 4]

representing the compatibility of the selected crop with each possible rotation.

The predicted angle is:

python
angle_logits.argmax(dim=1)

with the mapping:

text
0 →   0°
1 →  90°
2 → 180°
3 → 270°

This means that the model does not have an independent angle classifier. Instead, angle prediction is derived from the same crop/rotation compatibility scores used for candidate selection.


Model Output

The model returns a custom PuzzlePieceClassifierOutput:

python
@dataclass
class PuzzlePieceClassifierOutput(ModelOutput):
    crop_scores: torch.Tensor = None
    angle_logits: torch.Tensor = None

The two outputs are:

crop_scores

Shape:

text
[B, Nc]

Each value represents the best compatibility score for a candidate crop across its four possible rotations.

It is calculated as:

python
crop_scores = all_angle_logits.max(dim=-1).values

The predicted crop is:

python
crop_scores.argmax(dim=1)

angle_logits

Shape:

text
[B, 4]

These are the four rotation scores for the selected candidate crop.

The predicted angle class is:

python
angle_logits.argmax(dim=1)

The complete internal flow is therefore:

text
[B, Nc, 4]
       │
       ├── max(rotation) ──→ crop_scores [B, Nc]
       │
       └── select winner ──→ angle_logits [B, 4]

Input

The model receives two tensors.

Piece

python
piece_pixel_values

Shape:

text
[B, C, H, W]

This represents the isolated puzzle piece.

Candidate Crops

python
crop_pixel_values

Shape:

text
[B, Nc, C, H, W]

where Nc is the number of candidate crops.

Inside forward(), the candidates are flattened before being processed by DINOv2:

python
[B, Nc, C, H, W]
        │
        ▼
[B × Nc, C, H, W]

The candidate representations are then reshaped back to:

text
[B, Nc, hidden_size]

after feature extraction.


Dataset

The training dataset is composed of both:

text
Synthetic puzzle pieces
Real puzzle pieces

Real pieces are selected from annotations containing:

text
valid = true
bbox != None
puzzle != None

The original image is cropped using the bounding box and rotated according to its annotation before being prepared for the model.

Synthetic pieces are generated from known puzzle layouts and masks.


Candidate Generation

Each training sample contains:

text
1 positive candidate
+
hard negatives
+
random negatives

The positive candidate is always included.

Hard negatives are taken from:

python
piece.top3_hard_negatives

The remaining candidates are sampled randomly from the other puzzle positions.

The candidates are then shuffled, and the label is the position of the positive candidate in the shuffled list.

The implementation supports a configurable number of candidates, with six candidates used by default in PieceSample.

Conceptually:

text
Candidate Set
│
├── Positive
├── Hard Negative
├── Hard Negative
├── Hard Negative
├── Random Negative
└── Random Negative

This makes the crop-ranking task substantially harder than simply comparing a piece against arbitrary unrelated crops.


Hard Negative Mining

Hard negatives are updated dynamically during training.

After computing the crop scores, the model selects the top four candidates:

python
batch_topk = crop_scores.topk(4, dim=1)

These candidates are used to update the hard-negative list for future samples.

The correct candidate is removed from the hard-negative list, and up to three difficult candidates are retained.

The process can therefore be represented as:

text
Current model
     │
     ▼
Score candidates
     │
     ▼
Top-K candidates
     │
     ▼
Remove positive
     │
     ▼
Store difficult negatives
     │
     ▼
Future training samples

This creates an online hard-negative mining process.


Data Augmentation

Training samples can receive image augmentations including:

  • —brightness;
  • —contrast;
  • —color;
  • —Gaussian noise;
  • —Gaussian blur;
  • —combinations of the above.

The combined random augmentation applies each transformation independently with a probability of 50%.

The training samples use random_example as their image transformer.


Train / Validation Split

The available puzzle pieces are split using:

python
train_test_split(
    puzzles_pieces,
    test_size=0.2,
    random_state=86,
    stratify=puzzles_pieces_labels
)

The stratification label is the puzzle name.

Therefore, the split maintains the distribution of puzzle sources between training and validation.

The resulting datasets are:

text
80% training
20% validation

Dataset Class

The PyTorch dataset is:

python
PuzzlePieceClassifierDataset

Each item contains:

python
{
    "crop_pixel_values": ...,
    "piece_pixel_values": ...,
    "crop_labels": ...,
    "angle_labels": ...,
    "indices": ...,
    "candidate_indices": ...
}

The implementation converts the selected crop and angle into separate labels.

The angle labels correspond directly to the four angle classes.


Loss

The model uses two Cross Entropy losses:

text
Crop Loss
Angle Loss

The total loss is:

text
Loss =
    Crop Loss
    +
    angle_weight × Angle Loss

The two tasks contribute equally to the training objective.


Metrics

The training process evaluates the two tasks independently as well as jointly.

Crop Accuracy

Percentage of samples where the correct candidate is ranked first.

python
crop_scores.argmax(dim=1)

Angle Accuracy

Percentage of samples where the correct angle is predicted.

python
angle_logits.argmax(dim=1)

Joint Accuracy

A sample is considered correct only when:

text
crop prediction == correct crop
AND
angle prediction == correct angle

The implementation calculates all three metrics.


Crop Ranking Metrics

Because crop selection is a ranking problem, additional metrics are used.

Top-2 Accuracy

The correct crop must appear among the two highest-scoring candidates.

Top-3 Accuracy

The correct crop must appear among the three highest-scoring candidates.

Mean Reciprocal Rank

MRR evaluates how highly the correct candidate is ranked.

For example:

text
Rank 1 → 1.000
Rank 2 → 0.500
Rank 3 → 0.333
Rank 4 → 0.250

The implementation calculates MRR directly from the ordering of crop_scores.

These metrics are particularly useful because a correct candidate ranked second or third can still be useful to a downstream puzzle-search system.


Training Strategy

Training is performed in two phases.

Phase 1 — Train Heads

The DINOv2 backbone is frozen:

python
backbone_requires_grad=False

The custom heads are trained using a relatively aggressive learning rate:

text
Learning rate: 1e-3

The phase is configured for up to:

text
150 epochs

with early stopping patience:

text
4 epochs

The best model is selected according to:

text
validation loss

and saved together with the processor.

The purpose of this phase is to train the custom heads while preserving the pretrained DINOv2 representation.


Phase 2 — Fine-Tune DINOv2

The best checkpoint from Phase 1 is loaded and the DINOv2 backbone is unfrozen:

python
backbone_requires_grad=True

A substantially smaller learning rate is used:

text
Learning rate: 5e-6

The phase runs for up to:

text
40 epochs

with early stopping patience:

text
4 epochs

The training batch size is reduced to:

text
2

to accommodate the computational cost of running DINOv2 multiple times per sample.

The complete training strategy is therefore:

text
Phase 1
────────────────────────────
DINOv2        Frozen
Custom Heads  Trainable
LR            1e-3
Max Epochs    150
Patience      4
       │
       ▼
Best Checkpoint
       │
       ▼
Phase 2
────────────────────────────
DINOv2        Trainable
Custom Heads  Trainable
LR            5e-6
Max Epochs    40
Patience      4

Loading the Model

Because the model implements a custom Hugging Face PreTrainedModel and defines an auto_map, it should be loaded with remote/custom code enabled when loading the repository.

python
from transformers import AutoModel, AutoImageProcessor

MODEL_ID = "pablo-moreira/puzzle-piece-classifier"

processor = AutoImageProcessor.from_pretrained(MODEL_ID)

model = AutoModel.from_pretrained(
    MODEL_ID,
    trust_remote_code=True,
)

model.eval()

The custom model configuration maps:

text
AutoConfig
    → PuzzlePieceClassifierConfig

AutoModel
    → PuzzlePieceClassifier

and the processor use default Hugging Face AutoImageProcessor implementation.

Inference

The model requires:

text
1 puzzle piece
+
N candidate crops

The images are processed using the standard Hugging Face AutoImageProcessor.

Conceptually:

python
piece_inputs = processor(
    images=[piece_image],
    return_tensors="pt",
)

crop_inputs = processor(
    images=crop_images,
    return_tensors="pt",
)

The tensors need to be arranged as:

text
piece_pixel_values:
    [1, C, H, W]

crop_pixel_values:
    [1, N, C, H, W]

Then:

python
with torch.no_grad():
    outputs = model(
        piece_pixel_values=piece_pixel_values,
        crop_pixel_values=crop_pixel_values,
    )

Internally, the model evaluates all four rotations against every candidate crop:

text
                    Candidate Crops
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
       Crop 0         Crop 1          Crop N
          │              │              │
     ┌────┼────┐    ┌────┼────┐    ┌────┼────┐
     ▼    ▼    ▼    ▼    ▼    ▼    ▼    ▼    ▼
     0°  90°  180° 0°  90° 180° ...       270°
          │
          ▼
    Cross Attention
          │
          ▼
     Rotation Scores
          │
          ▼
    Best Crop + Angle

Reading the Prediction

Best Crop

python
crop_index = outputs.crop_scores.argmax(dim=1).item()

The value identifies the candidate crop with the highest compatibility score.

For example:

text
crop_index = 4

means that the fifth candidate crop is considered the best match.

Predicted Angle

python
angle_index = outputs.angle_logits.argmax(dim=1).item()

Convert the class to degrees:

python
angle = angle_index * 90

The mapping is:

text
0 →   0°
1 →  90°
2 → 180°
3 → 270°

Complete Inference Example

python
import torch

from transformers import (
    AutoModel,
    AutoImageProcessor,
)

from PIL import Image


MODEL_ID = "pablo-moreira/puzzle-piece-classifier"


# Load processor
processor = AutoImageProcessor.from_pretrained(MODEL_ID)

# Load model
model = AutoModel.from_pretrained(
    MODEL_ID,
    trust_remote_code=True,
)

model.eval()


# Load the isolated puzzle piece
piece_image = Image.open(
    "piece.png"
).convert("RGB")


# Load candidate crops
crop_images = [
    Image.open("crop_0.png").convert("RGB"),
    Image.open("crop_1.png").convert("RGB"),
    Image.open("crop_2.png").convert("RGB"),
    Image.open("crop_3.png").convert("RGB"),
    Image.open("crop_4.png").convert("RGB"),
    Image.open("crop_5.png").convert("RGB"),
]


# Process piece
piece_pixel_values = processor(
    images=[piece_image],
    return_tensors="pt",
)["pixel_values"]


# Process candidates
crop_pixel_values = processor(
    images=crop_images,
    return_tensors="pt",
)["pixel_values"]


# Add batch dimension to candidate crops
crop_pixel_values = crop_pixel_values.unsqueeze(0)


# Inference
with torch.no_grad():
    outputs = model(
        piece_pixel_values=piece_pixel_values,
        crop_pixel_values=crop_pixel_values,
    )


# Best candidate
crop_index = outputs.crop_scores.argmax(
    dim=1
).item()


# Best angle
angle_index = outputs.angle_logits.argmax(
    dim=1
).item()

angle = angle_index * 90


print("Best crop:", crop_index)
print("Angle:", angle)

The model will therefore return something conceptually equivalent to:

text
Best crop: 4
Angle: 90

meaning:

text
Candidate crop 4
should be used for the piece,
with a rotation of 90 degrees.

Training vs. Inference

There is an important difference between training and inference.

During training, the correct candidate can be passed as:

python
crop_labels

The model then uses the correct crop for the angle branch.

During inference, crop_labels is omitted:

python
outputs = model(
    crop_pixel_values=crop_pixel_values,
    piece_pixel_values=piece_pixel_values,
)

The model selects:

python
argmax(crop_scores)

and uses that candidate for angle prediction.

Therefore, the production pipeline is:

text
Piece
 │
 ▼
DINOv2
 │
 ├─────────────── Candidate 0
 ├─────────────── Candidate 1
 ├─────────────── Candidate 2
 ├─────────────── ...
 └─────────────── Candidate N
 │
 ▼
Crop Scores
 │
 ▼
Best Candidate
 │
 ▼
Selected Candidate + Piece
 │
 ▼
DINOv2 Patch Tokens
 │
 ├── Piece rotated 0°
 ├── Piece rotated 90°
 ├── Piece rotated 180°
 └── Piece rotated 270°
 │
 ▼
Cosine Similarity
 │
 ▼
Attention Pooling
 │
 ▼
Angle Prediction

Output Shapes

For:

text
batch_size = 1
number_of_candidates = 6

the model produces:

python
outputs.crop_scores.shape
text
torch.Size([1, 6])

and:

python
outputs.angle_logits.shape
text
torch.Size([1, 4])

The dimensions therefore represent:

text
crop_scores
    [batch, candidates]

angle_logits
    [batch, angles]

Limitations

Rotation Classes

The model predicts only four discrete rotations:

text
0°
90°
180°
270°

It does not directly predict arbitrary continuous angles.


Puzzle Domain

The model is specialized for jigsaw puzzles.

It is not intended as a general image matching model.


PuzzleMap Pipeline

The classifier is designed to operate after the PuzzleMap detection model.

The complete workflow can be represented as:

text
                    Puzzle Image
                         │
                         ▼
              Puzzle Piece Detection
                         │
                         ▼
                   Bounding Boxes
                         │
             ┌───────────┴───────────┐
             │                       │
             ▼                       ▼
       Piece Crop              Puzzle Candidates
             │                       │
             └───────────┬───────────┘
                         ▼
              Puzzle Piece Classifier
                         │
                  ┌──────┴──────┐
                  │             │
                  ▼             ▼
             Best Crop       Rotation
                  │             │
                  └──────┬──────┘
                         ▼
                Piece Localization
                + Orientation
                         │
                         ▼
              Other Puzzle Analysis
                         │
                         ▼
                 Puzzle Assembly

The detector answers:

text
Where are the pieces?

The classifier answers:

text
Which puzzle position does this piece belong to?
How should it be rotated?

Together they form the localization and matching stages of PuzzleMap.


Version Comparison

The following table summarizes the main milestones in the development of the Puzzle Piece Classifier.

Attributev17v18v19v20v21
Number of pieces41314131416741671393
-> Real pieces507507543543643
-> Synthetic pieces3624362436243624750
Number of train pieces33043304333333331114
Number of val pieces827827834834279
Number of puzzles77778
-> Puzzles with real pieces33337
Best epoch42110112
Train accuracy0.43830.91890.87010.94180.8034
Train angle accuracy0.51630.98400.97510.99130.9417
Train angle loss0.85580.38780.41320.02630.1520
Train crop accuracy0.84780.93370.89170.94690.8420
Train crop loss0.37800.17750.26520.14830.4145
Train loss1.23380.56540.67840.17460.5665
Train MRR0.91800.96490.94240.97130.9130
Train Top-2 crop0.96910.98940.98140.98860.9578
Train Top-3 crop0.99210.99760.99640.99760.9901
Validation accuracy0.47100.95890.93050.98680.9570
Validation angle accuracy0.48550.96620.94600.99400.9749
Validation angle loss0.83690.41270.46540.02160.0891
Validation crop accuracy0.96860.99030.97360.99040.9785
Validation crop loss0.07860.02310.06090.02710.0583
Validation loss0.49700.22950.52620.04870.1474
Validation MRR0.98410.99520.98680.99500.9886
Validation Top-2 crop0.99881.00001.00000.99880.9964
Validation Top-3 crop1.00001.00001.00001.00001.0000

Version 21

Version 21 focuses on dataset restructuring and improved train/validation stratification, while maintaining the patch-level cross-attention architecture introduced in Version 20.

The dataset was significantly reduced and rebalanced, increasing the relative proportion of real-world puzzle pieces while retaining synthetic examples for additional variation. This change was intended to improve dataset quality and provide a more representative evaluation of model generalization.

The train/validation splitting strategy was also redesigned to reduce potential data leakage:

  • —Real pieces are now grouped by their complete puzzle coordinate (puzzle.row.column), ensuring that the same physical piece cannot appear in both the training and validation sets.
  • —Synthetic pieces are grouped by puzzle and row (puzzle.row), keeping highly correlated pieces generated from the same source structure within the same split.
  • —This replaces the previous strategy, which stratified pieces only by puzzle name.

Together, these changes produce a more reliable validation set and a more realistic assessment of the model's ability to generalize, particularly to real-world puzzle pieces.

Version 20

Version 20 introduces a new architecture based on patch-level cross-attention between the puzzle piece and candidate crops.

  • —Replaced the previous CLS-based crop representation with DINOv2 patch tokens.
  • —Added a shared patch_proj projection for piece and crop patch tokens.
  • —Added an 8-head MultiheadAttention layer for piece-to-crop cross-attention.
  • —The rotated piece acts as the Query.
  • —Candidate crop patches act as Key and Value.
  • —Added a residual connection followed by LayerNorm after cross-attention.
  • —Added learned attention pooling over the cross-attended patch representations.
  • —The crop head now produces four scores per candidate, one for each possible rotation.
  • —The final crop score is the maximum score across the four rotations.
  • —The angle prediction is obtained from the four scores belonging to the selected crop.
  • —Maintained the four discrete angle classes: 0°, 90°, 180°, and 270°.
  • —Switched to the default Hugging Face AutoImageProcessor, eliminating the need for custom processor code.
  • —The processor no longer requires trust_remote_code=True.

Version 19

  • —Introduced DINOv2 patch-token similarity for angle prediction.
  • —Added learned attention pooling over the patch-level similarities.
  • —The crop branch uses the DINOv2 CLS representation with dedicated projections.
  • —Maintained four discrete angle classes: 0°, 90°, 180°, and 270°.
  • —Achieved 97.36% crop accuracy and 94.60% angle accuracy.
  • —Achieved 98.68% MRR and 100% Top-2/Top-3 crop accuracy.
  • —Represents the final architecture documented for the model.

Version 18

  • —Fixed the rotation handling for 90° and 270°.
  • —This was the major breakthrough in the angle prediction task.
  • —Angle accuracy increased from 48.55% to 96.62%.
  • —Joint accuracy increased from 47.10% to 95.89%.
  • —Crop accuracy reached 99.03%.
  • —Achieved the lowest validation loss among the reported versions: 0.2295.

Version 17

  • —Introduced the combined Crop Head + Angle Head architecture.
  • —Added prediction of the four possible piece rotations.
  • —Crop matching was already highly accurate.
  • —Angle prediction remained close to random/ambiguous behavior, reaching only 48.55% validation accuracy.
  • —The experiment was therefore abandoned.

The recorded experiment history is maintained in the training notebook.


Citation

If you use this model in your project, please reference:

text
Pablo Moreira.
Puzzle Piece Classifier.
PuzzleMap project.

License

This repository is released under the terms specified by the repository license.

The underlying facebook/dinov2-base model and the PuzzleMap dataset are subject to their respective licenses and terms of use.

Users are responsible for verifying the licensing requirements of the underlying pretrained model, dataset, and other dependencies used in their applications.