amfora.core.detection#

Image preprocessing, sherd masking, and the two feature detectors (blob and contour). This module also contains the paste-anchored MAD-scaled pop gate, the watershed cluster-recovery and multigrain-split passes, and the edge-band rejection helpers.

The full source lives in src/amfora/core/detection.py. Function docstrings are pulled directly from there into the listing below.

amfora.core.detection.sherd_mask(sherd_scan, gray=False, scan_dpi=1200, crop_buffer=125, auto_crop=True)[source]#

Enhanced sherd masking with optimal edge detection and adaptive parameters.

By default the mask is automatically cropped to the tightest bounding box of the detected sherd contour plus crop_buffer pixels on every side. This removes irrelevant background pixels from all downstream computations, significantly reducing processing time for large scans. Set auto_crop=False to skip cropping and retain the full original image dimensions (useful for stitching results back into a larger scan).

Parameters:
  • sherd_scan (numpy.ndarray) – A scanned image of the sherd for which you want a mask

  • gray (bool, optional) – If True returns single channel masked grayscale images; if False creates color masks (default: False)

  • scan_dpi (int, optional) – Scan resolution for adaptive parameter scaling (default: 1200) Valid range: 150-2400 DPI

  • crop_buffer (int, optional) – Extra pixels to keep beyond the sherd bounding box on all four sides (default: 75). Ignored when auto_crop=False.

  • auto_crop (bool, optional) – If True (default), crop the returned mask to the sherd bounding box plus crop_buffer. If False, return a full-size mask matching the original image dimensions.

Returns:

(mask, (y1, y2, x1, x2)) where mask is the binary mask (grayscale uint8 or 3-channel) and the second element is the crop rectangle in the original image’s pixel coordinates. When auto_crop=True the mask is already cropped; apply the same crop to the source image with apply_mask(image, mask, crop) or directly as image[y1:y2, x1:x2]. When auto_crop=False the crop rectangle spans the full image (0, H, 0, W) and the mask is full-size.

Return type:

tuple

amfora.core.detection.full_image_mask(image, gray=False)[source]#

Build a sherd_mask-shaped return for an image that is already pre-masked (i.e. the sherd fills the entire frame and there is no background to segment away). Use this in place of sherd_mask when the input is a backgroundless / tight-cropped sherd image so the GrabCut pipeline is skipped entirely.

Parameters:
  • image (numpy.ndarray) – Image whose full extent is treated as the sherd.

  • gray (bool, optional) – If True returns a single-channel mask; otherwise a 3-channel mask (matches sherd_mask’s default). Default: False.

Returns:

(mask, crop, best_contour) matching sherd_mask’s signature. mask is filled with 255 across the full image, crop is (0, H, 0, W, 0, 0, 0, 0), and best_contour is the rectangular contour traced around the image perimeter (image-coordinate space) so downstream geometry (e.g. minAreaRect orientation) still has a sherd boundary to work from.

Return type:

tuple

amfora.core.detection.apply_mask(image, mask, crop=None)[source]#

Apply a mask to an image (single image version of super_zorro_cv).

Parameters:
  • image (numpy.ndarray) – Original image to mask

  • mask (numpy.ndarray) – Mask to apply (from sherd_mask function). Must already be cropped to match the region described by crop if crop is provided.

  • crop (tuple or None, optional) – (y1, y2, x1, x2, pad_top, pad_bottom, pad_left, pad_right) crop rectangle returned by sherd_mask. The first four elements define the slice into the original image; the last four (optional) give zero-padding needed when the sherd is near the scan edge. When provided the image is sliced and padded to match the (already-cropped-and-padded) mask. When None the image is used as-is and must already match the mask dimensions.

Returns:

Masked image, cropped to the sherd bounding region when crop is provided.

Return type:

numpy.ndarray

amfora.core.detection.clahe_enhance(masked_image, clip_limit=2.0, tile_grid=(8, 8))[source]#

Apply CLAHE to the L* channel of a masked sherd image.

Enhances local contrast between paste and inclusions/voids so the downstream blob and contour detectors see a wider, cleaner intra-sherd intensity range. Operates in CIELAB to stay consistent with the rest of the analysis pipeline (sherd_blobs, contour_detection, and the color analysis all work in Lab).

