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        data = 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        if data is None:
 454            assert False, "Error - could not find any data for %s" % (imageName)
 455        
 456        # return a hyimage
 457        out = hylite.HyImage( out, wav=data.get_wavelengths() )
 458        if data.has_band_names():
 459            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!
 460                out.set_band_names(data.get_band_names())
 461
 462        if (self.groups is not None) and (len(self.groups) > 0):
 463            out.header['groups'] = self.groups
 464        if (self.group_ticks is not None) and (len(self.group_ticks) > 0):
 465            out.header['group ticks'] = self.group_ticks
 466
 467        if outline is not None:
 468            self.add_outlines(out, color=outline, xx = xstep, yy = ystep )
 469        return out
 470
 471    def quick_plot(self, band=0, rot=False, xx=1, yy=1, interval=5, **kwds):
 472        """
 473        Quickly plot this template for QAQC.
 474        :param band: The band(s) to plot. Default is 0 (plot only box ID).
 475        :param rot: True if the template should be rotated 90 degrees before plotting.
 476        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
 477        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
 478        :param interval: Interval between depth ticks to add to plot.
 479        :keywords: Keywords are passed to hylite.HyImage.quick_plot( ... ).
 480        :return: fig,ax from the matplotlib figure created.
 481        """
 482        img = self.toImage()
 483        img.data = img.data[ ::xx, ::yy, : ]
 484        if rot:
 485            img.rot90()
 486        fig, ax = img.quick_plot(band, tscale=True, **kwds )
 487        self.add_ticks(ax, rot=rot, xx=xx, yy=yy, interval=interval)
 488        return fig, ax
 489
 490    def add_ticks(self, ax, interval=5, *, depth_ticks: bool = True, group_ticks: bool = True, rot: bool = False,
 491                  xx: int = 1, yy: int = 1, angle: float = 45):
 492        """
 493        Add depth and or group ticks (as stored in this template) to a matplotlib plot.
 494
 495        :param ax: The matplotlib axes object to set x- and y- ticks / labels too.
 496        :param interval: The interval (in m) between depth ticks. Default is 5 m.
 497        :param depth_ticks:  True (default) if depth ticks should be plotted.
 498        :param group_ticks: True (default) if group ticks should be plotted.
 499        :param rot: If True, the x- and y- axes are flipped (e.g. if image was rotated relative to this template before plotting).
 500        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
 501        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
 502        :param angle: rotation used for the x-ticks. Default is 45 degrees.
 503        """
 504        a = self.depth_axis
 505        if rot:
 506            a = int(1 - a)
 507            _xx = xx
 508            xx = yy
 509            yy = _xx
 510
 511        # get depth and group ticks
 512        if depth_ticks:
 513            zt, zz = self.get_depth_ticks( interval )
 514        if group_ticks:
 515            gt,gg = self.get_group_ticks()
 516
 517        if a == 0:
 518            if depth_ticks:
 519                ax.set_xticks(zt / xx )
 520                ax.set_xticklabels( ["%.1f" % z for z in zz], rotation=angle )
 521                #ax.tick_params('x', labelrotation=angle )
 522            if group_ticks and self.group_ticks is not None:
 523                ax.set_yticks(gt / yy )
 524                ax.set_yticklabels( ["%s" % g for g in gg] )
 525        else:
 526            if depth_ticks:
 527                ax.set_yticks(zt / xx )
 528                ax.set_yticklabels(["%.1f" % z for z in zz])
 529            if group_ticks:
 530                ax.set_xticks(gt / yy)
 531                ax.set_xticklabels(["%s" % g for g in gg], rotation=angle)
 532                #ax.tick_params('x', labelrotation=angle)
 533
 534    def get_group_ticks(self):
 535        """
 536        :return: The position and label of group ticks defined in this template, or [], [] if None are defined.
 537        """
 538        if (self.groups is not None) and (self.group_ticks is not None):
 539            return self.group_ticks, self.groups
 540        else:
 541            return np.array([]), np.array([])
 542
 543    def get_depth_ticks(self, interval=1.0):
 544        """
 545        Get evenly spaced depth ticks for pretty plotting.
 546        :param interval: The desired spacing between depth ticks
 547        :return: Depth tick positions and values. If depth_ticks and depths are not defined, this will return empty lists.
 548        """
 549        if (self.from_depth is None) or (self.to_depth is None) or (self.depth_ticks is None) or (self.depths is None):
 550            return np.array([]), np.array([])
 551        else:
 552            zz = np.arange(self.from_depth - self.from_depth % interval,
 553                           self.to_depth + interval - self.to_depth % interval, interval )[1:]
 554
 555            tt = np.interp( zz, self.depths, self.depth_ticks)
 556
 557            return tt, zz
 558
 559    def add_outlines(self, image, color=0.4, mode='thick', xx: int = 1, yy: int = 1):
 560        """
 561        Add outlines from this template to the specified image.
 562
 563        :param image: a HyImage instance to add colours too. Note that this will be updated in-place.
 564        :param color: a float or tuple containing the values of the colour to apply.
 565        :param mode: outline mode. Options are ‘thick’, ‘inner’, ‘outer’, ‘subpixel’ (see skimage.segmentation.mark_boundaries for details).
 566        """
 567        dtype = image.data.dtype  # store this for later
 568
 569        # get mask to outline
 570        mask = self.index[::xx, ::yy, 1] != -1
 571
 572        # sort out colour
 573        if isinstance(color, float) or isinstance(color, int):
 574            color = tuple([color for i in range(image.band_count())])
 575        assert len(color) == image.band_count(), "Error - colour must have same number of bands as image. %d != %d" % (
 576        len(color), image.band_count())
 577        if (np.array(color) > 1).any():
 578            color = np.array(color) / 255.
 579
 580        # mark boundaries using scikit-image
 581        from skimage.segmentation import mark_boundaries
 582        image.data = mark_boundaries(image.data, mask, color=color, mode=mode)
 583
 584        if (dtype == np.uint8):
 585            image.data = (image.data * 255)  # scikit image transforms our data to 0 - 1 range...
 586
 587    def toImage(self):
 588        """
 589        Convert this Template object to a HyImage instance with the relevant additional hole and box lists stored
 590        in the header file. This can be saved and then later converted back to a Template using fromImage( ... ).
 591        :return: A HyImage representation of this template.
 592        """
 593        image = hylite.HyImage(self.index)
 594        image.header['root'] = self.root
 595        image.header['boxes'] = self.boxes
 596        if self.from_depth is not None:
 597            image.header['from_depth'] = self.from_depth
 598        if self.to_depth is not None:
 599            image.header['to_depth'] = self.to_depth
 600        if self.center_depth is not None:
 601            image.header['center_depth'] = self.center_depth
 602        if self.groups is not None:
 603            image.header['groups'] = self.groups
 604        if self.group_ticks is not None:
 605            image.header['group_ticks'] = self.group_ticks
 606        if self.depths is not None:
 607            image.header['depths'] = self.depths
 608        if self.depth_ticks is not None:
 609            image.header['depth_ticks'] = self.depth_ticks
 610        if self.depth_axis is not None:
 611            image.header['depth_axis'] = self.depth_axis
 612
 613        return image
 614
 615    @classmethod
 616    def fromImage(cls, image):
 617        """
 618        Convert a HyImage with the relevant header information to a Template instance. Useful for IO.
 619        :param image: The HyImage instance containing the template mapping and relevant header metadata
 620                        (lists of hole and box names).
 621        :return:
 622        """
 623        assert 'root' in image.header, 'Error - image must have a "root" key in its header'
 624        assert 'boxes' in image.header, 'Error - image must have a "boxes" key in its header'
 625        assert image.band_count() == 3, 'Error - image must have four bands [holeID, boxID, xidx, yidx]'
 626        root = image.header['root']
 627        boxes = image.header.get_list('boxes')
 628        from_depth = None
 629        to_depth = None
 630        groups = None
 631        group_ticks = None
 632        depths = None
 633        depth_ticks = None
 634        depth_axis = None
 635        if 'from_depth' in image.header:
 636            from_depth = float(image.header['from_depth'])
 637        if 'to_depth' in image.header:
 638            to_depth = float(image.header['to_depth'])
 639        if 'groups' in image.header:
 640            groups = image.header.get_list('groups')
 641        if 'group_ticks' in image.header:
 642            group_ticks = image.header.get_list('group_ticks')
 643        if 'depths' in image.header:
 644            depths = image.header.get_list('depths')
 645        if 'depth_ticks' in image.header:
 646            depth_ticks = image.header.get_list('depth_ticks')
 647        if 'depth_axis' in image.header:
 648            depth_axis = int(image.header['depth_axis'])
 649        return Template([os.path.join( root, b) for b in boxes], image.data, from_depth=from_depth, to_depth=to_depth,
 650                        groups=groups, group_ticks=group_ticks, depths=depths, depth_ticks=depth_ticks, depth_axis=depth_axis)
 651
 652    def rot90(self):
 653        """
 654        Rotate this template by 90 degrees.
 655        """
 656        self.index = np.rot90(self.index, axes=(0, 1))
 657        self.depth_axis = int(1 - self.depth_axis)
 658
 659    def crop(self, min_depth : float, max_depth : float, axis : int , offset : float = 0):
 660        """
 661        Crop this template to the specified depth range.
 662
 663        :param min_depth: The minimum allowable depth.
 664        :param max_depth: The maximum allowable depth.
 665        :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).
 666        :param offset: A depth to subtract from min_depth and max_depth prior to cropping.
 667        :return: A copy of this template, cropped to the specific range, or None if no overlap exists.
 668        """
 669        # check there is overlap
 670        if (self.from_depth is None) or (self.to_depth is None):
 671            assert False, "Error - template has no depth information."
 672
 673        # interpolate depth
 674        zz = np.linspace( self.from_depth, self.to_depth, self.index.shape[axis] ) - offset
 675        mask = (zz >= min_depth) & (zz <= max_depth)
 676        if not mask.any():
 677            return None # no overlap
 678
 679        if axis == 0:
 680            ix = self.index[mask, :, : ]
 681        else:
 682            ix = self.index[:, mask, : ]
 683
 684        # print( min_depth, max_depth, self.from_depth, self.to_depth, np.min(zz[mask]), np.max(zz[mask]) )
 685        return Template( [os.path.join(self.root, b) for b in self.boxes], ix,
 686                         from_depth = np.min(zz[mask]),
 687                         to_depth = np.max(zz[mask]) ) # return cropped template
 688
 689    @classmethod
 690    def stack(cls, templates: list, xstep : int = 1, ystep : int = 1, axis=1):
 691        """
 692        Stack a list of templates along the specified axis (similar to np.vstack and np.hstack).
 693
 694        :param templates: A list of template objects to stack.
 695        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when generating large mosaics.
 696        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when generating large mosaics.
 697        :param axis: The axis to stack along. Set as zero to stack in the x-direction and 1 to stack in the
 698                     y-direction.
 699        """
 700
 701        # resolve all unique paths
 702        paths = set()
 703        for t in templates:
 704            for b in t.boxes:
 705                paths.add(os.path.join(t.root, b))
 706                assert os.path.exists(os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
 707
 708        # get root (lowest common base) and express boxes as relative paths to this
 709        paths = list(paths)
 710
 711        # initialise output
 712        if axis == 0:
 713            out = np.full((sum([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
 714                            max([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
 715        else:
 716            out = np.full((max([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
 717                            sum([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
 718
 719        # loop through templates and stack
 720        p = 0
 721        groups = []
 722        group_ticks = []
 723        for i, t in enumerate(templates):
 724            # copy block of indices across
 725            if axis == 0:
 726                out[p:(p + t.index[::xstep, ::ystep, :].shape[0]),
 727                        0:t.index[::xstep, ::ystep, :].shape[1], :] = t.index[::xstep, ::ystep, :]
 728            else:
 729                out[0:t.index[::xstep, ::ystep, :].shape[0],
 730                p:(p + t.index[::xstep, ::ystep, :].shape[1]), :] = t.index[::xstep, ::ystep, :]
 731
 732            # update box indices
 733            for j, b in enumerate(t.boxes):
 734                mask = np.full((out.shape[0], out.shape[1]), False)
 735                if axis == 0:
 736                    mask[p:(p + t.index[::xstep, ::ystep, :].shape[0]), 0:t.index[::xstep, ::ystep, :].shape[1]] = (t.index[::xstep, ::ystep, 0] == j)
 737                else:
 738                    mask[0:t.index[::xstep, ::ystep, :].shape[0], p:(p + t.index[::xstep, ::ystep, :].shape[1])] = (t.index[::xstep, ::ystep, 0] == j)
 739                out[mask, 0] = paths.index(os.path.join(t.root, b))
 740
 741            # update groups and group ticks (these are useful for subsequent plotting)
 742
 743            if t.groups is not None:
 744                groups += list(t.groups)
 745                if axis == 0:
 746                    group_ticks += list( np.array(t.group_ticks) / xstep + p )
 747                else:
 748                    group_ticks += list(np.array(t.group_ticks) / ystep + p)
 749
 750            # update start point
 751            p += t.index[::xstep, ::ystep, :].shape[axis]
 752
 753        # get span of depths
 754        from_depth = None
 755        to_depth = None
 756        if np.array([t.center_depth is not None for t in templates]).all():
 757            from_depth = np.min([t.from_depth for t in templates])
 758            to_depth = np.max([t.to_depth for t in templates])
 759
 760        # generate depth ticks
 761        # i = 1 - axis # if axis is 1, we tick along axis = 0, if axis is 0, we tick along axis = 1
 762        ticks = [templates[0].index.shape[axis] / 2]
 763        depths = [templates[0].from_depth]
 764        for i, T in enumerate(templates[1:]):
 765            ticks.append(ticks[-1] + templates[i - 1].index.shape[axis] / 2 + T.index.shape[axis] / 2)
 766            depths.append(T.from_depth)
 767
 768        # return new Template instance
 769        return Template(paths, out, from_depth = from_depth, to_depth = to_depth,
 770                        groups=groups, group_ticks=group_ticks,
 771                        depths=depths, depth_ticks=ticks, depth_axis=axis)
 772
 773    def getDepths(self, res: float = 1e-3):
 774        """
 775        Return a 1D array of the depths corresponding to each pixel. Assumes a linear mapping
 776        between the templates from_depth and to_depth.
 777        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
 778                    the start and end of this template. If specified, the start_depth is used as an
 779                    anchor and the depth of pixels below this computed to match the resolution. This is important
 780                    to preserve true scale when core boxes contain gaps.
 781        :return: A 1D array containing depth information for each pixel in this template.
 782        """
 783        axis = self.depth_axis
 784        if res is None:
 785            return np.linspace(self.from_depth, self.to_depth, self.index.shape[axis])
 786        else:
 787            to_depth = self.from_depth + self.index.shape[axis] * res
 788            return np.linspace(self.from_depth, to_depth, self.index.shape[axis])
 789
 790    def getGrid(self, grid=50, minor=True, labels=True, background=True, res : float = 1e-3):
 791        """
 792        Create a depth grid image to accompany HSI mosaics.
 793
 794        :param grid: The grid step, in mm. Default is 50.
 795        :param minor: True if minor ticks (with half the spacing of the major ticks) should be plotted.
 796        :param labels: True if label text describing the meterage should be added.
 797        :param background: True if background outlines of the core blocks should be added.
 798        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
 799                    the start and end of this template. If specified, the start_depth is used as an
 800                    anchor and the depth of pixels below this computed to match the resolution. This is important
 801                    to preserve true scale when core boxes contain gaps.
 802        :return: A HyImage instance containing the grid image.
 803        """
 804        # import this here in case of problematic cv2 install
 805        import cv2
 806
 807        # get background image showing core blocks
 808        img = np.zeros((self.index.shape[0], self.index.shape[1], 3), dtype=np.uint8)
 809        if background:
 810            img[:, :, 1] = img[:, :, 2] = 120 * (self.index[:, :, 2] > 1)
 811
 812        # interpolate depth
 813        zz = self.getDepths(res=res)
 814
 815        # add ticks
 816        ignore = set()
 817        for i, z in enumerate(zz):
 818            zi = int(z * 1000)
 819
 820            # major ticks
 821            if zi not in ignore:
 822                if (zi % int(grid)) == 0:
 823                    # add tick
 824                    img[i, :, :] = 255
 825                    ignore.add(zi)
 826
 827                    # add depth label
 828                    if labels:
 829                        l = "%.2f" % z
 830                        font = cv2.FONT_HERSHEY_SIMPLEX
 831                        img = cv2.putText(img,
 832                                          l, (0, i - 3), font, 0.5, (255, 255, 255), 1, bottomLeftOrigin=False)
 833            # minor ticks
 834            if (zi not in ignore) and minor:
 835                if (int(z * 1000) % int(grid / 2)) == 0:
 836                    img[i, ::3, :] = 255
 837                    ignore.add(zi)
 838
 839        return hylite.HyImage(img)
 840
 841    def __lt__(self, other):
 842        """
 843        Do comparisons based on depth of centerpoint. Used for quickly sorting templates by depth.
 844        """
 845        if self.center_depth is None:
 846            assert False, 'Error - please define depth data for Template to use < functions.'
 847        if isinstance(other, Template):
 848            if other.center_depth is None:
 849                assert False, 'Error - please define depth data for Template to use < functions.'
 850            return self.center_depth < other.center_depth
 851        else:
 852            return other > self.center_depth # use operator from other class
 853
 854    def __gt__(self, other):
 855        """
 856        Do comparisons based on depth of centerpoint. Used for quickly sorting templates by depth.
 857        """
 858        if self.center_depth is None:
 859            assert False, 'Error - please define depth data for Template to use > functions.'
 860        if isinstance(other, Template):
 861            if other.center_depth is None:
 862                assert False, 'Error - please define depth data for Template to use < functions.'
 863            return self.center_depth > other.center_depth
 864        else:
 865            return other < self.center_depth # use operator from other class
 866
 867
 868class Canvas(MutableMapping):
 869    """
 870    A utility class for creating collections of templates and combining them into potentially complex layouts. This
 871    stores groups of templates, which can then be sorted and arranged in various ways (e.g., arranging groups as
 872    columns and cropping to a specific depth range, with individual drillhole offsets).
 873    """
 874
 875    def __init__(self, *args, **kwargs):
 876        self.store = dict()
 877        self.update(dict(*args, **kwargs))  # use the free update to set keys
 878
 879
 880    def hpole(self, from_depth: float = None, to_depth: float = None, scaled=False, groups: list = None,
 881                    res: float = 1e-3, depth_offsets: dict = {}, pad: int = 5 ):
 882        """
 883        Construct a "horizontal pole" type template for visualising and corellating between one or more drillholes.
 884        This has a layout as follows:
 885
 886                          -------------------------------------------------
 887        core (group) 1 - |  [xxxxxxxxxx] [xxxx]         [xxxxxxxxxxxxxxx]  |
 888        core (group) 2 - |  [xxxxx]       [xxxxxxxxxxxx]         [xxxxxx]  |
 889        core (group) 3 - |  [xxxxxxxx] [xxxxxxxxxxxxxxxxxx][xxxxxxxxxxxx]  |
 890                          -------------------------------------------------
 891
 892        :param from_depth: The top depth of the template view area, or None to include all depths.
 893        :param to_depth: The lower depth of the template view area, or None to include all depths.
 894        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 895                (with small gaps representing non-contiguous intervals).
 896        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 897        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 898        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 899                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 900                                in absolute terms.
 901        :param pad: Padding for template stacking. Default is 5.
 902        :return: A single combined Template class in horizontal pole layout.
 903        """
 904
 905        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
 906                                                                           groups, to_depth )
 907
 908        # compute width of output image
 909        w = pad
 910        for g in groups:
 911            w += np.max([T.index.shape[1] for T in S[g.lower()]]) + pad
 912
 913        if scaled:
 914            # compute image dimension in depth direction
 915            nz = int(np.abs(to_depth - from_depth) / res)
 916
 917            # compute corresponding depths
 918            z = np.linspace(from_depth, to_depth, nz)
 919        else:
 920            # determine maximum size of stacked boxes, including gaps, and hence template dimensions
 921            z = []
 922            for g in groups:
 923                nz = 0 # this is the dimensions of our output in pixels
 924                for i, T in enumerate(S[g.lower()]):
 925                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
 926                        nz += 10*pad # add in gaps for non-contiguous cores
 927                        z.append( np.linspace(S[g.lower()][i-1].to_depth, T.from_depth, 10*pad ) )
 928
 929                    nz += T.index.shape[0] + pad
 930                    z.append(np.linspace(T.from_depth, T.to_depth, T.index.shape[0]))
 931                    z.append([T.to_depth for i in range(pad)])
 932            z = np.hstack(z)
 933
 934        assert len(z) == nz, "Error - %d depths and %d pixels. Should be the same." % (len(z), nz) # debugging
 935
 936        # build index
 937        index = np.full((nz, w, 3), -1, dtype=int)
 938        tticks = []  # store tick positions in transverse direction (y-axis for hpole)
 939
 940        # stack templates
 941        _y = pad
 942        for g in groups:
 943            g = g.lower()
 944            for T in S[g]:
 945                # find depth position of center and copy data across
 946                if len(T.boxes) > 1:
 947                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
 948                else:
 949                    six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
 950                    eix = min(T.index.shape[0],
 951                              (index.shape[0] - six))  # end index in template (to allow for possible overflows)
 952
 953                    # copy data!
 954                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
 955                    index[six:(six + T.index.shape[0]), _y:(_y + T.index.shape[1]), 0] = bix  # set box index
 956
 957                    index[six:(six + eix), _y:(_y + T.index.shape[1]), 1:] = T.index[0:eix, :,
 958                                                                             1:]  # copy pixel indices
 959
 960            # step to the right
 961            h = int(np.max([T.index.shape[1] for T in S[g]]) + pad)
 962            tticks.append(int(_y + (h / 2)))
 963            _y += h
 964
 965        out = Template(paths, index, from_depth, to_depth, depth_axis=0,
 966            groups = groups, group_ticks = tticks, depths = z, depth_ticks = np.arange(len(z)))
 967
 968        # done!
 969        return out
 970
 971    def vfence(self, from_depth: float = None, to_depth: float = None, scaled=False,
 972               groups: list = None, depth_offsets : dict = {}, pad: int = 5):
 973        """
 974        Construct a "horizontal fence" type template for visualising drillholes in a condensed way.
 975        This has a layout as follows:
 976
 977            core 1       core 2         core 3
 978    1  - | ======== | | =========| | ========== |
 979         | ======== | | =========| | ========== |
 980    2  - | ======== | | ======   | | =====      |
 981         | ======== |     gap      | ========== |
 982    3  - | ======   | | =========| | ========== |
 983         | ======== | | =========| | ========== |
 984
 985        :param from_depth: The top depth of the template view area, or None to include all depths.
 986        :param to_depth: The lower depth of the template view area, or None to include all depths.
 987        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 988                        (with small gaps representing non-contiguous intervals).
 989        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 990        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 991        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 992                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 993                                in absolute terms.
 994        :param pad: Padding for template stacking. Default is 5.
 995        :return: A single combined Template class in horizontal pole layout.
 996        """
 997
 998        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
 999                                                                           groups, to_depth )
1000
1001        # compute width used for each group and hence image width
1002        # also compute y-scale based maximum template height to depth covered ratio
1003        w = pad # width
1004        ys = np.inf # shared y-axis pixel to depth scale (meters per pixel)
1005        for g in groups:
1006            w = w + np.max([T.index.shape[0] for T in S[g.lower()]]) + pad
1007            for T in S[g.lower()]:
1008                ys = min( ys, abs(T.to_depth - T.from_depth) / T.index.shape[1] )
1009
1010        # compute image dimension in depth direction
1011        if scaled:
1012            # determine depth-scale along y-axis (distance down hole per pixel)
1013            nz = int(np.abs(to_depth - from_depth) / ys)
1014            z = np.linspace(from_depth, to_depth, nz ) # depth per pixel array (kinda...)
1015
1016            # build index
1017            index = np.full((w, nz + pad, 3), -1, dtype=int)
1018
1019        else:
1020            # determine maximum height of stacked boxes, including gaps, and hence template dimensions
1021            heights = []
1022            for g in groups:
1023                h = 0
1024                for i, T in enumerate(S[g.lower()]):
1025                    h += T.index.shape[1] + pad
1026                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
1027                        h += T.index.shape[1] # add in gaps for non-contiguous cores
1028                heights.append(h)
1029
1030            ymax = np.max( heights )
1031
1032            # build index
1033            index = np.full((w, ymax + pad, 3), -1, dtype=int)
1034
1035        tticks = []  # store group tick positions in transverse direction (x-axis)
1036
1037        # stack templates
1038        _x = pad
1039        for g in groups: # loop through groups
1040            zticks = []  # store depth ticks in the down-hole direction (y-axis)
1041            zvals = []  # store corresponding depth values
1042
1043            g = g.lower()
1044            six=0
1045            for i,T in enumerate(S[g]): # loop through templates in this group
1046                # find depth position of center and copy data across
1047                if len(T.boxes) > 1:
1048                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
1049                else:
1050                    if scaled:
1051                        six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
1052                    else:
1053                        # add gaps for non-contiguous templates
1054                        if i > 0 and (abs(S[g][i - 1].to_depth - T.from_depth) > 0.5):
1055                            six += T.index.shape[1]  # add full-box sized gap
1056
1057                    # copy data!
1058                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
1059                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 0 ] = bix
1060                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 1:] = T.index[:, :, 1:]  # copy pixel indices
1061
1062                    # store depth ticks
1063                    zticks.append(six)
1064                    zvals.append(T.from_depth)
1065
1066                    if not scaled:
1067                        six += T.index.shape[1]+pad # increment position
1068
1069            # step to the right
1070            w = int(np.max([T.index.shape[0] for T in S[g]]) + pad) # compute max width of core blocks in this group
1071            tticks.append(int(_x + (w / 2))) # store group ticks
1072            _x += w # step to the right
1073
1074        zticks.append(index.shape[1])
1075        zvals.append(T.to_depth) # add tick at bottom of final template / box
1076
1077        if scaled and len(groups) > 1:
1078            zvals = None
1079            depth_ticks = None # these are not defined if more than one hole is present
1080        else:
1081            # interpolate zvals to get a depth value for each pixel
1082            zvals = np.interp(np.arange(0,index.shape[1]), zticks, zvals )
1083            zticks = np.arange(index.shape[1])
1084        out = Template(paths, index, from_depth, to_depth, depth_axis=1,
1085                       groups = groups, group_ticks = tticks, depths = zvals, depth_ticks = zticks )
1086
1087        # done!
1088        return out
1089
1090
1091    def hfence(self, *args):
1092        """
1093        Construct a "horizontal fence" type template for visualising boreholes in a condensed way. This
1094        is identical to the vfence(...) layout, but rotated 90 degrees such that depth increases to the right.
1095
1096        :param args: All arguments are passed to vfence. The results are then rotated to the horizontal orientation.
1097        :return:
1098        """
1099        out = self.vfence(*args)
1100        out.rot90()
1101        return out
1102
1103    def vpole(self, *args):
1104        """
1105        Construct a "vertical pole" type template for visualising and corellating between one or more drillcores. This
1106        is identical to the hpole(...) layout, but rotated 90 degrees such that cores are vertical and depth increases
1107        downwards.
1108
1109        :param args: All arguments are passed to hpole. The results are then rotated to vertical orientation.
1110        :return:
1111        """
1112        out = self.hpole(*args)
1113        # out.index = np.transpose(out.index, (1, 0, 2)) # rotate to vertical
1114        out.rot90()
1115        return out
1116
1117    def _preprocessTemplates(self, depth_offsets, from_depth, groups, to_depth):
1118        # parse from_depth and to_depth if needed
1119        if from_depth is None:
1120            from_depth = np.min([np.min([t.from_depth for t in v]) for (k, v) in self.store.items()])
1121        if to_depth is None:
1122            to_depth = np.max([np.max([t.to_depth for t in v]) for (k, v) in self.store.items()])
1123        # ensure depth template keys are lower case!
1124        offs = {}
1125        for k, v in depth_offsets.items():
1126            offs[k.lower()] = v
1127        # crop templates to the relevant view area, and discard ones that do not fit
1128        cropped = {}
1129        for k, v in self.store.items():
1130            for T in v:
1131                assert T.from_depth is not None, "Error - depth info must be defined for template to be added."
1132                assert T.to_depth is not None, "Error - depth info must be defined for template to be added."
1133                T = T.crop(from_depth, to_depth, T.depth_axis, offs.get(k, 0))
1134                if T is not None:
1135                    # store
1136                    cropped[k.lower()] = cropped.get(k.lower(), [])
1137                    cropped[k.lower()].append(T)
1138
1139        assert len(cropped) > 0, "Error - no templates are within depth range!"
1140
1141        # sort templates by order in each group
1142        S = {}
1143        for k, v in cropped.items():
1144            S[k.lower()] = sorted(v)
1145        # resolve all unique paths
1146        paths = set()
1147        for k, v in S.items():
1148            for t in v:
1149                for b in t.boxes:
1150                    paths.add(os.path.join(t.root, b))
1151                    assert os.path.exists(
1152                        os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
1153        paths = list(paths)
1154        # get group names to plot if not specified
1155        if groups is None:
1156            groups = list(S.keys())
1157        return S, from_depth, to_depth, groups, paths
1158
1159
1160    def add(self, group, template):
1161        """
1162        Add the specified template to this Canvas collection.
1163
1164        :param group: The name of the group to add this template to.
1165        :param template: The template object.
1166        """
1167        self.__setitem__(group, template)
1168
1169    def __getitem__(self, key):
1170        return self.store[self._keytransform(key)]
1171
1172    def __setitem__(self, key, value):
1173        """
1174        A shorthand way to add items to canvas.
1175        """
1176        assert isinstance(value, Template), "Error - only Templates can be added to a Canvas (for now...)"
1177        v = self.store.get(self._keytransform(key), [])
1178        v.append(value)
1179        self.store[self._keytransform(key)] = v
1180
1181    def __delitem__(self, key):
1182        del self.store[self._keytransform(key)]
1183
1184    def __iter__(self):
1185        return iter(self.store)
1186
1187    def __len__(self):
1188        return len(self.store)
1189
1190    def _keytransform(self, key):
1191        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        data = None
401        for i, box in enumerate(self.boxes):
402
403            # get this box
404            path = os.path.join( self.root, box )
405            if strict:
406                assert os.path.exists(path), "Error - could not load data from %d"
407
408            # load the required data
409            try:
410                try:
411                    if '.' in imageName:
412                        data = io.load(os.path.join(path, imageName) ) # extension is provided
413                    else:
414                        data = io.load(path).get(imageName) # look in box directory
415                except (AttributeError, AssertionError) as e:
416                    try:
417                        if '.' in imageName:
418                            data = io.load(os.path.join(path, 'results.hyc/%s'%imageName))  # extension is provided
419                        else:
420                            data = io.load(path).results.get(imageName) # look in results directory
421                    except:
422                        if strict:
423                            assert False, "Error - could not find data %s in directory %s" % (imageName, path)
424                        else:
425                            continue
426
427                data.decompress()
428            except (AttributeError, AssertionError) as e:
429                if strict:
430                    assert False, "Error - could not find data %s in directory %s" % (imageName, path )
431                else:
432                    continue
433
434            # get index and subsample as needed
435            index = self.index[::xstep, ::ystep]
436            if scale != 1:
437                index = (index * scale).astype(int) # scale coordinates
438                index[...,0] = self.index[::xstep, ::ystep, 0] # don't scale box IDs
439
440            # create output array
441            if bands is not None:
442                data = data.export_bands(bands)
443            if out is None:
444                # initialise output array now we know how many bands we're dealing with
445                out = np.zeros( (index.shape[0], index.shape[1], data.band_count() ), dtype=data.data.dtype )
446
447            # copy data as defined in index array
448            mask = (index[..., 0] == i) & (index[..., -1] != -1 )
449            if mask.any():
450                out[ mask, : ] = data.data[ index[mask, 1], index[mask, 2], : ]
451        if len( data.get_wavelengths() ) != data.band_count():
452            data.set_wavelengths(np.arange(data.band_count()))
453        
454        if data is None:
455            assert False, "Error - could not find any data for %s" % (imageName)
456        
457        # return a hyimage
458        out = hylite.HyImage( out, wav=data.get_wavelengths() )
459        if data.has_band_names():
460            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!
461                out.set_band_names(data.get_band_names())
462
463        if (self.groups is not None) and (len(self.groups) > 0):
464            out.header['groups'] = self.groups
465        if (self.group_ticks is not None) and (len(self.group_ticks) > 0):
466            out.header['group ticks'] = self.group_ticks
467
468        if outline is not None:
469            self.add_outlines(out, color=outline, xx = xstep, yy = ystep )
470        return out
471
472    def quick_plot(self, band=0, rot=False, xx=1, yy=1, interval=5, **kwds):
473        """
474        Quickly plot this template for QAQC.
475        :param band: The band(s) to plot. Default is 0 (plot only box ID).
476        :param rot: True if the template should be rotated 90 degrees before plotting.
477        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
478        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
479        :param interval: Interval between depth ticks to add to plot.
480        :keywords: Keywords are passed to hylite.HyImage.quick_plot( ... ).
481        :return: fig,ax from the matplotlib figure created.
482        """
483        img = self.toImage()
484        img.data = img.data[ ::xx, ::yy, : ]
485        if rot:
486            img.rot90()
487        fig, ax = img.quick_plot(band, tscale=True, **kwds )
488        self.add_ticks(ax, rot=rot, xx=xx, yy=yy, interval=interval)
489        return fig, ax
490
491    def add_ticks(self, ax, interval=5, *, depth_ticks: bool = True, group_ticks: bool = True, rot: bool = False,
492                  xx: int = 1, yy: int = 1, angle: float = 45):
493        """
494        Add depth and or group ticks (as stored in this template) to a matplotlib plot.
495
496        :param ax: The matplotlib axes object to set x- and y- ticks / labels too.
497        :param interval: The interval (in m) between depth ticks. Default is 5 m.
498        :param depth_ticks:  True (default) if depth ticks should be plotted.
499        :param group_ticks: True (default) if group ticks should be plotted.
500        :param rot: If True, the x- and y- axes are flipped (e.g. if image was rotated relative to this template before plotting).
501        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
502        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
503        :param angle: rotation used for the x-ticks. Default is 45 degrees.
504        """
505        a = self.depth_axis
506        if rot:
507            a = int(1 - a)
508            _xx = xx
509            xx = yy
510            yy = _xx
511
512        # get depth and group ticks
513        if depth_ticks:
514            zt, zz = self.get_depth_ticks( interval )
515        if group_ticks:
516            gt,gg = self.get_group_ticks()
517
518        if a == 0:
519            if depth_ticks:
520                ax.set_xticks(zt / xx )
521                ax.set_xticklabels( ["%.1f" % z for z in zz], rotation=angle )
522                #ax.tick_params('x', labelrotation=angle )
523            if group_ticks and self.group_ticks is not None:
524                ax.set_yticks(gt / yy )
525                ax.set_yticklabels( ["%s" % g for g in gg] )
526        else:
527            if depth_ticks:
528                ax.set_yticks(zt / xx )
529                ax.set_yticklabels(["%.1f" % z for z in zz])
530            if group_ticks:
531                ax.set_xticks(gt / yy)
532                ax.set_xticklabels(["%s" % g for g in gg], rotation=angle)
533                #ax.tick_params('x', labelrotation=angle)
534
535    def get_group_ticks(self):
536        """
537        :return: The position and label of group ticks defined in this template, or [], [] if None are defined.
538        """
539        if (self.groups is not None) and (self.group_ticks is not None):
540            return self.group_ticks, self.groups
541        else:
542            return np.array([]), np.array([])
543
544    def get_depth_ticks(self, interval=1.0):
545        """
546        Get evenly spaced depth ticks for pretty plotting.
547        :param interval: The desired spacing between depth ticks
548        :return: Depth tick positions and values. If depth_ticks and depths are not defined, this will return empty lists.
549        """
550        if (self.from_depth is None) or (self.to_depth is None) or (self.depth_ticks is None) or (self.depths is None):
551            return np.array([]), np.array([])
552        else:
553            zz = np.arange(self.from_depth - self.from_depth % interval,
554                           self.to_depth + interval - self.to_depth % interval, interval )[1:]
555
556            tt = np.interp( zz, self.depths, self.depth_ticks)
557
558            return tt, zz
559
560    def add_outlines(self, image, color=0.4, mode='thick', xx: int = 1, yy: int = 1):
561        """
562        Add outlines from this template to the specified image.
563
564        :param image: a HyImage instance to add colours too. Note that this will be updated in-place.
565        :param color: a float or tuple containing the values of the colour to apply.
566        :param mode: outline mode. Options are ‘thick’, ‘inner’, ‘outer’, ‘subpixel’ (see skimage.segmentation.mark_boundaries for details).
567        """
568        dtype = image.data.dtype  # store this for later
569
570        # get mask to outline
571        mask = self.index[::xx, ::yy, 1] != -1
572
573        # sort out colour
574        if isinstance(color, float) or isinstance(color, int):
575            color = tuple([color for i in range(image.band_count())])
576        assert len(color) == image.band_count(), "Error - colour must have same number of bands as image. %d != %d" % (
577        len(color), image.band_count())
578        if (np.array(color) > 1).any():
579            color = np.array(color) / 255.
580
581        # mark boundaries using scikit-image
582        from skimage.segmentation import mark_boundaries
583        image.data = mark_boundaries(image.data, mask, color=color, mode=mode)
584
585        if (dtype == np.uint8):
586            image.data = (image.data * 255)  # scikit image transforms our data to 0 - 1 range...
587
588    def toImage(self):
589        """
590        Convert this Template object to a HyImage instance with the relevant additional hole and box lists stored
591        in the header file. This can be saved and then later converted back to a Template using fromImage( ... ).
592        :return: A HyImage representation of this template.
593        """
594        image = hylite.HyImage(self.index)
595        image.header['root'] = self.root
596        image.header['boxes'] = self.boxes
597        if self.from_depth is not None:
598            image.header['from_depth'] = self.from_depth
599        if self.to_depth is not None:
600            image.header['to_depth'] = self.to_depth
601        if self.center_depth is not None:
602            image.header['center_depth'] = self.center_depth
603        if self.groups is not None:
604            image.header['groups'] = self.groups
605        if self.group_ticks is not None:
606            image.header['group_ticks'] = self.group_ticks
607        if self.depths is not None:
608            image.header['depths'] = self.depths
609        if self.depth_ticks is not None:
610            image.header['depth_ticks'] = self.depth_ticks
611        if self.depth_axis is not None:
612            image.header['depth_axis'] = self.depth_axis
613
614        return image
615
616    @classmethod
617    def fromImage(cls, image):
618        """
619        Convert a HyImage with the relevant header information to a Template instance. Useful for IO.
620        :param image: The HyImage instance containing the template mapping and relevant header metadata
621                        (lists of hole and box names).
622        :return:
623        """
624        assert 'root' in image.header, 'Error - image must have a "root" key in its header'
625        assert 'boxes' in image.header, 'Error - image must have a "boxes" key in its header'
626        assert image.band_count() == 3, 'Error - image must have four bands [holeID, boxID, xidx, yidx]'
627        root = image.header['root']
628        boxes = image.header.get_list('boxes')
629        from_depth = None
630        to_depth = None
631        groups = None
632        group_ticks = None
633        depths = None
634        depth_ticks = None
635        depth_axis = None
636        if 'from_depth' in image.header:
637            from_depth = float(image.header['from_depth'])
638        if 'to_depth' in image.header:
639            to_depth = float(image.header['to_depth'])
640        if 'groups' in image.header:
641            groups = image.header.get_list('groups')
642        if 'group_ticks' in image.header:
643            group_ticks = image.header.get_list('group_ticks')
644        if 'depths' in image.header:
645            depths = image.header.get_list('depths')
646        if 'depth_ticks' in image.header:
647            depth_ticks = image.header.get_list('depth_ticks')
648        if 'depth_axis' in image.header:
649            depth_axis = int(image.header['depth_axis'])
650        return Template([os.path.join( root, b) for b in boxes], image.data, from_depth=from_depth, to_depth=to_depth,
651                        groups=groups, group_ticks=group_ticks, depths=depths, depth_ticks=depth_ticks, depth_axis=depth_axis)
652
653    def rot90(self):
654        """
655        Rotate this template by 90 degrees.
656        """
657        self.index = np.rot90(self.index, axes=(0, 1))
658        self.depth_axis = int(1 - self.depth_axis)
659
660    def crop(self, min_depth : float, max_depth : float, axis : int , offset : float = 0):
661        """
662        Crop this template to the specified depth range.
663
664        :param min_depth: The minimum allowable depth.
665        :param max_depth: The maximum allowable depth.
666        :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).
667        :param offset: A depth to subtract from min_depth and max_depth prior to cropping.
668        :return: A copy of this template, cropped to the specific range, or None if no overlap exists.
669        """
670        # check there is overlap
671        if (self.from_depth is None) or (self.to_depth is None):
672            assert False, "Error - template has no depth information."
673
674        # interpolate depth
675        zz = np.linspace( self.from_depth, self.to_depth, self.index.shape[axis] ) - offset
676        mask = (zz >= min_depth) & (zz <= max_depth)
677        if not mask.any():
678            return None # no overlap
679
680        if axis == 0:
681            ix = self.index[mask, :, : ]
682        else:
683            ix = self.index[:, mask, : ]
684
685        # print( min_depth, max_depth, self.from_depth, self.to_depth, np.min(zz[mask]), np.max(zz[mask]) )
686        return Template( [os.path.join(self.root, b) for b in self.boxes], ix,
687                         from_depth = np.min(zz[mask]),
688                         to_depth = np.max(zz[mask]) ) # return cropped template
689
690    @classmethod
691    def stack(cls, templates: list, xstep : int = 1, ystep : int = 1, axis=1):
692        """
693        Stack a list of templates along the specified axis (similar to np.vstack and np.hstack).
694
695        :param templates: A list of template objects to stack.
696        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when generating large mosaics.
697        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when generating large mosaics.
698        :param axis: The axis to stack along. Set as zero to stack in the x-direction and 1 to stack in the
699                     y-direction.
700        """
701
702        # resolve all unique paths
703        paths = set()
704        for t in templates:
705            for b in t.boxes:
706                paths.add(os.path.join(t.root, b))
707                assert os.path.exists(os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
708
709        # get root (lowest common base) and express boxes as relative paths to this
710        paths = list(paths)
711
712        # initialise output
713        if axis == 0:
714            out = np.full((sum([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
715                            max([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
716        else:
717            out = np.full((max([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
718                            sum([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
719
720        # loop through templates and stack
721        p = 0
722        groups = []
723        group_ticks = []
724        for i, t in enumerate(templates):
725            # copy block of indices across
726            if axis == 0:
727                out[p:(p + t.index[::xstep, ::ystep, :].shape[0]),
728                        0:t.index[::xstep, ::ystep, :].shape[1], :] = t.index[::xstep, ::ystep, :]
729            else:
730                out[0:t.index[::xstep, ::ystep, :].shape[0],
731                p:(p + t.index[::xstep, ::ystep, :].shape[1]), :] = t.index[::xstep, ::ystep, :]
732
733            # update box indices
734            for j, b in enumerate(t.boxes):
735                mask = np.full((out.shape[0], out.shape[1]), False)
736                if axis == 0:
737                    mask[p:(p + t.index[::xstep, ::ystep, :].shape[0]), 0:t.index[::xstep, ::ystep, :].shape[1]] = (t.index[::xstep, ::ystep, 0] == j)
738                else:
739                    mask[0:t.index[::xstep, ::ystep, :].shape[0], p:(p + t.index[::xstep, ::ystep, :].shape[1])] = (t.index[::xstep, ::ystep, 0] == j)
740                out[mask, 0] = paths.index(os.path.join(t.root, b))
741
742            # update groups and group ticks (these are useful for subsequent plotting)
743
744            if t.groups is not None:
745                groups += list(t.groups)
746                if axis == 0:
747                    group_ticks += list( np.array(t.group_ticks) / xstep + p )
748                else:
749                    group_ticks += list(np.array(t.group_ticks) / ystep + p)
750
751            # update start point
752            p += t.index[::xstep, ::ystep, :].shape[axis]
753
754        # get span of depths
755        from_depth = None
756        to_depth = None
757        if np.array([t.center_depth is not None for t in templates]).all():
758            from_depth = np.min([t.from_depth for t in templates])
759            to_depth = np.max([t.to_depth for t in templates])
760
761        # generate depth ticks
762        # i = 1 - axis # if axis is 1, we tick along axis = 0, if axis is 0, we tick along axis = 1
763        ticks = [templates[0].index.shape[axis] / 2]
764        depths = [templates[0].from_depth]
765        for i, T in enumerate(templates[1:]):
766            ticks.append(ticks[-1] + templates[i - 1].index.shape[axis] / 2 + T.index.shape[axis] / 2)
767            depths.append(T.from_depth)
768
769        # return new Template instance
770        return Template(paths, out, from_depth = from_depth, to_depth = to_depth,
771                        groups=groups, group_ticks=group_ticks,
772                        depths=depths, depth_ticks=ticks, depth_axis=axis)
773
774    def getDepths(self, res: float = 1e-3):
775        """
776        Return a 1D array of the depths corresponding to each pixel. Assumes a linear mapping
777        between the templates from_depth and to_depth.
778        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
779                    the start and end of this template. If specified, the start_depth is used as an
780                    anchor and the depth of pixels below this computed to match the resolution. This is important
781                    to preserve true scale when core boxes contain gaps.
782        :return: A 1D array containing depth information for each pixel in this template.
783        """
784        axis = self.depth_axis
785        if res is None:
786            return np.linspace(self.from_depth, self.to_depth, self.index.shape[axis])
787        else:
788            to_depth = self.from_depth + self.index.shape[axis] * res
789            return np.linspace(self.from_depth, to_depth, self.index.shape[axis])
790
791    def getGrid(self, grid=50, minor=True, labels=True, background=True, res : float = 1e-3):
792        """
793        Create a depth grid image to accompany HSI mosaics.
794
795        :param grid: The grid step, in mm. Default is 50.
796        :param minor: True if minor ticks (with half the spacing of the major ticks) should be plotted.
797        :param labels: True if label text describing the meterage should be added.
798        :param background: True if background outlines of the core blocks should be added.
799        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
800                    the start and end of this template. If specified, the start_depth is used as an
801                    anchor and the depth of pixels below this computed to match the resolution. This is important
802                    to preserve true scale when core boxes contain gaps.
803        :return: A HyImage instance containing the grid image.
804        """
805        # import this here in case of problematic cv2 install
806        import cv2
807
808        # get background image showing core blocks
809        img = np.zeros((self.index.shape[0], self.index.shape[1], 3), dtype=np.uint8)
810        if background:
811            img[:, :, 1] = img[:, :, 2] = 120 * (self.index[:, :, 2] > 1)
812
813        # interpolate depth
814        zz = self.getDepths(res=res)
815
816        # add ticks
817        ignore = set()
818        for i, z in enumerate(zz):
819            zi = int(z * 1000)
820
821            # major ticks
822            if zi not in ignore:
823                if (zi % int(grid)) == 0:
824                    # add tick
825                    img[i, :, :] = 255
826                    ignore.add(zi)
827
828                    # add depth label
829                    if labels:
830                        l = "%.2f" % z
831                        font = cv2.FONT_HERSHEY_SIMPLEX
832                        img = cv2.putText(img,
833                                          l, (0, i - 3), font, 0.5, (255, 255, 255), 1, bottomLeftOrigin=False)
834            # minor ticks
835            if (zi not in ignore) and minor:
836                if (int(z * 1000) % int(grid / 2)) == 0:
837                    img[i, ::3, :] = 255
838                    ignore.add(zi)
839
840        return hylite.HyImage(img)
841
842    def __lt__(self, other):
843        """
844        Do comparisons based on depth of centerpoint. Used for quickly sorting templates by depth.
845        """
846        if self.center_depth is None:
847            assert False, 'Error - please define depth data for Template to use < functions.'
848        if isinstance(other, Template):
849            if other.center_depth is None:
850                assert False, 'Error - please define depth data for Template to use < functions.'
851            return self.center_depth < other.center_depth
852        else:
853            return other > self.center_depth # use operator from other class
854
855    def __gt__(self, other):
856        """
857        Do comparisons based on depth of centerpoint. Used for quickly sorting templates by depth.
858        """
859        if self.center_depth is None:
860            assert False, 'Error - please define depth data for Template to use > functions.'
861        if isinstance(other, Template):
862            if other.center_depth is None:
863                assert False, 'Error - please define depth data for Template to use < functions.'
864            return self.center_depth > other.center_depth
865        else:
866            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        data = None
401        for i, box in enumerate(self.boxes):
402
403            # get this box
404            path = os.path.join( self.root, box )
405            if strict:
406                assert os.path.exists(path), "Error - could not load data from %d"
407
408            # load the required data
409            try:
410                try:
411                    if '.' in imageName:
412                        data = io.load(os.path.join(path, imageName) ) # extension is provided
413                    else:
414                        data = io.load(path).get(imageName) # look in box directory
415                except (AttributeError, AssertionError) as e:
416                    try:
417                        if '.' in imageName:
418                            data = io.load(os.path.join(path, 'results.hyc/%s'%imageName))  # extension is provided
419                        else:
420                            data = io.load(path).results.get(imageName) # look in results directory
421                    except:
422                        if strict:
423                            assert False, "Error - could not find data %s in directory %s" % (imageName, path)
424                        else:
425                            continue
426
427                data.decompress()
428            except (AttributeError, AssertionError) as e:
429                if strict:
430                    assert False, "Error - could not find data %s in directory %s" % (imageName, path )
431                else:
432                    continue
433
434            # get index and subsample as needed
435            index = self.index[::xstep, ::ystep]
436            if scale != 1:
437                index = (index * scale).astype(int) # scale coordinates
438                index[...,0] = self.index[::xstep, ::ystep, 0] # don't scale box IDs
439
440            # create output array
441            if bands is not None:
442                data = data.export_bands(bands)
443            if out is None:
444                # initialise output array now we know how many bands we're dealing with
445                out = np.zeros( (index.shape[0], index.shape[1], data.band_count() ), dtype=data.data.dtype )
446
447            # copy data as defined in index array
448            mask = (index[..., 0] == i) & (index[..., -1] != -1 )
449            if mask.any():
450                out[ mask, : ] = data.data[ index[mask, 1], index[mask, 2], : ]
451        if len( data.get_wavelengths() ) != data.band_count():
452            data.set_wavelengths(np.arange(data.band_count()))
453        
454        if data is None:
455            assert False, "Error - could not find any data for %s" % (imageName)
456        
457        # return a hyimage
458        out = hylite.HyImage( out, wav=data.get_wavelengths() )
459        if data.has_band_names():
460            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!
461                out.set_band_names(data.get_band_names())
462
463        if (self.groups is not None) and (len(self.groups) > 0):
464            out.header['groups'] = self.groups
465        if (self.group_ticks is not None) and (len(self.group_ticks) > 0):
466            out.header['group ticks'] = self.group_ticks
467
468        if outline is not None:
469            self.add_outlines(out, color=outline, xx = xstep, yy = ystep )
470        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):
472    def quick_plot(self, band=0, rot=False, xx=1, yy=1, interval=5, **kwds):
473        """
474        Quickly plot this template for QAQC.
475        :param band: The band(s) to plot. Default is 0 (plot only box ID).
476        :param rot: True if the template should be rotated 90 degrees before plotting.
477        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
478        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
479        :param interval: Interval between depth ticks to add to plot.
480        :keywords: Keywords are passed to hylite.HyImage.quick_plot( ... ).
481        :return: fig,ax from the matplotlib figure created.
482        """
483        img = self.toImage()
484        img.data = img.data[ ::xx, ::yy, : ]
485        if rot:
486            img.rot90()
487        fig, ax = img.quick_plot(band, tscale=True, **kwds )
488        self.add_ticks(ax, rot=rot, xx=xx, yy=yy, interval=interval)
489        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):
491    def add_ticks(self, ax, interval=5, *, depth_ticks: bool = True, group_ticks: bool = True, rot: bool = False,
492                  xx: int = 1, yy: int = 1, angle: float = 45):
493        """
494        Add depth and or group ticks (as stored in this template) to a matplotlib plot.
495
496        :param ax: The matplotlib axes object to set x- and y- ticks / labels too.
497        :param interval: The interval (in m) between depth ticks. Default is 5 m.
498        :param depth_ticks:  True (default) if depth ticks should be plotted.
499        :param group_ticks: True (default) if group ticks should be plotted.
500        :param rot: If True, the x- and y- axes are flipped (e.g. if image was rotated relative to this template before plotting).
501        :param xx: subsampling in the x-direction (useful for large templates!). Default is 1 (no subsampling).
502        :param yy: subsampling in the y-direction (useful for large templates!). Default is 1 (no subsampling).
503        :param angle: rotation used for the x-ticks. Default is 45 degrees.
504        """
505        a = self.depth_axis
506        if rot:
507            a = int(1 - a)
508            _xx = xx
509            xx = yy
510            yy = _xx
511
512        # get depth and group ticks
513        if depth_ticks:
514            zt, zz = self.get_depth_ticks( interval )
515        if group_ticks:
516            gt,gg = self.get_group_ticks()
517
518        if a == 0:
519            if depth_ticks:
520                ax.set_xticks(zt / xx )
521                ax.set_xticklabels( ["%.1f" % z for z in zz], rotation=angle )
522                #ax.tick_params('x', labelrotation=angle )
523            if group_ticks and self.group_ticks is not None:
524                ax.set_yticks(gt / yy )
525                ax.set_yticklabels( ["%s" % g for g in gg] )
526        else:
527            if depth_ticks:
528                ax.set_yticks(zt / xx )
529                ax.set_yticklabels(["%.1f" % z for z in zz])
530            if group_ticks:
531                ax.set_xticks(gt / yy)
532                ax.set_xticklabels(["%s" % g for g in gg], rotation=angle)
533                #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):
535    def get_group_ticks(self):
536        """
537        :return: The position and label of group ticks defined in this template, or [], [] if None are defined.
538        """
539        if (self.groups is not None) and (self.group_ticks is not None):
540            return self.group_ticks, self.groups
541        else:
542            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):
544    def get_depth_ticks(self, interval=1.0):
545        """
546        Get evenly spaced depth ticks for pretty plotting.
547        :param interval: The desired spacing between depth ticks
548        :return: Depth tick positions and values. If depth_ticks and depths are not defined, this will return empty lists.
549        """
550        if (self.from_depth is None) or (self.to_depth is None) or (self.depth_ticks is None) or (self.depths is None):
551            return np.array([]), np.array([])
552        else:
553            zz = np.arange(self.from_depth - self.from_depth % interval,
554                           self.to_depth + interval - self.to_depth % interval, interval )[1:]
555
556            tt = np.interp( zz, self.depths, self.depth_ticks)
557
558            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):
560    def add_outlines(self, image, color=0.4, mode='thick', xx: int = 1, yy: int = 1):
561        """
562        Add outlines from this template to the specified image.
563
564        :param image: a HyImage instance to add colours too. Note that this will be updated in-place.
565        :param color: a float or tuple containing the values of the colour to apply.
566        :param mode: outline mode. Options are ‘thick’, ‘inner’, ‘outer’, ‘subpixel’ (see skimage.segmentation.mark_boundaries for details).
567        """
568        dtype = image.data.dtype  # store this for later
569
570        # get mask to outline
571        mask = self.index[::xx, ::yy, 1] != -1
572
573        # sort out colour
574        if isinstance(color, float) or isinstance(color, int):
575            color = tuple([color for i in range(image.band_count())])
576        assert len(color) == image.band_count(), "Error - colour must have same number of bands as image. %d != %d" % (
577        len(color), image.band_count())
578        if (np.array(color) > 1).any():
579            color = np.array(color) / 255.
580
581        # mark boundaries using scikit-image
582        from skimage.segmentation import mark_boundaries
583        image.data = mark_boundaries(image.data, mask, color=color, mode=mode)
584
585        if (dtype == np.uint8):
586            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):
588    def toImage(self):
589        """
590        Convert this Template object to a HyImage instance with the relevant additional hole and box lists stored
591        in the header file. This can be saved and then later converted back to a Template using fromImage( ... ).
592        :return: A HyImage representation of this template.
593        """
594        image = hylite.HyImage(self.index)
595        image.header['root'] = self.root
596        image.header['boxes'] = self.boxes
597        if self.from_depth is not None:
598            image.header['from_depth'] = self.from_depth
599        if self.to_depth is not None:
600            image.header['to_depth'] = self.to_depth
601        if self.center_depth is not None:
602            image.header['center_depth'] = self.center_depth
603        if self.groups is not None:
604            image.header['groups'] = self.groups
605        if self.group_ticks is not None:
606            image.header['group_ticks'] = self.group_ticks
607        if self.depths is not None:
608            image.header['depths'] = self.depths
609        if self.depth_ticks is not None:
610            image.header['depth_ticks'] = self.depth_ticks
611        if self.depth_axis is not None:
612            image.header['depth_axis'] = self.depth_axis
613
614        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):
616    @classmethod
617    def fromImage(cls, image):
618        """
619        Convert a HyImage with the relevant header information to a Template instance. Useful for IO.
620        :param image: The HyImage instance containing the template mapping and relevant header metadata
621                        (lists of hole and box names).
622        :return:
623        """
624        assert 'root' in image.header, 'Error - image must have a "root" key in its header'
625        assert 'boxes' in image.header, 'Error - image must have a "boxes" key in its header'
626        assert image.band_count() == 3, 'Error - image must have four bands [holeID, boxID, xidx, yidx]'
627        root = image.header['root']
628        boxes = image.header.get_list('boxes')
629        from_depth = None
630        to_depth = None
631        groups = None
632        group_ticks = None
633        depths = None
634        depth_ticks = None
635        depth_axis = None
636        if 'from_depth' in image.header:
637            from_depth = float(image.header['from_depth'])
638        if 'to_depth' in image.header:
639            to_depth = float(image.header['to_depth'])
640        if 'groups' in image.header:
641            groups = image.header.get_list('groups')
642        if 'group_ticks' in image.header:
643            group_ticks = image.header.get_list('group_ticks')
644        if 'depths' in image.header:
645            depths = image.header.get_list('depths')
646        if 'depth_ticks' in image.header:
647            depth_ticks = image.header.get_list('depth_ticks')
648        if 'depth_axis' in image.header:
649            depth_axis = int(image.header['depth_axis'])
650        return Template([os.path.join( root, b) for b in boxes], image.data, from_depth=from_depth, to_depth=to_depth,
651                        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):
653    def rot90(self):
654        """
655        Rotate this template by 90 degrees.
656        """
657        self.index = np.rot90(self.index, axes=(0, 1))
658        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):
660    def crop(self, min_depth : float, max_depth : float, axis : int , offset : float = 0):
661        """
662        Crop this template to the specified depth range.
663
664        :param min_depth: The minimum allowable depth.
665        :param max_depth: The maximum allowable depth.
666        :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).
667        :param offset: A depth to subtract from min_depth and max_depth prior to cropping.
668        :return: A copy of this template, cropped to the specific range, or None if no overlap exists.
669        """
670        # check there is overlap
671        if (self.from_depth is None) or (self.to_depth is None):
672            assert False, "Error - template has no depth information."
673
674        # interpolate depth
675        zz = np.linspace( self.from_depth, self.to_depth, self.index.shape[axis] ) - offset
676        mask = (zz >= min_depth) & (zz <= max_depth)
677        if not mask.any():
678            return None # no overlap
679
680        if axis == 0:
681            ix = self.index[mask, :, : ]
682        else:
683            ix = self.index[:, mask, : ]
684
685        # print( min_depth, max_depth, self.from_depth, self.to_depth, np.min(zz[mask]), np.max(zz[mask]) )
686        return Template( [os.path.join(self.root, b) for b in self.boxes], ix,
687                         from_depth = np.min(zz[mask]),
688                         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):
690    @classmethod
691    def stack(cls, templates: list, xstep : int = 1, ystep : int = 1, axis=1):
692        """
693        Stack a list of templates along the specified axis (similar to np.vstack and np.hstack).
694
695        :param templates: A list of template objects to stack.
696        :param xstep: Step to use in the x-direction. Useful for skipping pixels in the source image when generating large mosaics.
697        :param ystep: Step to use in the y-direction. Useful for skipping pixels in the source image when generating large mosaics.
698        :param axis: The axis to stack along. Set as zero to stack in the x-direction and 1 to stack in the
699                     y-direction.
700        """
701
702        # resolve all unique paths
703        paths = set()
704        for t in templates:
705            for b in t.boxes:
706                paths.add(os.path.join(t.root, b))
707                assert os.path.exists(os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
708
709        # get root (lowest common base) and express boxes as relative paths to this
710        paths = list(paths)
711
712        # initialise output
713        if axis == 0:
714            out = np.full((sum([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
715                            max([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
716        else:
717            out = np.full((max([t.index[::xstep, ::ystep, :].shape[0] for t in templates]),
718                            sum([t.index[::xstep, ::ystep, :].shape[1] for t in templates]), 3), -1)
719
720        # loop through templates and stack
721        p = 0
722        groups = []
723        group_ticks = []
724        for i, t in enumerate(templates):
725            # copy block of indices across
726            if axis == 0:
727                out[p:(p + t.index[::xstep, ::ystep, :].shape[0]),
728                        0:t.index[::xstep, ::ystep, :].shape[1], :] = t.index[::xstep, ::ystep, :]
729            else:
730                out[0:t.index[::xstep, ::ystep, :].shape[0],
731                p:(p + t.index[::xstep, ::ystep, :].shape[1]), :] = t.index[::xstep, ::ystep, :]
732
733            # update box indices
734            for j, b in enumerate(t.boxes):
735                mask = np.full((out.shape[0], out.shape[1]), False)
736                if axis == 0:
737                    mask[p:(p + t.index[::xstep, ::ystep, :].shape[0]), 0:t.index[::xstep, ::ystep, :].shape[1]] = (t.index[::xstep, ::ystep, 0] == j)
738                else:
739                    mask[0:t.index[::xstep, ::ystep, :].shape[0], p:(p + t.index[::xstep, ::ystep, :].shape[1])] = (t.index[::xstep, ::ystep, 0] == j)
740                out[mask, 0] = paths.index(os.path.join(t.root, b))
741
742            # update groups and group ticks (these are useful for subsequent plotting)
743
744            if t.groups is not None:
745                groups += list(t.groups)
746                if axis == 0:
747                    group_ticks += list( np.array(t.group_ticks) / xstep + p )
748                else:
749                    group_ticks += list(np.array(t.group_ticks) / ystep + p)
750
751            # update start point
752            p += t.index[::xstep, ::ystep, :].shape[axis]
753
754        # get span of depths
755        from_depth = None
756        to_depth = None
757        if np.array([t.center_depth is not None for t in templates]).all():
758            from_depth = np.min([t.from_depth for t in templates])
759            to_depth = np.max([t.to_depth for t in templates])
760
761        # generate depth ticks
762        # i = 1 - axis # if axis is 1, we tick along axis = 0, if axis is 0, we tick along axis = 1
763        ticks = [templates[0].index.shape[axis] / 2]
764        depths = [templates[0].from_depth]
765        for i, T in enumerate(templates[1:]):
766            ticks.append(ticks[-1] + templates[i - 1].index.shape[axis] / 2 + T.index.shape[axis] / 2)
767            depths.append(T.from_depth)
768
769        # return new Template instance
770        return Template(paths, out, from_depth = from_depth, to_depth = to_depth,
771                        groups=groups, group_ticks=group_ticks,
772                        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):
774    def getDepths(self, res: float = 1e-3):
775        """
776        Return a 1D array of the depths corresponding to each pixel. Assumes a linear mapping
777        between the templates from_depth and to_depth.
778        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
779                    the start and end of this template. If specified, the start_depth is used as an
780                    anchor and the depth of pixels below this computed to match the resolution. This is important
781                    to preserve true scale when core boxes contain gaps.
782        :return: A 1D array containing depth information for each pixel in this template.
783        """
784        axis = self.depth_axis
785        if res is None:
786            return np.linspace(self.from_depth, self.to_depth, self.index.shape[axis])
787        else:
788            to_depth = self.from_depth + self.index.shape[axis] * res
789            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):
791    def getGrid(self, grid=50, minor=True, labels=True, background=True, res : float = 1e-3):
792        """
793        Create a depth grid image to accompany HSI mosaics.
794
795        :param grid: The grid step, in mm. Default is 50.
796        :param minor: True if minor ticks (with half the spacing of the major ticks) should be plotted.
797        :param labels: True if label text describing the meterage should be added.
798        :param background: True if background outlines of the core blocks should be added.
799        :param res: The known resolution of the image data. If None, depths are simply stretched evenly between
800                    the start and end of this template. If specified, the start_depth is used as an
801                    anchor and the depth of pixels below this computed to match the resolution. This is important
802                    to preserve true scale when core boxes contain gaps.
803        :return: A HyImage instance containing the grid image.
804        """
805        # import this here in case of problematic cv2 install
806        import cv2
807
808        # get background image showing core blocks
809        img = np.zeros((self.index.shape[0], self.index.shape[1], 3), dtype=np.uint8)
810        if background:
811            img[:, :, 1] = img[:, :, 2] = 120 * (self.index[:, :, 2] > 1)
812
813        # interpolate depth
814        zz = self.getDepths(res=res)
815
816        # add ticks
817        ignore = set()
818        for i, z in enumerate(zz):
819            zi = int(z * 1000)
820
821            # major ticks
822            if zi not in ignore:
823                if (zi % int(grid)) == 0:
824                    # add tick
825                    img[i, :, :] = 255
826                    ignore.add(zi)
827
828                    # add depth label
829                    if labels:
830                        l = "%.2f" % z
831                        font = cv2.FONT_HERSHEY_SIMPLEX
832                        img = cv2.putText(img,
833                                          l, (0, i - 3), font, 0.5, (255, 255, 255), 1, bottomLeftOrigin=False)
834            # minor ticks
835            if (zi not in ignore) and minor:
836                if (int(z * 1000) % int(grid / 2)) == 0:
837                    img[i, ::3, :] = 255
838                    ignore.add(zi)
839
840        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):
 869class Canvas(MutableMapping):
 870    """
 871    A utility class for creating collections of templates and combining them into potentially complex layouts. This
 872    stores groups of templates, which can then be sorted and arranged in various ways (e.g., arranging groups as
 873    columns and cropping to a specific depth range, with individual drillhole offsets).
 874    """
 875
 876    def __init__(self, *args, **kwargs):
 877        self.store = dict()
 878        self.update(dict(*args, **kwargs))  # use the free update to set keys
 879
 880
 881    def hpole(self, from_depth: float = None, to_depth: float = None, scaled=False, groups: list = None,
 882                    res: float = 1e-3, depth_offsets: dict = {}, pad: int = 5 ):
 883        """
 884        Construct a "horizontal pole" type template for visualising and corellating between one or more drillholes.
 885        This has a layout as follows:
 886
 887                          -------------------------------------------------
 888        core (group) 1 - |  [xxxxxxxxxx] [xxxx]         [xxxxxxxxxxxxxxx]  |
 889        core (group) 2 - |  [xxxxx]       [xxxxxxxxxxxx]         [xxxxxx]  |
 890        core (group) 3 - |  [xxxxxxxx] [xxxxxxxxxxxxxxxxxx][xxxxxxxxxxxx]  |
 891                          -------------------------------------------------
 892
 893        :param from_depth: The top depth of the template view area, or None to include all depths.
 894        :param to_depth: The lower depth of the template view area, or None to include all depths.
 895        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 896                (with small gaps representing non-contiguous intervals).
 897        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 898        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 899        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 900                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 901                                in absolute terms.
 902        :param pad: Padding for template stacking. Default is 5.
 903        :return: A single combined Template class in horizontal pole layout.
 904        """
 905
 906        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
 907                                                                           groups, to_depth )
 908
 909        # compute width of output image
 910        w = pad
 911        for g in groups:
 912            w += np.max([T.index.shape[1] for T in S[g.lower()]]) + pad
 913
 914        if scaled:
 915            # compute image dimension in depth direction
 916            nz = int(np.abs(to_depth - from_depth) / res)
 917
 918            # compute corresponding depths
 919            z = np.linspace(from_depth, to_depth, nz)
 920        else:
 921            # determine maximum size of stacked boxes, including gaps, and hence template dimensions
 922            z = []
 923            for g in groups:
 924                nz = 0 # this is the dimensions of our output in pixels
 925                for i, T in enumerate(S[g.lower()]):
 926                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
 927                        nz += 10*pad # add in gaps for non-contiguous cores
 928                        z.append( np.linspace(S[g.lower()][i-1].to_depth, T.from_depth, 10*pad ) )
 929
 930                    nz += T.index.shape[0] + pad
 931                    z.append(np.linspace(T.from_depth, T.to_depth, T.index.shape[0]))
 932                    z.append([T.to_depth for i in range(pad)])
 933            z = np.hstack(z)
 934
 935        assert len(z) == nz, "Error - %d depths and %d pixels. Should be the same." % (len(z), nz) # debugging
 936
 937        # build index
 938        index = np.full((nz, w, 3), -1, dtype=int)
 939        tticks = []  # store tick positions in transverse direction (y-axis for hpole)
 940
 941        # stack templates
 942        _y = pad
 943        for g in groups:
 944            g = g.lower()
 945            for T in S[g]:
 946                # find depth position of center and copy data across
 947                if len(T.boxes) > 1:
 948                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
 949                else:
 950                    six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
 951                    eix = min(T.index.shape[0],
 952                              (index.shape[0] - six))  # end index in template (to allow for possible overflows)
 953
 954                    # copy data!
 955                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
 956                    index[six:(six + T.index.shape[0]), _y:(_y + T.index.shape[1]), 0] = bix  # set box index
 957
 958                    index[six:(six + eix), _y:(_y + T.index.shape[1]), 1:] = T.index[0:eix, :,
 959                                                                             1:]  # copy pixel indices
 960
 961            # step to the right
 962            h = int(np.max([T.index.shape[1] for T in S[g]]) + pad)
 963            tticks.append(int(_y + (h / 2)))
 964            _y += h
 965
 966        out = Template(paths, index, from_depth, to_depth, depth_axis=0,
 967            groups = groups, group_ticks = tticks, depths = z, depth_ticks = np.arange(len(z)))
 968
 969        # done!
 970        return out
 971
 972    def vfence(self, from_depth: float = None, to_depth: float = None, scaled=False,
 973               groups: list = None, depth_offsets : dict = {}, pad: int = 5):
 974        """
 975        Construct a "horizontal fence" type template for visualising drillholes in a condensed way.
 976        This has a layout as follows:
 977
 978            core 1       core 2         core 3
 979    1  - | ======== | | =========| | ========== |
 980         | ======== | | =========| | ========== |
 981    2  - | ======== | | ======   | | =====      |
 982         | ======== |     gap      | ========== |
 983    3  - | ======   | | =========| | ========== |
 984         | ======== | | =========| | ========== |
 985
 986        :param from_depth: The top depth of the template view area, or None to include all depths.
 987        :param to_depth: The lower depth of the template view area, or None to include all depths.
 988        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 989                        (with small gaps representing non-contiguous intervals).
 990        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 991        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 992        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 993                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 994                                in absolute terms.
 995        :param pad: Padding for template stacking. Default is 5.
 996        :return: A single combined Template class in horizontal pole layout.
 997        """
 998
 999        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
1000                                                                           groups, to_depth )
1001
1002        # compute width used for each group and hence image width
1003        # also compute y-scale based maximum template height to depth covered ratio
1004        w = pad # width
1005        ys = np.inf # shared y-axis pixel to depth scale (meters per pixel)
1006        for g in groups:
1007            w = w + np.max([T.index.shape[0] for T in S[g.lower()]]) + pad
1008            for T in S[g.lower()]:
1009                ys = min( ys, abs(T.to_depth - T.from_depth) / T.index.shape[1] )
1010
1011        # compute image dimension in depth direction
1012        if scaled:
1013            # determine depth-scale along y-axis (distance down hole per pixel)
1014            nz = int(np.abs(to_depth - from_depth) / ys)
1015            z = np.linspace(from_depth, to_depth, nz ) # depth per pixel array (kinda...)
1016
1017            # build index
1018            index = np.full((w, nz + pad, 3), -1, dtype=int)
1019
1020        else:
1021            # determine maximum height of stacked boxes, including gaps, and hence template dimensions
1022            heights = []
1023            for g in groups:
1024                h = 0
1025                for i, T in enumerate(S[g.lower()]):
1026                    h += T.index.shape[1] + pad
1027                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
1028                        h += T.index.shape[1] # add in gaps for non-contiguous cores
1029                heights.append(h)
1030
1031            ymax = np.max( heights )
1032
1033            # build index
1034            index = np.full((w, ymax + pad, 3), -1, dtype=int)
1035
1036        tticks = []  # store group tick positions in transverse direction (x-axis)
1037
1038        # stack templates
1039        _x = pad
1040        for g in groups: # loop through groups
1041            zticks = []  # store depth ticks in the down-hole direction (y-axis)
1042            zvals = []  # store corresponding depth values
1043
1044            g = g.lower()
1045            six=0
1046            for i,T in enumerate(S[g]): # loop through templates in this group
1047                # find depth position of center and copy data across
1048                if len(T.boxes) > 1:
1049                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
1050                else:
1051                    if scaled:
1052                        six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
1053                    else:
1054                        # add gaps for non-contiguous templates
1055                        if i > 0 and (abs(S[g][i - 1].to_depth - T.from_depth) > 0.5):
1056                            six += T.index.shape[1]  # add full-box sized gap
1057
1058                    # copy data!
1059                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
1060                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 0 ] = bix
1061                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 1:] = T.index[:, :, 1:]  # copy pixel indices
1062
1063                    # store depth ticks
1064                    zticks.append(six)
1065                    zvals.append(T.from_depth)
1066
1067                    if not scaled:
1068                        six += T.index.shape[1]+pad # increment position
1069
1070            # step to the right
1071            w = int(np.max([T.index.shape[0] for T in S[g]]) + pad) # compute max width of core blocks in this group
1072            tticks.append(int(_x + (w / 2))) # store group ticks
1073            _x += w # step to the right
1074
1075        zticks.append(index.shape[1])
1076        zvals.append(T.to_depth) # add tick at bottom of final template / box
1077
1078        if scaled and len(groups) > 1:
1079            zvals = None
1080            depth_ticks = None # these are not defined if more than one hole is present
1081        else:
1082            # interpolate zvals to get a depth value for each pixel
1083            zvals = np.interp(np.arange(0,index.shape[1]), zticks, zvals )
1084            zticks = np.arange(index.shape[1])
1085        out = Template(paths, index, from_depth, to_depth, depth_axis=1,
1086                       groups = groups, group_ticks = tticks, depths = zvals, depth_ticks = zticks )
1087
1088        # done!
1089        return out
1090
1091
1092    def hfence(self, *args):
1093        """
1094        Construct a "horizontal fence" type template for visualising boreholes in a condensed way. This
1095        is identical to the vfence(...) layout, but rotated 90 degrees such that depth increases to the right.
1096
1097        :param args: All arguments are passed to vfence. The results are then rotated to the horizontal orientation.
1098        :return:
1099        """
1100        out = self.vfence(*args)
1101        out.rot90()
1102        return out
1103
1104    def vpole(self, *args):
1105        """
1106        Construct a "vertical pole" type template for visualising and corellating between one or more drillcores. This
1107        is identical to the hpole(...) layout, but rotated 90 degrees such that cores are vertical and depth increases
1108        downwards.
1109
1110        :param args: All arguments are passed to hpole. The results are then rotated to vertical orientation.
1111        :return:
1112        """
1113        out = self.hpole(*args)
1114        # out.index = np.transpose(out.index, (1, 0, 2)) # rotate to vertical
1115        out.rot90()
1116        return out
1117
1118    def _preprocessTemplates(self, depth_offsets, from_depth, groups, to_depth):
1119        # parse from_depth and to_depth if needed
1120        if from_depth is None:
1121            from_depth = np.min([np.min([t.from_depth for t in v]) for (k, v) in self.store.items()])
1122        if to_depth is None:
1123            to_depth = np.max([np.max([t.to_depth for t in v]) for (k, v) in self.store.items()])
1124        # ensure depth template keys are lower case!
1125        offs = {}
1126        for k, v in depth_offsets.items():
1127            offs[k.lower()] = v
1128        # crop templates to the relevant view area, and discard ones that do not fit
1129        cropped = {}
1130        for k, v in self.store.items():
1131            for T in v:
1132                assert T.from_depth is not None, "Error - depth info must be defined for template to be added."
1133                assert T.to_depth is not None, "Error - depth info must be defined for template to be added."
1134                T = T.crop(from_depth, to_depth, T.depth_axis, offs.get(k, 0))
1135                if T is not None:
1136                    # store
1137                    cropped[k.lower()] = cropped.get(k.lower(), [])
1138                    cropped[k.lower()].append(T)
1139
1140        assert len(cropped) > 0, "Error - no templates are within depth range!"
1141
1142        # sort templates by order in each group
1143        S = {}
1144        for k, v in cropped.items():
1145            S[k.lower()] = sorted(v)
1146        # resolve all unique paths
1147        paths = set()
1148        for k, v in S.items():
1149            for t in v:
1150                for b in t.boxes:
1151                    paths.add(os.path.join(t.root, b))
1152                    assert os.path.exists(
1153                        os.path.join(t.root, b)), "Error - one or more template directories do not exist?"
1154        paths = list(paths)
1155        # get group names to plot if not specified
1156        if groups is None:
1157            groups = list(S.keys())
1158        return S, from_depth, to_depth, groups, paths
1159
1160
1161    def add(self, group, template):
1162        """
1163        Add the specified template to this Canvas collection.
1164
1165        :param group: The name of the group to add this template to.
1166        :param template: The template object.
1167        """
1168        self.__setitem__(group, template)
1169
1170    def __getitem__(self, key):
1171        return self.store[self._keytransform(key)]
1172
1173    def __setitem__(self, key, value):
1174        """
1175        A shorthand way to add items to canvas.
1176        """
1177        assert isinstance(value, Template), "Error - only Templates can be added to a Canvas (for now...)"
1178        v = self.store.get(self._keytransform(key), [])
1179        v.append(value)
1180        self.store[self._keytransform(key)] = v
1181
1182    def __delitem__(self, key):
1183        del self.store[self._keytransform(key)]
1184
1185    def __iter__(self):
1186        return iter(self.store)
1187
1188    def __len__(self):
1189        return len(self.store)
1190
1191    def _keytransform(self, key):
1192        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)
876    def __init__(self, *args, **kwargs):
877        self.store = dict()
878        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):
881    def hpole(self, from_depth: float = None, to_depth: float = None, scaled=False, groups: list = None,
882                    res: float = 1e-3, depth_offsets: dict = {}, pad: int = 5 ):
883        """
884        Construct a "horizontal pole" type template for visualising and corellating between one or more drillholes.
885        This has a layout as follows:
886
887                          -------------------------------------------------
888        core (group) 1 - |  [xxxxxxxxxx] [xxxx]         [xxxxxxxxxxxxxxx]  |
889        core (group) 2 - |  [xxxxx]       [xxxxxxxxxxxx]         [xxxxxx]  |
890        core (group) 3 - |  [xxxxxxxx] [xxxxxxxxxxxxxxxxxx][xxxxxxxxxxxx]  |
891                          -------------------------------------------------
892
893        :param from_depth: The top depth of the template view area, or None to include all depths.
894        :param to_depth: The lower depth of the template view area, or None to include all depths.
895        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
896                (with small gaps representing non-contiguous intervals).
897        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
898        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
899        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
900                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
901                                in absolute terms.
902        :param pad: Padding for template stacking. Default is 5.
903        :return: A single combined Template class in horizontal pole layout.
904        """
905
906        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
907                                                                           groups, to_depth )
908
909        # compute width of output image
910        w = pad
911        for g in groups:
912            w += np.max([T.index.shape[1] for T in S[g.lower()]]) + pad
913
914        if scaled:
915            # compute image dimension in depth direction
916            nz = int(np.abs(to_depth - from_depth) / res)
917
918            # compute corresponding depths
919            z = np.linspace(from_depth, to_depth, nz)
920        else:
921            # determine maximum size of stacked boxes, including gaps, and hence template dimensions
922            z = []
923            for g in groups:
924                nz = 0 # this is the dimensions of our output in pixels
925                for i, T in enumerate(S[g.lower()]):
926                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
927                        nz += 10*pad # add in gaps for non-contiguous cores
928                        z.append( np.linspace(S[g.lower()][i-1].to_depth, T.from_depth, 10*pad ) )
929
930                    nz += T.index.shape[0] + pad
931                    z.append(np.linspace(T.from_depth, T.to_depth, T.index.shape[0]))
932                    z.append([T.to_depth for i in range(pad)])
933            z = np.hstack(z)
934
935        assert len(z) == nz, "Error - %d depths and %d pixels. Should be the same." % (len(z), nz) # debugging
936
937        # build index
938        index = np.full((nz, w, 3), -1, dtype=int)
939        tticks = []  # store tick positions in transverse direction (y-axis for hpole)
940
941        # stack templates
942        _y = pad
943        for g in groups:
944            g = g.lower()
945            for T in S[g]:
946                # find depth position of center and copy data across
947                if len(T.boxes) > 1:
948                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
949                else:
950                    six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
951                    eix = min(T.index.shape[0],
952                              (index.shape[0] - six))  # end index in template (to allow for possible overflows)
953
954                    # copy data!
955                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
956                    index[six:(six + T.index.shape[0]), _y:(_y + T.index.shape[1]), 0] = bix  # set box index
957
958                    index[six:(six + eix), _y:(_y + T.index.shape[1]), 1:] = T.index[0:eix, :,
959                                                                             1:]  # copy pixel indices
960
961            # step to the right
962            h = int(np.max([T.index.shape[1] for T in S[g]]) + pad)
963            tticks.append(int(_y + (h / 2)))
964            _y += h
965
966        out = Template(paths, index, from_depth, to_depth, depth_axis=0,
967            groups = groups, group_ticks = tticks, depths = z, depth_ticks = np.arange(len(z)))
968
969        # done!
970        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):
 972    def vfence(self, from_depth: float = None, to_depth: float = None, scaled=False,
 973               groups: list = None, depth_offsets : dict = {}, pad: int = 5):
 974        """
 975        Construct a "horizontal fence" type template for visualising drillholes in a condensed way.
 976        This has a layout as follows:
 977
 978            core 1       core 2         core 3
 979    1  - | ======== | | =========| | ========== |
 980         | ======== | | =========| | ========== |
 981    2  - | ======== | | ======   | | =====      |
 982         | ======== |     gap      | ========== |
 983    3  - | ======   | | =========| | ========== |
 984         | ======== | | =========| | ========== |
 985
 986        :param from_depth: The top depth of the template view area, or None to include all depths.
 987        :param to_depth: The lower depth of the template view area, or None to include all depths.
 988        :param scaled: If True, a constant scale will be used on the z-axis. If False (default), cores will be stacked vertically
 989                        (with small gaps representing non-contiguous intervals).
 990        :param groups: Names of the groups to plot (in order!). If None (default) then all groups are plotted.
 991        :param res: Resolution of the imagery in meters (used when deriving vertical scale). Defaults to 1e-3 (1 mm).
 992        :param depth_offsets: A dictionary containing depth values to be subtracted from sub-templates with matching
 993                                group names. Useful for e.g., plotting boreholes relative to a marker horizon rather than
 994                                in absolute terms.
 995        :param pad: Padding for template stacking. Default is 5.
 996        :return: A single combined Template class in horizontal pole layout.
 997        """
 998
 999        S, from_depth, to_depth, groups, paths = self._preprocessTemplates(depth_offsets, from_depth,
1000                                                                           groups, to_depth )
1001
1002        # compute width used for each group and hence image width
1003        # also compute y-scale based maximum template height to depth covered ratio
1004        w = pad # width
1005        ys = np.inf # shared y-axis pixel to depth scale (meters per pixel)
1006        for g in groups:
1007            w = w + np.max([T.index.shape[0] for T in S[g.lower()]]) + pad
1008            for T in S[g.lower()]:
1009                ys = min( ys, abs(T.to_depth - T.from_depth) / T.index.shape[1] )
1010
1011        # compute image dimension in depth direction
1012        if scaled:
1013            # determine depth-scale along y-axis (distance down hole per pixel)
1014            nz = int(np.abs(to_depth - from_depth) / ys)
1015            z = np.linspace(from_depth, to_depth, nz ) # depth per pixel array (kinda...)
1016
1017            # build index
1018            index = np.full((w, nz + pad, 3), -1, dtype=int)
1019
1020        else:
1021            # determine maximum height of stacked boxes, including gaps, and hence template dimensions
1022            heights = []
1023            for g in groups:
1024                h = 0
1025                for i, T in enumerate(S[g.lower()]):
1026                    h += T.index.shape[1] + pad
1027                    if (i > 0) and (abs(T.from_depth - S[g.lower()][i-1].to_depth) > 0.5):
1028                        h += T.index.shape[1] # add in gaps for non-contiguous cores
1029                heights.append(h)
1030
1031            ymax = np.max( heights )
1032
1033            # build index
1034            index = np.full((w, ymax + pad, 3), -1, dtype=int)
1035
1036        tticks = []  # store group tick positions in transverse direction (x-axis)
1037
1038        # stack templates
1039        _x = pad
1040        for g in groups: # loop through groups
1041            zticks = []  # store depth ticks in the down-hole direction (y-axis)
1042            zvals = []  # store corresponding depth values
1043
1044            g = g.lower()
1045            six=0
1046            for i,T in enumerate(S[g]): # loop through templates in this group
1047                # find depth position of center and copy data across
1048                if len(T.boxes) > 1:
1049                    assert False, "Error, cannot use multi-box templates on a Canvas (yet)"
1050                else:
1051                    if scaled:
1052                        six = int(np.argmin(np.abs(z - T.from_depth)))  # start index in z array
1053                    else:
1054                        # add gaps for non-contiguous templates
1055                        if i > 0 and (abs(S[g][i - 1].to_depth - T.from_depth) > 0.5):
1056                            six += T.index.shape[1]  # add full-box sized gap
1057
1058                    # copy data!
1059                    bix = int(paths.index(os.path.join(T.root, T.boxes[0])))
1060                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 0 ] = bix
1061                    index[ _x:(_x + T.index.shape[0] ) , six:(six+T.index.shape[1]), 1:] = T.index[:, :, 1:]  # copy pixel indices
1062
1063                    # store depth ticks
1064                    zticks.append(six)
1065                    zvals.append(T.from_depth)
1066
1067                    if not scaled:
1068                        six += T.index.shape[1]+pad # increment position
1069
1070            # step to the right
1071            w = int(np.max([T.index.shape[0] for T in S[g]]) + pad) # compute max width of core blocks in this group
1072            tticks.append(int(_x + (w / 2))) # store group ticks
1073            _x += w # step to the right
1074
1075        zticks.append(index.shape[1])
1076        zvals.append(T.to_depth) # add tick at bottom of final template / box
1077
1078        if scaled and len(groups) > 1:
1079            zvals = None
1080            depth_ticks = None # these are not defined if more than one hole is present
1081        else:
1082            # interpolate zvals to get a depth value for each pixel
1083            zvals = np.interp(np.arange(0,index.shape[1]), zticks, zvals )
1084            zticks = np.arange(index.shape[1])
1085        out = Template(paths, index, from_depth, to_depth, depth_axis=1,
1086                       groups = groups, group_ticks = tticks, depths = zvals, depth_ticks = zticks )
1087
1088        # done!
1089        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):
1092    def hfence(self, *args):
1093        """
1094        Construct a "horizontal fence" type template for visualising boreholes in a condensed way. This
1095        is identical to the vfence(...) layout, but rotated 90 degrees such that depth increases to the right.
1096
1097        :param args: All arguments are passed to vfence. The results are then rotated to the horizontal orientation.
1098        :return:
1099        """
1100        out = self.vfence(*args)
1101        out.rot90()
1102        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):
1104    def vpole(self, *args):
1105        """
1106        Construct a "vertical pole" type template for visualising and corellating between one or more drillcores. This
1107        is identical to the hpole(...) layout, but rotated 90 degrees such that cores are vertical and depth increases
1108        downwards.
1109
1110        :param args: All arguments are passed to hpole. The results are then rotated to vertical orientation.
1111        :return:
1112        """
1113        out = self.hpole(*args)
1114        # out.index = np.transpose(out.index, (1, 0, 2)) # rotate to vertical
1115        out.rot90()
1116        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):
1161    def add(self, group, template):
1162        """
1163        Add the specified template to this Canvas collection.
1164
1165        :param group: The name of the group to add this template to.
1166        :param template: The template object.
1167        """
1168        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