CataNET-Terrain-v1.0-Small

A dual-head MobileNetV3-Small classifier that reads a single, top-down-rectified Catan hex tile patch and predicts both its terrain type (6 classes) and resource number token (13-way head; 12 real values plus a placeholder slot) in one forward pass. This is the second stage of the CataNET pipeline, consuming patches extracted by the CataNET-Meridian pose model.

This is the smaller/faster of the two CataNET-Terrain variants — see CataNET-Terrain-v1.0-Large for a larger backbone trading inference speed for accuracy.

Table of Contents

Model Details

Model Description

CataNET-Terrain-v1.0-Small is a dual-head classifier built on the mobilenet_v3_small architecture (via torchvision), trained from scratch — not fine-tuned from ImageNet or any other pretrained checkpoint. The shared MobileNetV3 feature extractor feeds two independent classification heads:

  • Terrain head: 6 classes — desert, fields, forest, hills, mountains, pasture
  • Token head: 13 classes — none, robber, 2-6, 7 (unused placeholder, never a valid label), 8-12

Input is expected to be a single 224×224 top-down-rectified crop of one hex tile (i.e. already isolated and perspective-corrected — not a full board photo).

  • Developed by: nithinmanoj10
  • Model type: Multi-task (dual-head) image classification
  • License: MIT
  • Finetuned from model: None — trained from randomly-initialized mobilenet_v3_small weights, no pretrained backbone.

Uses

Direct Use

Classifies a single, pre-cropped, top-down hex patch. Not intended to be run on a raw board photo directly — patches must first be extracted and perspective-rectified by an upstream pose/detection stage (see CataNET-Meridian). See How to Get Started, and note the required inference-mode workaround in Limitations before integrating this model.

Downstream Use

Intended as the terrain/token classification stage of the full CataNET pipeline: pose detection → per-hex patch extraction (homography-based rectification) → this model → structured board-state assembly, alongside a separate piece-color classifier for settlements/cities/roads.

Out-of-Scope Use

  • Not intended for anything other than the standard 19-hex Settlers of Catan base game board's terrain/token vocabulary.
  • Not a general-purpose texture or material classifier.
  • Does not detect hex position/geometry (see CataNET-Meridian) or player pieces (see the companion Lynx MicroCNN piece-color classifier).

Limitations

  • Requires model.train() at inference time, not model.eval() — this is intentional, not an oversight, but it's easy to get wrong. The checkpoint was validated with the model left in training mode (only nn.Dropout layers forced to .eval()), because BatchNorm behaves incorrectly with this checkpoint under standard .eval() mode. Critically, this means BatchNorm uses per-batch statistics — running inference with a batch size of 1 will produce degenerate, near-constant output (in internal testing, this collapsed predictions to a single class regardless of input). Always run inference on a real batch of multiple patches at once (e.g. all hexes from one board), never a lone single-image batch.
  • Token accuracy is substantially better than raw numbers alone suggest, but check your ground truth. In this model's own evaluation, roughly 20% of real-photo instances had to be excluded from token scoring because the token was cropped/illegible in that specific patch — not because of a model failure. If you're benchmarking this model yourself, budget for the same issue.
  • The robber class is a known, consistent weak point. In evaluation, robber-occupied tiles were misclassified as "no token" (none) in every single tested instance (0/6), same as the Large variant. Likely a sim-to-real gap in how the synthetic robber piece asset compares to a real physical piece.
  • Token 9 is a notably weaker point for this Small variant specifically — in evaluation it was misread as 5 in 8/12 instances, a substantially worse rate than the Large variant on the same data, suggesting the smaller backbone is more sensitive to this particular digit confusion.
  • forest/mountains terrain confusion, same as the Large variant — see Evaluation.
  • Trained entirely on synthetic data, composited from procedurally-generated terrain patches — a sim-to-real gap should be assumed beyond what's captured in the (small) real-photo evaluation set below.

Training Details

Training Data

