Skip to content

ascota_classification.decoration

The classification decoration module provides functionality for classifying pottery decoration patterns into two main categories: impressed and incised. Impressed decorations are created by pressing objects into the clay, while incised decorations are made by cutting or carving into the clay surface. The module uses a pre-trained DINOv2 ViT-L/14 model for feature extraction combined with an optimized Linear Logistic Regression classifier, providing accurate and reliable classification of pottery decoration patterns from images with transparent backgrounds.


Decoration classification module for pottery images with transparent backgrounds.

This module classifies pottery decoration patterns into two categories: - Impressed: decorations made by pressing objects into the clay - Incised: decorations made by cutting/carving into the clay

The classification uses a pre-trained DINOv2 ViT-L/14 model with optimized logistic regression classifier.

_load_dino_model

_load_dino_model(device)

Load the DINOv2 ViT-L/14 model for feature extraction.

Parameters:

Name Type Description Default
device device

PyTorch device (cpu or cuda)

required

Returns:

Type Description
AutoModel

Loaded DINOv2 model in eval mode

Source code in src/ascota_classification/decoration.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def _load_dino_model(device: torch.device) -> AutoModel:
    """
    Load the DINOv2 ViT-L/14 model for feature extraction.

    Args:
        device: PyTorch device (cpu or cuda)

    Returns:
        Loaded DINOv2 model in eval mode
    """
    try:
        model = AutoModel.from_pretrained('facebook/dinov2-large')
        model.eval()
        model.to(device)
        return model
    except Exception as e:
        raise RuntimeError(f"Failed to load DINOv2 model: {e}")

_extract_features

_extract_features(image, model, device)

Extract DINO features from pottery image.

Parameters:

Name Type Description Default
image Image

PIL Image with transparent background (RGBA)

required
model AutoModel

Pre-loaded DINOv2 model

required
device device

PyTorch device

required

Returns:

Type Description
ndarray

Feature vector as numpy array

Source code in src/ascota_classification/decoration.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def _extract_features(
    image: Image.Image,
    model: AutoModel,
    device: torch.device
) -> np.ndarray:
    """
    Extract DINO features from pottery image.

    Args:
        image: PIL Image with transparent background (RGBA)
        model: Pre-loaded DINOv2 model
        device: PyTorch device

    Returns:
        Feature vector as numpy array
    """
    # Convert RGBA to RGB with white background
    if image.mode == 'RGBA':
        # Create white background
        background = Image.new('RGB', image.size, (255, 255, 255))
        background.paste(image, mask=image.split()[-1])  # Use alpha channel as mask
        image = background
    elif image.mode != 'RGB':
        image = image.convert('RGB')

    # Standard DINO preprocessing
    transform = transforms.Compose([
        transforms.Resize(224),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])

    # Preprocess image
    image_tensor = transform(image).unsqueeze(0).to(device)

    # Extract features
    with torch.no_grad():
        outputs = model(image_tensor)
        # Use pooler output if available, otherwise mean of last hidden state
        if hasattr(outputs, 'pooler_output') and outputs.pooler_output is not None:
            features = outputs.pooler_output
        else:
            features = outputs.last_hidden_state.mean(dim=1)

    return features.cpu().numpy().flatten()

_load_classifier

_load_classifier(model_path)

Load the trained logistic regression classifier and its parameters.

Parameters:

Name Type Description Default
model_path Path

Path to the saved model file

required

Returns:

Type Description
Tuple[any, Optional[Dict]]

Tuple of (classifier, parameters dict or None)

Source code in src/ascota_classification/decoration.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def _load_classifier(model_path: Path) -> Tuple[any, Optional[Dict]]:
    """
    Load the trained logistic regression classifier and its parameters.

    Args:
        model_path: Path to the saved model file

    Returns:
        Tuple of (classifier, parameters dict or None)
    """
    if not model_path.exists():
        raise FileNotFoundError(
            f"Model file not found: {model_path}\n"
            f"Please ensure the trained model is available at this location."
        )

    try:
        classifier = joblib.load(model_path)

        # Try to load parameters file if it exists
        params_path = model_path.parent / model_path.name.replace('_optimized.pkl', '_params.pkl')
        params = None
        if params_path.exists():
            params = joblib.load(params_path)

        return classifier, params
    except Exception as e:
        raise RuntimeError(f"Failed to load classifier: {e}")