Parameters:
  • masked_image (numpy.ndarray) – BGR image with the non-sherd background already set to zero (output of apply_mask).

  • clip_limit (float, optional) – CLAHE contrast clipping limit (default: 2.0). Higher values give more aggressive enhancement; values above ~4 tend to amplify noise.

  • tile_grid (tuple of int, optional) – CLAHE tile grid size (default: (8, 8)). Smaller tiles give more local adaptation but can introduce boundary artifacts in low-texture regions.

Returns:

BGR image with CLAHE-enhanced L*; background pixels (those that were zero on input) are re-zeroed so the mask remains intact.

Return type:

numpy.ndarray

Notes

CLAHE is applied to the full L* channel and then the original background (any pixel that was zero across all three input channels) is re-zeroed. Tiles spanning the sherd boundary see a bimodal histogram (black background + sherd); the clip_limit of 2.0 keeps the resulting boundary artifacts well below the inclusion-detection thresholds.

amfora.core.detection.setup_robust_blob_params(image, scan_dpi, blob_type='light', size_params=None)[source]#

Create robust blob detector parameters with adaptive thresholding and optional size overrides.

This function automatically calculates optimal detection parameters based on image characteristics, but allows user-specified size filtering to override the defaults.

Parameters:
  • image (numpy.ndarray) – Grayscale image for parameter calculation

  • scan_dpi (int) – Scan resolution in dots per inch (150-2400)

  • blob_type (str) –

    Type of blobs to detect: - ‘light’: inclusions (bright features on darker background) - ‘dark_inclusion’: inclusions (dark minerals — ferruginous grains,

    magnetite, biotite, dark grog). Uses the same adaptive dark thresholding as ‘dark’ but with inclusion-level size limits and strict shape filters: circularity >= 0.3, convexity >= 0.7, inertia ratio >= 0.35 (~3:1 max elongation). These ensure only high-confidence compact mineral grains are captured, leaving irregular dark features to the void detector.

    • ’dark’: voids (dark features/pores). Uses upper-bound shape filters (maxCircularity = 0.85, maxConvexity = 0.85) to reject near-perfect circles and very smooth convex shapes that are almost certainly mineral grains rather than voids.

  • size_params (dict, optional) –

    Size filtering parameters to override defaults. If None, uses:

    Inclusions (blob_type=’light’): - min: 0.1mm (fine silt boundary, Wentworth scale) - max: 15mm (very coarse gravel)

    Voids (blob_type=’dark’): - min: 0.25mm (macroscopic voids from organic burnout) - max: 15mm (larger voids are likely artifacts)

    For inclusions, provide: - ‘min_inclusion_area_px’: int, minimum area in pixels - ‘max_inclusion_area_px’: int, maximum area in pixels

    For voids, provide: - ‘min_void_area_px’: int, minimum area in pixels - ‘max_void_area_px’: int, maximum area in pixels

    Example for detecting inclusions up to 2cm diameter at 1200 DPI:

    dpcm = 1200 * 0.3937  # ~472 dots per cm
    max_area = int(np.pi * (2.0 / 2 * dpcm) ** 2)  # 2cm diameter
    size_params = {
        'min_inclusion_area_px': 50,
        'max_inclusion_area_px': max_area
    }
    

Returns:

Optimized parameters for blob detection with adaptive thresholding

Return type:

cv2.SimpleBlobDetector_Params

amfora.core.detection.sherd_blobs(image, scan_dpi=1200, size_params=None, blob_params=None, blur_scale=1.0, channels=('B', 'G', 'R'), combine_mode='union', vote_min=2, enhance_contrast=True, clahe_clip=2.0, clahe_grid=(8, 8), void_intensity_max=60.0, paste_pop_k=2.0, paste_pop_floor=8.0, edge_band_px=None)[source]#

Enhanced blob detection with robust, adaptive parameters and customizable size filtering.

