Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/Value Multiplicity Info #166

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 118 additions & 10 deletions pkg/tag/generate_tag_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,110 @@

logging.basicConfig(level=logging.DEBUG)

VMInfo = NamedTuple('VMInfo', [
('min', int),
('max', int),
('step', int)])

Tag = NamedTuple('Tag', [
('group', int),
('elem', int),
('vr', str),
('name', str),
('vm', str)])
('vm', str),
('vm_info', VMInfo)])


vmRegex = re.compile(r'(?P<MIN>\d+)(?:-(?P<MAX>\d+)?(?P<HAS_N>[n])?)?')
'''
The above regex parsed vms of the following conventions:
- '1'
- '1-2'
- '1-n'
- '1-2n'

Breakdown of regex:

Named Capture Group MIN (?P<MIN>\d+)
\d+ matches a digit (equal to [0-9])
+ Quantifier — Matches between one and unlimited times, as many times as
possible, giving back as needed (greedy)

Non-capturing group (?:-(?P<MAX>\d+)?(?P<HAS_N>[n])?)?
? Quantifier — Matches between zero and one times, as many times as possible,
giving back as needed (greedy)

- matches the character - literally (case sensitive)

Named Capture Group MAX (?P<MAX>\d+)?
? Quantifier — Matches between zero and one times, as many times as possible,
giving back as needed (greedy)

\d+ matches a digit (equal to [0-9])
+ Quantifier — Matches between one and unlimited times, as many times as
possible, giving back as needed (greedy)

Named Capture Group HAS_N (?P<HAS_N>[n])?
? Quantifier — Matches between zero and one times, as many times as possible,
giving back as needed (greedy)

Match a single character present in the list below [n]
'''


def parse_vm(vm: str) -> VMInfo:
match = vmRegex.fullmatch(vm)
if match is None:
raise ValueError(f'"{vm}" is not a valid Value Multiplicity')

groups = match.groupdict()
try:
min_str = groups["MIN"]
except KeyError:
raise ValueError(
f'"{vm}" is not a valid Value Multiplicity: could not parse min',
)

# If there is not explicit max value, it is the same as the min value.
max_str = groups["MAX"]
has_n = groups["HAS_N"]

# If there is an 'n' AND a MAX value, that means we parsed something like '2-2n':
# the max is actually the step scalar and the max value is unbounded (-1).
if has_n and max_str:
step_str = max_str
max_str = "-1"
# If there is an 'n' and no max string we parsed something like '1-n'. The mac
# value is unbounded and the step value is 1.
elif has_n and not max_str:
max_str = "-1"
step_str = "1"
# If there is no max and no 'n', we parsed something like '1'. Max is equal to min,
# and the step value is 1.
elif not max_str:
max_str = min_str
step_str = "1"
# Otherwise we parsed something like 1-2, and step value is 1.
else:
step_str = "1"

try:
min = int(min_str)
except BaseException as err:
raise ValueError(f"error parsing min: {err}") from err

try:
max = int(max_str)
except BaseException as err:
raise ValueError(f"error parsing max: {err}")

try:
step = int(step_str)
except BaseException as err:
raise ValueError(f"error parsing step: {err}")

return VMInfo(min=min, max=max, step=step)


def list_tags() -> List[Tag]:
global DATA
Expand All @@ -31,19 +129,28 @@ def list_tags() -> List[Tag]:
if vr == "XS":
# Its generally safe to treat XS as unsigned. See
# https://github.com/dgobbi/vtk-dicom/issues/38 for
# some discussions.
# some discussions.
vr = "US"
elif vr == "OX":
# TODO(saito) I'm less sure about the OX rule. Where is
# this crap defined in the standard??
# TODO(saito) I'm less sure about the OX rule. Where is
# this crap defined in the standard??
vr = "OW"

vm = m.group(5)

try:
vm_info = parse_vm(vm)
except Exception as error:
logging.error(f"Error parsing vm '{vm}': {error}", vm)
ok = False
continue

