Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Last active January 8, 2022 17:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ssokolow/51884c2e45bf91af22d8 to your computer and use it in GitHub Desktop.
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
#!/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