Last active
January 8, 2022 17:45
-
-
Save ssokolow/51884c2e45bf91af22d8 to your computer and use it in GitHub Desktop.
Code which implements a "rubber matrix" and uses it to produce sparse output in ImageMagick's montage utiility
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python2 | |
# -*- coding: utf-8 -*- | |
""" | |
@todo: Decide on how to give this a command-line interface. | |
""" | |
from __future__ import (absolute_import, division, print_function, | |
with_statement, unicode_literals) | |
__author__ = "Stephan Sokolow (deitarion/SSokolow)" | |
__appname__ = "Sparse Matrix Wrapper for ImageMagic's 'montage' utility" | |
__version__ = "0.1" | |
__license__ = "MIT" | |
import functools, subprocess | |
from UserDict import DictMixin | |
import logging | |
log = logging.getLogger(__name__) | |
DEFAULT_OPTIONS = ['-label', '%t', | |
'-background', 'none', | |
'-geometry', '1x1<+2+2'] | |
def _dimensions_check(func): | |
"""Decorator to unify the dimensionality checks for get/set/del""" | |
@functools.wraps(func) | |
def wrapper(self, keys, *args): # pylint: disable=missing-docstring | |
if self.dims is None: | |
self.dims = len(keys) | |
if len(keys) != self.dims: | |
raise KeyError("Invalid dimensions: len(%r) != %s" | |
% (keys, self.dims)) | |
return func(self, keys, *args) | |
return wrapper | |
class RubberMatrix(object, DictMixin): | |
"""Dict-based, self-resizing, sparse matrix object with support for a | |
whole-matrix iteration (full and empty cells) which only shows | |
rows/columns/etc. which actually have content. | |
""" | |
def __init__(self, dims=None, null_val=None): | |
""" | |
@param dims: The number of dimensions to enforce. | |
The default value of C{None} causes this to be determined by the | |
key of the first value to be inserted. | |
@type dims: C{int} | |
@param null_val: The value to return when accessing an empty cell. | |
""" | |
self._dict = {} | |
self.dims = dims # Treat this as read-only or things will break | |
self.null_val = null_val | |
@_dimensions_check | |
def __getitem__(self, keys): | |
if Ellipsis in keys: | |
# TODO: Test this code | |
return [y for x, y in self.iteritems() | |
if all(kx in (x, Ellipsis) for k, kx in zip(keys, x))] | |
else: | |
return self._dict.get(keys, self.null_val) | |
@_dimensions_check | |
def __setitem__(self, keys, value): | |
self._dict[keys] = value | |
@_dimensions_check | |
def __delitem__(self, keys): | |
if keys in self._dict: | |
del self._dict[keys] | |
def _get_depthmap(self): | |
"""Build a dict showing which rows/cols/etc. actually exist""" | |
_seqs = {} | |
for key in self._dict.keys(): | |
for depth, keyval in enumerate(key): | |
_seqs.setdefault(depth, set()).add(keyval) | |
return {x: sorted(y) for x, y in _seqs.items()} | |
def _iterkeys_padded(self, key_prefix, depthmap): | |
"""Inner recursive function for iterkeys_padded()""" | |
depth = len(key_prefix) | |
for key in sorted(depthmap.get(depth, {})): | |
if depth + 1 < self.dims: | |
for x in self._iterkeys_padded(key_prefix + (key,), depthmap): | |
yield x | |
else: | |
yield key_prefix + (key,) | |
def iterkeys_padded(self): | |
"""iterkeys() through all cells rather than just ones with data""" | |
depthmap = self._get_depthmap() | |
return self._iterkeys_padded(tuple(), depthmap) | |
def iteritems_padded(self): | |
"""iteritems() through all cells rather than just ones with data""" | |
for x in self.iterkeys_padded(): | |
yield (x, self[x]) | |
def itervalues_padded(self): | |
"""itervalues() through all cells rather than just ones with data""" | |
for x in self.iterkeys_padded(): | |
yield self[x] | |
def keys(self): | |
"""D.keys() -> list of D's keys""" | |
return self._dict.keys() | |
def __iter__(self): | |
return self._dict.__iter__() | |
# pylint: disable=dangerous-default-value | |
def sparse_montage(inputs, outpath, options=DEFAULT_OPTIONS): | |
"""Apply ImageMagick's montage to an (x,y)-keyed dict of image paths. | |
@type inputs: C{{(x, y): str}} | |
@param outpath: A full file path, including extension. | |
@param options: An C{argv} fragment to pass to C{montage} | |
See http://www.imagemagick.org/Usage/montage/ for details | |
""" | |
if isinstance(inputs, RubberMatrix): | |
in_matrix = inputs | |
else: | |
in_matrix = RubberMatrix(dims=2, null_val='null:') | |
if isinstance(inputs, dict): | |
inputs = inputs.items() | |
for coords, path in inputs: | |
in_matrix[coords] = path | |
# TODO: Look for an elegant way to move rowlen-getting to RubberMatrix | |
allkeys = list(in_matrix.iterkeys_padded()) | |
rowlen = sum(1 for x in allkeys if x[0] == allkeys[0][0]) | |
subprocess.call(['montage', '-tile', '%sx' % rowlen] | |
+ options + [in_matrix[x] for x in allkeys] + [outpath]) | |
# vim: set sw=4 sts=4 expandtab : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment