-
Notifications
You must be signed in to change notification settings - Fork 10
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
Add batchset for rendering a hierarchy of containers (single file) #52
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -134,6 +134,51 @@ | |
# be updated individually. | ||
""" | ||
|
||
BATCHSET_HELP = """Set rendering settings from a hierachy of rendering | ||
definitions corresponding to an OMERO container hierarchy. | ||
|
||
'projects' and 'datasets' are supported as top level containers, 'name' | ||
must be provided. | ||
If multiple containers have the same name they will all be processed. | ||
Top level keys beginning with '_' are ignored, so they could for example | ||
be used as YAML anchors. | ||
|
||
Definitions can be applied to projects, datasets, or datasets in projects. | ||
|
||
'renderdef' is a rendering definition as passed to 'set', except that | ||
'version' is omitted. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this because the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it would simplify things if we avoided mixed versioning though that's probably not a major problem to support. You're right that it should be specified somewhere though, so the question is should we require the version
|
||
|
||
Example: | ||
|
||
projects: | ||
# Apply renderdef to all Images in Project A | ||
- name: Project A | ||
renderdef: | ||
channels: | ||
... | ||
|
||
# Apply renderdef to all Images in Dataset 1 in Project B | ||
- name: Project B | ||
datasets: | ||
- name: Dataset 1 | ||
renderdef: | ||
channels: | ||
... | ||
|
||
datasets: | ||
# Apply the channel_group1 renderdef anchor to all Images in Dataset 2 | ||
- name: Dataset 2 | ||
renderdef: | ||
channels: | ||
*channel_group1 | ||
|
||
_anchors: &channel_group1 | ||
1: | ||
active: true | ||
label: Plk1 | ||
|
||
""" | ||
|
||
TEST_HELP = """Test that underlying pixel data is available | ||
|
||
The syntax for specifying objects is: <object>:<id> | ||
|
@@ -372,6 +417,7 @@ def _configure(self, parser): | |
info = parser.add(sub, self.info, INFO_HELP) | ||
copy = parser.add(sub, self.copy, COPY_HELP) | ||
set_cmd = parser.add(sub, self.set, SET_HELP) | ||
batchset = parser.add(sub, self.batchset, BATCHSET_HELP) | ||
edit = parser.add(sub, self.edit, EDIT_HELP) | ||
test = parser.add(sub, self.test, TEST_HELP) | ||
|
||
|
@@ -386,7 +432,10 @@ def _configure(self, parser): | |
x.add_argument("object", type=render_type, help=tgt_help, | ||
nargs="+") | ||
|
||
for x in (copy, set_cmd, edit): | ||
batchset.add_argument("path", help=( | ||
"Directory containing subdirectories with renderdefs.")) | ||
|
||
for x in (copy, set_cmd, edit, batchset): | ||
x.add_argument( | ||
"--skipthumbs", help="Do not regenerate thumbnails " | ||
"immediately", action="store_true") | ||
|
@@ -400,13 +449,14 @@ def _configure(self, parser): | |
copy.add_argument("target", type=render_type, help=tgt_help, | ||
nargs="+") | ||
|
||
for x in (set_cmd, edit): | ||
for x in (set_cmd, edit, batchset): | ||
x.add_argument( | ||
"--disable", help="Disable non specified channels ", | ||
action="store_true") | ||
x.add_argument( | ||
"--ignore-errors", help="Do not error on mismatching" | ||
" rendering settings", action="store_true") | ||
for x in (set_cmd, edit): | ||
x.add_argument( | ||
"channels", | ||
help="Local file or OriginalFile:ID which specifies the " | ||
|
@@ -430,6 +480,11 @@ def _lookup(self, gateway, type, oid): | |
self.ctx.die(110, "No such %s: %s" % (type, oid)) | ||
return obj | ||
|
||
def _lookup_name(self, gateway, type, name): | ||
gateway.SERVICE_OPTS.setOmeroGroup('-1') | ||
objs = gateway.getObjects(type, attributes={'name': name}) | ||
return list(objs) | ||
|
||
def render_images(self, gateway, object, batch=100): | ||
""" | ||
Get the images. | ||
|
@@ -667,21 +722,89 @@ def set(self, args): | |
""" Implements the 'set' command """ | ||
data = self._load_rendering_settings( | ||
args.channels, session=self.client.getSession()) | ||
self._set(data, args.object, | ||
args.ignore_errors, args.disable, args.skipthumbs) | ||
|
||
def _get_batchset_targets(self, otype, descriptor): | ||
""" Gets targets and renderdefs for the 'batchset' command """ | ||
def require_one(required, keys): | ||
if sum((r in keys) for r in required) != 1: | ||
raise ValueError( | ||
'Exactly one of {} required'.format(required)) | ||
|
||
descriptor_keys = list(descriptor.keys()) | ||
|
||
require_one(['name'], descriptor_keys) | ||
if otype == 'Project': | ||
require_one(['datasets', 'renderdef'], descriptor_keys) | ||
else: | ||
require_one(['renderdef'], descriptor_keys) | ||
|
||
parents = self._lookup_name(self.gateway, otype, descriptor['name']) | ||
if not parents: | ||
raise Exception(f"No match for Project: {descriptor['name']}") | ||
for parent in parents: | ||
if 'renderdef' in descriptor_keys: | ||
yield parent, descriptor['renderdef'] | ||
else: | ||
dataset_renderdefs = dict( | ||
(d['name'], d) for d in descriptor['datasets']) | ||
expected_names = set(dataset_renderdefs.keys()) | ||
for dataset in parent.listChildren(): | ||
if dataset.name in dataset_renderdefs: | ||
for target in self._get_batchset_targets( | ||
'Dataset', dataset_renderdefs[dataset.name]): | ||
yield target | ||
try: | ||
expected_names.remove(dataset.name) | ||
except KeyError: | ||
# Multiple containers have the same name | ||
pass | ||
if expected_names: | ||
raise Exception( | ||
'No match for datasets: {}'.format(expected_names)) | ||
|
||
@gateway_required | ||
def batchset(self, args): | ||
""" Implements the 'batchset' command """ | ||
try: | ||
batch_data = pydict_text_io.load( | ||
args.path, session=self.client.getSession()) | ||
except Exception as e: | ||
self.ctx.dbg(e) | ||
self.ctx.die(103, "Could not read %s" % args.path) | ||
for key, containers in batch_data.items(): | ||
if key.startswith('_'): | ||
continue | ||
if key not in {'projects', 'datasets'}: | ||
raise NotImplementedError( | ||
'Invalid batchset container: {}'.format(key)) | ||
for container in containers: | ||
for c, renderdef in self._get_batchset_targets( | ||
f'{key[0].upper()}{key[1:-1]}', container): | ||
self.ctx.out('Applying settings to ' | ||
f'{c.OMERO_CLASS}:{c.id} {c.name}') | ||
self._set( | ||
renderdef, c._obj, | ||
args.ignore_errors, args.disable, args.skipthumbs) | ||
|
||
def _set(self, data, target, ignore_errors, disable, skipthumbs): | ||
""" Utility method to apply rendering settings """ | ||
(namedict, cindices, rangelist, colourlist, minmaxlist) = \ | ||
self._read_channels(data) | ||
greyscale = data.get('greyscale', None) | ||
if greyscale is not None: | ||
self.ctx.dbg('greyscale=%s' % greyscale) | ||
|
||
iids = [] | ||
for img in self.render_images(self.gateway, args.object, batch=1): | ||
for img in self.render_images(self.gateway, target, batch=1): | ||
iids.append(img.id) | ||
|
||
(def_z, def_t) = self._read_default_planes( | ||
img, data, ignore_errors=args.ignore_errors) | ||
img, data, ignore_errors=ignore_errors) | ||
|
||
active_channels = [] | ||
if not args.disable: | ||
if not disable: | ||
# Calling set_active_channels will disable channels which | ||
# are not specified. | ||
# Need to reset ALL active channels after set_active_channels() | ||
|
@@ -730,7 +853,7 @@ def set(self, args): | |
img.saveDefaults() | ||
self.ctx.dbg( | ||
"Updated rendering settings for Image:%s" % img.id) | ||
if not args.skipthumbs: | ||
if not skipthumbs: | ||
self._generate_thumbs([img]) | ||
except Exception as e: | ||
self.ctx.err('ERROR: %s' % e) | ||
|
@@ -739,7 +862,7 @@ def set(self, args): | |
|
||
if not iids: | ||
self.ctx.die(113, "ERROR: No images found for %s %d" % | ||
(args.object.__class__.__name__, args.object.id._val)) | ||
(target.__class__.__name__, target.id._val)) | ||
|
||
if namedict: | ||
self._update_channel_names(self.gateway, iids, namedict) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new hierarchy makes sense and the
renderdef
key has the advantage of containerizing the render metadata. Looking at the examples, I was wondering whether a scheme where each node would be either a containers key (projects
,datasets
, ...) OR directly a render element had been tested/would be an option e.g.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think keeping the renderdef self-contained will make this spec easier to update, otherwise when adding a key to either the container definition or the renderdef spec you'll need to consider future conflicts or potential confusion.
In addition keeping the renderdef under it's own key means this could potentially be extended to handle other metadata e.g. for setting tags or other annotations.