Parameters:
  • image (numpy.ndarray) – Image array of a scanned sherd (not file path)

  • scan_dpi (int, optional) – Scan resolution in dots per inch (default: 1200) Valid range: 150-2400 DPI

  • size_params (dict, optional) – Dictionary containing size filtering parameters: - min_inclusion_area_px: minimum inclusion area in pixels - max_inclusion_area_px: maximum inclusion area in pixels - min_void_area_px: minimum void area in pixels - max_void_area_px: maximum void area in pixels

  • channels (tuple of str, optional) – Channels to run blob detection on. Default ('B', 'G', 'R') runs detection on each of OpenCV’s native BGR channels (not RGB) and combines the results, so inclusions that only contrast strongly in one channel (e.g. iron-rich grains in R, organic dark cores in B) get picked up. Valid entries also include 'L' (CIELAB lightness) — pass channels=('L',) to recover the pre-multi-channel L*-only behavior. L* is excluded from the default because it’s a perceptually-weighted blend of B/G/R, so including it gives features visible in L* an extra redundant vote in the combination step.

  • combine_mode ({'union', 'vote'}, optional) – How to merge per-channel detections when len(channels) > 1. Default 'union' pools detections and removes spatial duplicates without requiring cross-channel agreement. This catches monochromatic features that only contrast strongly in one channel — e.g. an iron-bearing mineral grain in sand temper may register as warm-toned against a cream matrix and thus pop in B (where the warm grain reads dark) while showing near-zero contrast in R (where both grain and matrix read bright). The prior 'vote' default with vote_min=2 was dropping roughly half of these legitimate single-channel detections. Noise rejection is instead handled by the paste-anchored pop gate (paste_pop_k / paste_pop_floor, sampled on raw, pre-CLAHE BGR), which is a stronger discriminator than per-channel agreement: it directly measures whether a candidate’s interior is statistically distinct from the sherd’s paste in noise-floor units. Use 'vote' only if you have a specific reason to require cross-channel agreement (e.g. very noisy scans where the pop gate alone is insufficient).

  • vote_min (int, optional) – Minimum number of channels that must agree for a feature to be kept when combine_mode='vote' (default: 2 of 3 BGR channels). Ignored under the default combine_mode='union'.

  • enhance_contrast (bool, optional) – Apply CLAHE to each requested channel before detection (default: True). Set to False if you’ve already pre-applied contrast enhancement to the input image — otherwise the detector handles CLAHE per channel internally.

  • clahe_clip (float / tuple, optional) – Forwarded to cv2.createCLAHE when enhance_contrast=True.

  • clahe_grid (float / tuple, optional) – Forwarded to cv2.createCLAHE when enhance_contrast=True.

  • void_intensity_max (float in 0..255, optional) – Maximum allowed mean pixel intensity inside a void keypoint’s disc, sampled from the (pre-blur) channel (default: 60). Mirrors the gate in contour_detection: a real pore reads near-black inside, while a dark mineral inclusion is just darker paste and stays well above black. Without this gate, dark mineral grains on light-grey fabrics show up in the void list because the dark-void blob detector’s upper-bound shape filters alone can’t separate them from grains. Lower (e.g. 45) for stricter void detection; raise (e.g. 90) for low-contrast scans.

  • paste_pop_k (float, optional) –

    Minimum “pop” required for a candidate inclusion to be kept, expressed as a multiple of the paste’s per-channel Median Absolute Deviation (default: 2.0 for blobs — looser than the contour detector’s 2.5 default because SimpleBlobDetector’s own shape filters already cull most noise, so the pop gate can afford to be more permissive without losing precision). “Pop” here is informal shorthand for how much the feature stands out against the sherd’s paste — concretely, on the raw (pre-CLAHE) BGR channels of the masked input image:

    paste_ref = per-channel median of non-zero pixels
    paste_MAD = per-channel median(|pixel - paste_ref|)
    keep iff  |median(disc) - paste_ref|  >=  max(K * MAD, floor)
              on at least one BGR channel
    

    The MAD scaling makes the same K work across paste types — smooth cream paste (MAD ~5) gets a tight absolute threshold, mottled grog-tempered paste (MAD ~18) gets a loose one. For normal data σ ≈ 1.4826 · MAD, so K=2.0 corresponds to roughly a 3σ detection threshold. Comparing against the global paste reference (instead of the local ring used by earlier versions) is what fixes the dense-cluster failure mode where bright paste pockets between dark grains were detected as light inclusions and dark cluster grains were rejected for having dark neighbors. Set to 0 to disable; raise (e.g. 2.5-3.0) to tighten precision on fine-grained fabrics; lower (e.g. 1.5) when chasing very subtle features.

  • paste_pop_floor (float in 0..255, optional) – Absolute brightness floor under the MAD-scaled threshold (default: 8.0). Effective threshold per channel is max(paste_pop_k * MAD, paste_pop_floor). Inactive on every real flatbed scan (where MAD ≥ ~7 across our calibration set), but protects against pathological near-zero-MAD inputs (synthetic test images, heavily slipped pieces) where a tiny MAD would otherwise collapse the gate.

  • edge_band_px (int or None, optional) –

    Width of the band inside the sherd mask boundary that is treated as “edge”; any blob whose center falls in this band is rejected as a CLAHE tile-boundary artifact or unmasked-overhang splotch. Default None uses the same value as contour_detection (max(5, 4 % of shorter image dimension)), so both detectors share an effective search area and analyze_single_sherd’s effective_detection_area_cm2 denominator stays consistent with both. Set to 0 to disable. Dictionary to override any cv2.SimpleBlobDetector_Params attributes after the adaptive defaults are calculated by setup_robust_blob_params. Applies to all three internal detectors (light-inclusion, dark-inclusion, and dark-void).

    Shape filtering keys: - filterByCircularity (bool), minCircularity (float 0–1)

    Default: disabled. Enable to restrict detection to compact grains. e.g. minCircularity=0.7 captures near-circular (quartz-like) grains and rejects elongated minerals (biotite laths, feldspar needles).

    • filterByConvexity (bool), minConvexity (float 0–1) Default: disabled. Enable to reject grains with deep concavities.

    • filterByInertia (bool), minInertiaRatio (float 0–1) Default: True / 0.2 (allows up to ~5:1 aspect ratio). This is the primary shape filter for inclusions and directly mirrors the inclusion_max_aspect_ratio parameter in contour_detection:

      minInertiaRatio = 1 / max_aspect_ratio 0.2 ↔ max_aspect_ratio = 5.0 (the shared default)

      Decreasing minInertiaRatio accepts more elongated shapes: e.g. 0.1 → ~10:1 max, 0.05 → ~20:1 (very elongated laths). Increasing restricts to more equant grains: e.g. 0.5 → ~4:1 max, 0.9 → ~1.2:1 (near-circular only).

    • minDistBetweenBlobs (float, pixels) Default: adaptive (~1.2× sqrt of min area). Increase to avoid double-counting adjacent touching grains.

    Threshold keys (override the adaptive calculation): - minThreshold, maxThreshold (float 0–255) - thresholdStep (float)

    Note: blobColor and filterByColor are set internally to select light vs dark features and should NOT be overridden here.

    Example — restrict to near-circular grains (quartz, oolites):

    inclusions, voids = amfora.sherd_blobs(
        masked_img, scan_dpi=SCAN_DPI,
        blob_params={'filterByCircularity': True, 'minCircularity': 0.7}
    )
    

    Example — accept highly elongated blobs (same as setting max_aspect_ratio=10 in contour_detection):

    inclusions, _ = amfora.sherd_blobs(
        masked_img, scan_dpi=SCAN_DPI,
        blob_params={'filterByInertia': True, 'minInertiaRatio': 0.1}
    )
    