tag = Tag(group=m.group(1),
elem=m.group(2),
vr=vr,
name=m.group(4),
vm=m.group(5))

vm=vm,
vm_info=vm_info)

if not re.match('^[0-9A-Fa-f]+$', tag.group) or not re.match('^[0-9A-Fa-f]+$', tag.elem):
continue
Expand All @@ -56,15 +163,15 @@ def list_tags() -> List[Tag]:
def generate(out: IO[str]):
tags = list_tags()

print("package dicomtag", file=out)
print("package tag", file=out)
print("", file=out)
print("// Code generated from generate_tag_definitions.py. DO NOT EDIT.", file=out)
for t in tags:
if t.name.find("RETIRED") >= 0:
continue
print(f'var {t.name} = Tag{{0x{t.group}, 0x{t.elem}}}', file=out)

print("var tagDict map[Tag]TagInfo", file=out)
print("var tagDict map[Tag]Info", file=out)
print("", file=out)
print("func init() {", file=out)
print(" maybeInitTagDict()", file=out)
Expand All @@ -73,9 +180,10 @@ def generate(out: IO[str]):
print(" if len(tagDict) > 0 {", file=out)
print(" return", file=out)
print(" }", file=out)
print(" tagDict = make(map[Tag]TagInfo)", file=out)
print(" tagDict = make(map[Tag]Info)", file=out)
for t in tags:
print(f' tagDict[Tag{{0x{t.group}, 0x{t.elem}}}] = TagInfo{{Tag{{0x{t.group}, 0x{t.elem}}}, "{t.vr}", "{t.name}", "{t.vm}"}}', file=out)
vm_info = t.vm_info
print(f' tagDict[Tag{{0x{t.group}, 0x{t.elem}}}] = Info{{Tag{{0x{t.group}, 0x{t.elem}}}, "{t.vr}", "{t.name}", "{t.vm}", VMInfo{{{vm_info.min}, {vm_info.max}, {vm_info.step}}}}}', file=out)
print("}", file=out)


Expand Down
27 changes: 26 additions & 1 deletion pkg/tag/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,29 @@ func (t Tag) String() string {
return fmt.Sprintf("(%04x,%04x)", t.Group, t.Element)
}

// VM info stores parsed information about the Value Multiplicity (cardinality) of the
// tag.
type VMInfo struct {
// The minimum number of values the Value Multiplicity allows
Minimum int
// The maximum number of values the Value Multiplicity allows. If -1, Maximum
// is unbounded. If equal to Minimum, there is only a single VM allowed.
Maximum int
// Some multiplicities are described like '2-2n', where maximum must be divisible by
// 2. In these cases, step will be equal to y for VM = 'x-yn'
Step int
}

// Returns true if value can only be single.
func (vmInfo VMInfo) IsSingleValue() bool {
return vmInfo.Minimum == 1 && vmInfo.Maximum == 1
}

// Returns true the number of maximum values is unbounded.
func (vmInfo VMInfo) IsUnbounded() bool {
return vmInfo.Maximum == -1
}

// Info stores detailed information about a Tag defined in the DICOM
// standard.
type Info struct {
Expand All @@ -71,6 +94,8 @@ type Info struct {
Name string
// Cardinality (# of values expected in the element)
VM string
// Parsed Value Multiplicity information extracted from VM.
VMInfo VMInfo
}

// MetadataGroup is the value of Tag.Group for metadata tags.
Expand Down Expand Up @@ -154,7 +179,7 @@ func Find(tag Tag) (Info, error) {
if !ok {
// (0000-u-ffff,0000) UL GenericGroupLength 1 GENERIC
if tag.Group%2 == 0 && tag.Element == 0x0000 {
entry = Info{tag, "UL", "GenericGroupLength", "1"}
entry = Info{tag, "UL", "GenericGroupLength", "1", VMInfo{1, 1, 1}}
} else {
return Info{}, fmt.Errorf("Could not find tag (0x%x, 0x%x) in dictionary", tag.Group, tag.Element)
}
Expand Down
Loading