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()
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.
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).
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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
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.
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.
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.
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.
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.
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).
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.
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.
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
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
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