Returns:

(inclusion_blobs, void_blobs) — Two lists of cv2.KeyPoint. inclusion_blobs contains both light and dark mineral inclusions; void_blobs contains all detected dark voids.

Return type:

tuple

Notes

Internally three detectors run: light-inclusions, dark-inclusions, and dark-voids. The dark-inclusion detector uses the same adaptive dark thresholding as the void detector but applies inclusion-level size limits and strict shape filters (circularity >= 0.2, convexity >= 0.5, inertia ratio >= 0.35) to capture only high-confidence dark mineral grains (ferruginous, magnetite, biotite, dark grog). The void detector uses upper-bound shape filters (maxCircularity = 0.85, maxConvexity = 0.85) to reject features that are too regular — near-perfect circles or very smooth convex shapes are almost certainly mineral grains, not voids. Together the lower-bound (dark-inclusion) and upper-bound (void) filters form complementary shape discriminators, but on real masked sherds the blur smooths concavities and dark mineral grains can still pass the void detector’s upper-bound shape gates. A second gate — the void_intensity_max brightness filter — therefore drops any void keypoint whose disc isn’t actually near-black, mirroring the gate in contour_detection. This makes the void/inclusion classification effectively mutually exclusive on common pottery samples.

blob_params overrides are applied to all three detectors. Note that blobColor and filterByColor are set internally per detector and should NOT be overridden.

