hycore.templates

Functions for creating and arranging template files to create various types of mosiacs.

   1"""
   2Functions for creating and arranging template files to create various types of mosiacs.
   3"""
   4import hylite
   5from hylite import io
   6import numpy as np
   7import os
   8from pathlib import Path
   9from collections.abc import MutableMapping
  10
  11import hycore
  12
  13# the below code avoids occasional errors when running large templates
  14from PIL import Image, ImageFile
  15ImageFile.LOAD_TRUNCATED_IMAGES = True
  16
  17################################################################
  18## Labelling functions. These identify or flag parts of a core
  19## box or sample that are of interest during mosaicing
  20################################################################
  21def get_bounds(mask: hylite.HyImage, pad: int = 0):
  22    """
  23    Get the bounds of the foreground area in the given mask.
  24
  25    Args:
  26     - mask = A HyImage instance containing the foreground mask in the first band (background pixels flagged as 0 or False).
  27     - pad = Number of pixels padding to add to the masked area (N.B. this will not excede the image dimensions though).
  28
  29    Returns:
  30     - xmin,xmax,ymin,ymax = The bounding box of the foreground pixels.
  31    """
  32    if isinstance(mask, hylite.HyImage):
  33        mask = mask.data[..., 0]
  34    else:
  35        mask = mask.squeeze()
  36
  37    xmin = np.argmax(mask.any(axis=1))
  38    xmax = mask.shape[0] - np.argmax(mask.any(axis=1)[::-1])
  39    ymin = np.argmax(mask.any(axis=0))
  40    ymax = mask.shape[1] - np.argmax(mask.any(axis=0)[::-1])
  41
  42    if pad > 0:
  43        xmin = max(0, xmin - pad)
  44        xmax = min(mask.shape[0], xmax + pad)
  45        ymin = max(0, ymin - pad)
  46        ymax = min(mask.shape[1], ymax + pad)
  47
  48    return int(xmin), int(xmax), int(ymin), int(ymax)
  49
  50def get_breaks(mask: hylite.HyImage, axis: int = 0, thresh: float = 0.2):
  51    """
  52    Identify breaks in the foreground mask as local minima after summing in the specified axis.
  53
  54    Args:
  55     - axis = The axis along which to sum the mask before identifying minima.
  56     - thresh = the threshold used to define a "break", as a fraction of the maximum count (if a float is passed), or a specific value (if an int is past).
  57    """
  58    c = np.sum(mask.data[..., 0], axis=axis)
  59    if isinstance(thresh, float):
  60        thresh = np.max(c) * thresh
  61
  62    breaks = np.argwhere(np.diff((c > thresh).flatten(), axis=0)).flatten()
  63    if len(breaks) > 2:
  64        breaks = 0.5 * (breaks[2:][::2] + breaks[1:-1][::2])
  65        return breaks.astype(int)
  66    else:
  67        return []  # no breaks
  68
  69def label_sticks(mask: hylite.HyImage, axis: int = 0, thresh=0.2):
  70    """
  71    Identify and label sticks of core arranged in a box as follows:
  72
  73     -------------------
  74    |    stick 1      |
  75    |    stick 2      |
  76    |    stick 3      |
  77    -------------------
  78
  79    :param mask: A HyImage or numpy array containing 0s for all background and box pixels.
  80    :param axis: The long axis of each sticks. Default is 0 (x-axis).
  81    :param thresh: The threshold used to define breaks in the core (see get_breaks).
  82    :return: An updated mask with non-background pixels labelled according to their corresponding position
  83             in the core tray (from top to bottom if axis=0).
  84    """
  85
  86    # get bounds and breaks
  87    xmin, xmax, ymin, ymax = get_bounds(mask)
  88    breaks = get_breaks(mask, axis=axis, thresh=thresh)
  89
  90    # populate sticks
  91    idx = np.zeros((mask.xdim(), mask.ydim()))
  92    if axis == 0:
  93        steps = np.hstack([ymin, breaks, ymax])
  94
  95        # build stick template from each step
  96        for n, (i0, i1) in enumerate(zip(steps[:-1], steps[1:])):
  97            idx[:, int(i0):int(i1)] = n + 1
  98
  99    elif axis == 1:
 100        steps = np.hstack([xmin, breaks, xmax])
 101        for n, (i0, i1) in enumerate(zip(steps[:-1], steps[1:])):
 102            idx[int(i0):int(i1), :] = n + 1
 103
 104    else:
 105        assert False, "Error - axis should be 0 or 1, not %s" % axis
 106
 107    # intersect with mask
 108    idx[mask.data[..., 0] == 0] = 0
 109
 110    return hylite.HyImage(idx)
 111
 112def label_blocks(mask: hylite.HyImage):
 113    """
 114    Identify and label contiguous blocks. Useful for e.g. extracting samples or scanned thick-section blocks.
 115    :param mask: A HyImage or numpy array containing 0s for all background and box pixels.
 116    :return: An updated mask with non-background pixels labelled according to the contiguous block they belong to.
 117    """
 118
 119    from skimage.measure import label
 120    return hylite.HyImage(label(mask.data[..., 0]))
 121
 122################################################################
 123## Unwrap functions: These construct HyImage instances containing
 124## pixel mappings, that can be used to construct Templates
 125################################################################
 126def unwrap_bounds(mask: hylite.HyImage, *, pad: int = 1):
 127    """
 128    Construct and index template containing a mapping that just clips data to the mask (with the specified padding)
 129
 130    :param mask: The mask that defines the clipping operation.
 131    :param pad: Any padding (in pixels) to apply to this. Default is 1.
 132    :return: A HyImage instance with a clipped shape and containing the x,y coordinates of the source pixels (for creating a template).
 133    """
 134
 135    # get bounds and build indices
 136    xmn, xmx, ymn, ymx = get_bounds(mask, pad=pad)
 137    yy, xx = np.meshgrid(np.arange(mask.ydim()), np.arange(mask.xdim()))  # build coordinate arrays
 138    xy = np.dstack([xx, yy])
 139    xy[mask.data[..., 0] == 0, :] = -1
 140    idx = xy[xmn:xmx, ymn:ymx]
 141    return hylite.HyImage(idx)
 142
 143
 144def unwrap_tray(mask: hylite.HyImage, method='sticks', axis=0,
 145                flipx=False, flipy=False,
 146                thresh: float = 0.2,
 147                pad: int = 5, from_depth=0, to_depth=1):
 148    """
 149    Create a template that splits a core tray into individual "sticks"
 150    and then lays them end to end:
 151
 152    -------------------
 153    |    stick 1      |            ---------------------------
 154    |    stick 2      |       ==> | stick 1  stick 2  stick 3 |   if axis = 0
 155    |    stick 3      |            ---------------------------
 156    -------------------
 157
 158    or
 159
 160     -------------------
 161    |    stick 1      |            ----------
 162    |    stick 2      |       ==> | stick 1 |
 163    |                 |           | stick 2 |  if axis = 1
 164    |                 |           | stick 3 |
 165    |    stick 3      |            ----------
 166    -------------------
 167
 168
 169    :param mask: A HyImage instance containing 0s for all background and box pixels.
 170    :param method: The unwrapping method to use. Default is 'sticks' (see above), although 'blocks' is also possible
 171                    (label_blocks will be used instead of label_sticks).
 172    :param axis: The axis to stack the unwrapped segments along.
 173    :param flipx: True if the sticks should be ordered from right-to-left.
 174    :param flipy: True if the sticks should be ordered from bottom-to-top.
 175    :param thresh: The threshold used to define breaks in the core (see get_breaks). Default is 20% of the max count.
 176    :param pad: Number of pixels to include between sticks and on the edge of the image.
 177    :param from_depth: The depth of the start of this core box, for creating depth ticks. Default is 0. Tick positions and depth values will be stored in the resulting image's header.
 178    :param to_depth: The depth of the end of this core box, for creating depth ticks. Default is 1. Tick positions and depth values will be stored in the resulting image's header.
 179    :return: A HyImage instance containing the x,y coordinates of the unwrapped sticks.
 180    """
 181    if 'sticks' in method.lower():
 182        sticks = label_sticks(mask, axis=0, thresh=thresh)
 183    else:
 184        sticks = label_blocks(mask)
 185
 186    yy, xx = np.meshgrid(np.arange(mask.ydim()), np.arange(mask.xdim()))  # build coordinate arrays
 187    xy = np.dstack([xx, yy])
 188
 189    # extract chunks
 190    chunks = []
 191    for i in np.arange(1, np.max(sticks.data) + 1):
 192        msk = sticks.data[..., 0] == i  # get this segment
 193        xmn, xmx, ymn, ymx = get_bounds(msk, pad=pad)  # find its bounds
 194        idx = xy[xmn:xmx, ymn:ymx, :]  # get indices
 195        idx[~msk[xmn:xmx, ymn:ymx]] = -1  # also transfer background pixels
 196        chunks.append(xy[xmn:xmx, ymn:ymx, :])  # and store
 197
 198    # stack chunks
 199    if axis == 0:
 200        xdim = np.sum([c.shape[0] for c in chunks])
 201        ydim = np.max([c.shape[1] for c in chunks])
 202    else:
 203        xdim = np.max([c.shape[0] for c in chunks])
 204        ydim = np.sum([c.shape[1] for c in chunks])
 205
 206    idx = np.full((xdim, ydim, 2), -1, dtype=int)
 207    _o = 0
 208    ticks = []  # also store depth markers (ticks)
 209    depths = []  # and corresponding hole depths
 210    if flipy:
 211        chunks = chunks[::-1] # loop through chunks from bottom to top
 212    for i, c in enumerate(chunks):
 213        if flipx:
 214            c = c[::-1, :] # core runs right to left
 215        if axis == 0:
 216            idx[_o:(_o + c.shape[0]), 0:c.shape[1], :] = c
 217            ticks.append(_o)
 218            _o += c.shape[0]
 219        else:
 220            idx[0:c.shape[0], _o:(_o + c.shape[1]), :] = c
 221            ticks.append(_o + int(c.shape[1] / 2))
 222            _o += c.shape[1]
 223        depths.append(round(from_depth + i * (to_depth - from_depth) / len(chunks), 2))
 224
 225    out = hylite.HyImage(idx)
 226    out.header['depths'] = depths
 227    out.header['ticks'] = ticks
 228    out.header['tickAxis'] = axis
 229    return out
 230
 231################################################################
 232## Template factory
 233## These functions create templates for sets of boxes.
 234################################################################
 235def buildStack(boxes: hycore.Box, *, pad=1, axis=0, transpose=False):
 236    """
 237    Build a simple template that stacks data horizontally or vertically
 238
 239    :param boxes: A list of Box objects to stack.
 240    :param pad: Padding to add between boxes. Default is 1.
 241    :param axis: 0 to stack horizontally, 1 to stack vertically.
 242    :param transpose: If True, templates are transposed before stacking.
 243    :return: A template object containing the stacked indices.
 244    """
 245
 246    # get shed directory
 247    templates = []
 248    for b in boxes:
 249        try:
 250            mask = b.mask
 251        except:
 252            assert False, "Error - box %s must have a mask defined for it to be included in the stack" % b.name
 253
 254        # get clip index and construct template
 255        iClip = unwrap_bounds(mask, pad=pad)
 256        if transpose:
 257            iClip.data = np.transpose(iClip.data, (1, 0, 2))
 258
 259        templates.append(Template(b.getDirectory(), iClip,
 260                                  from_depth = b.start, to_depth = b.end,
 261                                  groups=[b.name], group_ticks=[iClip.xdim() / 2 ]))
 262
 263    # do stack
 264    return Template.stack(templates, axis=axis )
 265
 266################################################################
 267## Templates and Canvas classes encapsulate
 268## mosaicing transformations and can be used to push data around
 269## to derive mosaic images.
 270################################################################
 271
 272def compositeStack(template, images : list, bands : list = [(0, 1, 2)], *, axis=1, vmin=2, vmax=98, **kwargs):
 273    """
 274    Pass multiple images through the provided template and then stack the results along the specified axis.
 275
 276    :param template: The Template object to use to define mapping between output images and input images.
 277    :param images: A list of image names to resolve. If a single image is passed then this function reduces to be equivalent
 278                    to Template.apply( ... ).
 279    :param axis: The axis to stack the resulting output images along. Default is 1 (stack vertically along the y-axis).
 280    :param bands: A list of tuples defining the bands to be visualised for each image listed in `images`. These tuples
 281                  must have matching lengths!
 282    :param vmin: Percentile clip to apply separately to each dataset before doing stacking. Set as None to disable.
 283    :param vmax: Percentile clip to apply separately to each dataset before doing stacking. Set as None to disable.
 284    :param kwargs: All keyword arguments are passed to Template.apply
 285    :return: A composited and stacked image.
 286    """
 287
 288    # apply template to all input images
 289    I = []
 290    if not isinstance(bands, list): # allow people to pass e.g., (0,1,2) bands and have this extended
 291        bands = [bands] * len(images) # for each image.
 292    for i, b in zip(images, bands):
 293        I.append( template.apply(i, b, **kwargs) )
 294        if (vmin is not None):
 295            I[-1].percent_clip( vmin, vmax )
 296
 297    # stack results
 298    out = I[0]
 299    h = out.data.shape[axis]
 300    out.data = np.concatenate([i.data for i in I], axis=axis)
 301
 302    # add some metadata that can be useful for later plotting
 303    out.header['images'] = images
 304    out.header['bands'] = bands
 305    out.header['image_ticks'] = [i*h + 0.5 * h for i in range(len(images))]
 306    out.set_wavelengths(None) # remove wavelength info as this is invalid
 307
 308    # return
 309    return out
 310
 311class Template(object):
 312    """
 313    A special type of image dataset that stores drillcore, box and pixel IDs such that data from
 314    multiple sources can be re-arranged and combined into mosaick images.
 315    """
 316
 317    def __init__(self, boxes: list, index: np.ndarray, from_depth : float = None, to_depth : float = None,
 318                 groups=None, group_ticks=None, depths=None, depth_ticks=None, depth_axis=0):
 319        """
 320        Create a new Template instance.
 321        :param boxes: A list of paths for each box directory that is included in this template.
 322        :param index: An index array defining where data for index[x,y] should be sourced from. This should have four
 323                        bands specifying the: holeID (index in holes list), boxID (index in boxes list), xcoord (in source
 324                        image) and ycoord (in source image).
 325        :param from_depth: The top (smallest depth) of this template.
 326        :param to_depth: The bottom (largest depth) of this template.
 327        :param groups: Group names (e.g., boreholes) in this template.
 328        :param group_ticks: Pixel coordinates of ticks for these groups.
 329        :param depths: Depth values for ticks in the depth direction.
 330        :param depth_ticks: Pixel coordinates of the ticks corresponding to these depth values.
 331        :param depth_axis: Axis of this template that depth ticks correspond to.
 332        """
 333
 334        # check data types
 335        if isinstance(index, hylite.HyImage):
 336            index = index.data
 337        if isinstance(boxes, str) or isinstance(boxes, Path):
 338            boxes = [boxes]  # wrap in a list
 339
 340        # get root path and express everything as relative to that
 341        root = os.path.commonpath(boxes)
 342        if root == boxes[0]: # only one box (or all the same)
 343            root = os.path.dirname(boxes[0])
 344            self.boxes = [os.path.basename(b) for b in boxes]
 345        else:
 346            self.boxes = [os.path.relpath(b, root) for b in boxes]
 347
 348        for b in self.boxes:
 349            assert os.path.exists(os.path.join(root, b)), "Error box %s does not exist." % os.path.join(root, b)
 350
 351        # check dimensionality of index
 352        if index.shape[-1] == 2:
 353            index = np.dstack([np.zeros((index.shape[0], index.shape[1]), dtype=int), index])
 354
 355        assert index.shape[-1] == 3, "Error - index must have 3 bands (box_index, xcoord, ycoord)"
 356
 357        self.root = str(root)
 358        self.index = index.astype(int)
 359
 360        if (from_depth is not None) and (to_depth is not None):
 361            self.from_depth = min( from_depth, to_depth)
 362            self.to_depth = max( from_depth, to_depth )
 363            self.center_depth = 0.5*(from_depth + to_depth)
 364        else:
 365            self.center_depth = None
 366            self.from_depth = None
 367            self.to_depth = None
 368
 369        self.groups = None
 370        self.group_ticks = None
 371        self.depths = None
 372        self.depth_ticks = None
 373        self.depth_axis = depth_axis
 374
 375        if groups is not None:
 376            self.groups = np.array(groups)
 377        if group_ticks is not None:
 378            self.group_ticks = np.array(group_ticks)
 379        if depths is not None:
 380            self.depths = np.array(depths)
 381        if depth_ticks is not None:
 382            self.depth_ticks = np.array(depth_ticks)
 383
 384    def apply(self, imageName, bands=None, strict=False, xstep : int = 1, ystep : int = 1, scale : float = 1,
 385                    outline=None):
 386        """
 387        Apply this template to the specified dataset in each box.
 388
 389        :param imageName: The name of the image file to extract data from in each box, e.g., 'FENIX'.
 390        :param bands: The bands to export from the source image. Default is None (export all bands). See HyData.export_bands for possible formats.
 391        :param strict: If False (default), skip files that could not be found.
 392        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when building large mosaics.
 393        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when building large mosaics.
 394        :param scale: Scale factor to apply to pixel coordinates in this mosaic. Used for applying to images that are different resolutions.
 395        :param outline: Tuple containing the colour used to outline masked areas, or None to disable.
 396        :return: A HyImage instance containing the mosaic populated with the requested bands.
 397        """
 398        out = None
 399        for i, box in enumerate(self.boxes):
 400
 401            # get this box
 402            path = os.path.join( self.root, box )
 403            if strict:
 404                assert os.path.exists(path), "Error - could not load data from %d"
 405
 406            # load the required data
 407            try:
 408                try:
 409                    if '.' in imageName:
 410                        data = io.load(os.path.join(path, imageName) ) # extension is provided
 411                    else:
 412                        data = io.load(path).get(imageName) # look in box directory
 413                except (AttributeError, AssertionError) as e:
 414                    try:
 415                        if '.' in imageName:
 416                            data = io.load(os.path.join(path, 'results.hyc/%s'%imageName))  # extension is provided
 417                        else:
 418                            data = io.load(path).results.get(imageName) # look in results directory
 419                    except:
 420                        if strict:
 421                            assert False, "Error - could not find data %s in directory %s" % (imageName, path)
 422                        else:
 423                            continue
 424
 425                data.decompress()
 426            except (AttributeError, AssertionError) as e:
 427                if strict:
 428                    assert False, "Error - could not find data %s in directory %s" % (imageName, path )
 429                else:
 430                    continue
 431
 432            # get index and subsample as needed
 433            index = self.index[::xstep, ::ystep]
 434            if scale != 1:
 435                index = (index * scale).astype(int) # scale coordinates
 436                index[...,0] = self.index[::xstep, ::ystep, 0] # don't scale box IDs
 437
 438            # create output array
 439            if bands is not None:
 440                data = data.export_bands(bands)
 441            if out is None:
 442                # initialise output array now we know how many bands we're dealing with
 443                out = np.zeros( (index.shape[0], index.shape[1], data.band_count() ), dtype=data.data.dtype )
 444
 445            # copy data as defined in index array
 446            mask = (index[..., 0] == i) & (index[..., -1] != -1 )
 447            if mask.any():
 448                out[ mask, : ] = data.data[ index[mask, 1], index[mask, 2], : ]
 449        if len( data.get_wavelengths() ) != data.band_count():
 450            data.set_wavelengths(np.arange(data.band_count()))
 451
 452        # return a hyimage
 453        out = hylite.HyImage( out, wav=data.get_wavelengths() )
 454        if data.has_band_names():
 455            if len(data.get_band_names()) == out.band_count(): # sometimes this is not the case if we load a PNG with a header from a .dat file!
 456                out.set_band_names(data.get_band_names())
 457
 458        if (self.groups is not None) and (len(self.groups) > 0):
 459            out.header['groups'] = self.groups
 460        if (self.group_ticks is not None) and (len(self.group_ticks) > 0):
 461            out.header['group ticks'] = self.group_ticks
 462
 463        if outline is not None:
 464            self.add_outlines(out, color=outline, xx = xstep, yy = ystep )
 465        return out
 466
 467    def quick_plot(self, band=0, rot=False, xx=1, yy=1, interval=5, **kwds):
 468        """
 469        Quickly plot this template for QAQC.
 470        :param band: The band(s) to plot. Default is 0 (plot only box ID).
 471        :param rot: True if the template should be rotated 90 degrees before plotting.
 472        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
 473        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
 474        :param interval: Interval between depth ticks to add to plot.
 475        :keywords: Keywords are passed to hylite.HyImage.quick_plot( ... ).
 476        :return: fig,ax from the matplotlib figure created.
 477        """
 478        img = self.toImage()
 479        img.data = img.data[ ::xx, ::yy, : ]
 480        if rot:
 481            img.rot90()
 482        fig, ax = img.quick_plot(band, tscale=True, **kwds )
 483        self.add_ticks(ax, rot=rot, xx=xx, yy=yy, interval=interval)
 484        return fig, ax
 485
 486    def add_ticks(self, ax, interval=5, *, depth_ticks: bool = True, group_ticks: bool = True, rot: bool = False,
 487                  xx: int = 1, yy: int = 1, angle: float = 45):
 488        """
 489        Add depth and or group ticks (as stored in this template) to a matplotlib plot.
 490
 491        :param ax: The matplotlib axes object to set x- and y- ticks / labels too.
 492        :param interval: The interval (in m) between depth ticks. Default is 5 m.
 493        :param depth_ticks:  True (default) if depth ticks should be plotted.
 494        :param group_ticks: True (default) if group ticks should be plotted.
 495        :param rot: If True, the x- and y- axes are flipped (e.g. if image was rotated relative to this template before plotting).
 496        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
 497        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
 498        :param angle: rotation used for the x-ticks. Default is 45 degrees.
 499        """
 500        a = self.depth_axis
 501        if rot:
 502            a = int(1 - a)
 503            _xx = xx
 504            xx = yy
 505            yy = _xx
 506
 507        # get depth and group ticks
 508        if depth_ticks:
 509            zt, zz = self.get_depth_ticks( interval )
 510        if group_ticks:
 511            gt,gg = self.get_group_ticks()
 512
 513        if a == 0:
 514            if depth_ticks:
 515                ax.set_xticks(zt / xx )
 516                ax.set_xticklabels( ["%.1f" % z for z in zz], rotation=angle )
 517                #ax.tick_params('x', labelrotation=angle )
 518            if group_ticks and self.group_ticks is not None:
 519                ax.set_yticks(gt / yy )
 520                ax.set_yticklabels( ["%s" % g for g in gg] )
 521        else:
 522            if depth_ticks:
 523                ax.set_yticks(zt / xx )
 524                ax.set_yticklabels(["%.1f" % z for z in zz])
 525            if group_ticks:
 526                ax.set_xticks(gt / yy)
 527                ax.set_xticklabels(["%s" % g for g in gg], rotation=angle)
 528                #ax.tick_params('x', labelrotation=angle)
 529
 530    def get_group_ticks(self):
 531        """
 532        :return: The position and label of group ticks defined in this template, or [], [] if None are defined.
 533        """
 534        if (self.groups is not None) and (self.group_ticks is not None):
 535            return self.group_ticks, self.groups
 536        else:
 537            return np.array([]), np.array([])
 538
 539    def get_depth_ticks(self, interval=1.0):
 540        """
 541        Get evenly spaced depth ticks for pretty plotting.
 542        :param interval: The desired spacing between depth ticks
 543        :return: Depth tick positions and values. If depth_ticks and depths are not defined, this will return empty lists.
 544        """
 545        if (self.from_depth is None) or (self.to_depth is None) or (self.depth_ticks is None) or (self.depths is None):
 546            return np.array([]), np.array([])
 547        else:
 548            zz = np.arange(self.from_depth - self.from_depth % interval,
 549                           self.to_depth + interval - self.to_depth % interval, interval )[1:]
 550
 551            tt = np.interp( zz, self.depths, self.depth_ticks)
 552
 553            return tt, zz
 554
 555    def add_outlines(self, image, color=0.4, mode='thick', xx: int = 1, yy: int = 1):
 556        """
 557        Add outlines from this template to the specified image.
 558
 559        :param image: a HyImage instance to add colours too. Note that this will be updated in-place.
 560        :param color: a float or tuple containing the values of the colour to apply.
 561        :param mode: outline mode. Options are ‘thick’, ‘inner’, ‘outer’, ‘subpixel’ (see skimage.segmentation.mark_boundaries for details).
 562        """
 563        dtype = image.data.dtype  # store this for later
 564
 565        # get mask to outline
 566        mask = self.index[::xx, ::yy, 1] != -1
 567
 568        # sort out colour
 569        if isinstance(color, float) or isinstance(color, int):
 570            color = tuple([color for i in range(image.band_count())])
 571        assert len(color) == image.band_count(), "Error - colour must have same number of bands as image. %d != %d" % (
 572        len(color), image.band_count())
 573        if (np.array(color) > 1).any():
 574            color = np.array(color) / 255.
 575
 576        # mark boundaries using scikit-image
 577        from skimage.segmentation import mark_boundaries
 578        image.data = mark_boundaries(image.data, mask, color=color, mode=mode)
 579
 580        if (dtype == np.uint8):
 581            image.data = (image.data * 255)  # scikit image transforms our data to 0 - 1 range...
 582
 583    def toImage(self):
 584        """
 585        Convert this Template object to a HyImage instance with the relevant additional hole and box lists stored
 586        in the header file. This can be saved and then later converted back to a Template using fromImage( ... ).
 587        :return: A HyImage representation of this template.
 588        """
 589        image = hylite.HyImage(self.index)
 590        image.header['root'] = self.root
 591        image.header['boxes'] = self.boxes
 592        if self.from_depth is not None:
 593            image.header['from_depth'] = self.from_depth
 594        if self.to_depth is not None:
 595            image.header['to_depth'] = self.to_depth
 596        if self.center_depth is not None:
 597            image.header['center_depth'] = self.center_depth
 598        if self.groups is not None:
 599            image.header['groups'] = self.groups
 600        if self.group_ticks is not None:
 601            image.header['group_ticks'] = self.group_ticks
 602        if self.depths is not None:
 603            image.header['depths'] = self.depths
 604        if self.depth_ticks is not None:
 605            image.header['depth_ticks'] = self.depth_ticks
 606        if self.depth_axis is not None:
 607            image.header['depth_axis'] = self.depth_axis
 608
 609        return image
 610
 611    @classmethod
 612    def fromImage(cls, image):
 613        """
 614        Convert a HyImage with the relevant header information to a Template instance. Useful for IO.
 615        :param image: The HyImage instance containing the template mapping and relevant header metadata
 616                        (lists of hole and box names).
 617        :return:
 618        """
 619        assert 'root' in image.header, 'Error - image must have a "root" key in its header'
 620        assert 'boxes' in image.header, 'Error - image must have a "boxes" key in its header'
 621        assert image.band_count() == 3, 'Error - image must have four bands [holeID, boxID, xidx, yidx]'
 622        root = image.header['root']
 623        boxes = image.header.get_list('boxes')
 624        from_depth = None
 625        to_depth = None
 626        groups = None
 627        group_ticks = None
 628        depths = None
 629        depth_ticks = None
 630        depth_axis = None
 631        if 'from_depth' in image.header:
 632            from_depth = float(image.header['from_depth'])
 633        if 'to_depth' in image.header:
 634            to_depth = float(image.header['to_depth'])
 635        if 'groups' in image.header:
 636            groups = image.header.get_list('groups')
 637        if 'group_ticks' in image.header:
 638            group_ticks = image.header.get_list('group_ticks')
 639        if 'depths' in image.header:
 640            depths = image.header.get_list('depths')
 641        if 'depth_ticks' in image.header:
 642            depth_ticks = image.header.get_list('depth_ticks')
 643        if 'depth_axis' in image.header:
 644            depth_axis = int(image.header['depth_axis'])
 645        return Template([os.path.join( root, b) for b in boxes], image.data, from_depth=from_depth, to_depth=to_depth,
 646                        groups=groups, group_ticks=group_ticks, depths=depths, depth_ticks=depth_ticks, depth_axis=depth_axis)
 647
 648    def rot90(self):
 649        """
 650        Rotate this template by 90 degrees.
 651        """
 652        self.index = np.rot90(self.index, axes=(0, 1))
 653        self.depth_axis = int(1 - self.depth_axis)
 654
 655    def crop(self, min_depth : float, max_depth : float, axis : int , offset : float = 0):
 656        """
 657        Crop this template to the specified depth range.
 658
 659        :param min_depth: The minimum allowable depth.
 660        :param max_depth: The maximum allowable depth.
 661        :param axis: The axis along which depth is interpolated in this template. Should be 0 (x-axis is depth axis) or 1 (y-axis is depth axis).
 662        :param offset: A depth to subtract from min_depth and max_depth prior to cropping.
 663        :return: A copy of this template, cropped to the specific range, or None if no overlap exists.
 664        """
 665        # check there is overlap
 666        if (self.from_depth is None) or (self.to_depth is None):
 667            assert False, "Error - template has no depth information."
 668
 669        # interpolate depth
 670        zz = np.linspace( self.from_depth, self.to_depth, self.index.shape[axis] ) - offset
 671        mask = (zz >= min_depth) & (zz <= max_depth)
 672        if not mask.any():
 673            return None # no overlap
 674
 675        if axis == 0:
 676            ix = self.index[mask, :, : ]
 677        else:
 678            ix = self.index[:, mask, : ]
 679
 680        # print( min_depth, max_depth, self.from_depth, self.to_depth, np.min(zz[mask]), np.max(zz[mask]) )
 681        return Template( [os.path.join(self.root, b) for b in self.boxes], ix,
 682                         from_depth = np.min(zz[mask]),
 683                         to_depth = np.max(zz[mask]) ) # return cropped template
 684
 685    @classmethod
 686    def stack(cls, templates: list, xstep : int = 1, ystep : int = 1, axis=1):
 687        """
 688        Stack a list of templates along the specified axis (similar to np.vstack and np.hstack).
 689
 690        :param templates: A list of template objects to stack.
 691        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when generating large mosaics.
 692        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when generating large mosaics.
 693        :param axis: The axis to stack along. Set as zero to stack in the x-direction and 1 to stack in the
 694                     y-direction.
 695        """
 696
 697        # resolve all unique paths
 698        paths = set()
 699        for t in templates:
 700            for b in t.boxes:
 701                paths.add(os.path.join(t.root, b))
 702                assert os.path.exists(os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
 703
 704        # get root (lowest common base) and express boxes as relative paths to this
 705        paths = list(paths)
 706
 707        # initialise output
 708        if axis == 0:
 709            out = np.full((sum([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
 710                            max([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
 711        else:
 712            out = np.full((max([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
 713                            sum([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
 714
 715        # loop through templates and stack
 716        p = 0
 717        groups = []
 718        group_ticks = []
 719        for i, t in enumerate(templates):
 720            # copy block of indices across
 721            if axis == 0:
 722                out[p:(p + t.index[::xstep, ::ystep, :].shape[0]),
 723                        0:t.index[::xstep, ::ystep, :].shape[1], :] = t.index[::xstep, ::ystep, :]
 724            else:
 725                out[0:t.index[::xstep, ::ystep, :].shape[0],
 726                p:(p + t.index[::xstep, ::ystep, :].shape[1]), :] = t.index[::xstep, ::ystep, :]
 727
 728            # update box indices
 729            for j, b in enumerate(t.boxes):
 730                mask = np.full((out.shape[0], out.shape[1]), False)
 731                if axis == 0:
 732                    mask[p:(p + t.index[::xstep, ::ystep, :].shape[0]), 0:t.index[::xstep, ::ystep, :].shape[1]] = (t.index[::xstep, ::ystep, 0] == j)
 733                else:
 734                    mask[0:t.index[::xstep, ::ystep, :].shape[0], p:(p + t.index[::xstep, ::ystep, :].shape[1])] = (t.index[::xstep, ::ystep, 0] == j)
 735                out[mask, 0] = paths.index(os.path.join(t.root, b))
 736
 737            # update groups and group ticks (these are useful for subsequent plotting)
 738
 739            if t.groups is not None:
 740                groups += list(t.groups)
 741                if axis == 0:
 742                    group_ticks += list( np.array(t.group_ticks) / xstep + p )
 743                else:
 744                    group_ticks += list(np.array(t.group_ticks) / ystep + p)
 745
 746            # update start point
 747            p += t.index[::xstep, ::ystep, :].shape[axis]
 748
 749        # get span of depths
 750        from_depth = None
 751        to_depth = None
 752        if np.array([t.center_depth is not None for t in templates]).all():
 753            from_depth = np.min([t.from_depth for t in templates])
 754            to_depth = np.max([t.to_depth for t in templates])
 755
 756        # generate depth ticks
 757        # i = 1 - axis # if axis is 1, we tick along axis = 0, if axis is 0, we tick along axis = 1
 758        ticks = [templates[0].index.shape[axis] / 2]
 759        depths = [templates[0].from_depth]
 760        for i, T in enumerate(templates[1:]):
 761            ticks.append(ticks[-1] + templates[i - 1].index.shape[axis] / 2 + T.index.shape[axis] / 2)
 762            depths.append(T.from_depth)
 763
 764        # return new Template instance
 765        return Template(paths, out, from_depth = from_depth, to_depth = to_depth,
 766                        groups=groups, group_ticks=group_ticks,
 767                        depths=depths, depth_ticks=ticks, depth_axis=axis)
 768
 769    def getDepths(self, res: float = 1e-3):
 770        """
 771        Return a 1D array of the depths corresponding to each pixel. Assumes a linear mapping
 772        between the templates from_depth and to_depth.
 773        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
 774                    the start and end of this template. If specified, the start_depth is used as an
 775                    anchor and the depth of pixels below this computed to match the resolution. This is important
 776                    to preserve true scale when core boxes contain gaps.
 777        :return: A 1D array containing depth information for each pixel in this template.
 778        """
 779        axis = self.depth_axis
 780        if res is None:
 781            return np.linspace(self.from_depth, self.to_depth, self.index.shape[axis])
 782        else:
 783            to_depth = self.from_depth + self.index.shape[axis] * res
 784            return np.linspace(self.from_depth, to_depth, self.index.shape[axis])
 785
 786    def getGrid(self, grid=50, minor=True, labels=True, background=True, res : float = 1e-3):
 787        """
 788        Create a depth grid image to accompany HSI mosaics.
 789
 790        :param grid: The grid step, in mm. Default is 50.
 791        :param minor: True if minor ticks (with half the spacing of the major ticks) should be plotted.
 792        :param labels: True if label text describing the meterage should be added.
 793        :param background: True if background outlines of the core blocks should be added.
 794        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
 795                    the start and end of this template. If specified, the start_depth is used as an
 796                    anchor and the depth of pixels below this computed to match the resolution. This is important
 797                    to preserve true scale when core boxes contain gaps.
 798        :return: A HyImage instance containing the grid image.
 799        """
 800        # import this here in case of problematic cv2 install
 801        import cv2
 802
 803        # get background image showing core blocks
 804        img = np.zeros((self.index.shape[0], self.index.shape[1], 3), dtype=np.uint8)
 805        if background:
 806            img[:, :, 1] = img[:, :, 2] = 120 * (self.index[:, :, 2] > 1)
 807
 808        # interpolate depth
 809        zz = self.getDepths(res=res)
 810
 811        # add ticks
 812        ignore = set()
 813        for i, z in enumerate(zz):
 814            zi = int(z * 1000)
 815
 816            # major ticks
 817            if zi not in ignore:
 818                if (zi % int(grid)) == 0:
 819                    # add tick
 820                    img[i, :, :] = 255
 821                    ignore.add(zi)
 822
 823                    # add depth label
 824                    if labels:
 825                        l = "%.2f" % z
 826                        font = cv2.FONT_HERSHEY_SIMPLEX
 827                        img = cv2.putText(img,
 828                                          l, (0, i - 3), font, 0.5, (255, 255, 255), 1, bottomLeftOrigin=False)
 829            # minor ticks
 830            if (zi not in ignore) and minor:
 831                if (int(z * 1000) % int(grid / 2)) == 0:
 832                    img[i, ::3, :] = 255
 833                    ignore.add(zi)
 834
 835        return hylite.HyImage(img)
 836
 837    def __lt__(self, other):
 838        """
 839        Do comparisons based on depth of centerpoint. Used for quickly sorting templates by depth.
 840        """
 841        if self.center_depth is None:
 842            assert False, 'Error - please define depth data for Template to use < functions.'
 843        if isinstance(other, Template):
 844            if other.center_depth is None:
 845                assert False, 'Error - please define depth data for Template to use < functions.'
 846            return self.center_depth < other.center_depth
 847        else:
 848            return other > self.center_depth # use operator from other class
 849
 850    def __gt__(self, other):
 851        """
 852        Do comparisons based on depth of centerpoint. Used for quickly sorting templates by depth.
 853        """
 854        if self.center_depth is None:
 855            assert False, 'Error - please define depth data for Template to use > functions.'
 856        if isinstance(other, Template):
 857            if other.center_depth is None:
 858                assert False, 'Error - please define depth data for Template to use < functions.'
 859            return self.center_depth > other.center_depth
 860        else:
 861            return other < self.center_depth # use operator from other class
 862
 863
 864class Canvas(MutableMapping):
 865    """
 866    A utility class for creating collections of templates and combining them into potentially complex layouts. This
 867    stores groups of templates, which can then be sorted and arranged in various ways (e.g., arranging groups as
 868    columns and cropping to a specific depth range, with individual drillhole offsets).
 869    """
 870
 871    def __init__(self, *args, **kwargs):
 872        self.store = dict()
 873        self.update(dict(*args, **kwargs))  # use the free update to set keys
 874
 875
 876    def hpole(self, from_depth: float = None, to_depth: float = None, scaled=False, groups: list = None,
 877                    res: float = 1e-3, depth_offsets: dict = {}, pad: int = 5 ):
 878        """
 879        Construct a "horizontal pole" type template for visualising and corellating between one or more drillholes.
 880        This has a layout as follows:
 881
 882                          -------------------------------------------------
 883        core (group) 1 - |  [xxxxxxxxxx] [xxxx]         [xxxxxxxxxxxxxxx]  |
 884        core (group) 2 - |  [xxxxx]       [xxxxxxxxxxxx]         [xxxxxx]  |
 885        core (group) 3 - |  [xxxxxxxx] [xxxxxxxxxxxxxxxxxx][xxxxxxxxxxxx]  |
 886                          -------------------------------------------------
 887
 888        :param from_depth: The top depth of the template view area, or None to include all depths.
 889        :param to_depth: The lower depth of the template view area, or None to include all depths.
 890        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 891                (with small gaps representing non-contiguous intervals).
 892        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 893        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 894        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 895                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 896                                in absolute terms.
 897        :param pad: Padding for template stacking. Default is 5.
 898        :return: A single combined Template class in horizontal pole layout.
 899        """
 900
 901        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
 902                                                                           groups, to_depth )
 903
 904        # compute width of output image
 905        w = pad
 906        for g in groups:
 907            w += np.max([T.index.shape[1] for T in S[g.lower()]]) + pad
 908
 909        if scaled:
 910            # compute image dimension in depth direction
 911            nz = int(np.abs(to_depth - from_depth) / res)
 912
 913            # compute corresponding depths
 914            z = np.linspace(from_depth, to_depth, nz)
 915        else:
 916            # determine maximum size of stacked boxes, including gaps, and hence template dimensions
 917            z = []
 918            for g in groups:
 919                nz = 0 # this is the dimensions of our output in pixels
 920                for i, T in enumerate(S[g.lower()]):
 921                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
 922                        nz += 10*pad # add in gaps for non-contiguous cores
 923                        z.append( np.linspace(S[g.lower()][i-1].to_depth, T.from_depth, 10*pad ) )
 924
 925                    nz += T.index.shape[0] + pad
 926                    z.append(np.linspace(T.from_depth, T.to_depth, T.index.shape[0]))
 927                    z.append([T.to_depth for i in range(pad)])
 928            z = np.hstack(z)
 929
 930        assert len(z) == nz, "Error - %d depths and %d pixels. Should be the same." % (len(z), nz) # debugging
 931
 932        # build index
 933        index = np.full((nz, w, 3), -1, dtype=int)
 934        tticks = []  # store tick positions in transverse direction (y-axis for hpole)
 935
 936        # stack templates
 937        _y = pad
 938        for g in groups:
 939            g = g.lower()
 940            for T in S[g]:
 941                # find depth position of center and copy data across
 942                if len(T.boxes) > 1:
 943                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
 944                else:
 945                    six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
 946                    eix = min(T.index.shape[0],
 947                              (index.shape[0] - six))  # end index in template (to allow for possible overflows)
 948
 949                    # copy data!
 950                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
 951                    index[six:(six + T.index.shape[0]), _y:(_y + T.index.shape[1]), 0] = bix  # set box index
 952
 953                    index[six:(six + eix), _y:(_y + T.index.shape[1]), 1:] = T.index[0:eix, :,
 954                                                                             1:]  # copy pixel indices
 955
 956            # step to the right
 957            h = int(np.max([T.index.shape[1] for T in S[g]]) + pad)
 958            tticks.append(int(_y + (h / 2)))
 959            _y += h
 960
 961        out = Template(paths, index, from_depth, to_depth, depth_axis=0,
 962            groups = groups, group_ticks = tticks, depths = z, depth_ticks = np.arange(len(z)))
 963
 964        # done!
 965        return out
 966
 967    def vfence(self, from_depth: float = None, to_depth: float = None, scaled=False,
 968               groups: list = None, depth_offsets : dict = {}, pad: int = 5):
 969        """
 970        Construct a "horizontal fence" type template for visualising drillholes in a condensed way.
 971        This has a layout as follows:
 972
 973            core 1       core 2         core 3
 974    1  - | ======== | | =========| | ========== |
 975         | ======== | | =========| | ========== |
 976    2  - | ======== | | ======   | | =====      |
 977         | ======== |     gap      | ========== |
 978    3  - | ======   | | =========| | ========== |
 979         | ======== | | =========| | ========== |
 980
 981        :param from_depth: The top depth of the template view area, or None to include all depths.
 982        :param to_depth: The lower depth of the template view area, or None to include all depths.
 983        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 984                        (with small gaps representing non-contiguous intervals).
 985        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 986        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 987        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 988                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 989                                in absolute terms.
 990        :param pad: Padding for template stacking. Default is 5.
 991        :return: A single combined Template class in horizontal pole layout.
 992        """
 993
 994        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
 995                                                                           groups, to_depth )
 996
 997        # compute width used for each group and hence image width
 998        # also compute y-scale based maximum template height to depth covered ratio
 999        w = pad # width
1000        ys = np.inf # shared y-axis pixel to depth scale (meters per pixel)
1001        for g in groups:
1002            w = w + np.max([T.index.shape[0] for T in S[g.lower()]]) + pad
1003            for T in S[g.lower()]:
1004                ys = min( ys, abs(T.to_depth - T.from_depth) / T.index.shape[1] )
1005
1006        # compute image dimension in depth direction
1007        if scaled:
1008            # determine depth-scale along y-axis (distance down hole per pixel)
1009            nz = int(np.abs(to_depth - from_depth) / ys)
1010            z = np.linspace(from_depth, to_depth, nz ) # depth per pixel array (kinda...)
1011
1012            # build index
1013            index = np.full((w, nz + pad, 3), -1, dtype=int)
1014
1015        else:
1016            # determine maximum height of stacked boxes, including gaps, and hence template dimensions
1017            heights = []
1018            for g in groups:
1019                h = 0
1020                for i, T in enumerate(S[g.lower()]):
1021                    h += T.index.shape[1] + pad
1022                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
1023                        h += T.index.shape[1] # add in gaps for non-contiguous cores
1024                heights.append(h)
1025
1026            ymax = np.max( heights )
1027
1028            # build index
1029            index = np.full((w, ymax + pad, 3), -1, dtype=int)
1030
1031        tticks = []  # store group tick positions in transverse direction (x-axis)
1032
1033        # stack templates
1034        _x = pad
1035        for g in groups: # loop through groups
1036            zticks = []  # store depth ticks in the down-hole direction (y-axis)
1037            zvals = []  # store corresponding depth values
1038
1039            g = g.lower()
1040            six=0
1041            for i,T in enumerate(S[g]): # loop through templates in this group
1042                # find depth position of center and copy data across
1043                if len(T.boxes) > 1:
1044                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
1045                else:
1046                    if scaled:
1047                        six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
1048                    else:
1049                        # add gaps for non-contiguous templates
1050                        if i > 0 and (abs(S[g][i - 1].to_depth - T.from_depth) > 0.5):
1051                            six += T.index.shape[1]  # add full-box sized gap
1052
1053                    # copy data!
1054                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
1055                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 0 ] = bix
1056                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 1:] = T.index[:, :, 1:]  # copy pixel indices
1057
1058                    # store depth ticks
1059                    zticks.append(six)
1060                    zvals.append(T.from_depth)
1061
1062                    if not scaled:
1063                        six += T.index.shape[1]+pad # increment position
1064
1065            # step to the right
1066            w = int(np.max([T.index.shape[0] for T in S[g]]) + pad) # compute max width of core blocks in this group
1067            tticks.append(int(_x + (w / 2))) # store group ticks
1068            _x += w # step to the right
1069
1070        zticks.append(index.shape[1])
1071        zvals.append(T.to_depth) # add tick at bottom of final template / box
1072
1073        if scaled and len(groups) > 1:
1074            zvals = None
1075            depth_ticks = None # these are not defined if more than one hole is present
1076        else:
1077            # interpolate zvals to get a depth value for each pixel
1078            zvals = np.interp(np.arange(0,index.shape[1]), zticks, zvals )
1079            zticks = np.arange(index.shape[1])
1080        out = Template(paths, index, from_depth, to_depth, depth_axis=1,
1081                       groups = groups, group_ticks = tticks, depths = zvals, depth_ticks = zticks )
1082
1083        # done!
1084        return out
1085
1086
1087    def hfence(self, *args):
1088        """
1089        Construct a "horizontal fence" type template for visualising boreholes in a condensed way. This
1090        is identical to the vfence(...) layout, but rotated 90 degrees such that depth increases to the right.
1091
1092        :param args: All arguments are passed to vfence. The results are then rotated to the horizontal orientation.
1093        :return:
1094        """
1095        out = self.vfence(*args)
1096        out.rot90()
1097        return out
1098
1099    def vpole(self, *args):
1100        """
1101        Construct a "vertical pole" type template for visualising and corellating between one or more drillcores. This
1102        is identical to the hpole(...) layout, but rotated 90 degrees such that cores are vertical and depth increases
1103        downwards.
1104
1105        :param args: All arguments are passed to hpole. The results are then rotated to vertical orientation.
1106        :return:
1107        """
1108        out = self.hpole(*args)
1109        # out.index = np.transpose(out.index, (1, 0, 2)) # rotate to vertical
1110        out.rot90()
1111        return out
1112
1113    def _preprocessTemplates(self, depth_offsets, from_depth, groups, to_depth):
1114        # parse from_depth and to_depth if needed
1115        if from_depth is None:
1116            from_depth = np.min([np.min([t.from_depth for t in v]) for (k, v) in self.store.items()])
1117        if to_depth is None:
1118            to_depth = np.max([np.max([t.to_depth for t in v]) for (k, v) in self.store.items()])
1119        # ensure depth template keys are lower case!
1120        offs = {}
1121        for k, v in depth_offsets.items():
1122            offs[k.lower()] = v
1123        # crop templates to the relevant view area, and discard ones that do not fit
1124        cropped = {}
1125        for k, v in self.store.items():
1126            for T in v:
1127                assert T.from_depth is not None, "Error - depth info must be defined for template to be added."
1128                assert T.to_depth is not None, "Error - depth info must be defined for template to be added."
1129                T = T.crop(from_depth, to_depth, T.depth_axis, offs.get(k, 0))
1130                if T is not None:
1131                    # store
1132                    cropped[k.lower()] = cropped.get(k.lower(), [])
1133                    cropped[k.lower()].append(T)
1134
1135        assert len(cropped) > 0, "Error - no templates are within depth range!"
1136
1137        # sort templates by order in each group
1138        S = {}
1139        for k, v in cropped.items():
1140            S[k.lower()] = sorted(v)
1141        # resolve all unique paths
1142        paths = set()
1143        for k, v in S.items():
1144            for t in v:
1145                for b in t.boxes:
1146                    paths.add(os.path.join(t.root, b))
1147                    assert os.path.exists(
1148                        os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
1149        paths = list(paths)
1150        # get group names to plot if not specified
1151        if groups is None:
1152            groups = list(S.keys())
1153        return S, from_depth, to_depth, groups, paths
1154
1155
1156    def add(self, group, template):
1157        """
1158        Add the specified template to this Canvas collection.
1159
1160        :param group: The name of the group to add this template to.
1161        :param template: The template object.
1162        """
1163        self.__setitem__(group, template)
1164
1165    def __getitem__(self, key):
1166        return self.store[self._keytransform(key)]
1167
1168    def __setitem__(self, key, value):
1169        """
1170        A shorthand way to add items to canvas.
1171        """
1172        assert isinstance(value, Template), "Error - only Templates can be added to a Canvas (for now...)"
1173        v = self.store.get(self._keytransform(key), [])
1174        v.append(value)
1175        self.store[self._keytransform(key)] = v
1176
1177    def __delitem__(self, key):
1178        del self.store[self._keytransform(key)]
1179
1180    def __iter__(self):
1181        return iter(self.store)
1182
1183    def __len__(self):
1184        return len(self.store)
1185
1186    def _keytransform(self, key):
1187        return key.lower()
def get_bounds(mask: hylite.hyimage.HyImage, pad: int = 0):
22def get_bounds(mask: hylite.HyImage, pad: int = 0):
23    """
24    Get the bounds of the foreground area in the given mask.
25
26    Args:
27     - mask = A HyImage instance containing the foreground mask in the first band (background pixels flagged as 0 or False).
28     - pad = Number of pixels padding to add to the masked area (N.B. this will not excede the image dimensions though).
29
30    Returns:
31     - xmin,xmax,ymin,ymax = The bounding box of the foreground pixels.
32    """
33    if isinstance(mask, hylite.HyImage):
34        mask = mask.data[..., 0]
35    else:
36        mask = mask.squeeze()
37
38    xmin = np.argmax(mask.any(axis=1))
39    xmax = mask.shape[0] - np.argmax(mask.any(axis=1)[::-1])
40    ymin = np.argmax(mask.any(axis=0))
41    ymax = mask.shape[1] - np.argmax(mask.any(axis=0)[::-1])
42
43    if pad > 0:
44        xmin = max(0, xmin - pad)
45        xmax = min(mask.shape[0], xmax + pad)
46        ymin = max(0, ymin - pad)
47        ymax = min(mask.shape[1], ymax + pad)
48
49    return int(xmin), int(xmax), int(ymin), int(ymax)

Get the bounds of the foreground area in the given mask.

Args:

  • mask = A HyImage instance containing the foreground mask in the first band (background pixels flagged as 0 or False).
  • pad = Number of pixels padding to add to the masked area (N.B. this will not excede the image dimensions though).

Returns:

  • xmin,xmax,ymin,ymax = The bounding box of the foreground pixels.
def get_breaks(mask: hylite.hyimage.HyImage, axis: int = 0, thresh: float = 0.2):
51def get_breaks(mask: hylite.HyImage, axis: int = 0, thresh: float = 0.2):
52    """
53    Identify breaks in the foreground mask as local minima after summing in the specified axis.
54
55    Args:
56     - axis = The axis along which to sum the mask before identifying minima.
57     - thresh = the threshold used to define a "break", as a fraction of the maximum count (if a float is passed), or a specific value (if an int is past).
58    """
59    c = np.sum(mask.data[..., 0], axis=axis)
60    if isinstance(thresh, float):
61        thresh = np.max(c) * thresh
62
63    breaks = np.argwhere(np.diff((c > thresh).flatten(), axis=0)).flatten()
64    if len(breaks) > 2:
65        breaks = 0.5 * (breaks[2:][::2] + breaks[1:-1][::2])
66        return breaks.astype(int)
67    else:
68        return []  # no breaks

Identify breaks in the foreground mask as local minima after summing in the specified axis.

Args:

  • axis = The axis along which to sum the mask before identifying minima.
  • thresh = the threshold used to define a "break", as a fraction of the maximum count (if a float is passed), or a specific value (if an int is past).
def label_sticks(mask: hylite.hyimage.HyImage, axis: int = 0, thresh=0.2):
 70def label_sticks(mask: hylite.HyImage, axis: int = 0, thresh=0.2):
 71    """
 72    Identify and label sticks of core arranged in a box as follows:
 73
 74     -------------------
 75    |    stick 1      |
 76    |    stick 2      |
 77    |    stick 3      |
 78    -------------------
 79
 80    :param mask: A HyImage or numpy array containing 0s for all background and box pixels.
 81    :param axis: The long axis of each sticks. Default is 0 (x-axis).
 82    :param thresh: The threshold used to define breaks in the core (see get_breaks).
 83    :return: An updated mask with non-background pixels labelled according to their corresponding position
 84             in the core tray (from top to bottom if axis=0).
 85    """
 86
 87    # get bounds and breaks
 88    xmin, xmax, ymin, ymax = get_bounds(mask)
 89    breaks = get_breaks(mask, axis=axis, thresh=thresh)
 90
 91    # populate sticks
 92    idx = np.zeros((mask.xdim(), mask.ydim()))
 93    if axis == 0:
 94        steps = np.hstack([ymin, breaks, ymax])
 95
 96        # build stick template from each step
 97        for n, (i0, i1) in enumerate(zip(steps[:-1], steps[1:])):
 98            idx[:, int(i0):int(i1)] = n + 1
 99
100    elif axis == 1:
101        steps = np.hstack([xmin, breaks, xmax])
102        for n, (i0, i1) in enumerate(zip(steps[:-1], steps[1:])):
103            idx[int(i0):int(i1), :] = n + 1
104
105    else:
106        assert False, "Error - axis should be 0 or 1, not %s" % axis
107
108    # intersect with mask
109    idx[mask.data[..., 0] == 0] = 0
110
111    return hylite.HyImage(idx)

Identify and label sticks of core arranged in a box as follows:


| stick 1 | | stick 2 |

| stick 3 |

Parameters
  • mask: A HyImage or numpy array containing 0s for all background and box pixels.
  • axis: The long axis of each sticks. Default is 0 (x-axis).
  • thresh: The threshold used to define breaks in the core (see get_breaks).
Returns

An updated mask with non-background pixels labelled according to their corresponding position in the core tray (from top to bottom if axis=0).

def label_blocks(mask: hylite.hyimage.HyImage):
113def label_blocks(mask: hylite.HyImage):
114    """
115    Identify and label contiguous blocks. Useful for e.g. extracting samples or scanned thick-section blocks.
116    :param mask: A HyImage or numpy array containing 0s for all background and box pixels.
117    :return: An updated mask with non-background pixels labelled according to the contiguous block they belong to.
118    """
119
120    from skimage.measure import label
121    return hylite.HyImage(label(mask.data[..., 0]))

Identify and label contiguous blocks. Useful for e.g. extracting samples or scanned thick-section blocks.

Parameters
  • mask: A HyImage or numpy array containing 0s for all background and box pixels.
Returns

An updated mask with non-background pixels labelled according to the contiguous block they belong to.

def unwrap_bounds(mask: hylite.hyimage.HyImage, *, pad: int = 1):
127def unwrap_bounds(mask: hylite.HyImage, *, pad: int = 1):
128    """
129    Construct and index template containing a mapping that just clips data to the mask (with the specified padding)
130
131    :param mask: The mask that defines the clipping operation.
132    :param pad: Any padding (in pixels) to apply to this. Default is 1.
133    :return: A HyImage instance with a clipped shape and containing the x,y coordinates of the source pixels (for creating a template).
134    """
135
136    # get bounds and build indices
137    xmn, xmx, ymn, ymx = get_bounds(mask, pad=pad)
138    yy, xx = np.meshgrid(np.arange(mask.ydim()), np.arange(mask.xdim()))  # build coordinate arrays
139    xy = np.dstack([xx, yy])
140    xy[mask.data[..., 0] == 0, :] = -1
141    idx = xy[xmn:xmx, ymn:ymx]
142    return hylite.HyImage(idx)

Construct and index template containing a mapping that just clips data to the mask (with the specified padding)

Parameters
  • mask: The mask that defines the clipping operation.
  • pad: Any padding (in pixels) to apply to this. Default is 1.
Returns

A HyImage instance with a clipped shape and containing the x,y coordinates of the source pixels (for creating a template).

def unwrap_tray( mask: hylite.hyimage.HyImage, method='sticks', axis=0, flipx=False, flipy=False, thresh: float = 0.2, pad: int = 5, from_depth=0, to_depth=1):
145def unwrap_tray(mask: hylite.HyImage, method='sticks', axis=0,
146                flipx=False, flipy=False,
147                thresh: float = 0.2,
148                pad: int = 5, from_depth=0, to_depth=1):
149    """
150    Create a template that splits a core tray into individual "sticks"
151    and then lays them end to end:
152
153    -------------------
154    |    stick 1      |            ---------------------------
155    |    stick 2      |       ==> | stick 1  stick 2  stick 3 |   if axis = 0
156    |    stick 3      |            ---------------------------
157    -------------------
158
159    or
160
161     -------------------
162    |    stick 1      |            ----------
163    |    stick 2      |       ==> | stick 1 |
164    |                 |           | stick 2 |  if axis = 1
165    |                 |           | stick 3 |
166    |    stick 3      |            ----------
167    -------------------
168
169
170    :param mask: A HyImage instance containing 0s for all background and box pixels.
171    :param method: The unwrapping method to use. Default is 'sticks' (see above), although 'blocks' is also possible
172                    (label_blocks will be used instead of label_sticks).
173    :param axis: The axis to stack the unwrapped segments along.
174    :param flipx: True if the sticks should be ordered from right-to-left.
175    :param flipy: True if the sticks should be ordered from bottom-to-top.
176    :param thresh: The threshold used to define breaks in the core (see get_breaks). Default is 20% of the max count.
177    :param pad: Number of pixels to include between sticks and on the edge of the image.
178    :param from_depth: The depth of the start of this core box, for creating depth ticks. Default is 0. Tick positions and depth values will be stored in the resulting image's header.
179    :param to_depth: The depth of the end of this core box, for creating depth ticks. Default is 1. Tick positions and depth values will be stored in the resulting image's header.
180    :return: A HyImage instance containing the x,y coordinates of the unwrapped sticks.
181    """
182    if 'sticks' in method.lower():
183        sticks = label_sticks(mask, axis=0, thresh=thresh)
184    else:
185        sticks = label_blocks(mask)
186
187    yy, xx = np.meshgrid(np.arange(mask.ydim()), np.arange(mask.xdim()))  # build coordinate arrays
188    xy = np.dstack([xx, yy])
189
190    # extract chunks
191    chunks = []
192    for i in np.arange(1, np.max(sticks.data) + 1):
193        msk = sticks.data[..., 0] == i  # get this segment
194        xmn, xmx, ymn, ymx = get_bounds(msk, pad=pad)  # find its bounds
195        idx = xy[xmn:xmx, ymn:ymx, :]  # get indices
196        idx[~msk[xmn:xmx, ymn:ymx]] = -1  # also transfer background pixels
197        chunks.append(xy[xmn:xmx, ymn:ymx, :])  # and store
198
199    # stack chunks
200    if axis == 0:
201        xdim = np.sum([c.shape[0] for c in chunks])
202        ydim = np.max([c.shape[1] for c in chunks])
203    else:
204        xdim = np.max([c.shape[0] for c in chunks])
205        ydim = np.sum([c.shape[1] for c in chunks])
206
207    idx = np.full((xdim, ydim, 2), -1, dtype=int)
208    _o = 0
209    ticks = []  # also store depth markers (ticks)
210    depths = []  # and corresponding hole depths
211    if flipy:
212        chunks = chunks[::-1] # loop through chunks from bottom to top
213    for i, c in enumerate(chunks):
214        if flipx:
215            c = c[::-1, :] # core runs right to left
216        if axis == 0:
217            idx[_o:(_o + c.shape[0]), 0:c.shape[1], :] = c
218            ticks.append(_o)
219            _o += c.shape[0]
220        else:
221            idx[0:c.shape[0], _o:(_o + c.shape[1]), :] = c
222            ticks.append(_o + int(c.shape[1] / 2))
223            _o += c.shape[1]
224        depths.append(round(from_depth + i * (to_depth - from_depth) / len(chunks), 2))
225
226    out = hylite.HyImage(idx)
227    out.header['depths'] = depths
228    out.header['ticks'] = ticks
229    out.header['tickAxis'] = axis
230    return out

Create a template that splits a core tray into individual "sticks" and then lays them end to end:


| stick 1 | --------------------------- | stick 2 | ==> | stick 1 stick 2 stick 3 | if axis = 0

| stick 3 | ---------------------------

or


| stick 1 | ---------- | stick 2 | ==> | stick 1 | | | | stick 2 | if axis = 1 | | | stick 3 |

| stick 3 | ----------

Parameters
  • mask: A HyImage instance containing 0s for all background and box pixels.
  • method: The unwrapping method to use. Default is 'sticks' (see above), although 'blocks' is also possible (label_blocks will be used instead of label_sticks).
  • axis: The axis to stack the unwrapped segments along.
  • flipx: True if the sticks should be ordered from right-to-left.
  • flipy: True if the sticks should be ordered from bottom-to-top.
  • thresh: The threshold used to define breaks in the core (see get_breaks). Default is 20% of the max count.
  • pad: Number of pixels to include between sticks and on the edge of the image.
  • from_depth: The depth of the start of this core box, for creating depth ticks. Default is 0. Tick positions and depth values will be stored in the resulting image's header.
  • to_depth: The depth of the end of this core box, for creating depth ticks. Default is 1. Tick positions and depth values will be stored in the resulting image's header.
Returns

A HyImage instance containing the x,y coordinates of the unwrapped sticks.

def buildStack(boxes: hycore.coreshed.Box, *, pad=1, axis=0, transpose=False):
236def buildStack(boxes: hycore.Box, *, pad=1, axis=0, transpose=False):
237    """
238    Build a simple template that stacks data horizontally or vertically
239
240    :param boxes: A list of Box objects to stack.
241    :param pad: Padding to add between boxes. Default is 1.
242    :param axis: 0 to stack horizontally, 1 to stack vertically.
243    :param transpose: If True, templates are transposed before stacking.
244    :return: A template object containing the stacked indices.
245    """
246
247    # get shed directory
248    templates = []
249    for b in boxes:
250        try:
251            mask = b.mask
252        except:
253            assert False, "Error - box %s must have a mask defined for it to be included in the stack" % b.name
254
255        # get clip index and construct template
256        iClip = unwrap_bounds(mask, pad=pad)
257        if transpose:
258            iClip.data = np.transpose(iClip.data, (1, 0, 2))
259
260        templates.append(Template(b.getDirectory(), iClip,
261                                  from_depth = b.start, to_depth = b.end,
262                                  groups=[b.name], group_ticks=[iClip.xdim() / 2 ]))
263
264    # do stack
265    return Template.stack(templates, axis=axis )

Build a simple template that stacks data horizontally or vertically

Parameters
  • boxes: A list of Box objects to stack.
  • pad: Padding to add between boxes. Default is 1.
  • axis: 0 to stack horizontally, 1 to stack vertically.
  • transpose: If True, templates are transposed before stacking.
Returns

A template object containing the stacked indices.

def compositeStack( template, images: list, bands: list = [(0, 1, 2)], *, axis=1, vmin=2, vmax=98, **kwargs):
273def compositeStack(template, images : list, bands : list = [(0, 1, 2)], *, axis=1, vmin=2, vmax=98, **kwargs):
274    """
275    Pass multiple images through the provided template and then stack the results along the specified axis.
276
277    :param template: The Template object to use to define mapping between output images and input images.
278    :param images: A list of image names to resolve. If a single image is passed then this function reduces to be equivalent
279                    to Template.apply( ... ).
280    :param axis: The axis to stack the resulting output images along. Default is 1 (stack vertically along the y-axis).
281    :param bands: A list of tuples defining the bands to be visualised for each image listed in `images`. These tuples
282                  must have matching lengths!
283    :param vmin: Percentile clip to apply separately to each dataset before doing stacking. Set as None to disable.
284    :param vmax: Percentile clip to apply separately to each dataset before doing stacking. Set as None to disable.
285    :param kwargs: All keyword arguments are passed to Template.apply
286    :return: A composited and stacked image.
287    """
288
289    # apply template to all input images
290    I = []
291    if not isinstance(bands, list): # allow people to pass e.g., (0,1,2) bands and have this extended
292        bands = [bands] * len(images) # for each image.
293    for i, b in zip(images, bands):
294        I.append( template.apply(i, b, **kwargs) )
295        if (vmin is not None):
296            I[-1].percent_clip( vmin, vmax )
297
298    # stack results
299    out = I[0]
300    h = out.data.shape[axis]
301    out.data = np.concatenate([i.data for i in I], axis=axis)
302
303    # add some metadata that can be useful for later plotting
304    out.header['images'] = images
305    out.header['bands'] = bands
306    out.header['image_ticks'] = [i*h + 0.5 * h for i in range(len(images))]
307    out.set_wavelengths(None) # remove wavelength info as this is invalid
308
309    # return
310    return out

Pass multiple images through the provided template and then stack the results along the specified axis.

Parameters
  • template: The Template object to use to define mapping between output images and input images.
  • images: A list of image names to resolve. If a single image is passed then this function reduces to be equivalent to Template.apply( ... ).
  • axis: The axis to stack the resulting output images along. Default is 1 (stack vertically along the y-axis).
  • bands: A list of tuples defining the bands to be visualised for each image listed in images. These tuples must have matching lengths!
  • vmin: Percentile clip to apply separately to each dataset before doing stacking. Set as None to disable.
  • vmax: Percentile clip to apply separately to each dataset before doing stacking. Set as None to disable.
  • kwargs: All keyword arguments are passed to Template.apply
Returns

A composited and stacked image.

class Template:
312class Template(object):
313    """
314    A special type of image dataset that stores drillcore, box and pixel IDs such that data from
315    multiple sources can be re-arranged and combined into mosaick images.
316    """
317
318    def __init__(self, boxes: list, index: np.ndarray, from_depth : float = None, to_depth : float = None,
319                 groups=None, group_ticks=None, depths=None, depth_ticks=None, depth_axis=0):
320        """
321        Create a new Template instance.
322        :param boxes: A list of paths for each box directory that is included in this template.
323        :param index: An index array defining where data for index[x,y] should be sourced from. This should have four
324                        bands specifying the: holeID (index in holes list), boxID (index in boxes list), xcoord (in source
325                        image) and ycoord (in source image).
326        :param from_depth: The top (smallest depth) of this template.
327        :param to_depth: The bottom (largest depth) of this template.
328        :param groups: Group names (e.g., boreholes) in this template.
329        :param group_ticks: Pixel coordinates of ticks for these groups.
330        :param depths: Depth values for ticks in the depth direction.
331        :param depth_ticks: Pixel coordinates of the ticks corresponding to these depth values.
332        :param depth_axis: Axis of this template that depth ticks correspond to.
333        """
334
335        # check data types
336        if isinstance(index, hylite.HyImage):
337            index = index.data
338        if isinstance(boxes, str) or isinstance(boxes, Path):
339            boxes = [boxes]  # wrap in a list
340
341        # get root path and express everything as relative to that
342        root = os.path.commonpath(boxes)
343        if root == boxes[0]: # only one box (or all the same)
344            root = os.path.dirname(boxes[0])
345            self.boxes = [os.path.basename(b) for b in boxes]
346        else:
347            self.boxes = [os.path.relpath(b, root) for b in boxes]
348
349        for b in self.boxes:
350            assert os.path.exists(os.path.join(root, b)), "Error box %s does not exist." % os.path.join(root, b)
351
352        # check dimensionality of index
353        if index.shape[-1] == 2:
354            index = np.dstack([np.zeros((index.shape[0], index.shape[1]), dtype=int), index])
355
356        assert index.shape[-1] == 3, "Error - index must have 3 bands (box_index, xcoord, ycoord)"
357
358        self.root = str(root)
359        self.index = index.astype(int)
360
361        if (from_depth is not None) and (to_depth is not None):
362            self.from_depth = min( from_depth, to_depth)
363            self.to_depth = max( from_depth, to_depth )
364            self.center_depth = 0.5*(from_depth + to_depth)
365        else:
366            self.center_depth = None
367            self.from_depth = None
368            self.to_depth = None
369
370        self.groups = None
371        self.group_ticks = None
372        self.depths = None
373        self.depth_ticks = None
374        self.depth_axis = depth_axis
375
376        if groups is not None:
377            self.groups = np.array(groups)
378        if group_ticks is not None:
379            self.group_ticks = np.array(group_ticks)
380        if depths is not None:
381            self.depths = np.array(depths)
382        if depth_ticks is not None:
383            self.depth_ticks = np.array(depth_ticks)
384
385    def apply(self, imageName, bands=None, strict=False, xstep : int = 1, ystep : int = 1, scale : float = 1,
386                    outline=None):
387        """
388        Apply this template to the specified dataset in each box.
389
390        :param imageName: The name of the image file to extract data from in each box, e.g., 'FENIX'.
391        :param bands: The bands to export from the source image. Default is None (export all bands). See HyData.export_bands for possible formats.
392        :param strict: If False (default), skip files that could not be found.
393        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when building large mosaics.
394        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when building large mosaics.
395        :param scale: Scale factor to apply to pixel coordinates in this mosaic. Used for applying to images that are different resolutions.
396        :param outline: Tuple containing the colour used to outline masked areas, or None to disable.
397        :return: A HyImage instance containing the mosaic populated with the requested bands.
398        """
399        out = None
400        for i, box in enumerate(self.boxes):
401
402            # get this box
403            path = os.path.join( self.root, box )
404            if strict:
405                assert os.path.exists(path), "Error - could not load data from %d"
406
407            # load the required data
408            try:
409                try:
410                    if '.' in imageName:
411                        data = io.load(os.path.join(path, imageName) ) # extension is provided
412                    else:
413                        data = io.load(path).get(imageName) # look in box directory
414                except (AttributeError, AssertionError) as e:
415                    try:
416                        if '.' in imageName:
417                            data = io.load(os.path.join(path, 'results.hyc/%s'%imageName))  # extension is provided
418                        else:
419                            data = io.load(path).results.get(imageName) # look in results directory
420                    except:
421                        if strict:
422                            assert False, "Error - could not find data %s in directory %s" % (imageName, path)
423                        else:
424                            continue
425
426                data.decompress()
427            except (AttributeError, AssertionError) as e:
428                if strict:
429                    assert False, "Error - could not find data %s in directory %s" % (imageName, path )
430                else:
431                    continue
432
433            # get index and subsample as needed
434            index = self.index[::xstep, ::ystep]
435            if scale != 1:
436                index = (index * scale).astype(int) # scale coordinates
437                index[...,0] = self.index[::xstep, ::ystep, 0] # don't scale box IDs
438
439            # create output array
440            if bands is not None:
441                data = data.export_bands(bands)
442            if out is None:
443                # initialise output array now we know how many bands we're dealing with
444                out = np.zeros( (index.shape[0], index.shape[1], data.band_count() ), dtype=data.data.dtype )
445
446            # copy data as defined in index array
447            mask = (index[..., 0] == i) & (index[..., -1] != -1 )
448            if mask.any():
449                out[ mask, : ] = data.data[ index[mask, 1], index[mask, 2], : ]
450        if len( data.get_wavelengths() ) != data.band_count():
451            data.set_wavelengths(np.arange(data.band_count()))
452
453        # return a hyimage
454        out = hylite.HyImage( out, wav=data.get_wavelengths() )
455        if data.has_band_names():
456            if len(data.get_band_names()) == out.band_count(): # sometimes this is not the case if we load a PNG with a header from a .dat file!
457                out.set_band_names(data.get_band_names())
458
459        if (self.groups is not None) and (len(self.groups) > 0):
460            out.header['groups'] = self.groups
461        if (self.group_ticks is not None) and (len(self.group_ticks) > 0):
462            out.header['group ticks'] = self.group_ticks
463
464        if outline is not None:
465            self.add_outlines(out, color=outline, xx = xstep, yy = ystep )
466        return out
467
468    def quick_plot(self, band=0, rot=False, xx=1, yy=1, interval=5, **kwds):
469        """
470        Quickly plot this template for QAQC.
471        :param band: The band(s) to plot. Default is 0 (plot only box ID).
472        :param rot: True if the template should be rotated 90 degrees before plotting.
473        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
474        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
475        :param interval: Interval between depth ticks to add to plot.
476        :keywords: Keywords are passed to hylite.HyImage.quick_plot( ... ).
477        :return: fig,ax from the matplotlib figure created.
478        """
479        img = self.toImage()
480        img.data = img.data[ ::xx, ::yy, : ]
481        if rot:
482            img.rot90()
483        fig, ax = img.quick_plot(band, tscale=True, **kwds )
484        self.add_ticks(ax, rot=rot, xx=xx, yy=yy, interval=interval)
485        return fig, ax
486
487    def add_ticks(self, ax, interval=5, *, depth_ticks: bool = True, group_ticks: bool = True, rot: bool = False,
488                  xx: int = 1, yy: int = 1, angle: float = 45):
489        """
490        Add depth and or group ticks (as stored in this template) to a matplotlib plot.
491
492        :param ax: The matplotlib axes object to set x- and y- ticks / labels too.
493        :param interval: The interval (in m) between depth ticks. Default is 5 m.
494        :param depth_ticks:  True (default) if depth ticks should be plotted.
495        :param group_ticks: True (default) if group ticks should be plotted.
496        :param rot: If True, the x- and y- axes are flipped (e.g. if image was rotated relative to this template before plotting).
497        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
498        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
499        :param angle: rotation used for the x-ticks. Default is 45 degrees.
500        """
501        a = self.depth_axis
502        if rot:
503            a = int(1 - a)
504            _xx = xx
505            xx = yy
506            yy = _xx
507
508        # get depth and group ticks
509        if depth_ticks:
510            zt, zz = self.get_depth_ticks( interval )
511        if group_ticks:
512            gt,gg = self.get_group_ticks()
513
514        if a == 0:
515            if depth_ticks:
516                ax.set_xticks(zt / xx )
517                ax.set_xticklabels( ["%.1f" % z for z in zz], rotation=angle )
518                #ax.tick_params('x', labelrotation=angle )
519            if group_ticks and self.group_ticks is not None:
520                ax.set_yticks(gt / yy )
521                ax.set_yticklabels( ["%s" % g for g in gg] )
522        else:
523            if depth_ticks:
524                ax.set_yticks(zt / xx )
525                ax.set_yticklabels(["%.1f" % z for z in zz])
526            if group_ticks:
527                ax.set_xticks(gt / yy)
528                ax.set_xticklabels(["%s" % g for g in gg], rotation=angle)
529                #ax.tick_params('x', labelrotation=angle)
530
531    def get_group_ticks(self):
532        """
533        :return: The position and label of group ticks defined in this template, or [], [] if None are defined.
534        """
535        if (self.groups is not None) and (self.group_ticks is not None):
536            return self.group_ticks, self.groups
537        else:
538            return np.array([]), np.array([])
539
540    def get_depth_ticks(self, interval=1.0):
541        """
542        Get evenly spaced depth ticks for pretty plotting.
543        :param interval: The desired spacing between depth ticks
544        :return: Depth tick positions and values. If depth_ticks and depths are not defined, this will return empty lists.
545        """
546        if (self.from_depth is None) or (self.to_depth is None) or (self.depth_ticks is None) or (self.depths is None):
547            return np.array([]), np.array([])
548        else:
549            zz = np.arange(self.from_depth - self.from_depth % interval,
550                           self.to_depth + interval - self.to_depth % interval, interval )[1:]
551
552            tt = np.interp( zz, self.depths, self.depth_ticks)
553
554            return tt, zz
555
556    def add_outlines(self, image, color=0.4, mode='thick', xx: int = 1, yy: int = 1):
557        """
558        Add outlines from this template to the specified image.
559
560        :param image: a HyImage instance to add colours too. Note that this will be updated in-place.
561        :param color: a float or tuple containing the values of the colour to apply.
562        :param mode: outline mode. Options are ‘thick’, ‘inner’, ‘outer’, ‘subpixel’ (see skimage.segmentation.mark_boundaries for details).
563        """
564        dtype = image.data.dtype  # store this for later
565
566        # get mask to outline
567        mask = self.index[::xx, ::yy, 1] != -1
568
569        # sort out colour
570        if isinstance(color, float) or isinstance(color, int):
571            color = tuple([color for i in range(image.band_count())])
572        assert len(color) == image.band_count(), "Error - colour must have same number of bands as image. %d != %d" % (
573        len(color), image.band_count())
574        if (np.array(color) > 1).any():
575            color = np.array(color) / 255.
576
577        # mark boundaries using scikit-image
578        from skimage.segmentation import mark_boundaries
579        image.data = mark_boundaries(image.data, mask, color=color, mode=mode)
580
581        if (dtype == np.uint8):
582            image.data = (image.data * 255)  # scikit image transforms our data to 0 - 1 range...
583
584    def toImage(self):
585        """
586        Convert this Template object to a HyImage instance with the relevant additional hole and box lists stored
587        in the header file. This can be saved and then later converted back to a Template using fromImage( ... ).
588        :return: A HyImage representation of this template.
589        """
590        image = hylite.HyImage(self.index)
591        image.header['root'] = self.root
592        image.header['boxes'] = self.boxes
593        if self.from_depth is not None:
594            image.header['from_depth'] = self.from_depth
595        if self.to_depth is not None:
596            image.header['to_depth'] = self.to_depth
597        if self.center_depth is not None:
598            image.header['center_depth'] = self.center_depth
599        if self.groups is not None:
600            image.header['groups'] = self.groups
601        if self.group_ticks is not None:
602            image.header['group_ticks'] = self.group_ticks
603        if self.depths is not None:
604            image.header['depths'] = self.depths
605        if self.depth_ticks is not None:
606            image.header['depth_ticks'] = self.depth_ticks
607        if self.depth_axis is not None:
608            image.header['depth_axis'] = self.depth_axis
609
610        return image
611
612    @classmethod
613    def fromImage(cls, image):
614        """
615        Convert a HyImage with the relevant header information to a Template instance. Useful for IO.
616        :param image: The HyImage instance containing the template mapping and relevant header metadata
617                        (lists of hole and box names).
618        :return:
619        """
620        assert 'root' in image.header, 'Error - image must have a "root" key in its header'
621        assert 'boxes' in image.header, 'Error - image must have a "boxes" key in its header'
622        assert image.band_count() == 3, 'Error - image must have four bands [holeID, boxID, xidx, yidx]'
623        root = image.header['root']
624        boxes = image.header.get_list('boxes')
625        from_depth = None
626        to_depth = None
627        groups = None
628        group_ticks = None
629        depths = None
630        depth_ticks = None
631        depth_axis = None
632        if 'from_depth' in image.header:
633            from_depth = float(image.header['from_depth'])
634        if 'to_depth' in image.header:
635            to_depth = float(image.header['to_depth'])
636        if 'groups' in image.header:
637            groups = image.header.get_list('groups')
638        if 'group_ticks' in image.header:
639            group_ticks = image.header.get_list('group_ticks')
640        if 'depths' in image.header:
641            depths = image.header.get_list('depths')
642        if 'depth_ticks' in image.header:
643            depth_ticks = image.header.get_list('depth_ticks')
644        if 'depth_axis' in image.header:
645            depth_axis = int(image.header['depth_axis'])
646        return Template([os.path.join( root, b) for b in boxes], image.data, from_depth=from_depth, to_depth=to_depth,
647                        groups=groups, group_ticks=group_ticks, depths=depths, depth_ticks=depth_ticks, depth_axis=depth_axis)
648
649    def rot90(self):
650        """
651        Rotate this template by 90 degrees.
652        """
653        self.index = np.rot90(self.index, axes=(0, 1))
654        self.depth_axis = int(1 - self.depth_axis)
655
656    def crop(self, min_depth : float, max_depth : float, axis : int , offset : float = 0):
657        """
658        Crop this template to the specified depth range.
659
660        :param min_depth: The minimum allowable depth.
661        :param max_depth: The maximum allowable depth.
662        :param axis: The axis along which depth is interpolated in this template. Should be 0 (x-axis is depth axis) or 1 (y-axis is depth axis).
663        :param offset: A depth to subtract from min_depth and max_depth prior to cropping.
664        :return: A copy of this template, cropped to the specific range, or None if no overlap exists.
665        """
666        # check there is overlap
667        if (self.from_depth is None) or (self.to_depth is None):
668            assert False, "Error - template has no depth information."
669
670        # interpolate depth
671        zz = np.linspace( self.from_depth, self.to_depth, self.index.shape[axis] ) - offset
672        mask = (zz >= min_depth) & (zz <= max_depth)
673        if not mask.any():
674            return None # no overlap
675
676        if axis == 0:
677            ix = self.index[mask, :, : ]
678        else:
679            ix = self.index[:, mask, : ]
680
681        # print( min_depth, max_depth, self.from_depth, self.to_depth, np.min(zz[mask]), np.max(zz[mask]) )
682        return Template( [os.path.join(self.root, b) for b in self.boxes], ix,
683                         from_depth = np.min(zz[mask]),
684                         to_depth = np.max(zz[mask]) ) # return cropped template
685
686    @classmethod
687    def stack(cls, templates: list, xstep : int = 1, ystep : int = 1, axis=1):
688        """
689        Stack a list of templates along the specified axis (similar to np.vstack and np.hstack).
690
691        :param templates: A list of template objects to stack.
692        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when generating large mosaics.
693        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when generating large mosaics.
694        :param axis: The axis to stack along. Set as zero to stack in the x-direction and 1 to stack in the
695                     y-direction.
696        """
697
698        # resolve all unique paths
699        paths = set()
700        for t in templates:
701            for b in t.boxes:
702                paths.add(os.path.join(t.root, b))
703                assert os.path.exists(os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
704
705        # get root (lowest common base) and express boxes as relative paths to this
706        paths = list(paths)
707
708        # initialise output
709        if axis == 0:
710            out = np.full((sum([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
711                            max([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
712        else:
713            out = np.full((max([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
714                            sum([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
715
716        # loop through templates and stack
717        p = 0
718        groups = []
719        group_ticks = []
720        for i, t in enumerate(templates):
721            # copy block of indices across
722            if axis == 0:
723                out[p:(p + t.index[::xstep, ::ystep, :].shape[0]),
724                        0:t.index[::xstep, ::ystep, :].shape[1], :] = t.index[::xstep, ::ystep, :]
725            else:
726                out[0:t.index[::xstep, ::ystep, :].shape[0],
727                p:(p + t.index[::xstep, ::ystep, :].shape[1]), :] = t.index[::xstep, ::ystep, :]
728
729            # update box indices
730            for j, b in enumerate(t.boxes):
731                mask = np.full((out.shape[0], out.shape[1]), False)
732                if axis == 0:
733                    mask[p:(p + t.index[::xstep, ::ystep, :].shape[0]), 0:t.index[::xstep, ::ystep, :].shape[1]] = (t.index[::xstep, ::ystep, 0] == j)
734                else:
735                    mask[0:t.index[::xstep, ::ystep, :].shape[0], p:(p + t.index[::xstep, ::ystep, :].shape[1])] = (t.index[::xstep, ::ystep, 0] == j)
736                out[mask, 0] = paths.index(os.path.join(t.root, b))
737
738            # update groups and group ticks (these are useful for subsequent plotting)
739
740            if t.groups is not None:
741                groups += list(t.groups)
742                if axis == 0:
743                    group_ticks += list( np.array(t.group_ticks) / xstep + p )
744                else:
745                    group_ticks += list(np.array(t.group_ticks) / ystep + p)
746
747            # update start point
748            p += t.index[::xstep, ::ystep, :].shape[axis]
749
750        # get span of depths
751        from_depth = None
752        to_depth = None
753        if np.array([t.center_depth is not None for t in templates]).all():
754            from_depth = np.min([t.from_depth for t in templates])
755            to_depth = np.max([t.to_depth for t in templates])
756
757        # generate depth ticks
758        # i = 1 - axis # if axis is 1, we tick along axis = 0, if axis is 0, we tick along axis = 1
759        ticks = [templates[0].index.shape[axis] / 2]
760        depths = [templates[0].from_depth]
761        for i, T in enumerate(templates[1:]):
762            ticks.append(ticks[-1] + templates[i - 1].index.shape[axis] / 2 + T.index.shape[axis] / 2)
763            depths.append(T.from_depth)
764
765        # return new Template instance
766        return Template(paths, out, from_depth = from_depth, to_depth = to_depth,
767                        groups=groups, group_ticks=group_ticks,
768                        depths=depths, depth_ticks=ticks, depth_axis=axis)
769
770    def getDepths(self, res: float = 1e-3):
771        """
772        Return a 1D array of the depths corresponding to each pixel. Assumes a linear mapping
773        between the templates from_depth and to_depth.
774        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
775                    the start and end of this template. If specified, the start_depth is used as an
776                    anchor and the depth of pixels below this computed to match the resolution. This is important
777                    to preserve true scale when core boxes contain gaps.
778        :return: A 1D array containing depth information for each pixel in this template.
779        """
780        axis = self.depth_axis
781        if res is None:
782            return np.linspace(self.from_depth, self.to_depth, self.index.shape[axis])
783        else:
784            to_depth = self.from_depth + self.index.shape[axis] * res
785            return np.linspace(self.from_depth, to_depth, self.index.shape[axis])
786
787    def getGrid(self, grid=50, minor=True, labels=True, background=True, res : float = 1e-3):
788        """
789        Create a depth grid image to accompany HSI mosaics.
790
791        :param grid: The grid step, in mm. Default is 50.
792        :param minor: True if minor ticks (with half the spacing of the major ticks) should be plotted.
793        :param labels: True if label text describing the meterage should be added.
794        :param background: True if background outlines of the core blocks should be added.
795        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
796                    the start and end of this template. If specified, the start_depth is used as an
797                    anchor and the depth of pixels below this computed to match the resolution. This is important
798                    to preserve true scale when core boxes contain gaps.
799        :return: A HyImage instance containing the grid image.
800        """
801        # import this here in case of problematic cv2 install
802        import cv2
803
804        # get background image showing core blocks
805        img = np.zeros((self.index.shape[0], self.index.shape[1], 3), dtype=np.uint8)
806        if background:
807            img[:, :, 1] = img[:, :, 2] = 120 * (self.index[:, :, 2] > 1)
808
809        # interpolate depth
810        zz = self.getDepths(res=res)
811
812        # add ticks
813        ignore = set()
814        for i, z in enumerate(zz):
815            zi = int(z * 1000)
816
817            # major ticks
818            if zi not in ignore:
819                if (zi % int(grid)) == 0:
820                    # add tick
821                    img[i, :, :] = 255
822                    ignore.add(zi)
823
824                    # add depth label
825                    if labels:
826                        l = "%.2f" % z
827                        font = cv2.FONT_HERSHEY_SIMPLEX
828                        img = cv2.putText(img,
829                                          l, (0, i - 3), font, 0.5, (255, 255, 255), 1, bottomLeftOrigin=False)
830            # minor ticks
831            if (zi not in ignore) and minor:
832                if (int(z * 1000) % int(grid / 2)) == 0:
833                    img[i, ::3, :] = 255
834                    ignore.add(zi)
835
836        return hylite.HyImage(img)
837
838    def __lt__(self, other):
839        """
840        Do comparisons based on depth of centerpoint. Used for quickly sorting templates by depth.
841        """
842        if self.center_depth is None:
843            assert False, 'Error - please define depth data for Template to use < functions.'
844        if isinstance(other, Template):
845            if other.center_depth is None:
846                assert False, 'Error - please define depth data for Template to use < functions.'
847            return self.center_depth < other.center_depth
848        else:
849            return other > self.center_depth # use operator from other class
850
851    def __gt__(self, other):
852        """
853        Do comparisons based on depth of centerpoint. Used for quickly sorting templates by depth.
854        """
855        if self.center_depth is None:
856            assert False, 'Error - please define depth data for Template to use > functions.'
857        if isinstance(other, Template):
858            if other.center_depth is None:
859                assert False, 'Error - please define depth data for Template to use < functions.'
860            return self.center_depth > other.center_depth
861        else:
862            return other < self.center_depth # use operator from other class

A special type of image dataset that stores drillcore, box and pixel IDs such that data from multiple sources can be re-arranged and combined into mosaick images.

Template( boxes: list, index: numpy.ndarray, from_depth: float = None, to_depth: float = None, groups=None, group_ticks=None, depths=None, depth_ticks=None, depth_axis=0)
318    def __init__(self, boxes: list, index: np.ndarray, from_depth : float = None, to_depth : float = None,
319                 groups=None, group_ticks=None, depths=None, depth_ticks=None, depth_axis=0):
320        """
321        Create a new Template instance.
322        :param boxes: A list of paths for each box directory that is included in this template.
323        :param index: An index array defining where data for index[x,y] should be sourced from. This should have four
324                        bands specifying the: holeID (index in holes list), boxID (index in boxes list), xcoord (in source
325                        image) and ycoord (in source image).
326        :param from_depth: The top (smallest depth) of this template.
327        :param to_depth: The bottom (largest depth) of this template.
328        :param groups: Group names (e.g., boreholes) in this template.
329        :param group_ticks: Pixel coordinates of ticks for these groups.
330        :param depths: Depth values for ticks in the depth direction.
331        :param depth_ticks: Pixel coordinates of the ticks corresponding to these depth values.
332        :param depth_axis: Axis of this template that depth ticks correspond to.
333        """
334
335        # check data types
336        if isinstance(index, hylite.HyImage):
337            index = index.data
338        if isinstance(boxes, str) or isinstance(boxes, Path):
339            boxes = [boxes]  # wrap in a list
340
341        # get root path and express everything as relative to that
342        root = os.path.commonpath(boxes)
343        if root == boxes[0]: # only one box (or all the same)
344            root = os.path.dirname(boxes[0])
345            self.boxes = [os.path.basename(b) for b in boxes]
346        else:
347            self.boxes = [os.path.relpath(b, root) for b in boxes]
348
349        for b in self.boxes:
350            assert os.path.exists(os.path.join(root, b)), "Error box %s does not exist." % os.path.join(root, b)
351
352        # check dimensionality of index
353        if index.shape[-1] == 2:
354            index = np.dstack([np.zeros((index.shape[0], index.shape[1]), dtype=int), index])
355
356        assert index.shape[-1] == 3, "Error - index must have 3 bands (box_index, xcoord, ycoord)"
357
358        self.root = str(root)
359        self.index = index.astype(int)
360
361        if (from_depth is not None) and (to_depth is not None):
362            self.from_depth = min( from_depth, to_depth)
363            self.to_depth = max( from_depth, to_depth )
364            self.center_depth = 0.5*(from_depth + to_depth)
365        else:
366            self.center_depth = None
367            self.from_depth = None
368            self.to_depth = None
369
370        self.groups = None
371        self.group_ticks = None
372        self.depths = None
373        self.depth_ticks = None
374        self.depth_axis = depth_axis
375
376        if groups is not None:
377            self.groups = np.array(groups)
378        if group_ticks is not None:
379            self.group_ticks = np.array(group_ticks)
380        if depths is not None:
381            self.depths = np.array(depths)
382        if depth_ticks is not None:
383            self.depth_ticks = np.array(depth_ticks)

Create a new Template instance.

Parameters
  • boxes: A list of paths for each box directory that is included in this template.
  • index: An index array defining where data for index[x,y] should be sourced from. This should have four bands specifying the: holeID (index in holes list), boxID (index in boxes list), xcoord (in source image) and ycoord (in source image).
  • from_depth: The top (smallest depth) of this template.
  • to_depth: The bottom (largest depth) of this template.
  • groups: Group names (e.g., boreholes) in this template.
  • group_ticks: Pixel coordinates of ticks for these groups.
  • depths: Depth values for ticks in the depth direction.
  • depth_ticks: Pixel coordinates of the ticks corresponding to these depth values.
  • depth_axis: Axis of this template that depth ticks correspond to.
def apply( self, imageName, bands=None, strict=False, xstep: int = 1, ystep: int = 1, scale: float = 1, outline=None):
385    def apply(self, imageName, bands=None, strict=False, xstep : int = 1, ystep : int = 1, scale : float = 1,
386                    outline=None):
387        """
388        Apply this template to the specified dataset in each box.
389
390        :param imageName: The name of the image file to extract data from in each box, e.g., 'FENIX'.
391        :param bands: The bands to export from the source image. Default is None (export all bands). See HyData.export_bands for possible formats.
392        :param strict: If False (default), skip files that could not be found.
393        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when building large mosaics.
394        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when building large mosaics.
395        :param scale: Scale factor to apply to pixel coordinates in this mosaic. Used for applying to images that are different resolutions.
396        :param outline: Tuple containing the colour used to outline masked areas, or None to disable.
397        :return: A HyImage instance containing the mosaic populated with the requested bands.
398        """
399        out = None
400        for i, box in enumerate(self.boxes):
401
402            # get this box
403            path = os.path.join( self.root, box )
404            if strict:
405                assert os.path.exists(path), "Error - could not load data from %d"
406
407            # load the required data
408            try:
409                try:
410                    if '.' in imageName:
411                        data = io.load(os.path.join(path, imageName) ) # extension is provided
412                    else:
413                        data = io.load(path).get(imageName) # look in box directory
414                except (AttributeError, AssertionError) as e:
415                    try:
416                        if '.' in imageName:
417                            data = io.load(os.path.join(path, 'results.hyc/%s'%imageName))  # extension is provided
418                        else:
419                            data = io.load(path).results.get(imageName) # look in results directory
420                    except:
421                        if strict:
422                            assert False, "Error - could not find data %s in directory %s" % (imageName, path)
423                        else:
424                            continue
425
426                data.decompress()
427            except (AttributeError, AssertionError) as e:
428                if strict:
429                    assert False, "Error - could not find data %s in directory %s" % (imageName, path )
430                else:
431                    continue
432
433            # get index and subsample as needed
434            index = self.index[::xstep, ::ystep]
435            if scale != 1:
436                index = (index * scale).astype(int) # scale coordinates
437                index[...,0] = self.index[::xstep, ::ystep, 0] # don't scale box IDs
438
439            # create output array
440            if bands is not None:
441                data = data.export_bands(bands)
442            if out is None:
443                # initialise output array now we know how many bands we're dealing with
444                out = np.zeros( (index.shape[0], index.shape[1], data.band_count() ), dtype=data.data.dtype )
445
446            # copy data as defined in index array
447            mask = (index[..., 0] == i) & (index[..., -1] != -1 )
448            if mask.any():
449                out[ mask, : ] = data.data[ index[mask, 1], index[mask, 2], : ]
450        if len( data.get_wavelengths() ) != data.band_count():
451            data.set_wavelengths(np.arange(data.band_count()))
452
453        # return a hyimage
454        out = hylite.HyImage( out, wav=data.get_wavelengths() )
455        if data.has_band_names():
456            if len(data.get_band_names()) == out.band_count(): # sometimes this is not the case if we load a PNG with a header from a .dat file!
457                out.set_band_names(data.get_band_names())
458
459        if (self.groups is not None) and (len(self.groups) > 0):
460            out.header['groups'] = self.groups
461        if (self.group_ticks is not None) and (len(self.group_ticks) > 0):
462            out.header['group ticks'] = self.group_ticks
463
464        if outline is not None:
465            self.add_outlines(out, color=outline, xx = xstep, yy = ystep )
466        return out

Apply this template to the specified dataset in each box.

Parameters
  • imageName: The name of the image file to extract data from in each box, e.g., 'FENIX'.
  • bands: The bands to export from the source image. Default is None (export all bands). See HyData.export_bands for possible formats.
  • strict: If False (default), skip files that could not be found.
  • xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when building large mosaics.
  • ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when building large mosaics.
  • scale: Scale factor to apply to pixel coordinates in this mosaic. Used for applying to images that are different resolutions.
  • outline: Tuple containing the colour used to outline masked areas, or None to disable.
Returns

A HyImage instance containing the mosaic populated with the requested bands.

def quick_plot(self, band=0, rot=False, xx=1, yy=1, interval=5, **kwds):
468    def quick_plot(self, band=0, rot=False, xx=1, yy=1, interval=5, **kwds):
469        """
470        Quickly plot this template for QAQC.
471        :param band: The band(s) to plot. Default is 0 (plot only box ID).
472        :param rot: True if the template should be rotated 90 degrees before plotting.
473        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
474        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
475        :param interval: Interval between depth ticks to add to plot.
476        :keywords: Keywords are passed to hylite.HyImage.quick_plot( ... ).
477        :return: fig,ax from the matplotlib figure created.
478        """
479        img = self.toImage()
480        img.data = img.data[ ::xx, ::yy, : ]
481        if rot:
482            img.rot90()
483        fig, ax = img.quick_plot(band, tscale=True, **kwds )
484        self.add_ticks(ax, rot=rot, xx=xx, yy=yy, interval=interval)
485        return fig, ax

Quickly plot this template for QAQC.

Parameters
  • band: The band(s) to plot. Default is 0 (plot only box ID).
  • rot: True if the template should be rotated 90 degrees before plotting.
  • xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
  • yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
  • interval: Interval between depth ticks to add to plot. :keywords: Keywords are passed to hylite.HyImage.quick_plot( ... ).
Returns

fig,ax from the matplotlib figure created.

def add_ticks( self, ax, interval=5, *, depth_ticks: bool = True, group_ticks: bool = True, rot: bool = False, xx: int = 1, yy: int = 1, angle: float = 45):
487    def add_ticks(self, ax, interval=5, *, depth_ticks: bool = True, group_ticks: bool = True, rot: bool = False,
488                  xx: int = 1, yy: int = 1, angle: float = 45):
489        """
490        Add depth and or group ticks (as stored in this template) to a matplotlib plot.
491
492        :param ax: The matplotlib axes object to set x- and y- ticks / labels too.
493        :param interval: The interval (in m) between depth ticks. Default is 5 m.
494        :param depth_ticks:  True (default) if depth ticks should be plotted.
495        :param group_ticks: True (default) if group ticks should be plotted.
496        :param rot: If True, the x- and y- axes are flipped (e.g. if image was rotated relative to this template before plotting).
497        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
498        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
499        :param angle: rotation used for the x-ticks. Default is 45 degrees.
500        """
501        a = self.depth_axis
502        if rot:
503            a = int(1 - a)
504            _xx = xx
505            xx = yy
506            yy = _xx
507
508        # get depth and group ticks
509        if depth_ticks:
510            zt, zz = self.get_depth_ticks( interval )
511        if group_ticks:
512            gt,gg = self.get_group_ticks()
513
514        if a == 0:
515            if depth_ticks:
516                ax.set_xticks(zt / xx )
517                ax.set_xticklabels( ["%.1f" % z for z in zz], rotation=angle )
518                #ax.tick_params('x', labelrotation=angle )
519            if group_ticks and self.group_ticks is not None:
520                ax.set_yticks(gt / yy )
521                ax.set_yticklabels( ["%s" % g for g in gg] )
522        else:
523            if depth_ticks:
524                ax.set_yticks(zt / xx )
525                ax.set_yticklabels(["%.1f" % z for z in zz])
526            if group_ticks:
527                ax.set_xticks(gt / yy)
528                ax.set_xticklabels(["%s" % g for g in gg], rotation=angle)
529                #ax.tick_params('x', labelrotation=angle)

Add depth and or group ticks (as stored in this template) to a matplotlib plot.

Parameters
  • ax: The matplotlib axes object to set x- and y- ticks / labels too.
  • interval: The interval (in m) between depth ticks. Default is 5 m.
  • depth_ticks: True (default) if depth ticks should be plotted.
  • group_ticks: True (default) if group ticks should be plotted.
  • rot: If True, the x- and y- axes are flipped (e.g. if image was rotated relative to this template before plotting).
  • xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
  • yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
  • angle: rotation used for the x-ticks. Default is 45 degrees.
def get_group_ticks(self):
531    def get_group_ticks(self):
532        """
533        :return: The position and label of group ticks defined in this template, or [], [] if None are defined.
534        """
535        if (self.groups is not None) and (self.group_ticks is not None):
536            return self.group_ticks, self.groups
537        else:
538            return np.array([]), np.array([])
Returns

The position and label of group ticks defined in this template, or [], [] if None are defined.

def get_depth_ticks(self, interval=1.0):
540    def get_depth_ticks(self, interval=1.0):
541        """
542        Get evenly spaced depth ticks for pretty plotting.
543        :param interval: The desired spacing between depth ticks
544        :return: Depth tick positions and values. If depth_ticks and depths are not defined, this will return empty lists.
545        """
546        if (self.from_depth is None) or (self.to_depth is None) or (self.depth_ticks is None) or (self.depths is None):
547            return np.array([]), np.array([])
548        else:
549            zz = np.arange(self.from_depth - self.from_depth % interval,
550                           self.to_depth + interval - self.to_depth % interval, interval )[1:]
551
552            tt = np.interp( zz, self.depths, self.depth_ticks)
553
554            return tt, zz

Get evenly spaced depth ticks for pretty plotting.

Parameters
  • interval: The desired spacing between depth ticks
Returns

Depth tick positions and values. If depth_ticks and depths are not defined, this will return empty lists.

def add_outlines(self, image, color=0.4, mode='thick', xx: int = 1, yy: int = 1):
556    def add_outlines(self, image, color=0.4, mode='thick', xx: int = 1, yy: int = 1):
557        """
558        Add outlines from this template to the specified image.
559
560        :param image: a HyImage instance to add colours too. Note that this will be updated in-place.
561        :param color: a float or tuple containing the values of the colour to apply.
562        :param mode: outline mode. Options are ‘thick’, ‘inner’, ‘outer’, ‘subpixel’ (see skimage.segmentation.mark_boundaries for details).
563        """
564        dtype = image.data.dtype  # store this for later
565
566        # get mask to outline
567        mask = self.index[::xx, ::yy, 1] != -1
568
569        # sort out colour
570        if isinstance(color, float) or isinstance(color, int):
571            color = tuple([color for i in range(image.band_count())])
572        assert len(color) == image.band_count(), "Error - colour must have same number of bands as image. %d != %d" % (
573        len(color), image.band_count())
574        if (np.array(color) > 1).any():
575            color = np.array(color) / 255.
576
577        # mark boundaries using scikit-image
578        from skimage.segmentation import mark_boundaries
579        image.data = mark_boundaries(image.data, mask, color=color, mode=mode)
580
581        if (dtype == np.uint8):
582            image.data = (image.data * 255)  # scikit image transforms our data to 0 - 1 range...

Add outlines from this template to the specified image.

Parameters
  • image: a HyImage instance to add colours too. Note that this will be updated in-place.
  • color: a float or tuple containing the values of the colour to apply.
  • mode: outline mode. Options are ‘thick’, ‘inner’, ‘outer’, ‘subpixel’ (see skimage.segmentation.mark_boundaries for details).
def toImage(self):
584    def toImage(self):
585        """
586        Convert this Template object to a HyImage instance with the relevant additional hole and box lists stored
587        in the header file. This can be saved and then later converted back to a Template using fromImage( ... ).
588        :return: A HyImage representation of this template.
589        """
590        image = hylite.HyImage(self.index)
591        image.header['root'] = self.root
592        image.header['boxes'] = self.boxes
593        if self.from_depth is not None:
594            image.header['from_depth'] = self.from_depth
595        if self.to_depth is not None:
596            image.header['to_depth'] = self.to_depth
597        if self.center_depth is not None:
598            image.header['center_depth'] = self.center_depth
599        if self.groups is not None:
600            image.header['groups'] = self.groups
601        if self.group_ticks is not None:
602            image.header['group_ticks'] = self.group_ticks
603        if self.depths is not None:
604            image.header['depths'] = self.depths
605        if self.depth_ticks is not None:
606            image.header['depth_ticks'] = self.depth_ticks
607        if self.depth_axis is not None:
608            image.header['depth_axis'] = self.depth_axis
609
610        return image

Convert this Template object to a HyImage instance with the relevant additional hole and box lists stored in the header file. This can be saved and then later converted back to a Template using fromImage( ... ).

Returns

A HyImage representation of this template.

@classmethod
def fromImage(cls, image):
612    @classmethod
613    def fromImage(cls, image):
614        """
615        Convert a HyImage with the relevant header information to a Template instance. Useful for IO.
616        :param image: The HyImage instance containing the template mapping and relevant header metadata
617                        (lists of hole and box names).
618        :return:
619        """
620        assert 'root' in image.header, 'Error - image must have a "root" key in its header'
621        assert 'boxes' in image.header, 'Error - image must have a "boxes" key in its header'
622        assert image.band_count() == 3, 'Error - image must have four bands [holeID, boxID, xidx, yidx]'
623        root = image.header['root']
624        boxes = image.header.get_list('boxes')
625        from_depth = None
626        to_depth = None
627        groups = None
628        group_ticks = None
629        depths = None
630        depth_ticks = None
631        depth_axis = None
632        if 'from_depth' in image.header:
633            from_depth = float(image.header['from_depth'])
634        if 'to_depth' in image.header:
635            to_depth = float(image.header['to_depth'])
636        if 'groups' in image.header:
637            groups = image.header.get_list('groups')
638        if 'group_ticks' in image.header:
639            group_ticks = image.header.get_list('group_ticks')
640        if 'depths' in image.header:
641            depths = image.header.get_list('depths')
642        if 'depth_ticks' in image.header:
643            depth_ticks = image.header.get_list('depth_ticks')
644        if 'depth_axis' in image.header:
645            depth_axis = int(image.header['depth_axis'])
646        return Template([os.path.join( root, b) for b in boxes], image.data, from_depth=from_depth, to_depth=to_depth,
647                        groups=groups, group_ticks=group_ticks, depths=depths, depth_ticks=depth_ticks, depth_axis=depth_axis)

Convert a HyImage with the relevant header information to a Template instance. Useful for IO.

Parameters
  • image: The HyImage instance containing the template mapping and relevant header metadata (lists of hole and box names).
Returns
def rot90(self):
649    def rot90(self):
650        """
651        Rotate this template by 90 degrees.
652        """
653        self.index = np.rot90(self.index, axes=(0, 1))
654        self.depth_axis = int(1 - self.depth_axis)

Rotate this template by 90 degrees.

def crop( self, min_depth: float, max_depth: float, axis: int, offset: float = 0):
656    def crop(self, min_depth : float, max_depth : float, axis : int , offset : float = 0):
657        """
658        Crop this template to the specified depth range.
659
660        :param min_depth: The minimum allowable depth.
661        :param max_depth: The maximum allowable depth.
662        :param axis: The axis along which depth is interpolated in this template. Should be 0 (x-axis is depth axis) or 1 (y-axis is depth axis).
663        :param offset: A depth to subtract from min_depth and max_depth prior to cropping.
664        :return: A copy of this template, cropped to the specific range, or None if no overlap exists.
665        """
666        # check there is overlap
667        if (self.from_depth is None) or (self.to_depth is None):
668            assert False, "Error - template has no depth information."
669
670        # interpolate depth
671        zz = np.linspace( self.from_depth, self.to_depth, self.index.shape[axis] ) - offset
672        mask = (zz >= min_depth) & (zz <= max_depth)
673        if not mask.any():
674            return None # no overlap
675
676        if axis == 0:
677            ix = self.index[mask, :, : ]
678        else:
679            ix = self.index[:, mask, : ]
680
681        # print( min_depth, max_depth, self.from_depth, self.to_depth, np.min(zz[mask]), np.max(zz[mask]) )
682        return Template( [os.path.join(self.root, b) for b in self.boxes], ix,
683                         from_depth = np.min(zz[mask]),
684                         to_depth = np.max(zz[mask]) ) # return cropped template

Crop this template to the specified depth range.

Parameters
  • min_depth: The minimum allowable depth.
  • max_depth: The maximum allowable depth.
  • axis: The axis along which depth is interpolated in this template. Should be 0 (x-axis is depth axis) or 1 (y-axis is depth axis).
  • offset: A depth to subtract from min_depth and max_depth prior to cropping.
Returns

A copy of this template, cropped to the specific range, or None if no overlap exists.

@classmethod
def stack(cls, templates: list, xstep: int = 1, ystep: int = 1, axis=1):
686    @classmethod
687    def stack(cls, templates: list, xstep : int = 1, ystep : int = 1, axis=1):
688        """
689        Stack a list of templates along the specified axis (similar to np.vstack and np.hstack).
690
691        :param templates: A list of template objects to stack.
692        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when generating large mosaics.
693        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when generating large mosaics.
694        :param axis: The axis to stack along. Set as zero to stack in the x-direction and 1 to stack in the
695                     y-direction.
696        """
697
698        # resolve all unique paths
699        paths = set()
700        for t in templates:
701            for b in t.boxes:
702                paths.add(os.path.join(t.root, b))
703                assert os.path.exists(os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
704
705        # get root (lowest common base) and express boxes as relative paths to this
706        paths = list(paths)
707
708        # initialise output
709        if axis == 0:
710            out = np.full((sum([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
711                            max([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
712        else:
713            out = np.full((max([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
714                            sum([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
715
716        # loop through templates and stack
717        p = 0
718        groups = []
719        group_ticks = []
720        for i, t in enumerate(templates):
721            # copy block of indices across
722            if axis == 0:
723                out[p:(p + t.index[::xstep, ::ystep, :].shape[0]),
724                        0:t.index[::xstep, ::ystep, :].shape[1], :] = t.index[::xstep, ::ystep, :]
725            else:
726                out[0:t.index[::xstep, ::ystep, :].shape[0],
727                p:(p + t.index[::xstep, ::ystep, :].shape[1]), :] = t.index[::xstep, ::ystep, :]
728
729            # update box indices
730            for j, b in enumerate(t.boxes):
731                mask = np.full((out.shape[0], out.shape[1]), False)
732                if axis == 0:
733                    mask[p:(p + t.index[::xstep, ::ystep, :].shape[0]), 0:t.index[::xstep, ::ystep, :].shape[1]] = (t.index[::xstep, ::ystep, 0] == j)
734                else:
735                    mask[0:t.index[::xstep, ::ystep, :].shape[0], p:(p + t.index[::xstep, ::ystep, :].shape[1])] = (t.index[::xstep, ::ystep, 0] == j)
736                out[mask, 0] = paths.index(os.path.join(t.root, b))
737
738            # update groups and group ticks (these are useful for subsequent plotting)
739
740            if t.groups is not None:
741                groups += list(t.groups)
742                if axis == 0:
743                    group_ticks += list( np.array(t.group_ticks) / xstep + p )
744                else:
745                    group_ticks += list(np.array(t.group_ticks) / ystep + p)
746
747            # update start point
748            p += t.index[::xstep, ::ystep, :].shape[axis]
749
750        # get span of depths
751        from_depth = None
752        to_depth = None
753        if np.array([t.center_depth is not None for t in templates]).all():
754            from_depth = np.min([t.from_depth for t in templates])
755            to_depth = np.max([t.to_depth for t in templates])
756
757        # generate depth ticks
758        # i = 1 - axis # if axis is 1, we tick along axis = 0, if axis is 0, we tick along axis = 1
759        ticks = [templates[0].index.shape[axis] / 2]
760        depths = [templates[0].from_depth]
761        for i, T in enumerate(templates[1:]):
762            ticks.append(ticks[-1] + templates[i - 1].index.shape[axis] / 2 + T.index.shape[axis] / 2)
763            depths.append(T.from_depth)
764
765        # return new Template instance
766        return Template(paths, out, from_depth = from_depth, to_depth = to_depth,
767                        groups=groups, group_ticks=group_ticks,
768                        depths=depths, depth_ticks=ticks, depth_axis=axis)

Stack a list of templates along the specified axis (similar to np.vstack and np.hstack).

Parameters
  • templates: A list of template objects to stack.
  • xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when generating large mosaics.
  • ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when generating large mosaics.
  • axis: The axis to stack along. Set as zero to stack in the x-direction and 1 to stack in the y-direction.
def getDepths(self, res: float = 0.001):
770    def getDepths(self, res: float = 1e-3):
771        """
772        Return a 1D array of the depths corresponding to each pixel. Assumes a linear mapping
773        between the templates from_depth and to_depth.
774        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
775                    the start and end of this template. If specified, the start_depth is used as an
776                    anchor and the depth of pixels below this computed to match the resolution. This is important
777                    to preserve true scale when core boxes contain gaps.
778        :return: A 1D array containing depth information for each pixel in this template.
779        """
780        axis = self.depth_axis
781        if res is None:
782            return np.linspace(self.from_depth, self.to_depth, self.index.shape[axis])
783        else:
784            to_depth = self.from_depth + self.index.shape[axis] * res
785            return np.linspace(self.from_depth, to_depth, self.index.shape[axis])

Return a 1D array of the depths corresponding to each pixel. Assumes a linear mapping between the templates from_depth and to_depth.

Parameters
  • res: The known resolution of the image data. If None, depths are simply stretched evenly between the start and end of this template. If specified, the start_depth is used as an anchor and the depth of pixels below this computed to match the resolution. This is important to preserve true scale when core boxes contain gaps.
Returns

A 1D array containing depth information for each pixel in this template.

def getGrid( self, grid=50, minor=True, labels=True, background=True, res: float = 0.001):
787    def getGrid(self, grid=50, minor=True, labels=True, background=True, res : float = 1e-3):
788        """
789        Create a depth grid image to accompany HSI mosaics.
790
791        :param grid: The grid step, in mm. Default is 50.
792        :param minor: True if minor ticks (with half the spacing of the major ticks) should be plotted.
793        :param labels: True if label text describing the meterage should be added.
794        :param background: True if background outlines of the core blocks should be added.
795        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
796                    the start and end of this template. If specified, the start_depth is used as an
797                    anchor and the depth of pixels below this computed to match the resolution. This is important
798                    to preserve true scale when core boxes contain gaps.
799        :return: A HyImage instance containing the grid image.
800        """
801        # import this here in case of problematic cv2 install
802        import cv2
803
804        # get background image showing core blocks
805        img = np.zeros((self.index.shape[0], self.index.shape[1], 3), dtype=np.uint8)
806        if background:
807            img[:, :, 1] = img[:, :, 2] = 120 * (self.index[:, :, 2] > 1)
808
809        # interpolate depth
810        zz = self.getDepths(res=res)
811
812        # add ticks
813        ignore = set()
814        for i, z in enumerate(zz):
815            zi = int(z * 1000)
816
817            # major ticks
818            if zi not in ignore:
819                if (zi % int(grid)) == 0:
820                    # add tick
821                    img[i, :, :] = 255
822                    ignore.add(zi)
823
824                    # add depth label
825                    if labels:
826                        l = "%.2f" % z
827                        font = cv2.FONT_HERSHEY_SIMPLEX
828                        img = cv2.putText(img,
829                                          l, (0, i - 3), font, 0.5, (255, 255, 255), 1, bottomLeftOrigin=False)
830            # minor ticks
831            if (zi not in ignore) and minor:
832                if (int(z * 1000) % int(grid / 2)) == 0:
833                    img[i, ::3, :] = 255
834                    ignore.add(zi)
835
836        return hylite.HyImage(img)

Create a depth grid image to accompany HSI mosaics.

Parameters
  • grid: The grid step, in mm. Default is 50.
  • minor: True if minor ticks (with half the spacing of the major ticks) should be plotted.
  • labels: True if label text describing the meterage should be added.
  • background: True if background outlines of the core blocks should be added.
  • res: The known resolution of the image data. If None, depths are simply stretched evenly between the start and end of this template. If specified, the start_depth is used as an anchor and the depth of pixels below this computed to match the resolution. This is important to preserve true scale when core boxes contain gaps.
Returns

A HyImage instance containing the grid image.

class Canvas(collections.abc.MutableMapping):
 865class Canvas(MutableMapping):
 866    """
 867    A utility class for creating collections of templates and combining them into potentially complex layouts. This
 868    stores groups of templates, which can then be sorted and arranged in various ways (e.g., arranging groups as
 869    columns and cropping to a specific depth range, with individual drillhole offsets).
 870    """
 871
 872    def __init__(self, *args, **kwargs):
 873        self.store = dict()
 874        self.update(dict(*args, **kwargs))  # use the free update to set keys
 875
 876
 877    def hpole(self, from_depth: float = None, to_depth: float = None, scaled=False, groups: list = None,
 878                    res: float = 1e-3, depth_offsets: dict = {}, pad: int = 5 ):
 879        """
 880        Construct a "horizontal pole" type template for visualising and corellating between one or more drillholes.
 881        This has a layout as follows:
 882
 883                          -------------------------------------------------
 884        core (group) 1 - |  [xxxxxxxxxx] [xxxx]         [xxxxxxxxxxxxxxx]  |
 885        core (group) 2 - |  [xxxxx]       [xxxxxxxxxxxx]         [xxxxxx]  |
 886        core (group) 3 - |  [xxxxxxxx] [xxxxxxxxxxxxxxxxxx][xxxxxxxxxxxx]  |
 887                          -------------------------------------------------
 888
 889        :param from_depth: The top depth of the template view area, or None to include all depths.
 890        :param to_depth: The lower depth of the template view area, or None to include all depths.
 891        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 892                (with small gaps representing non-contiguous intervals).
 893        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 894        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 895        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 896                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 897                                in absolute terms.
 898        :param pad: Padding for template stacking. Default is 5.
 899        :return: A single combined Template class in horizontal pole layout.
 900        """
 901
 902        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
 903                                                                           groups, to_depth )
 904
 905        # compute width of output image
 906        w = pad
 907        for g in groups:
 908            w += np.max([T.index.shape[1] for T in S[g.lower()]]) + pad
 909
 910        if scaled:
 911            # compute image dimension in depth direction
 912            nz = int(np.abs(to_depth - from_depth) / res)
 913
 914            # compute corresponding depths
 915            z = np.linspace(from_depth, to_depth, nz)
 916        else:
 917            # determine maximum size of stacked boxes, including gaps, and hence template dimensions
 918            z = []
 919            for g in groups:
 920                nz = 0 # this is the dimensions of our output in pixels
 921                for i, T in enumerate(S[g.lower()]):
 922                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
 923                        nz += 10*pad # add in gaps for non-contiguous cores
 924                        z.append( np.linspace(S[g.lower()][i-1].to_depth, T.from_depth, 10*pad ) )
 925
 926                    nz += T.index.shape[0] + pad
 927                    z.append(np.linspace(T.from_depth, T.to_depth, T.index.shape[0]))
 928                    z.append([T.to_depth for i in range(pad)])
 929            z = np.hstack(z)
 930
 931        assert len(z) == nz, "Error - %d depths and %d pixels. Should be the same." % (len(z), nz) # debugging
 932
 933        # build index
 934        index = np.full((nz, w, 3), -1, dtype=int)
 935        tticks = []  # store tick positions in transverse direction (y-axis for hpole)
 936
 937        # stack templates
 938        _y = pad
 939        for g in groups:
 940            g = g.lower()
 941            for T in S[g]:
 942                # find depth position of center and copy data across
 943                if len(T.boxes) > 1:
 944                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
 945                else:
 946                    six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
 947                    eix = min(T.index.shape[0],
 948                              (index.shape[0] - six))  # end index in template (to allow for possible overflows)
 949
 950                    # copy data!
 951                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
 952                    index[six:(six + T.index.shape[0]), _y:(_y + T.index.shape[1]), 0] = bix  # set box index
 953
 954                    index[six:(six + eix), _y:(_y + T.index.shape[1]), 1:] = T.index[0:eix, :,
 955                                                                             1:]  # copy pixel indices
 956
 957            # step to the right
 958            h = int(np.max([T.index.shape[1] for T in S[g]]) + pad)
 959            tticks.append(int(_y + (h / 2)))
 960            _y += h
 961
 962        out = Template(paths, index, from_depth, to_depth, depth_axis=0,
 963            groups = groups, group_ticks = tticks, depths = z, depth_ticks = np.arange(len(z)))
 964
 965        # done!
 966        return out
 967
 968    def vfence(self, from_depth: float = None, to_depth: float = None, scaled=False,
 969               groups: list = None, depth_offsets : dict = {}, pad: int = 5):
 970        """
 971        Construct a "horizontal fence" type template for visualising drillholes in a condensed way.
 972        This has a layout as follows:
 973
 974            core 1       core 2         core 3
 975    1  - | ======== | | =========| | ========== |
 976         | ======== | | =========| | ========== |
 977    2  - | ======== | | ======   | | =====      |
 978         | ======== |     gap      | ========== |
 979    3  - | ======   | | =========| | ========== |
 980         | ======== | | =========| | ========== |
 981
 982        :param from_depth: The top depth of the template view area, or None to include all depths.
 983        :param to_depth: The lower depth of the template view area, or None to include all depths.
 984        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 985                        (with small gaps representing non-contiguous intervals).
 986        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 987        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 988        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 989                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 990                                in absolute terms.
 991        :param pad: Padding for template stacking. Default is 5.
 992        :return: A single combined Template class in horizontal pole layout.
 993        """
 994
 995        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
 996                                                                           groups, to_depth )
 997
 998        # compute width used for each group and hence image width
 999        # also compute y-scale based maximum template height to depth covered ratio
1000        w = pad # width
1001        ys = np.inf # shared y-axis pixel to depth scale (meters per pixel)
1002        for g in groups:
1003            w = w + np.max([T.index.shape[0] for T in S[g.lower()]]) + pad
1004            for T in S[g.lower()]:
1005                ys = min( ys, abs(T.to_depth - T.from_depth) / T.index.shape[1] )
1006
1007        # compute image dimension in depth direction
1008        if scaled:
1009            # determine depth-scale along y-axis (distance down hole per pixel)
1010            nz = int(np.abs(to_depth - from_depth) / ys)
1011            z = np.linspace(from_depth, to_depth, nz ) # depth per pixel array (kinda...)
1012
1013            # build index
1014            index = np.full((w, nz + pad, 3), -1, dtype=int)
1015
1016        else:
1017            # determine maximum height of stacked boxes, including gaps, and hence template dimensions
1018            heights = []
1019            for g in groups:
1020                h = 0
1021                for i, T in enumerate(S[g.lower()]):
1022                    h += T.index.shape[1] + pad
1023                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
1024                        h += T.index.shape[1] # add in gaps for non-contiguous cores
1025                heights.append(h)
1026
1027            ymax = np.max( heights )
1028
1029            # build index
1030            index = np.full((w, ymax + pad, 3), -1, dtype=int)
1031
1032        tticks = []  # store group tick positions in transverse direction (x-axis)
1033
1034        # stack templates
1035        _x = pad
1036        for g in groups: # loop through groups
1037            zticks = []  # store depth ticks in the down-hole direction (y-axis)
1038            zvals = []  # store corresponding depth values
1039
1040            g = g.lower()
1041            six=0
1042            for i,T in enumerate(S[g]): # loop through templates in this group
1043                # find depth position of center and copy data across
1044                if len(T.boxes) > 1:
1045                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
1046                else:
1047                    if scaled:
1048                        six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
1049                    else:
1050                        # add gaps for non-contiguous templates
1051                        if i > 0 and (abs(S[g][i - 1].to_depth - T.from_depth) > 0.5):
1052                            six += T.index.shape[1]  # add full-box sized gap
1053
1054                    # copy data!
1055                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
1056                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 0 ] = bix
1057                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 1:] = T.index[:, :, 1:]  # copy pixel indices
1058
1059                    # store depth ticks
1060                    zticks.append(six)
1061                    zvals.append(T.from_depth)
1062
1063                    if not scaled:
1064                        six += T.index.shape[1]+pad # increment position
1065
1066            # step to the right
1067            w = int(np.max([T.index.shape[0] for T in S[g]]) + pad) # compute max width of core blocks in this group
1068            tticks.append(int(_x + (w / 2))) # store group ticks
1069            _x += w # step to the right
1070
1071        zticks.append(index.shape[1])
1072        zvals.append(T.to_depth) # add tick at bottom of final template / box
1073
1074        if scaled and len(groups) > 1:
1075            zvals = None
1076            depth_ticks = None # these are not defined if more than one hole is present
1077        else:
1078            # interpolate zvals to get a depth value for each pixel
1079            zvals = np.interp(np.arange(0,index.shape[1]), zticks, zvals )
1080            zticks = np.arange(index.shape[1])
1081        out = Template(paths, index, from_depth, to_depth, depth_axis=1,
1082                       groups = groups, group_ticks = tticks, depths = zvals, depth_ticks = zticks )
1083
1084        # done!
1085        return out
1086
1087
1088    def hfence(self, *args):
1089        """
1090        Construct a "horizontal fence" type template for visualising boreholes in a condensed way. This
1091        is identical to the vfence(...) layout, but rotated 90 degrees such that depth increases to the right.
1092
1093        :param args: All arguments are passed to vfence. The results are then rotated to the horizontal orientation.
1094        :return:
1095        """
1096        out = self.vfence(*args)
1097        out.rot90()
1098        return out
1099
1100    def vpole(self, *args):
1101        """
1102        Construct a "vertical pole" type template for visualising and corellating between one or more drillcores. This
1103        is identical to the hpole(...) layout, but rotated 90 degrees such that cores are vertical and depth increases
1104        downwards.
1105
1106        :param args: All arguments are passed to hpole. The results are then rotated to vertical orientation.
1107        :return:
1108        """
1109        out = self.hpole(*args)
1110        # out.index = np.transpose(out.index, (1, 0, 2)) # rotate to vertical
1111        out.rot90()
1112        return out
1113
1114    def _preprocessTemplates(self, depth_offsets, from_depth, groups, to_depth):
1115        # parse from_depth and to_depth if needed
1116        if from_depth is None:
1117            from_depth = np.min([np.min([t.from_depth for t in v]) for (k, v) in self.store.items()])
1118        if to_depth is None:
1119            to_depth = np.max([np.max([t.to_depth for t in v]) for (k, v) in self.store.items()])
1120        # ensure depth template keys are lower case!
1121        offs = {}
1122        for k, v in depth_offsets.items():
1123            offs[k.lower()] = v
1124        # crop templates to the relevant view area, and discard ones that do not fit
1125        cropped = {}
1126        for k, v in self.store.items():
1127            for T in v:
1128                assert T.from_depth is not None, "Error - depth info must be defined for template to be added."
1129                assert T.to_depth is not None, "Error - depth info must be defined for template to be added."
1130                T = T.crop(from_depth, to_depth, T.depth_axis, offs.get(k, 0))
1131                if T is not None:
1132                    # store
1133                    cropped[k.lower()] = cropped.get(k.lower(), [])
1134                    cropped[k.lower()].append(T)
1135
1136        assert len(cropped) > 0, "Error - no templates are within depth range!"
1137
1138        # sort templates by order in each group
1139        S = {}
1140        for k, v in cropped.items():
1141            S[k.lower()] = sorted(v)
1142        # resolve all unique paths
1143        paths = set()
1144        for k, v in S.items():
1145            for t in v:
1146                for b in t.boxes:
1147                    paths.add(os.path.join(t.root, b))
1148                    assert os.path.exists(
1149                        os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
1150        paths = list(paths)
1151        # get group names to plot if not specified
1152        if groups is None:
1153            groups = list(S.keys())
1154        return S, from_depth, to_depth, groups, paths
1155
1156
1157    def add(self, group, template):
1158        """
1159        Add the specified template to this Canvas collection.
1160
1161        :param group: The name of the group to add this template to.
1162        :param template: The template object.
1163        """
1164        self.__setitem__(group, template)
1165
1166    def __getitem__(self, key):
1167        return self.store[self._keytransform(key)]
1168
1169    def __setitem__(self, key, value):
1170        """
1171        A shorthand way to add items to canvas.
1172        """
1173        assert isinstance(value, Template), "Error - only Templates can be added to a Canvas (for now...)"
1174        v = self.store.get(self._keytransform(key), [])
1175        v.append(value)
1176        self.store[self._keytransform(key)] = v
1177
1178    def __delitem__(self, key):
1179        del self.store[self._keytransform(key)]
1180
1181    def __iter__(self):
1182        return iter(self.store)
1183
1184    def __len__(self):
1185        return len(self.store)
1186
1187    def _keytransform(self, key):
1188        return key.lower()

A utility class for creating collections of templates and combining them into potentially complex layouts. This stores groups of templates, which can then be sorted and arranged in various ways (e.g., arranging groups as columns and cropping to a specific depth range, with individual drillhole offsets).

Canvas(*args, **kwargs)
872    def __init__(self, *args, **kwargs):
873        self.store = dict()
874        self.update(dict(*args, **kwargs))  # use the free update to set keys
def hpole( self, from_depth: float = None, to_depth: float = None, scaled=False, groups: list = None, res: float = 0.001, depth_offsets: dict = {}, pad: int = 5):
877    def hpole(self, from_depth: float = None, to_depth: float = None, scaled=False, groups: list = None,
878                    res: float = 1e-3, depth_offsets: dict = {}, pad: int = 5 ):
879        """
880        Construct a "horizontal pole" type template for visualising and corellating between one or more drillholes.
881        This has a layout as follows:
882
883                          -------------------------------------------------
884        core (group) 1 - |  [xxxxxxxxxx] [xxxx]         [xxxxxxxxxxxxxxx]  |
885        core (group) 2 - |  [xxxxx]       [xxxxxxxxxxxx]         [xxxxxx]  |
886        core (group) 3 - |  [xxxxxxxx] [xxxxxxxxxxxxxxxxxx][xxxxxxxxxxxx]  |
887                          -------------------------------------------------
888
889        :param from_depth: The top depth of the template view area, or None to include all depths.
890        :param to_depth: The lower depth of the template view area, or None to include all depths.
891        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
892                (with small gaps representing non-contiguous intervals).
893        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
894        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
895        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
896                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
897                                in absolute terms.
898        :param pad: Padding for template stacking. Default is 5.
899        :return: A single combined Template class in horizontal pole layout.
900        """
901
902        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
903                                                                           groups, to_depth )
904
905        # compute width of output image
906        w = pad
907        for g in groups:
908            w += np.max([T.index.shape[1] for T in S[g.lower()]]) + pad
909
910        if scaled:
911            # compute image dimension in depth direction
912            nz = int(np.abs(to_depth - from_depth) / res)
913
914            # compute corresponding depths
915            z = np.linspace(from_depth, to_depth, nz)
916        else:
917            # determine maximum size of stacked boxes, including gaps, and hence template dimensions
918            z = []
919            for g in groups:
920                nz = 0 # this is the dimensions of our output in pixels
921                for i, T in enumerate(S[g.lower()]):
922                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
923                        nz += 10*pad # add in gaps for non-contiguous cores
924                        z.append( np.linspace(S[g.lower()][i-1].to_depth, T.from_depth, 10*pad ) )
925
926                    nz += T.index.shape[0] + pad
927                    z.append(np.linspace(T.from_depth, T.to_depth, T.index.shape[0]))
928                    z.append([T.to_depth for i in range(pad)])
929            z = np.hstack(z)
930
931        assert len(z) == nz, "Error - %d depths and %d pixels. Should be the same." % (len(z), nz) # debugging
932
933        # build index
934        index = np.full((nz, w, 3), -1, dtype=int)
935        tticks = []  # store tick positions in transverse direction (y-axis for hpole)
936
937        # stack templates
938        _y = pad
939        for g in groups:
940            g = g.lower()
941            for T in S[g]:
942                # find depth position of center and copy data across
943                if len(T.boxes) > 1:
944                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
945                else:
946                    six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
947                    eix = min(T.index.shape[0],
948                              (index.shape[0] - six))  # end index in template (to allow for possible overflows)
949
950                    # copy data!
951                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
952                    index[six:(six + T.index.shape[0]), _y:(_y + T.index.shape[1]), 0] = bix  # set box index
953
954                    index[six:(six + eix), _y:(_y + T.index.shape[1]), 1:] = T.index[0:eix, :,
955                                                                             1:]  # copy pixel indices
956
957            # step to the right
958            h = int(np.max([T.index.shape[1] for T in S[g]]) + pad)
959            tticks.append(int(_y + (h / 2)))
960            _y += h
961
962        out = Template(paths, index, from_depth, to_depth, depth_axis=0,
963            groups = groups, group_ticks = tticks, depths = z, depth_ticks = np.arange(len(z)))
964
965        # done!
966        return out

Construct a "horizontal pole" type template for visualising and corellating between one or more drillholes. This has a layout as follows:

              -------------------------------------------------

core (group) 1 - | [xxxxxxxxxx] [xxxx] [xxxxxxxxxxxxxxx] | core (group) 2 - | [xxxxx] [xxxxxxxxxxxx] [xxxxxx] | core (group) 3 - | [xxxxxxxx] [xxxxxxxxxxxxxxxxxx][xxxxxxxxxxxx] | -------------------------------------------------

Parameters
  • from_depth: The top depth of the template view area, or None to include all depths.
  • to_depth: The lower depth of the template view area, or None to include all depths.
  • scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically (with small gaps representing non-contiguous intervals).
  • groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
  • res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
  • depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than in absolute terms.
  • pad: Padding for template stacking. Default is 5.
Returns

A single combined Template class in horizontal pole layout.

def vfence( self, from_depth: float = None, to_depth: float = None, scaled=False, groups: list = None, depth_offsets: dict = {}, pad: int = 5):
 968    def vfence(self, from_depth: float = None, to_depth: float = None, scaled=False,
 969               groups: list = None, depth_offsets : dict = {}, pad: int = 5):
 970        """
 971        Construct a "horizontal fence" type template for visualising drillholes in a condensed way.
 972        This has a layout as follows:
 973
 974            core 1       core 2         core 3
 975    1  - | ======== | | =========| | ========== |
 976         | ======== | | =========| | ========== |
 977    2  - | ======== | | ======   | | =====      |
 978         | ======== |     gap      | ========== |
 979    3  - | ======   | | =========| | ========== |
 980         | ======== | | =========| | ========== |
 981
 982        :param from_depth: The top depth of the template view area, or None to include all depths.
 983        :param to_depth: The lower depth of the template view area, or None to include all depths.
 984        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 985                        (with small gaps representing non-contiguous intervals).
 986        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 987        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 988        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 989                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 990                                in absolute terms.
 991        :param pad: Padding for template stacking. Default is 5.
 992        :return: A single combined Template class in horizontal pole layout.
 993        """
 994
 995        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
 996                                                                           groups, to_depth )
 997
 998        # compute width used for each group and hence image width
 999        # also compute y-scale based maximum template height to depth covered ratio
1000        w = pad # width
1001        ys = np.inf # shared y-axis pixel to depth scale (meters per pixel)
1002        for g in groups:
1003            w = w + np.max([T.index.shape[0] for T in S[g.lower()]]) + pad
1004            for T in S[g.lower()]:
1005                ys = min( ys, abs(T.to_depth - T.from_depth) / T.index.shape[1] )
1006
1007        # compute image dimension in depth direction
1008        if scaled:
1009            # determine depth-scale along y-axis (distance down hole per pixel)
1010            nz = int(np.abs(to_depth - from_depth) / ys)
1011            z = np.linspace(from_depth, to_depth, nz ) # depth per pixel array (kinda...)
1012
1013            # build index
1014            index = np.full((w, nz + pad, 3), -1, dtype=int)
1015
1016        else:
1017            # determine maximum height of stacked boxes, including gaps, and hence template dimensions
1018            heights = []
1019            for g in groups:
1020                h = 0
1021                for i, T in enumerate(S[g.lower()]):
1022                    h += T.index.shape[1] + pad
1023                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
1024                        h += T.index.shape[1] # add in gaps for non-contiguous cores
1025                heights.append(h)
1026
1027            ymax = np.max( heights )
1028
1029            # build index
1030            index = np.full((w, ymax + pad, 3), -1, dtype=int)
1031
1032        tticks = []  # store group tick positions in transverse direction (x-axis)
1033
1034        # stack templates
1035        _x = pad
1036        for g in groups: # loop through groups
1037            zticks = []  # store depth ticks in the down-hole direction (y-axis)
1038            zvals = []  # store corresponding depth values
1039
1040            g = g.lower()
1041            six=0
1042            for i,T in enumerate(S[g]): # loop through templates in this group
1043                # find depth position of center and copy data across
1044                if len(T.boxes) > 1:
1045                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
1046                else:
1047                    if scaled:
1048                        six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
1049                    else:
1050                        # add gaps for non-contiguous templates
1051                        if i > 0 and (abs(S[g][i - 1].to_depth - T.from_depth) > 0.5):
1052                            six += T.index.shape[1]  # add full-box sized gap
1053
1054                    # copy data!
1055                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
1056                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 0 ] = bix
1057                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 1:] = T.index[:, :, 1:]  # copy pixel indices
1058
1059                    # store depth ticks
1060                    zticks.append(six)
1061                    zvals.append(T.from_depth)
1062
1063                    if not scaled:
1064                        six += T.index.shape[1]+pad # increment position
1065
1066            # step to the right
1067            w = int(np.max([T.index.shape[0] for T in S[g]]) + pad) # compute max width of core blocks in this group
1068            tticks.append(int(_x + (w / 2))) # store group ticks
1069            _x += w # step to the right
1070
1071        zticks.append(index.shape[1])
1072        zvals.append(T.to_depth) # add tick at bottom of final template / box
1073
1074        if scaled and len(groups) > 1:
1075            zvals = None
1076            depth_ticks = None # these are not defined if more than one hole is present
1077        else:
1078            # interpolate zvals to get a depth value for each pixel
1079            zvals = np.interp(np.arange(0,index.shape[1]), zticks, zvals )
1080            zticks = np.arange(index.shape[1])
1081        out = Template(paths, index, from_depth, to_depth, depth_axis=1,
1082                       groups = groups, group_ticks = tticks, depths = zvals, depth_ticks = zticks )
1083
1084        # done!
1085        return out

Construct a "horizontal fence" type template for visualising drillholes in a condensed way. This has a layout as follows:

    core 1       core 2         core 3

1 - | ======== | | =========| | ========== | | ======== | | =========| | ========== | 2 - | ======== | | ====== | | ===== | | ======== | gap | ========== | 3 - | ====== | | =========| | ========== | | ======== | | =========| | ========== |

:param from_depth: The top depth of the template view area, or None to include all depths.
:param to_depth: The lower depth of the template view area, or None to include all depths.
:param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
                (with small gaps representing non-contiguous intervals).
:param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
:param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
:param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
                        group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
                        in absolute terms.
:param pad: Padding for template stacking. Default is 5.
:return: A single combined Template class in horizontal pole layout.
def hfence(self, *args):
1088    def hfence(self, *args):
1089        """
1090        Construct a "horizontal fence" type template for visualising boreholes in a condensed way. This
1091        is identical to the vfence(...) layout, but rotated 90 degrees such that depth increases to the right.
1092
1093        :param args: All arguments are passed to vfence. The results are then rotated to the horizontal orientation.
1094        :return:
1095        """
1096        out = self.vfence(*args)
1097        out.rot90()
1098        return out

Construct a "horizontal fence" type template for visualising boreholes in a condensed way. This is identical to the vfence(...) layout, but rotated 90 degrees such that depth increases to the right.

Parameters
  • args: All arguments are passed to vfence. The results are then rotated to the horizontal orientation.
Returns
def vpole(self, *args):
1100    def vpole(self, *args):
1101        """
1102        Construct a "vertical pole" type template for visualising and corellating between one or more drillcores. This
1103        is identical to the hpole(...) layout, but rotated 90 degrees such that cores are vertical and depth increases
1104        downwards.
1105
1106        :param args: All arguments are passed to hpole. The results are then rotated to vertical orientation.
1107        :return:
1108        """
1109        out = self.hpole(*args)
1110        # out.index = np.transpose(out.index, (1, 0, 2)) # rotate to vertical
1111        out.rot90()
1112        return out

Construct a "vertical pole" type template for visualising and corellating between one or more drillcores. This is identical to the hpole(...) layout, but rotated 90 degrees such that cores are vertical and depth increases downwards.

Parameters
  • args: All arguments are passed to hpole. The results are then rotated to vertical orientation.
Returns
def add(self, group, template):
1157    def add(self, group, template):
1158        """
1159        Add the specified template to this Canvas collection.
1160
1161        :param group: The name of the group to add this template to.
1162        :param template: The template object.
1163        """
1164        self.__setitem__(group, template)

Add the specified template to this Canvas collection.

Parameters
  • group: The name of the group to add this template to.
  • template: The template object.
Inherited Members
collections.abc.MutableMapping
pop
popitem
clear
update
setdefault
collections.abc.Mapping
get
keys
items
values