classify_pottery_decoration

classify_pottery_decoration(image, model_path=None, return_confidence=False, debug=False)

Classify pottery decoration pattern from an image with transparent background.

Parameters:

Name Type Description Default
image Image

PIL Image with transparent background (RGBA or RGB format)

required
model_path Optional[Path]

Path to trained model file. If None, uses default model path.

None
return_confidence bool

If True, include decision function scores in output

False
debug bool

If True, print debug information

False

Returns:

Type Description
Dict[str, any]

Dictionary containing: - "label": Classification result ("Impressed" or "Incised") - "method": "DINOv2 + Logistic Regression" - "confidence": (Optional) Decision function score if return_confidence=True - "model_params": (Optional) Model hyperparameters if available

Raises:

Type Description
FileNotFoundError

If model file is not found

RuntimeError

If model loading or inference fails

Examples:

>>> from PIL import Image
>>> img = Image.open("pottery_decoration.png")
>>> result = classify_pottery_decoration(img, debug=True)
>>> print(result["label"])
'Impressed'
>>> result = classify_pottery_decoration(img, return_confidence=True)
>>> print(f"Label: {result['label']}, Confidence: {result['confidence']:.4f}")
Label: Impressed, Confidence: 0.8532
Source code in src/ascota_classification/decoration.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def classify_pottery_decoration(
    image: Image.Image,
    model_path: Optional[Path] = None,
    return_confidence: bool = False,
    debug: bool = False
) -> Dict[str, any]:
    """
    Classify pottery decoration pattern from an image with transparent background.

    Args:
        image: PIL Image with transparent background (RGBA or RGB format)
        model_path: Path to trained model file. If None, uses default model path.
        return_confidence: If True, include decision function scores in output
        debug: If True, print debug information

    Returns:
        Dictionary containing:
            - "label": Classification result ("Impressed" or "Incised")
            - "method": "DINOv2 + Logistic Regression"
            - "confidence": (Optional) Decision function score if return_confidence=True
            - "model_params": (Optional) Model hyperparameters if available

    Raises:
        FileNotFoundError: If model file is not found
        RuntimeError: If model loading or inference fails

    Examples:
        >>> from PIL import Image
        >>> img = Image.open("pottery_decoration.png")
        >>> result = classify_pottery_decoration(img, debug=True)
        >>> print(result["label"])
        'Impressed'

        >>> result = classify_pottery_decoration(img, return_confidence=True)
        >>> print(f"Label: {result['label']}, Confidence: {result['confidence']:.4f}")
        Label: Impressed, Confidence: 0.8532
    """
    # Use default model path if not provided
    if model_path is None:
        # Convert default string path to Path object relative to module location
        model_path = Path(__file__).parent / DEFAULT_MODEL_PATH
    elif isinstance(model_path, str):
        # Convert string to Path object if needed
        model_path = Path(model_path)

    if debug:
        print(f"Using model: {model_path}")

    # Setup device
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    if debug:
        print(f"Using device: {device}")

    try:
        # Load DINOv2 model
        if debug:
            print("Loading DINOv2 model...")
        dino_model = _load_dino_model(device)

        # Extract features
        if debug:
            print("Extracting features...")
        features = _extract_features(image, dino_model, device)

        if debug:
            print(f"Feature shape: {features.shape}")

        # Load classifier
        if debug:
            print("Loading classifier...")
        classifier, params = _load_classifier(model_path)

        # Make prediction
        if debug:
            print("Making prediction...")

        # Reshape features for prediction (classifier expects 2D array)
        features_2d = features.reshape(1, -1)
        prediction = classifier.predict(features_2d)[0]

        # Map numeric prediction to label
        # Assuming 0 = impressed, 1 = incised based on sklearn.preprocessing.LabelEncoder
        label_map = {0: "Impressed", 1: "Incised"}
        label = label_map.get(prediction, "Unknown")

        result = {
            "label": label,
            "method": "DINOv2 + Logistic Regression"
        }

        # Add confidence score if requested
        if return_confidence:
            if hasattr(classifier, 'decision_function'):
                decision_score = classifier.decision_function(features_2d)[0]
                # For binary classification, convert to probability-like confidence
                # Higher absolute value means higher confidence
                confidence = abs(decision_score)
                result["confidence"] = float(confidence)
                result["decision_score"] = float(decision_score)

                if debug:
                    print(f"Decision score: {decision_score:.4f}")
                    print(f"Confidence: {confidence:.4f}")

        # Add model parameters if available
        if params is not None:
            result["model_params"] = params
            if debug:
                print(f"Model parameters: {params}")

        if debug:
            print(f"Classification result: {label}")

        return result

    except Exception as e:
        raise RuntimeError(f"Classification failed: {e}")

