Coverage for /usr/local/lib/python3.8/site-packages/spam/helpers/imageManipulation.py: 93%
281 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-11-22 13:26 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-11-22 13:26 +0000
1# Library of SPAM functions for manipulating images
2# Copyright (C) 2020 SPAM Contributors
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License as published by the Free
6# Software Foundation, either version 3 of the License, or (at your option)
7# any later version.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12# more details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program. If not, see <http://www.gnu.org/licenses/>.
17import numpy
18import progressbar
19import spam.label
20import tifffile
23def stackToArray(prefix, suffix=".tif", stack=range(10), digits="05d"):
24 """
25 Convert of stack of 2D sequential tif images into a 3D array.
27 Parameters
28 ----------
29 prefix : string
30 The common name of the 2D images files before the sequential number
32 suffix : string, default='.tif'
33 The common name and extension of the 2D images after the sequential number
35 stack : sequence, default=range(10)
36 The numbers of the slices with no formating (with no leading zeros)
38 digits : string, default='05d'
39 The format (number of digits) of the numbers (add leading zeros).
41 Returns
42 -------
43 numpy array
44 The 3D image
45 """
47 def _slice_name(pr, st, di, su):
48 return f"{pr}{st:{di}}{su}"
50 # Step 1 If nBytes is not defined: we open the first slice just for the dimensions
51 slice_name = _slice_name(prefix, stack[0], digits, suffix)
52 slice_im = tifffile.imread(slice_name)
54 # Step 2 create empty array of good size and type
55 ny, nx = slice_im.shape
56 nz = len(stack)
57 im = numpy.zeros([nz, ny, nx], dtype=slice_im.dtype)
59 # Step 3 stack all the slices
60 for i, s in enumerate(stack):
61 slice_name = _slice_name(prefix, s, digits, suffix)
62 im[i, :, :] = tifffile.imread(slice_name)
64 return im
67def crop(im, boxSize, boxOrigin=None):
68 """
69 This function crops an image using slicing.
71 Parameters
72 ----------
73 im: array
74 The image to crop.
76 boxSize: int
77 The size in voxels of the crop box (from boxOrigin). If a int, the size is the same for each axis. If a sequence, ``boxSize`` should contain one value for each axis.
79 boxOrigin : int, default=None
80 The coordinates in voxels of the origin of the crop box. If a int, the coordinates are the same for each axis.
81 If a tuple, ``boxOrigin`` should contain one value for each axis. If ``None`` the image is cropped from its centre.
83 Returns
84 -------
85 array
86 The cropped image.
87 """
89 # get center of the image
90 imCentre = [int(s) / 2 for s in im.shape]
91 # get sizes
92 (sz, sy, sx) = (boxSize, boxSize, boxSize) if isinstance(boxSize, int) else boxSize
94 # get box origin
95 if boxOrigin is not None:
96 (cz, cy, cx) = (boxOrigin, boxOrigin, boxOrigin) if isinstance(boxOrigin, int) else boxOrigin
97 else:
98 (cz, cy, cx) = (
99 imCentre[0] - sz // 2,
100 imCentre[1] - sy // 2,
101 imCentre[2] - sx // 2,
102 )
104 # test sizes
105 if cz + sz > im.shape[0] or cy + sy > im.shape[1] or cx + sx > im.shape[2]:
106 print("spam.helpers.imageManipulation.crop: box bigger than image.")
107 print("exit function.")
108 return -1
110 return im[int(cz) : int(cz + sz), int(cy) : int(cy + sy), int(cx) : int(cx + sx)]
113def rescale(im, scale=(0, 1)):
114 """
115 This function **rescales** the values of an image according to a scale
116 and save it to as 4 bytes floats (float32).
118 Parameters
119 ----------
120 im: array
121 The image to rescale
123 scale : (float, float), default=(0 1)
124 The min and max of the rescaled image
126 Returns
127 -------
128 array, float
129 The rescaled image.
131 Examples
132 --------
133 >>> im = numpy.random.randn( 100, 100, 100 ).astype( '<f4' )
134 produce float32 array of positive and negative numbers
135 >>> imRescaled = rescale( im, scale=[-1, 1] )
136 produce float32 array of numbers between -1 and 1
138 """
140 im_max = float(im.max())
141 im_min = float(im.min())
143 return (min(scale) + (max(scale) - min(scale)) * ((im.astype("<f4") - im_min) / (im_max - im_min))).astype("<f4")
146def rescaleToInteger(im, nBytes=1, scale=None):
147 """
148 This function **rescales** a 4 bytes float image values to a unsigned integers of ``nBytes``.
150 Parameters
151 ----------
152 im: float32 numpy array
153 The image to rescale
155 nBytes : int, default=1
156 The number of bytes of the unsigned interger output.
157 Possible values are power of 2
159 .. code-block:: text
161 reminder
162 1 byte = 8 bits -> ouput from 0 to 255
163 2 bytes = 16 bits -> ouput from 0 to 65 535
164 4 bytes = 32 bits -> ouput from 0 to 4 294 967 295
166 scale : (float, float), default=None
167 If None, the maximum and minimum use for the rescaling is the maximum and the minimum of the image
169 Returns
170 -------
171 numpy array, uint
172 The rescaled image
174 Examples
175 --------
176 >>> im = numpy.random.randn( 100, 100, 100 ).astype( '<f4' )
177 produce float32 array of positive and negative numbers
178 >>> imRescaled = rescaleToInteger( im, nBytes=4 )
179 produce uint32 array of numbers between 0 and 4 294 967 295
181 """
183 nBytes = int(nBytes)
184 if (nBytes & (nBytes - 1)) != 0:
185 raise ValueError(f"nBytes should be a power of 2 ({nBytes} given)")
187 # check if float32 given
188 if im.dtype is not numpy.dtype("float32"):
189 raise ValueError(f"image should be encode in float32 ({im.dtype} given)")
191 if scale is None:
192 # if no scale is given: it takes the max and min of the image
193 im_max = im.max()
194 im_min = im.min()
195 else:
196 # if a scale is given take it if larger (smaller) than max (min) of image
197 # im_max = max(scale) if max(scale) > im.max() else im.max()
198 # im_min = min(scale) if min(scale) < im.min() else im.min()
199 im_max = max(scale)
200 im_min = min(scale)
201 im[im > im_max] = im_max
202 im[im < im_min] = im_min
204 im_min = float(im_min)
205 im_max = float(im_max)
207 return ((2 ** (8 * nBytes) - 1) * ((im.astype("<f4") - im_min) / (im_max - im_min))).astype(f"<u{nBytes}")
210def convertUnsignedIntegers(im, nBytes=1):
211 """
212 This function **converts** an images of unsigned integers.
214 Note: this function does not rescale.
216 Parameters
217 ----------
218 im: array, uint
219 The image to convert.
221 nBytes : int, default=1
222 The number of bytes of the unsigned interger output.
223 Possible values are power of 2.
225 .. code-block:: text
227 reminder
228 1 byte = 8 bits -> ouput from 0 to 255
229 2 bytes = 16 bits -> ouput from 0 to 65 535
230 4 bytes = 32 bits -> ouput from 0 to 4 294 967 295
232 Returns
233 -------
234 array, uint
235 The converted image.
237 Examples
238 --------
239 >>> im = numpy.random.randint( 12, high=210, size=(100, 100, 100) ).astype( '<u1' )
240 produce an uint8 array of numbers between 12 and 210
241 >>> imRescaled = rescaleToInteger( im, nBytes=2 )
242 produce an uint16 array 3084 and 53970
244 """
246 nBytes = int(nBytes)
247 if (nBytes & (nBytes - 1)) != 0:
248 raise ValueError(f"nBytes should be a power of 2 ({nBytes} given)")
250 # number of bits of the output
251 nbo = 8 * nBytes
253 # number of bits of the input
254 inputType = im.dtype
255 if inputType == numpy.uint8:
256 nbi = 8
257 elif inputType == numpy.uint16:
258 nbi = 16
259 elif inputType == numpy.uint32:
260 nbi = 32
261 elif inputType == numpy.uint64:
262 nbi = 64
263 else:
264 raise ValueError(f"input image type should be unisgned integers ({inputType} given)")
266 return (float(2**nbo - 1) * (im) / float(2**nbi - 1)).astype("<u{}".format(nBytes))
269def singleShift(im, shift, axis, sub=0):
270 """
271 This function shift the image and replace the border by an substitution value.
273 It uses ``numpy.roll``.
275 Parameters
276 -----------
277 im : array
278 The input to shift.
279 shift : int
280 The number of places by which elements are shifted (from numpy.roll).
281 Default: 1
282 axis : int
283 The axis along which elements are shifted (from numpy.rool).
284 sub : foat, default=0
285 The substitution value of the border
287 Returns
288 -------
289 array :
290 The shifted image.
292 """
294 # Step 1: Cyclic permutation on im
295 im = numpy.roll(im, shift, axis=axis)
297 # Step 2: get image dimension
298 dim = len(im.shape)
300 # Step 3: modify the boundary with replacement value
301 if dim == 2: # if 2D image
302 if shift == 1 and axis == 0:
303 im[0, :] = sub
304 elif shift == -1 and axis == 0:
305 im[-1, :] = sub
306 elif shift == 1 and axis == 1:
307 im[:, 0] = sub
308 elif shift == -1 and axis == 1:
309 im[:, -1] = sub
310 elif dim == 3: # if 3D image
311 if shift >= 1 and axis == 0:
312 im[0:shift, :, :] = sub
313 elif shift <= -1 and axis == 0:
314 im[shift:, :, :] = sub
315 elif shift >= 1 and axis == 1:
316 im[:, 0:shift, :] = sub
317 elif shift <= -1 and axis == 1:
318 im[:, shift:, :] = sub
319 elif shift >= 1 and axis == 2:
320 im[:, :, 0:shift] = sub
321 elif shift <= -1 and axis == 2:
322 im[:, :, shift:] = sub
323 else:
324 print("spam.helpers.imageManipulation.singleShift: dim={}. Should be 2 or 3.".format(dim))
325 print("exit function.")
326 return -1
328 return im
331def multipleShifts(im, shifts, sub=0):
332 """
333 This function call ``singleShift`` multiple times.
335 Parameters
336 ----------
337 im : array
338 The input to shift.
339 shifts : [int, int, int]
340 Defines the number of shifts to apply in every axis.
342 .. code-block:: text
344 shift = [s_x, s_y, s_z] applies a shift of:
345 . s_x on axis 0
346 . s_y on axis 1
347 . s_z on axis 2
349 sub : float, default=0
350 The substitution value of the border
352 Returns
353 -------
354 array :
355 The shifted image.
357 """
359 # loop over the n axis
360 for i in range(len(shifts)):
361 # if the value of the shift is not 0 on axis i
362 if shifts[i]:
363 # we call singleShift (only once)
364 im = singleShift(im, shift=shifts[i], axis=i)
366 return im
369def _binarisation(im, threshold=0.0, boolean=False, op=">", mask=None):
370 """
371 This function binarise an input image according to a given threshold
373 It has an option to apply a mask to the binarized image to ignore the
374 outside of the sample/specimen
376 Parameters
377 -----------
378 im: array
379 The image to binarise.
381 threshold : float
382 the input limit value for binarization
384 boolean : bool
385 Changes the output format and phase distribution (see output)
387 op : string, default='>'
388 defines the thresholding operation
390 mask : array, default=None
391 The mask of the input image: is 0 outside the boundary(specimen) and 1 inside
393 Returns
394 --------
395 phases : array
396 The repartition of phases resulting the binarisation.
398 For operator '>' it gives, if ``boolean=True``:
400 .. code-block:: text
402 0 - masked parts (where mask equals 0)
403 0 - below threshold
404 1 - above threshold
406 and if ``boolean=False``
408 .. code-block:: text
410 0 - masked parts (where mask equals 0)
411 1 - below threshold
412 2 - above threshold
413 """
415 import operator
417 # Step 1: Get operator
418 operation = {
419 ">": operator.gt,
420 "<": operator.lt,
421 ">=": operator.ge,
422 "<=": operator.le,
423 "=": operator.eq,
424 }.get(op)
426 # Step 2: binarisation
427 phases = operation(im, threshold).astype("<u1")
429 # Step 3: rescaling if bool
430 if not boolean:
431 phases += 1
433 # Step 4: apply mask
434 if mask is not None:
435 phases = phases * mask
437 return phases
440def slicePadded(im, startStop, createMask=False, padValue=0, verbose=False):
441 """
442 Extract slice from im, padded with zeros, which is always of the dimensions asked (given from startStop)
444 Parameters
445 ----------
446 im : 3D numpy array
447 The image to be sliced
449 startStop : 6 component list of ints
450 This array contains:
451 [Zmin, Zmax, Ymin, Ymax, Xmin, Xmax]
453 createMask : bool, optional
454 If True, return a padded slice, which is False when the slice falls outside im
455 Default = False
457 Returns
458 -------
459 imSliced : 3D numpy array
460 The sliced image
462 mask : 3D numpy array of bools
463 The 3D mask, only returned if createMask is True
464 """
465 startStop = numpy.array(startStop).astype(int)
467 assert startStop[1] > startStop[0], "spam.helpers.slicePadded(): Zmax should be bigger than Zmin"
468 assert startStop[3] > startStop[2], "spam.helpers.slicePadded(): Ymax should be bigger than Ymin"
469 assert startStop[5] > startStop[4], "spam.helpers.slicePadded(): Xmax should be bigger than Xmin"
471 imSliced = (
472 numpy.zeros(
473 (
474 startStop[1] - startStop[0],
475 startStop[3] - startStop[2],
476 startStop[5] - startStop[4],
477 ),
478 dtype=im.dtype,
479 )
480 + padValue
481 )
483 start = numpy.array([startStop[0], startStop[2], startStop[4]])
484 stop = numpy.array([startStop[1], startStop[3], startStop[5]])
485 startOffset = numpy.array([max(0, start[0]), max(0, start[1]), max(0, start[2])])
486 stopOffset = numpy.array(
487 [
488 min(im.shape[0], stop[0]),
489 min(im.shape[1], stop[1]),
490 min(im.shape[2], stop[2]),
491 ]
492 )
493 startLocal = startOffset - start
494 stopLocal = startLocal + stopOffset - startOffset
496 # Check condition that we're asking for a slice of data wholly outside im
497 # This means either that the stop values are all smaller than 0
498 # OR the start are all bigger than im.shape
499 if numpy.any(stop < numpy.array([0, 0, 0])) or numpy.any(start >= numpy.array(im.shape)):
500 if verbose:
501 print("spam.helpers.slicePadded(): The extracted padded slice doesn't not touch the image!")
502 imSliced = imSliced.astype("<f4")
503 imSliced *= numpy.nan
504 if createMask:
505 return imSliced, numpy.zeros_like(imSliced, dtype=bool)
507 else:
508 imSliced[startLocal[0] : stopLocal[0], startLocal[1] : stopLocal[1], startLocal[2] : stopLocal[2],] = im[
509 startOffset[0] : stopOffset[0],
510 startOffset[1] : stopOffset[1],
511 startOffset[2] : stopOffset[2],
512 ]
513 if createMask:
514 mask = numpy.zeros_like(imSliced, dtype=bool)
515 mask[
516 startLocal[0] : stopLocal[0],
517 startLocal[1] : stopLocal[1],
518 startLocal[2] : stopLocal[2],
519 ] = 1
520 return imSliced, mask
522 return imSliced
525def splitImage(im, divisions, margin, verbose=True):
526 """
527 Divides the image in zDiv x yDiv x xDiv blocks, each block is padded
528 with a margin.
530 Parameters
531 ----------
532 im : 3D numpy array
533 The image to be splitted
535 divisions : 3-component list of ints
536 Desired number of blocks along Z, Y, X axes
538 margin : int
539 Overlapping margin between each block.
540 For applying a filter on subvolumes, it is recommended to use a margin of 1.5 times the filter diameter.
541 For labelled data it is recommended that the margin is at least 1.5 times bigger than the particles largest axis
543 verbose : bool
544 Print the parameters of the operations (number of blocks and margin)
545 Default = True
547 Returns
548 -------
549 Dictionary
550 Dictionary with keys labelled acoording to the position of the block along each axis (e.g., 000, 001, 002,...)
551 Each element (e.g., 001) within the dictionary carries the block origin and the resulting block, in that order
553 Note
554 ----
555 This function should be used along `spam.helpers.imageManipulation.rebuildImage()`
557 """
558 zDiv, yDiv, xDiv = divisions
560 # Check if the slices can be made
561 if zDiv >= im.shape[0]:
562 print("spam.helpers.imageManipulation.splitImage: Incorrect number of slices for axis z")
563 print("exit function.")
564 return -1
565 if yDiv >= im.shape[1]:
566 print("spam.helpers.imageManipulation.splitImage: Incorrect number of slices for axis y")
567 print("exit function.")
568 return -1
569 if xDiv >= im.shape[2]:
570 print("spam.helpers.imageManipulation.splitImage: Incorrect number of slices for axis x")
571 print("exit function.")
572 return -1
574 # Check that margin is not greater than the slice
575 if margin >= im.shape[0] / zDiv:
576 print("spam.helpers.imageManipulation.splitImage: Margin is too big for z axis")
577 print("exit function.")
578 return -1
579 if margin >= im.shape[1] / yDiv:
580 print("spam.helpers.imageManipulation.splitImage: Margin is too big for y axis")
581 print("exit function.")
582 return -1
583 if margin >= im.shape[2] / xDiv:
584 print("spam.helpers.imageManipulation.splitImage: Margin is too big for x axis")
585 print("exit function.")
586 return -1
587 # Print parameters if needed
588 if verbose:
589 print(
590 "spam.helpers.imageManipulation.splitImage: Working with margin of ",
591 margin,
592 ". The total number of blocks is ",
593 zDiv * yDiv * xDiv,
594 )
596 # Pad initial image with zeros on the edge
597 imPad = numpy.pad(im, margin, mode="edge")
599 # Compute size of blocks
600 zSize = int(im.shape[0] / zDiv)
601 ySize = int(im.shape[1] / yDiv)
602 xSize = int(im.shape[2] / xDiv)
604 # Create return dictionary
605 output = {}
607 # Iterate through each block
608 for zBlock in range(zDiv):
609 for yBlock in range(yDiv):
610 for xBlock in range(xDiv):
611 # Get the origin of each block
612 blockOrigin = numpy.array([zBlock * zSize, yBlock * ySize, xBlock * xSize])
613 blockSize = [zSize, ySize, xSize]
614 # Check if the size needs to be changed to fit the image
615 if zBlock == zDiv - 1 and im.shape[0] % zDiv != 0:
616 blockSize[0] = zSize + (im.shape[0] % zDiv)
617 if yBlock == yDiv - 1 and im.shape[1] % yDiv != 0:
618 blockSize[1] = ySize + (im.shape[1] % yDiv)
619 if xBlock == xDiv - 1 and im.shape[2] % xDiv != 0:
620 blockSize[2] = xSize + (im.shape[2] % xDiv)
622 # Generate block with the margin on all sides
623 imBlock = crop(
624 imPad,
625 (
626 blockSize[0] + 2 * margin,
627 blockSize[1] + 2 * margin,
628 blockSize[2] + 2 * margin,
629 ),
630 boxOrigin=(blockOrigin[0], blockOrigin[1], blockOrigin[2]),
631 )
632 # Save the results
633 output.update({str(zBlock) + str(yBlock) + str(xBlock): [blockOrigin, imBlock]})
634 # Save the margin
635 output.update({"margin": margin})
637 # Return
638 return output
641def rebuildImage(listBlocks, listCoordinates, margin, mode, keepLabels=False, verbose=True):
642 """
643 Rebuilds splitted image from `spam.helpers.imageManipulation.splitImage()`.
645 Parameters
646 ----------
647 listBlocks : list
648 List of the 3D blocks that will form the re-built the image.
649 Note: The order of listBlocks should be equivalent to the order of listCoordinates
651 listCoordinates : list
652 List of the origin coordinates of each block. (Usually taken from `spam.helpers.imageManipulation.splitImage()`)
653 Note: The order of listCoordinates should be equivalent to the order of listBlocks
655 margin : integer
656 Value of the margin used for the images. (Usually taken from `spam.helpers.imageManipulation.splitImage()`)
658 mode : string
659 'grey' : re-builds 3D greyscale arrays
660 'label' : re-builds 3D labelled arrays
662 keepLabels : bool
663 Do we need to want to keep the current labels from the blocks, or create a new one?
664 Default = False
666 verbose : bool
667 Print the evolution of the operation
668 Default = True
670 Returns
671 -------
672 imBuild : 3D numpy array
673 Re-built image without the margins
675 Note
676 ----
677 This function should be used along with `spam.helpers.imageManipulation.splitImage()`
679 """
681 # Checking if listBlocks and listCoordinates have the same length
682 if len(listBlocks) != len(listCoordinates):
683 print("spam.helpers.imageManipulation.splitImage: listBlocks and listCoordinates must have the same length")
684 return -1
686 # Transform listCoordinates into array
687 arrayCoord = numpy.asarray(listCoordinates)
689 # Checking if all the origin coordinates are different
690 _, counts = numpy.unique(arrayCoord, axis=0, return_counts=True)
691 if len(counts) != len(arrayCoord):
692 print("spam.helpers.imageManipulation.splitImage: coordinates in listCoordinates must be all different")
693 return -1
695 if verbose:
696 # Create progressbar
697 widgets = [
698 progressbar.FormatLabel(""),
699 " ",
700 progressbar.Bar(),
701 " ",
702 progressbar.AdaptiveETA(),
703 ]
704 pbar = progressbar.ProgressBar(widgets=widgets, maxval=len(listCoordinates))
705 pbar.start()
706 finishedBlocks = 0
708 if mode == "grey":
710 # Shape of the block opposite to the origine
711 shapeLast = listBlocks[numpy.argmax(numpy.sum(arrayCoord, axis=1))].shape
713 # Tentative size of the final image
714 zSize = numpy.amax(arrayCoord[:, 0]) + shapeLast[0] - 2 * margin
715 ySize = numpy.amax(arrayCoord[:, 1]) + shapeLast[1] - 2 * margin
716 xSize = numpy.amax(arrayCoord[:, 2]) + shapeLast[2] - 2 * margin
718 # Initialising rebuild image
719 imBuild = numpy.zeros((zSize, ySize, xSize))
721 # Loop on the length to lists, so to replace zeros in imBuild with the actual values at the right position
722 for i in range(len(listCoordinates)):
723 origin = listCoordinates[i]
724 # GP 01/03/2022: Changing to ease the charge on the memory
725 blockPad = listBlocks.pop(0)
726 # blockPad = listBlocks[i]
728 if margin == 0:
729 imBuild[
730 origin[0] : origin[0] + blockPad.shape[0] - 2 * margin,
731 origin[1] : origin[1] + blockPad.shape[1] - 2 * margin,
732 origin[2] : origin[2] + blockPad.shape[2] - 2 * margin,
733 ] = blockPad
734 else:
735 imBuild[
736 origin[0] : origin[0] + blockPad.shape[0] - 2 * margin,
737 origin[1] : origin[1] + blockPad.shape[1] - 2 * margin,
738 origin[2] : origin[2] + blockPad.shape[2] - 2 * margin,
739 ] = blockPad[margin:-margin, margin:-margin, margin:-margin]
740 if verbose:
741 # Update the progressbar
742 finishedBlocks += 1
743 widgets[0] = progressbar.FormatLabel("{}/{} ".format(finishedBlocks, len(listCoordinates)))
744 pbar.update(finishedBlocks)
745 print("\n\t")
746 return imBuild
748 if mode == "label":
750 # Shape of the block opposite to the origine
751 shapeLast = listBlocks[numpy.argmax(numpy.sum(arrayCoord, axis=1))].shape
753 # Size of the final image
754 zSize = numpy.amax(arrayCoord[:, 0]) + shapeLast[0] - 2 * margin
755 ySize = numpy.amax(arrayCoord[:, 1]) + shapeLast[1] - 2 * margin
756 xSize = numpy.amax(arrayCoord[:, 2]) + shapeLast[2] - 2 * margin
758 # Initialising rebuild image
759 imBuild = numpy.zeros((zSize, ySize, xSize))
760 # Loop over the lists
761 for i in range(len(listCoordinates)):
762 # Get the origin of the block
763 origin = listCoordinates[i]
764 # Get the block
765 # GP 01/03/2022: Changing to ease the charge on the memory
766 # block = listBlocks[i]
767 block = listBlocks.pop(0)
768 # Compute the bounding boxes
769 boundingBoxes = spam.label.boundingBoxes(block)
770 # Compute centre of mass
771 centresOfMass = spam.label.centresOfMass(block, boundingBoxes=boundingBoxes)
773 # List for classifying the labels
774 inside = []
775 outside = []
776 partial = []
778 # Check if each label is inside the true block - i.e., it is inside the block without the margin
779 for j in range(1, len(boundingBoxes), 1):
780 # Get the box
781 box = boundingBoxes[j]
782 # Check if the origin is inside the true block
783 checkOrigin = margin < box[0] < block.shape[0] - margin and margin < box[2] < block.shape[1] - margin and margin < box[4] < block.shape[2] - margin
784 # Check if the coordinate opposite to the origin is inside the true block
785 checkOpp = margin < box[1] < block.shape[0] - margin and margin < box[3] < block.shape[1] - margin and margin < box[5] < block.shape[2] - margin
786 if checkOrigin and checkOpp:
787 # Both points are inside
788 inside.append(j)
789 elif checkOrigin or checkOpp:
790 # Only one is inside
791 partial.append(j)
792 else:
793 # Both are outside
794 outside.append(j)
796 # Create true block array, keep only particles fully inside
797 trueBlock = spam.label.removeLabels(block, partial + outside)
799 if not keepLabels:
800 # Check that we have labels inside the block
801 if len(numpy.unique(trueBlock)) > 1:
802 # Make labels consecutive
803 trueBlock = spam.label.makeLabelsSequential(trueBlock)
804 trueBlock = numpy.where(trueBlock != 0, trueBlock + numpy.max(imBuild), trueBlock)
806 # IV (04-03-21): Info needed to avoid chopping grains that would be considered as outside particles
807 imBuildSubSet = imBuild[
808 origin[0] : origin[0] + trueBlock.shape[0] - 2 * margin,
809 origin[1] : origin[1] + trueBlock.shape[1] - 2 * margin,
810 origin[2] : origin[2] + trueBlock.shape[2] - 2 * margin,
811 ]
813 # Add the true block preserving previous info
814 imBuild[origin[0] : origin[0] + trueBlock.shape[0] - 2 * margin, origin[1] : origin[1] + trueBlock.shape[1] - 2 * margin, origin[2] : origin[2] + trueBlock.shape[2] - 2 * margin,] = (
815 trueBlock[margin:-margin, margin:-margin, margin:-margin] + imBuildSubSet
816 )
818 # Get current maximum label
819 tempMax = numpy.max(imBuild)
820 # Label counter
821 labCounter = 1
822 # Solve the labels inside partial list
823 if len(partial) > 0:
824 # Iterate trough each label
825 for label in partial:
826 # Get the bounding box
827 box = boundingBoxes[label]
828 # Get subset
829 labelSubset = spam.label.getLabel(
830 block,
831 label,
832 boundingBoxes=boundingBoxes,
833 centresOfMass=centresOfMass,
834 )
835 labelSubvol = labelSubset["subvol"].astype(int)
836 labelSubvol = numpy.where(labelSubvol != 0, labCounter + tempMax, labelSubvol)
837 # IV+GP implementation - Just put the box there, overwrite whatever you find there
838 # Get the subset from imBuild
839 imBuildSubset = imBuild[
840 origin[0] + box[0] - margin : origin[0] + box[1] + 1 - margin,
841 origin[1] + box[2] - margin : origin[1] + box[3] + 1 - margin,
842 origin[2] + box[4] - margin : origin[2] + box[5] + 1 - margin,
843 ]
844 # Change
845 imBuildSubset = numpy.where(labelSubvol != 0, labelSubvol, imBuildSubset)
846 # Put back
847 imBuild[
848 origin[0] + box[0] - margin : origin[0] + box[1] + 1 - margin,
849 origin[1] + box[2] - margin : origin[1] + box[3] + 1 - margin,
850 origin[2] + box[4] - margin : origin[2] + box[5] + 1 - margin,
851 ] = imBuildSubset
852 # Update label counter
853 labCounter += 1
855 if verbose:
856 # Update the progressbar
857 finishedBlocks += 1
858 widgets[0] = progressbar.FormatLabel("{}/{} ".format(finishedBlocks, len(listCoordinates)))
859 pbar.update(finishedBlocks)
861 if not keepLabels:
862 imBuild = spam.label.makeLabelsSequential(imBuild)
864 print("\n\t")
865 return imBuild
867 else:
868 # The mode is not correct
869 print("spam.helpers.imageManipulation.splitImage(): Incorrect mode, check your input")
871 return -1
874def checkerBoard(im1, im2, n=5, inv=False, rescale=True):
875 """
876 This function generates a "checkerboard" mix of two 2D images of the same size.
877 This is useful to see if they have been properly aligned, especially if the two images are
878 quantitatively different (i.e., one is a neutron tomography and the other is an x-ray tomography).
880 Parameters
881 ----------
882 im1 : 2D numpy array
883 This is the first image
885 im2 : 2D/3D numpy array
886 This is the second image, should be same shape as first image
888 n : integer, optional
889 The number of divisions of the checkerboard to aim for.
890 Default = 5
892 inv : bool, optional
893 Whether im2 should be -im2 in the checkerboard.
894 Default = False
896 rescale : bool, optional
897 Whether greylevels should be rescaled with spam.helpers.rescale.
898 Default = True
900 Returns
901 -------
902 im1G : checkerBoard mix of im1 and im2
903 """
904 if inv:
905 c = -1.0
906 else:
907 c = 1.0
909 if rescale:
910 import spam.helpers
912 im1 = spam.helpers.rescale(im1)
913 im2 = spam.helpers.rescale(im2)
915 # 2D version
916 if len(im1.shape) == 2:
917 # initialize
918 im1G = im1.copy()
920 # get number of pixel / square based on min size
921 nP = int(min(im1.shape) / n)
923 for x in range(im1.shape[0]):
924 for y in range(im1.shape[1]):
925 if int((x % (2 * nP)) / nP) + int((y % (2 * nP)) / nP) - 1:
926 im1G[x, y] = c * im2[x, y]
927 else:
928 print("checkerBoard works only with dim2 images")
929 return 0
931 return im1G
934# private functions
935# def _mask2D(im, erosion=False, structure=None, ):
936# """
937# get contour of 2D image.
938# """
940# import cv2
941# from scipy import ndimage
943# step 2: convert into uint8 if not the case
944# if im.dtype != 'uint8':
945# actually it rescales the image but it doesn't really amtter
946# im = rescaleToInteger(im, nBytes=1)
948# Step 3: ...
949# blur = cv2.GaussianBlur(im, (5, 5), 0)
950# _, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
951# _, contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
952# largest = 0
953# biggest = []
954# for contour in contours:
955# area = cv2.contourArea(contour)
956# if largest < area:
957# largest = area
958# biggest = contour
960# mask = numpy.zeros(im.shape, dtype='<u1')
961# cv2.drawContours(mask, [biggest], 0, 1, -1)
963# Step 4: apply erosion of the mask (which corresponds to an erosion of the specimen)
964# if erosion:
965# mask = ndimage.morphology.binary_erosion(
966# mask, structure=structure).astype(mask.dtype)
968# return mask