Trained on the terrain-patch portion of nithinmanoj10/CatanSynth-Terrain-v1-50K — procedurally-generated, pre-rectified 224×224 hex tile crops, each labeled with its ground-truth terrain and token (the label is encoded directly in each file's name and mirrored in a labels.csv). See the dataset card for exact split sizes and class distribution.

Training Procedure

Preprocessing

Inputs are normalized with dataset-specific (not ImageNet) statistics — use these exact values, not the standard ImageNet mean/std:

MEAN = [0.7209611535072327, 0.5950123071670532, 0.4485519230365753]
STD = [0.2021966278553009, 0.20071130990982056, 0.19387811422348022]

Training hyperparameters

Setting Value
Backbone mobilenet_v3_small (torchvision), trained from scratch
Batch size 512
Epochs 100
Optimizer AdamW, lr=2e-3, weight decay 1e-4
LR schedule Linear warmup (5 epochs) → cosine annealing
Loss Weighted sum of both heads' cross-entropy: 0.25 * terrain_loss + 0.75 * token_loss (token reading treated as the harder task)
Sampling WeightedRandomSampler, combining inverse-frequency weights for both terrain and token, to counter real class imbalance (e.g. desert/robber are rare relative to numbered tiles)

Evaluation

Testing Data

Evaluated on 120 real hex-tile patches, extracted (via ground-truth keypoints) from the same 12 real Catan board photographs used to evaluate the CataNET-Meridian pose models, hand-labeled for terrain type and resource token. Labels went through a correction pass after an initial labeling error was caught (menu-position mixups specifically affecting low-value tokens) and were re-verified against the raw images before this evaluation was finalized.

Metrics

  • Terrain / Token accuracy — top-1 accuracy per head.
  • Terrain / Token macro-F1 — unweighted mean of per-class F1, chosen specifically because both heads have real class imbalance (see Training Procedure) that raw accuracy alone would mask.
  • Joint accuracy — fraction of patches where both heads are simultaneously correct.

Results

Metric Value
Terrain accuracy 0.725
Terrain macro-F1 0.707
Token accuracy* 0.750
Token macro-F1* 0.569
Joint accuracy (both correct)* 0.615

* Computed on 96/120 patches; 24 were excluded where the number token was cropped/illegible in that specific patch (not scoreable against any model).

Summary

desert and pasture terrain, and most of tokens 8-12, are classified reliably. The model confuses forest↔mountains terrain, never correctly identifies a robber-occupied tile (0/6), and is noticeably weaker than the Large variant at distinguishing token 9 from 5 — see Limitations. N=120 patches / 12 photos, selected for clean detections rather than randomly sampled — sufficient to characterize behavior, not a large-scale benchmark.

How to Get Started with the Model

import torch
import torch.nn as nn
from huggingface_hub import hf_hub_download
from torchvision.models import mobilenet_v3_small
from torchvision import transforms
from PIL import Image

class CatanEnvironmentNet(nn.Module):
    def __init__(self, num_terrains=6, num_tokens=13):
        super().__init__()
        backbone = mobilenet_v3_small(weights=None)
        self.features = backbone.features
        self.pool = backbone.avgpool
        feature_dim = 576
        self.terrain_head = nn.Sequential(
            nn.Flatten(), nn.Linear(feature_dim, 128), nn.Hardswish(), nn.Dropout(0.2), nn.Linear(128, num_terrains)
        )
        self.token_head = nn.Sequential(
            nn.Flatten(), nn.Linear(feature_dim, 256), nn.Hardswish(), nn.Dropout(0.2), nn.Linear(256, num_tokens)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.pool(x)
        return self.terrain_head(x), self.token_head(x)

weights_path = hf_hub_download(repo_id="nithinmanoj10/CataNET-Terrain-v1.0-Small", filename="best.pth")
model = CatanEnvironmentNet()
model.load_state_dict(torch.load(weights_path, map_location="cpu"))

# IMPORTANT: see Limitations -- .train() (not .eval()), only Dropout forced to eval,
# and ALWAYS run on a real batch (not a single image) or BatchNorm output collapses.
model.train()
for m in model.modules():
    if isinstance(m, nn.Dropout):
        m.eval()

preprocess = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.7209611535072327, 0.5950123071670532, 0.4485519230365753],
        std=[0.2021966278553009, 0.20071130990982056, 0.19387811422348022],
    ),
])

TERRAIN_CLASSES = ["desert", "fields", "forest", "hills", "mountains", "pasture"]
TOKEN_CLASSES = ["none", "robber", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]

patches = [preprocess(Image.open(p).convert("RGB")) for p in ["hex_0.jpg", "hex_1.jpg", "..."]]  # a real batch, not one image
batch = torch.stack(patches)

with torch.no_grad():
    terrain_logits, token_logits = model(batch)
    terrains = [TERRAIN_CLASSES[i] for i in terrain_logits.argmax(1).tolist()]
    tokens = [TOKEN_CLASSES[i] for i in token_logits.argmax(1).tolist()]

Citation

No formal paper accompanies this model. If referencing it, please cite the repository directly:

@misc{catanet-terrain-small,
  author = {nithinmanoj10},
  title = {CataNET-Terrain-v1.0-Small},
  year = {2026},
  publisher = {Hugging Face},
  howpublished = {\url{https://huggingface.co/nithinmanoj10/CataNET-Terrain-v1.0-Small}}
}

Model Card Contact

nithinmanoj10 via Hugging Face.

Downloads last month
33
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for nithinmanoj10/CataNET-Terrain-v1.0-Small

Quantized
(1)
this model

Dataset used to train nithinmanoj10/CataNET-Terrain-v1.0-Small

Collection including nithinmanoj10/CataNET-Terrain-v1.0-Small

Evaluation results