blob.size represents the diameter of the detected blob in pixels. To convert to real-world measurements, use: diameter_cm = blob.size / (scan_dpi * 0.3937)

amfora.core.detection.contour_detection(image, scan_dpi=1200, size_params=None, shape_params=None, debug_mode=False, blur_scale=1.0, channels=('B', 'G', 'R'), combine_mode='union', vote_min=2, enhance_contrast=True, clahe_clip=2.0, clahe_grid=(8, 8), void_intensity_max=60.0, paste_pop_k=2.5, paste_pop_floor=8.0, watershed_enabled=True, multigrain_split_enabled=True, cluster_solidity_max=0.75, cluster_area_cm2_min=0.005)[source]#

Contour-based detection using the exact cv2_test.py methodology for individual inclusions.

This implements the approach from “Trying to find contours for individual inclusions”: 1. Threshold at 127 (not 125 from contour_counter) 2. Find contours using RETR_TREE, CHAIN_APPROX_SIMPLE 3. Sort by area (largest first) 4. Filter by solidity (convex hull ratio > 0.7)

Parameters:
  • image (numpy.ndarray) – Image array of a scanned sherd (not file path)

  • scan_dpi (int, optional) – Scan resolution in dots per inch (default: 1200) Valid range: 150-2400 DPI

  • size_params (dict, optional) –

    Size filtering parameters to override defaults. If None, uses:

    Inclusions: - min: 0.1 mm (smallest grain size for v. fine sand, Wentworth scale after accounting for elbow in chart) - max: 15mm (very coarse gravel)

    Voids: - min: 0.25mm (macroscopic voids from organic burnout) - max: 15mm (larger voids are likely artifacts)

    To override, provide a dict with: - ‘user_override’: bool, must be True to enable custom sizes - ‘min_inclusion_area_px’: int, minimum inclusion area in pixels - ‘max_inclusion_area_px’: int, maximum inclusion area in pixels - ‘min_void_area_px’: int, minimum void area in pixels - ‘max_void_area_px’: int, maximum void area in pixels

    Example for detecting features up to 2cm diameter at 1200 DPI:

    dpcm = 1200 * 0.3937  # ~472 dots per cm
    max_area = int(np.pi * (2.0 / 2 * dpcm) ** 2)  # 2cm diameter
    size_params = {
        'user_override': True,
        'min_inclusion_area_px': 50,
        'max_inclusion_area_px': max_area,
        'min_void_area_px': 100,
        'max_void_area_px': max_area
    }
    

  • shape_params (dict, optional) –

    Override the hardcoded shape-quality thresholds used to filter contours. If None, calibrated defaults are used.

    Keys: - inclusion_solidity_min (float, default 0.45)

    Ratio of contour area to convex hull area. Lower values (e.g. 0.3) accept more irregular, angular grains; higher values (e.g. 0.9) restrict to nearly-convex shapes only. The 0.45 default is permissive enough to capture angular ceramic temper (sub-angular to rounded grains in ceramic fabric score 0.6–0.95 still pass with margin).

    • inclusion_compactness_min (float, default 0.125) 4π · area / perimeter². A perfect circle = 1.0. Lower values accept more irregular outlines (e.g. angular grog fragments); higher values (e.g. 0.5) restrict to rounder, more compact grains.

    • void_solidity_min (float, default 0.1) Solidity lower bound for void contours. More permissive than for inclusions because firing voids from organic burnout can be very irregular.

    • void_compactness_min (float, default 0.06) 4π · area / perimeter² lower bound for void contours. Very permissive since organic-burnout voids can have highly irregular perimeters; tighter checks are handled by aspect ratio and the boundary-band gate.

    • void_solidity_max (float, default 1.01) Optional solidity upper bound for void contours. In principle voids are concave (low solidity) and inclusions are convex, but the DPI-scaled blur + contour-simplification pipeline rounds out concavities so on real masked sherds nearly all dark contours end up with solidity ≥ 0.5 regardless of class. The default leaves this gate effectively disabled; tighten it only if you know your scans preserve concavity well.

    • void_intensity_max (float in 0..255, default 60) Primary inclusion-vs-void discriminator. Maximum allowed mean pixel intensity inside a void contour, measured on the channel being processed. A real pore is a hole, so its interior reads near-black; a dark mineral inclusion is just darker paste with no near-black core. Lower this for stricter void detection (e.g. 45 to keep only deep blacks); raise it to count grey-toned cavities (e.g. 90 on low-contrast scans).

    • inclusion_max_aspect_ratio (float, default 4.0) Primary shape filter. Maximum allowed ratio of the longer side to the shorter side of the minimum-area bounding rectangle (from cv2.minAreaRect). Contours exceeding this ratio are rejected as wire-thin scan artifacts (dead pixel rows, calibration lines, thin scratches). This is the direct contour-detection counterpart of the blob detector’s minInertiaRatio filter:

      inclusion_max_aspect_ratio = 1 / minInertiaRatio 5.0 ↔ minInertiaRatio = 0.2 (the shared default for both detectors)

      Decreasing accepts fewer shapes (more equant only); increasing passes more elongated contours. Most ceramic inclusions (biotite laths, elongated grog) fall in the 2:1–4:1 range and are safely captured by the 4:1 default. Wire-thin artifacts typically exceed 10:1.

    • void_max_aspect_ratio (float, default 5.0)

      Maximum aspect ratio for void contours. Voids can be more elongated but still filter out wire-thin artifacts.

    • edge_band_px (int, default max(5, 4% of shorter image dimension))

      Width of the band inside the sherd mask boundary that is treated as “edge.” Any candidate contour with a vertex inside this band is rejected. Covers two failure modes: CLAHE tile-boundary leakage (the mask edge sits at a brightness discontinuity that CLAHE amplifies into apparent inclusions on the inner side), and unmasked-overhang artifacts (broken sherd-edge slivers that GrabCut leaves attached and read as dark splotches). ~4 % covers about half a CLAHE tile and most overhangs; e.g. ~40 px on a 1000×1000 crop, ~225 px on a 5669×5669 scan. analyze_single_sherd mirrors this band into the effective_detection_area_cm2 it uses as the denominator for density / area-percentage metrics, so they reflect the area actually searched. Set to 0 to disable.

    Example — strict detection, convex grains only:

    cr = amfora.contour_detection(
        masked_img, scan_dpi=SCAN_DPI,
        shape_params={
            'inclusion_solidity_min': 0.85,
            'inclusion_compactness_min': 0.45,
        }
    )
    

    Example — permissive detection, captures angular / irregular grains:

    cr = amfora.contour_detection(
        masked_img, scan_dpi=SCAN_DPI,
        shape_params={
            'inclusion_solidity_min': 0.3,
            'inclusion_compactness_min': 0.1,
            'void_solidity_min': 0.1,
        }
    )
    

  • debug_mode (bool, optional) – If True, prints a summary of candidate counts and filter decisions (default: False)

  • channels (tuple of str, optional) – Channels to run contour detection on. Default ('B', 'G', 'R') runs detection on each BGR channel and combines the results so inclusions that only contrast strongly in one channel get picked up. Valid entries also include 'L' (CIELAB lightness) — pass channels=('L',) to recover the pre-multi-channel behavior. See sherd_blobs for why L* is excluded by default.

  • combine_mode ({'union', 'vote'}, optional) – How to merge per-channel detections when len(channels) > 1. Default 'union' (matches analyze_single_sherd and sherd_blobs) pools detections and removes spatial duplicates via centroid containment without requiring cross-channel agreement — catches monochromatic features that single-channel detection alone would miss. Use 'vote' to require a contour’s centroid to fall inside the rasterized contour mask of at least vote_min channels for stricter noise rejection on low-contrast scans.

  • vote_min (int, optional) – Minimum number of channels that must agree for a contour to be kept when combine_mode='vote' (default: 2 of 3 BGR channels). Ignored under the default combine_mode='union'.

  • enhance_contrast (bool, optional) – Apply CLAHE to each requested channel before detection (default: True). Set to False if you’ve already pre-applied contrast enhancement to the input image — otherwise the detector handles CLAHE per channel internally.

  • clahe_clip (float / tuple, optional) – Forwarded to cv2.createCLAHE when enhance_contrast=True.

  • clahe_grid (float / tuple, optional) – Forwarded to cv2.createCLAHE when enhance_contrast=True.

