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_bufferpixels on every side. This removes irrelevant background pixels from all downstream computations, significantly reducing processing time for large scans. Setauto_crop=Falseto 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. Whenauto_crop=Truethe mask is already cropped; apply the same crop to the source image withapply_mask(image, mask, crop)or directly asimage[y1:y2, x1:x2]. Whenauto_crop=Falsethe 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_maskwhen 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)matchingsherd_mask’s signature.maskis filled with 255 across the full image,cropis(0, H, 0, W, 0, 0, 0, 0), andbest_contouris the rectangular contour traced around the image perimeter (image-coordinate space) so downstream geometry (e.g.minAreaRectorientation) 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
cropifcropis provided.crop (tuple or None, optional) –
(y1, y2, x1, x2, pad_top, pad_bottom, pad_left, pad_right)crop rectangle returned bysherd_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
cropis provided.- Return type:
- 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:
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_limitof 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) — passchannels=('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 withvote_min=2was 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 defaultcombine_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.createCLAHEwhenenhance_contrast=True.clahe_grid (float / tuple, optional) – Forwarded to
cv2.createCLAHEwhenenhance_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
Noneuses the same value ascontour_detection(max(5, 4 % of shorter image dimension)), so both detectors share an effective search area andanalyze_single_sherd’seffective_detection_area_cm2denominator 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_ratioparameter incontour_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_maxbrightness filter — therefore drops any void keypoint whose disc isn’t actually near-black, mirroring the gate incontour_detection. This makes the void/inclusion classification effectively mutually exclusive on common pottery samples.blob_paramsoverrides are applied to all three detectors. Note thatblobColorandfilterByColorare 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’sminInertiaRatiofilter: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_sherdmirrors this band into theeffective_detection_area_cm2it 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) — passchannels=('L',)to recover the pre-multi-channel behavior. Seesherd_blobsfor why L* is excluded by default.combine_mode ({'union', 'vote'}, optional) – How to merge per-channel detections when
len(channels) > 1. Default'union'(matchesanalyze_single_sherdandsherd_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 leastvote_minchannels 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 defaultcombine_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.createCLAHEwhenenhance_contrast=True.clahe_grid (float / tuple, optional) – Forwarded to
cv2.createCLAHEwhenenhance_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_channelkey 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 bysherd_maskbut, 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#
Pool contours from all three methods.
Drop anything smaller than
min_area_cm2(DPI-aware) or larger than 90% of the image (filters out the whole-frame contour).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).
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_sherdsis 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
maskis cropped to its sherd’s bounding box pluscrop_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
maskinstead. Useful for callers that already have a mask from a different source. Note:sherd_maskonly 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_maskwould 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_dirsofull_analysiscan 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 (filenamecolumn fromfull_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.csvinoutput_dirmapping 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 downstreamsherd_maskcan 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 intooutput_dirwith consistent<stem>[_N].<ext>naming. A single combinedmanifest.csvis written intooutput_dirso 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_scananddetect_multiple_sherds.crop_buffer – Forwarded to
split_multi_sherd_scananddetect_multiple_sherds.n_sherds – Forwarded to
split_multi_sherd_scananddetect_multiple_sherds.min_area_cm2 – Forwarded to
split_multi_sherd_scananddetect_multiple_sherds.apply_mask_to_output – Forwarded to
split_multi_sherd_scananddetect_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.csvinoutput_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