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