Returns:

Dictionary containing: - ‘inclusions’: list of inclusion contours (cv2 contour arrays) - ‘voids’: list of void contours (cv2 contour arrays) - ‘inclusion_areas’: list of inclusion areas in cm² - ‘void_areas’: list of void areas in cm² - ‘total_inclusions’: count of inclusions - ‘total_voids’: count of voids - ‘debug_info’: dict with candidate counts, filter thresholds, and rejection breakdown.

When multi-channel mode is active, also contains a per_channel key mapping each channel to its individual debug_info.

Return type:

dict

amfora.core.detection.detect_multiple_sherds(sherd_scan, scan_dpi=1200, crop_buffer=125, auto_crop=True, n_sherds=None, min_area_cm2=0.75, mask=None)[source]#

Detect one or many sherds in a single scan and return per-sherd masks/crops.

This is the multi-sherd counterpart to sherd_mask. When the scanning plate carries several pieces it runs the same Canny + Otsu + adaptive threshold pipeline used by sherd_mask but, instead of keeping only the largest contour, retains every contour that survives an absolute-size filter, a bbox-IoU deduplication pass, and a gap-based stopping rule.

Auto-count heuristic#

  1. Pool contours from all three methods.

  2. Drop anything smaller than min_area_cm2 (DPI-aware) or larger than 90% of the image (filters out the whole-frame contour).

  3. Sort descending by area and deduplicate any pair whose bounding boxes overlap with IoU > 0.5 (keeps the larger of the two — prevents the outer Canny ring and the filled Otsu interior of the same sherd from being counted twice).

  4. Walk consecutive area ratios and stop at the largest drop-off (area[i] / area[i-1] minimum). Everything before the gap is a real sherd; everything after is noise.