batch_classify_pottery_decoration

batch_classify_pottery_decoration(images, model_path=None, return_confidence=False, debug=False)

Classify multiple pottery decoration images efficiently.

This function loads the models once and reuses them for all images, making it more efficient than calling classify_pottery_decoration repeatedly.

Parameters:

Name Type Description Default
images list[Image]

List of PIL Images with transparent backgrounds

required
model_path Optional[Path]

Path to trained model file. If None, uses default model path.

None
return_confidence bool

If True, include confidence scores in output

False
debug bool

If True, print debug information

False

Returns:

Type Description
list[Dict[str, any]]

List of classification result dictionaries, one per image

Examples:

>>> from PIL import Image
>>> images = [Image.open(f"pottery_{i}.png") for i in range(5)]
>>> results = batch_classify_pottery_decoration(images)
>>> for i, result in enumerate(results):
...     print(f"Image {i}: {result['label']}")
Source code in src/ascota_classification/decoration.py
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def batch_classify_pottery_decoration(
    images: list[Image.Image],
    model_path: Optional[Path] = None,
    return_confidence: bool = False,
    debug: bool = False
) -> list[Dict[str, any]]:
    """
    Classify multiple pottery decoration images efficiently.

    This function loads the models once and reuses them for all images,
    making it more efficient than calling classify_pottery_decoration repeatedly.

    Args:
        images: List of PIL Images with transparent backgrounds
        model_path: Path to trained model file. If None, uses default model path.
        return_confidence: If True, include confidence scores in output
        debug: If True, print debug information

    Returns:
        List of classification result dictionaries, one per image

    Examples:
        >>> from PIL import Image
        >>> images = [Image.open(f"pottery_{i}.png") for i in range(5)]
        >>> results = batch_classify_pottery_decoration(images)
        >>> for i, result in enumerate(results):
        ...     print(f"Image {i}: {result['label']}")
    """
    # Use default model path if not provided
    if model_path is None:
        # Convert default string path to Path object relative to module location
        model_path = Path(__file__).parent / DEFAULT_MODEL_PATH
    elif isinstance(model_path, str):
        # Convert string to Path object if needed
        model_path = Path(model_path)

    if debug:
        print(f"Using model: {model_path}")
        print(f"Processing {len(images)} images...")

    # Setup device
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    if debug:
        print(f"Using device: {device}")

    try:
        # Load models once
        if debug:
            print("Loading DINOv2 model...")
        dino_model = _load_dino_model(device)

        if debug:
            print("Loading classifier...")
        classifier, params = _load_classifier(model_path)

        # Process all images
        results = []
        for i, image in enumerate(images):
            if debug and (i + 1) % 10 == 0:
                print(f"Processing image {i + 1}/{len(images)}...")

            # Extract features
            features = _extract_features(image, dino_model, device)

            # Make prediction
            features_2d = features.reshape(1, -1)
            prediction = classifier.predict(features_2d)[0]

            # Map to label
            label_map = {0: "Impressed", 1: "Incised"}
            label = label_map.get(prediction, "Unknown")

            result = {
                "label": label,
                "method": "DINOv2 + Logistic Regression"
            }

            # Add confidence if requested
            if return_confidence and hasattr(classifier, 'decision_function'):
                decision_score = classifier.decision_function(features_2d)[0]
                confidence = abs(decision_score)
                result["confidence"] = float(confidence)
                result["decision_score"] = float(decision_score)

            # Add model parameters to first result only
            if i == 0 and params is not None:
                result["model_params"] = params

            results.append(result)

        if debug:
            print(f"✓ Processed {len(images)} images successfully!")
            # Print summary
            impressed_count = sum(1 for r in results if r["label"] == "Impressed")
            incised_count = sum(1 for r in results if r["label"] == "Incised")
            print(f"Summary: {impressed_count} Impressed, {incised_count} Incised")

        return results

    except Exception as e:
        raise RuntimeError(f"Batch classification failed: {e}")