Extending the Catalog¶
This guide walks through adding support for a new imaging device to the device
catalog. The catalog data resides in the dicom_dre.default_catalog module;
the matching engine and factory functions reside in dicom_dre.catalog.
Note
This page is the how-to walkthrough. For the reference description of device rules, variants, scrub-region coordinates, and evaluation order, see Device catalog.
1. Inspect a representative DICOM file¶
Read the tags the catalog matches on from a real instance the device produced.
pydicom is available in the project environment:
import pydicom
ds = pydicom.dcmread("sample.dcm", stop_before_pixels=True)
for kw in (
"Manufacturer", "ManufacturerModelName", "Modality",
"SoftwareVersions", "ImageType", "Rows", "Columns",
"SOPClassUID", "BurnedInAnnotation", "SeriesDescription",
):
print(f"{kw:30} {ds.get(kw, '')!r}")
Collect several instances across the resolutions and software versions the device produces. Devices frequently emit more than one image size, and each size usually needs its own scrub region.
2. Choose the smallest set of identifying fields¶
Pick the fields that uniquely identify the device without over-constraining.
manufacturer + modality + manufacturer_model_name is the common baseline.
Add software_versions, sop_class_uid, or image_type_exclude only when
needed to separate this device from another rule or to exclude derived and
screenshot images.
Choose a match prefix per field (see String matching):
Use
^(starts-with) for model families like"^AXIOM".Use
=(exact) when a substring would collide with another model, for example"=S1000"versus"=S2000".Use a bare substring to require only that the tag contains a known token and the surrounding text varies. For example,
"KONICA"matches both"KONICA"and"KONICA MINOLTA", so it is not necessary to enumerate every vendor string variation.Use
/regex/for alternations or negative lookahead, for example"/^(?!YES$)/"to match anyBurnedInAnnotationthat is not exactlyYES.
3. Determine scrub regions¶
If the device burns patient text into the pixels, identify the rectangles to
blank. Open a representative image, note the pixel bounds of each text banner,
and express them as (x, y, width, height) tuples using the
coordinate system
described in the concept page. Bind each set of regions to the resolution it
applies to with a variant.
If the device produces clean pixels (no burned-in text), omit scrub
entirely. The engine allows the device with no blanking.
4. Write the device entry¶
Add the entry to the modality-appropriate list in default_catalog.py
(_cr_dx_devices, _ct_pet_devices, _nm_devices, _mammo_devices,
_breast_mri_devices, _us_devices, or _scrub_only_devices). Order matters:
the first matching allow or deny device takes precedence, so place a specific
rule before a more general one.
Single-resolution device with one scrub banner:
device(
"Acme UltraView 3000 US",
"allow",
manufacturer="Acme",
modality="US",
manufacturer_model_name="=UltraView 3000",
sop_class_uid=[f"^{_US_SOP_SINGLE}", f"^{_US_SOP_MULTI}"],
requires_ultrasound_regions=True,
rows=768,
cols=1024,
scrub=[(0, 0, 1024, 60)],
)
The _US_SOP_SINGLE, _US_SOP_MULTI, and _US_SOP_SC module constants are the
SOP Class UID prefixes for single-frame, multi-frame, and secondary-capture
ultrasound images. Ultrasound rules list the frame types they accept; other
modalities usually constrain sop_class_uid directly or omit it.
Multi-resolution device, one variant per image size:
device(
"Acme UltraView 3000 US",
"allow",
manufacturer="Acme",
modality="US",
manufacturer_model_name="=UltraView 3000",
sop_class_uid=[f"^{_US_SOP_SINGLE}", f"^{_US_SOP_MULTI}"],
variants=[
variant(rows=768, cols=1024, scrub=[(0, 0, 1024, 60)]),
variant(rows=600, cols=800, scrub=[(0, 0, 800, 48)]),
],
)
5. Handle a new modality¶
If the device belongs to a modality that the deny-list currently rejects, the
allow rule must be reachable before the exclusion. The engine always evaluates
device rules before exclusion rules, so an allow device for a denied modality
takes precedence. Confirm that no deny_when condition catches the modality
(for example the SECONDARY/DERIVED image-type denials) after the device
matches. Those only run when no device rule returns allow.
To introduce an entirely new modality group, create a new
list[DeviceRule] (following the _us_devices pattern) and append it to
default_devices at the bottom of the file:
default_devices: list[DeviceRule] = (
_cr_dx_devices
+ _ct_pet_devices
+ ...
+ _new_modality_devices
+ _scrub_only_devices
)
6. Match a tag without a dedicated field¶
Every match attribute has a dedicated device() parameter; there is no
free-form keyword dict. If an attribute that has no parameter yet identifies the
device, add an explicit field:
Add the field to
DeviceRule, thedevice()factory, and_match_device()incatalog.py, wiring it to the DICOM keyword via_match_field.Add the keyword to the
keywordslist inDicomTags.from_dataset()so the extractor reads the value. Otherwise the value is always empty and the match never succeeds. The regression fixtures capture exactly this keyword set, so an unextracted keyword also has no regression coverage.
device(
"Acme Breast US",
"allow",
manufacturer="Acme",
modality="US",
body_part_examined="/(?i)^BREAST$/",
)
7. Add a test¶
Add a unit test to tests/unit/test_default_catalog.py that builds a
DicomTags from a plain dict and asserts the decision. This does not require a
DICOM file:
def test_acme_ultraview_allows(self, catalog):
tags = DicomTags(
{
"Manufacturer": "Acme",
"Modality": "US",
"ManufacturerModelName": "UltraView 3000",
"SOPClassUID": "1.2.840.10008.5.1.4.1.1.6.1",
"Rows": "768",
"Columns": "1024",
"SequenceOfUltrasoundRegions": "present",
}
)
decision = catalog.evaluate(tags)
assert decision.action == "allow"
assert decision.scrub_regions == [ScrubRegion(0, 0, 1024, 60)]
Run the catalog tests:
uv run pytest tests/unit/test_default_catalog.py
8. Validate against real data¶
Run the catalog regression suite to confirm the new rule does not change the recorded decision for any existing fixture:
uv run pytest tests/unit/test_catalog_regression_fixtures.py
The fixtures under tests/unit/catalog_fixtures/ capture the technical matching
tags and the expected filter and scrub outcomes with no PHI or pixel data. If
the new device intentionally changes an outcome recorded in a fixture, update
the fixture to the corrected decision. Then run the full suite:
just test
For sites validating the included catalog against their own devices before trusting output, see Local validation.