If n_sherds is supplied, the gap rule is skipped and the top-N largest survivors are returned instead.

param sherd_scan:

The scanned image. Expected to be BGR (as returned by cv2.imread).

type sherd_scan:

numpy.ndarray

param scan_dpi:

Scan resolution for adaptive parameter scaling (default: 1200).

type scan_dpi:

int, optional

param crop_buffer:

Extra pixels kept beyond each sherd’s bounding box on all four sides when auto_crop=True (default: 125).

type crop_buffer:

int, optional

param auto_crop:

If True (default), each returned mask is cropped to its sherd’s bounding box plus crop_buffer. If False, every returned mask is full-image-sized.

type auto_crop:

bool, optional

param n_sherds:

Override the auto-count. When set, returns the top-N contours by area regardless of the gap heuristic. Default None = auto.

type n_sherds:

int, optional

param min_area_cm2:

Absolute lower bound on sherd area (default: 0.25 cm²). Contours below this are treated as noise.

type min_area_cm2:

float, optional

param mask:

Pre-computed multi-blob mask. When supplied, this function skips the edge pipeline and runs connected-components on mask instead. Useful for callers that already have a mask from a different source. Note: sherd_mask only ever produces a single-blob mask, so do not pass its output here.

type mask:

numpy.ndarray, optional

returns:

One entry per detected sherd, sorted descending by area. Each entry has the same keys sherd_mask would expose plus a few extras:

{
    'mask':     mask_slice,       # cropped+padded binary mask (uint8)
    'color_mask': color_mask_slice,  # 3-channel version of `mask`
    'crop':     (y1, y2, x1, x2, pad_top, pad_bottom, pad_left, pad_right),
    'contour':  contour,          # in image_cropped coordinates
    'bbox':     (x, y, w, h),     # bbox in image_cropped coords
    'centroid': (cx, cy),         # centroid in image_cropped coords
    'area':     area_px,          # contour area in pixels
    'area_cm2': area_cm2,         # contour area in cm²
}

Returns an empty list if no sherd survives the filters.

rtype:

list of dict

amfora.core.detection.split_multi_sherd_scan(image_path, output_dir, scan_dpi=1200, crop_buffer=125, n_sherds=None, min_area_cm2=0.25, write_manifest=True, manifest_path=None, apply_mask_to_output=False)[source]#

Split a (possibly multi-sherd) scan into one cropped image per sherd and write them to output_dir so full_analysis can consume them.

The output naming convention is:

N == 1 : <stem>.<ext>           (no suffix; behaves like a normal single-sherd scan)
N >= 2 : <stem>_1.<ext>, <stem>_2.<ext>, ...

The shared <stem> is the original filename’s stem, so downstream CSV rows (filename column from full_analysis) trace back to the source scan trivially.

Parameters:
  • image_path (str or pathlib.Path) – Path to the source scan.

  • output_dir (str or pathlib.Path) – Directory to write cropped per-sherd images into. Created if missing.

  • scan_dpi (int, optional) – Scan resolution (default: 1200).

  • crop_buffer (int, optional) – Pixels of padding around each sherd in the output crop (default: 125).

  • n_sherds (int, optional) – Force a specific number of sherds. Default None = auto-detect.

  • min_area_cm2 (float, optional) – Minimum sherd area (default: 0.25 cm²).

  • write_manifest (bool, optional) – If True (default), append a row per output file to manifest.csv in output_dir mapping it back to its source.

  • manifest_path (str or pathlib.Path, optional) – Override the default manifest location (output_dir/manifest.csv).

  • apply_mask_to_output (bool, optional) – If True, multiply each output crop by its mask so the background is black. Default False — write the raw crop so downstream sherd_mask can re-derive an accurate boundary.

Returns:

Paths of the written per-sherd images, in detection order (largest first).

Return type:

list of pathlib.Path

amfora.core.detection.prepare_multi_sherd_directory(input_dir, output_dir, scan_dpi=1200, crop_buffer=125, n_sherds=None, min_area_cm2=0.25, file_formats=None, write_manifest=True, apply_mask_to_output=False)[source]#

Batch wrapper for split_multi_sherd_scan.

Iterates every image in input_dir (recursively), splits each one, and writes the per-sherd crops into output_dir with consistent <stem>[_N].<ext> naming. A single combined manifest.csv is written into output_dir so every output file can be traced back to its source scan.

Parameters:
  • input_dir (str or pathlib.Path) – Directory of source scans (each scan may contain 1+ sherds).

  • output_dir (str or pathlib.Path) – Directory to write per-sherd images into.

  • scan_dpi – Forwarded to split_multi_sherd_scan and detect_multiple_sherds.

  • crop_buffer – Forwarded to split_multi_sherd_scan and detect_multiple_sherds.

  • n_sherds – Forwarded to split_multi_sherd_scan and detect_multiple_sherds.

  • min_area_cm2 – Forwarded to split_multi_sherd_scan and detect_multiple_sherds.

  • apply_mask_to_output – Forwarded to split_multi_sherd_scan and detect_multiple_sherds.

  • file_formats (list of str, optional) – Extensions to look for. Default: ['jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif'].

  • write_manifest (bool, optional) – Write a combined manifest.csv in output_dir (default True).

Returns:

All per-sherd image paths that were written.

Return type:

list of pathlib.Path

amfora.core.detection.super_zorro_cv(folder_read, folder_write, fileformat='jpeg', gray=False, scan_dpi=1200)[source]#

Enhanced batch sherd masking with optimal edge detection and adaptive parameters.

Parameters:
  • folder_read (str) – Path to folder containing images to process

  • folder_write (str) – Path to folder where masked images will be saved

  • fileformat (str, optional) – File format to process (default: ‘jpeg’)

  • gray (bool, optional) – If True saves single channel masked grayscale images; if False saves color masks (default: False)

  • scan_dpi (int, optional) – Scan resolution for adaptive parameter scaling (default: 1200) Valid range: 150-2400 DPI

Returns:

Saves processed images to folder_write

Return type:

None