# Copyright 2021 Mechanics of Microstructures Group
# at The University of Manchester
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import numpy as np
import networkx as nx
from defdap.quat import Quat
from defdap import plotting
from defdap.plotting import Plot, MapPlot, GrainPlot
from skimage.measure import profile_line
from defdap.utils import reportProgress
[docs]class Map(object):
"""
Base class for a map. Contains common functionality for all maps.
Attributes
----------
grainList : list of defdap.base.Grain
List of grains.
currGrainId : int
ID of last selected grain.
"""
def __init__(self):
self.xDim = None
self.yDim = None
self.grainList = None
self.currGrainId = None # ID of last selected grain
self.homogPoints = []
self.proxigramArr = None
self.neighbourNetwork = None
self.grainPlot = None
self.profilePlot = None
def __len__(self):
return len(self.grainList)
# allow array like getting of grains
def __getitem__(self, key):
# Check that grains have been detected in the map
self.checkGrainsDetected()
return self.grainList[key]
@property
def shape(self):
return self.yDim, self.xDim
[docs] def checkGrainsDetected(self, raiseExc=True):
"""Check if grains have been detected.
Parameters
----------
raiseExc : bool
If True then an expception is raised if grains have not been
detected.
Returns
-------
bool:
True if grains detected, False otherwise.
Raises
-------
Exception
If grains not detected.
"""
if (self.grainList is None or
type(self.grainList) is not list or
len(self.grainList) < 1):
if raiseExc:
raise Exception("No grains detected.")
else:
return False
return True
[docs] def plotGrainNumbers(self, dilateBoundaries=False, ax=None, **kwargs):
"""Plot a map with grains numbered.
Parameters
----------
dilateBoundaries : bool, optional
Set to true to dilate boundaries.
ax : matplotlib.axes.Axes, optional
axis to plot on, if not provided the current active axis is used.
kwargs : dict, optional
Keyword arguments passed to :func:`defdap.plotting.MapPlot.addGrainNumbers`
Returns
-------
defdap.plotting.MapPlot
"""
plot = plotting.MapPlot(self, ax=ax)
plot.addGrainBoundaries(colour='black', dilate=dilateBoundaries)
plot.addGrainNumbers(**kwargs)
return plot
[docs] def locateGrainID(self, clickEvent=None, displaySelected=False, **kwargs):
"""Interactive plot for identifying grains.
Parameters
----------
clickEvent : optional
Click handler to use.
displaySelected : bool, optional
If true, plot slip traces for grain selected by click.
kwargs : dict, optional
Keyword arguments passed to :func:`defdap.base.Map.plotDefault`
"""
# Check that grains have been detected in the map
self.checkGrainsDetected()
# reset current selected grain and plot euler map with click handler
self.currGrainId = None
plot = self.plotDefault(makeInteractive=True, **kwargs)
if clickEvent is None:
# default click handler which highlights grain and prints id
plot.addEventHandler(
'button_press_event',
lambda e, p: self.clickGrainID(e, p, displaySelected)
)
else:
# click handler loaded in as parameter. Pass current map
# object to it.
plot.addEventHandler('button_press_event', clickEvent)
return plot
[docs] def clickGrainID(self, event, plot, displaySelected):
"""Event handler to capture clicking on a map.
Parameters
----------
event :
Click event.
plot : defdap.plotting.MapPlot
Plot to capture clicks from.
displaySelected : bool
If true, plot the selected grain alone in pop-out window.
"""
# check if click was on the map
if event.inaxes is not plot.ax:
return
# grain id of selected grain
self.currGrainId = int(self.grains[int(event.ydata), int(event.xdata)] - 1)
print("Grain ID: {}".format(self.currGrainId))
# update the grain highlights layer in the plot
plot.addGrainHighlights([self.currGrainId], alpha=self.highlightAlpha)
if displaySelected:
currGrain = self[self.currGrainId]
if self.grainPlot is None or not self.grainPlot.exists:
self.grainPlot = currGrain.plotDefault(makeInteractive=True)
else:
self.grainPlot.clear()
self.grainPlot.callingGrain = currGrain
currGrain.plotDefault(plot=self.grainPlot)
self.grainPlot.draw()
[docs] def drawLineProfile(self, **kwargs):
"""Interactive plot for drawing a line profile of data.
Parameters
----------
kwargs : dict, optional
Keyword arguments passed to :func:`defdap.base.Map.plotDefault`
"""
plot = self.plotDefault(makeInteractive=True, **kwargs)
plot.addEventHandler('button_press_event', plot.lineSlice)
plot.addEventHandler('button_release_event', lambda e, p: plot.lineSlice(e, p,
action=self.calcLineProfile))
return plot
[docs] def calcLineProfile(self, plot, startEnd, **kwargs):
"""Calculate and plot the line profile.
Parameters
----------
plot : defdap.plotting.MapPlot
Plot to calculate the line profile for.
startEnd : array_like
Selected points (x0, y0, x1, y1).
kwargs : dict, optional
Keyword arguments passed to :func:`matplotlib.pyplot.plot`
"""
x0, y0 = startEnd[0:2]
x1, y1 = startEnd[2:4]
profile_length = np.sqrt((y1 - y0) ** 2 + (x1 - x0) ** 2)
# Extract the values along the line
zi = profile_line(
plot.imgLayers[0].get_array(),
(startEnd[1], startEnd[0]),
(startEnd[3], startEnd[2]),
mode='nearest'
)
xi = np.linspace(0, profile_length, len(zi))
if self.profilePlot is None or not self.profilePlot.exists:
self.profilePlot = Plot(makeInteractive=True)
else:
self.profilePlot.clear()
self.profilePlot.ax.plot(xi, zi, **kwargs)
self.profilePlot.ax.set_xlabel('Distance (pixels)')
self.profilePlot.ax.set_ylabel('Intensity')
self.profilePlot.draw()
[docs] def setHomogPoint(self, binSize=1, points=None, **kwargs):
"""
Interactive tool to set homologous points. Right-click on a point
then click 'save point' to append to the homologous points list.
Parameters
----------
binSize : int, optional
Binning applied to image, if applicable.
points : numpy.ndarray, optional
Array of (x,y) homologous points to set explicitly.
kwargs : dict, optional
Keyword arguments passed to :func:`defdap.base.Map.plotHomog`
"""
if points is None:
plot = self.plotHomog(makeInteractive=True, **kwargs)
# Plot stored homogo points if there are any
if len(self.homogPoints) > 0:
homogPoints = np.array(self.homogPoints) * binSize
plot.addPoints(homogPoints[:, 0], homogPoints[:, 1],
c='y', s=60)
else:
# add empty points layer to update later
plot.addPoints([None], [None], c='y', s=60)
# add empty points layer for current selected point
plot.addPoints([None], [None], c='w', s=60, marker='x')
plot.addEventHandler('button_press_event', self.clickHomog)
plot.addEventHandler('key_press_event', self.keyHomog)
plot.addButton("Save point",
lambda e, p: self.clickSaveHomog(e, p, binSize),
color="0.85", hovercolor="blue")
else:
self.homogPoints = points
[docs] def clickHomog(self, event, plot):
"""Event handler for capturing position when clicking on a map.
Parameters
----------
event :
Click event.
plot : defdap.plotting.MapPlot
Plot to monitor.
"""
# check if click was on the map
if event.inaxes is not plot.ax:
return
# right mouse click or shift + left mouse click
# shift click doesn't work in osx backend
if (event.button == 3 or
(event.button == 1 and event.key == 'shift')):
plot.addPoints([int(event.xdata)], [int(event.ydata)],
updateLayer=1)
[docs] def keyHomog(self, event, plot):
"""Event handler for moving position using keyboard after clicking on a map.
Parameters
----------
event :
Keypress event.
plot : defdap.plotting.MapPlot
Plot to monitor.
"""
keys = ['left', 'right', 'up', 'down']
key = event.key.split('+')
if key[-1] in keys:
# get the selected point
points = plot.imgLayers[plot.pointsLayerIDs[1]]
selPoint = points.get_offsets()[0]
# check if a point is selected
if selPoint[0] is not None and selPoint[1] is not None:
# print(event.key)
move = 1
if len(key) == 2 and key[0] == 'shift':
move = 10
if key[-1] == keys[0]:
selPoint[0] -= move
elif key[-1] == keys[1]:
selPoint[0] += move
elif key[-1] == keys[2]:
selPoint[1] -= move
elif key[-1] == keys[3]:
selPoint[1] += move
plot.addPoints([selPoint[0]], [selPoint[1]], updateLayer=1)
[docs] def clickSaveHomog(self, event, plot, binSize):
"""Append the selected point on the map to homogPoints.
Parameters
----------
event :
Button click event.
plot : defdap.plotting.MapPlot
Plot to monitor.
binSize : int, optional
Binning applied to image, if applicable.
"""
# get the selected point
points = plot.imgLayers[plot.pointsLayerIDs[1]]
selPoint = points.get_offsets()[0]
# Check if a point is selected
if selPoint[0] is not None and selPoint[1] is not None:
# remove selected point from plot
plot.addPoints([None], [None], updateLayer=1)
# then scale and add to homog points list
selPoint = tuple((selPoint / binSize).round().astype(int))
self.homogPoints.append(selPoint)
# update the plotted homog points
homogPoints = np.array(self.homogPoints) * binSize
plot.addPoints(homogPoints[:, 0], homogPoints[:, 1], updateLayer=0)
[docs] def updateHomogPoint(self, homogID, newPoint=None, delta=None):
"""
Update a homog point by either over writing it with a new point or
incrementing the current values.
Parameters
----------
homogID : int
ID (place in list) of point to update or -1 for all.
newPoint : tuple, optional
(x, y) coordinates of new point.
delta : tuple, optional
Increments to current point (dx, dy).
"""
if type(homogID) is not int:
raise Exception("homogID must be an integer.")
if homogID >= len(self.homogPoints):
raise Exception("homogID is out of range.")
# Update all points
if homogID < 0:
for i in range(len(self.homogPoints)):
self.updateHomogPoint(homogID=i, delta=delta)
# Update a single point
else:
# overwrite point
if newPoint is not None:
if type(newPoint) is not tuple and len(newPoint) != 2:
raise Exception("newPoint must be a 2 component tuple")
# increment current point
elif delta is not None:
if type(delta) is not tuple and len(delta) != 2:
raise Exception("delta must be a 2 component tuple")
newPoint = list(self.homogPoints[homogID])
newPoint[0] += delta[0]
newPoint[1] += delta[1]
newPoint = tuple(newPoint)
self.homogPoints[homogID] = newPoint
[docs] @reportProgress("constructing neighbour network")
def buildNeighbourNetwork(self):
"""Construct a list of neighbours
"""
# create network
nn = nx.Graph()
nn.add_nodes_from(self.grainList)
yLocs, xLocs = np.nonzero(self.boundaries)
totalPoints = len(xLocs)
for iPoint, (x, y) in enumerate(zip(xLocs, yLocs)):
# report progress
yield iPoint / totalPoints
if (x == 0 or y == 0 or x == self.grains.shape[1] - 1 or
y == self.grains.shape[0] - 1):
# exclude boundary pixels of map
continue
# use 4 nearest neighbour points as potential neighbour grains
# (this maybe needs changing considering the position of
# boundary pixels relative to the actual edges)
# use sets as they do not allow duplicate elements
# minus 1 on all as the grain image starts labeling at 1
neighbours = {
self.grains[y + 1, x] - 1,
self.grains[y - 1, x] - 1,
self.grains[y, x + 1] - 1,
self.grains[y, x - 1] - 1
}
# neighbours = set(neighbours)
# remove boundary points (-2) and points in small
# grains (-3) (Normally -1 and -2)
neighbours.discard(-2)
neighbours.discard(-3)
neighbours = tuple(neighbours)
nunNeig = len(neighbours)
if nunNeig <= 1:
continue
for i in range(nunNeig):
for j in range(i + 1, nunNeig):
# Add to network
grain = self[neighbours[i]]
neiGrain = self[neighbours[j]]
try:
# look up boundary
nn[grain][neiGrain]
except KeyError:
# neighbour relation doesn't exist so add it
nn.add_edge(grain, neiGrain)
self.neighbourNetwork = nn
[docs] def displayNeighbours(self, **kwargs):
return self.locateGrainID(
clickEvent=self.clickGrainNeighbours, **kwargs
)
[docs] def clickGrainNeighbours(self, event, plot):
"""Event handler to capture clicking and show neighbours of selected grain.
Parameters
----------
event :
Click event.
plot : defdap.plotting.MapPlot
Plot to monitor.
"""
# check if click was on the map
if event.inaxes is not plot.ax:
return
# grain id of selected grain
grainId = int(self.grains[int(event.ydata), int(event.xdata)] - 1)
if grainId < 0:
return
self.currGrainId = grainId
grain = self[grainId]
# find first and second nearest neighbours
firstNeighbours = list(self.neighbourNetwork.neighbors(grain))
highlightGrains = [grain] + firstNeighbours
secondNeighbours = []
for firstNeighbour in firstNeighbours:
trialSecondNeighbours = list(
self.neighbourNetwork.neighbors(firstNeighbour)
)
for secondNeighbour in trialSecondNeighbours:
if (secondNeighbour not in highlightGrains and
secondNeighbour not in secondNeighbours):
secondNeighbours.append(secondNeighbour)
highlightGrains.extend(secondNeighbours)
highlightGrains = [grain.grainID for grain in highlightGrains]
highlightColours = ['white']
highlightColours.extend(['yellow'] * len(firstNeighbours))
highlightColours.append('green')
# update the grain highlights layer in the plot
plot.addGrainHighlights(highlightGrains, grainColours=highlightColours)
@property
def proxigram(self):
"""Proxigram for a map.
Returns
-------
numpy.ndarray
Distance from a grain boundary at each point in map.
"""
self.calcProxigram(forceCalc=False)
return self.proxigramArr
[docs] @reportProgress("calculating proxigram")
def calcProxigram(self, numTrials=500, forceCalc=True):
"""Calculate distance from a grain boundary at each point in map.
Parameters
----------
numTrials : int, optional
number of trials.
forceCalc : bool, optional
Force calculation even is proxigramArr is populated.
"""
if self.proxigramArr is not None and not forceCalc:
return
proxBoundaries = np.copy(self.boundaries)
proxShape = proxBoundaries.shape
# ebsd boundary arrays have extra boundary along right and
# bottom edge. These need to be removed right edge
if np.all(proxBoundaries[:, -1] == -1):
proxBoundaries[:, -1] = proxBoundaries[:, -2]
# bottom edge
if np.all(proxBoundaries[-1, :] == -1):
proxBoundaries[-1, :] = proxBoundaries[-2, :]
# create list of positions of each boundary point
indexBoundaries = []
for index, value in np.ndenumerate(proxBoundaries):
if value == -1:
indexBoundaries.append(index)
# add 0.5 to boundary coordiantes as they are placed on the
# bottom right edge pixels of grains
indexBoundaries = np.array(indexBoundaries) + 0.5
# array of x and y coordinate of each pixel in the map
coords = np.zeros((2, proxShape[0], proxShape[1]), dtype=float)
coords[0], coords[1] = np.meshgrid(
range(proxShape[0]), range(proxShape[1]), indexing='ij'
)
# array to store trial distance from each boundary point
trialDistances = np.full((numTrials + 1, proxShape[0], proxShape[1]),
1000, dtype=float)
# loop over each boundary point (p) and calculate distance from
# p to all points in the map store minimum once numTrails have
# been made and start a new batch of trials
numBoundaryPoints = len(indexBoundaries)
j = 1
for i, indexBoundary in enumerate(indexBoundaries):
trialDistances[j] = np.sqrt((coords[0] - indexBoundary[0])**2
+ (coords[1] - indexBoundary[1])**2)
if j == numTrials:
# find current minimum distances and store
trialDistances[0] = trialDistances.min(axis=0)
j = 0
# report progress
yield i / numBoundaryPoints
j += 1
# find final minimum distances to a boundary
self.proxigramArr = trialDistances.min(axis=0)
trialDistances = None
[docs] def calcGrainAv(self, mapData, grainIds=-1):
"""Calculate grain average of any DIC map data.
Parameters
----------
mapData : numpy.ndarray
Array of map data to grain average. This must be cropped!
grainIds : list, optional
grainIDs to perform operation on, set to -1 for all grains.
Returns
-------
numpy.ndarray
Array containing the grain average values.
"""
# Check that grains have been detected in the map
self.checkGrainsDetected()
if type(grainIds) is int and grainIds == -1:
grainIds = range(len(self))
grainAvData = np.zeros(len(grainIds))
for i, grainId in enumerate(grainIds):
grain = self[grainId]
grainData = grain.grainData(mapData)
grainAvData[i] = grainData.mean()
return grainAvData
[docs] def grainDataToMapData(self, grainData, grainIds=-1, bg=0):
"""Create a map array with each grain filled with the given
values.
Parameters
----------
grainData : list or numpy.ndarray
Grain values. This can be a single value per grain or RGB
values.
grainIds : list of int or int, optional
IDs of grains to plot for. Use -1 for all grains in the map.
bg : int or real, optional
Value to fill the background with.
Returns
-------
grainMap: numpy.ndarray
Array filled with grain data values
"""
# Check that grains have been detected in the map
self.checkGrainsDetected()
if type(grainIds) is int:
if grainIds == -1:
grainIds = range(len(self))
else:
grainIds = [grainIds]
grainData = np.array(grainData)
if grainData.shape[0] != len(grainIds):
raise ValueError("The length of supplied grain data does not"
"match the number of grains.")
if len(grainData.shape) == 1:
mapShape = [self.yDim, self.xDim]
elif len(grainData.shape) == 2 and grainData.shape[1] == 3:
mapShape = [self.yDim, self.xDim, 3]
else:
raise ValueError("The grain data supplied must be either a"
"single value or RGB values per grain.")
grainMap = np.full(mapShape, bg, dtype=grainData.dtype)
for grainId, grainValue in zip(grainIds, grainData):
for coord in self[grainId].coordList:
grainMap[coord[1], coord[0]] = grainValue
return grainMap
[docs] def plotGrainDataMap(
self, mapData=None, grainData=None, grainIds=-1, bg=0, **kwargs
):
"""Plot a grain map with grains coloured by given data. The data
can be provided as a list of values per grain or as a map which
a grain average will be applied.
Parameters
----------
mapData : numpy.ndarray, optional
Array of map data. This must be cropped! Either mapData or
grainData must be supplied.
grainData : list or np.array, optional
Grain values. This an be a single value per grain or RGB
values. You must supply either mapData or grainData.
grainIds: list of int or int, optional
IDs of grains to plot for. Use -1 for all grains in the map.
bg: int or real, optional
Value to fill the background with.
kwargs : dict, optional
Keyword arguments passed to :func:`defdap.plotting.MapPlot.create`
Returns
-------
plot: defdap.plotting.MapPlot
Plot object created
"""
# Set default plot parameters then update with any input
plotParams = {}
plotParams.update(kwargs)
if grainData is None:
if mapData is None:
raise ValueError("Either 'mapData' or 'grainData' must "
"be supplied.")
else:
grainData = self.calcGrainAv(mapData, grainIds=grainIds)
grainMap = self.grainDataToMapData(grainData, grainIds=grainIds,
bg=bg)
plot = MapPlot.create(self, grainMap, **plotParams)
return plot
[docs] def plotGrainDataIPF(
self, direction, mapData=None, grainData=None, grainIds=-1,
**kwargs
):
"""
Plot IPF of grain reference (average) orientations with
points coloured by grain average values from map data.
Parameters
----------
direction : numpy.ndarray
Vector of reference direction for the IPF.
mapData : numpy.ndarray
Array of map data. This must be cropped! Either mapData or
grainData must be supplied.
grainData : list or np.array, optional
Grain values. This an be a single value per grain or RGB
values. You must supply either mapData or grainData.
grainIds: list of int or int, optional
IDs of grains to plot for. Use -1 for all grains in the map.
kwargs : dict, optional
Keyword arguments passed to :func:`defdap.quat.Quat.plotIPF`
"""
# Set default plot parameters then update with any input
plotParams = {}
plotParams.update(kwargs)
if grainData is None:
if mapData is None:
raise ValueError("Either 'mapData' or 'grainData' must "
"be supplied.")
else:
grainData = self.calcGrainAv(mapData, grainIds=grainIds)
# Check that grains have been detected in the map
self.checkGrainsDetected()
if type(grainIds) is int and grainIds == -1:
grainIds = range(len(self))
if len(grainData) != len(grainIds):
raise Exception("Must be 1 value for each grain in grainData.")
grainOri = np.empty(len(grainIds), dtype=Quat)
for i, grainId in enumerate(grainIds):
grain = self[grainId]
grainOri[i] = grain.refOri
plot = Quat.plotIPF(grainOri, direction, self.crystalSym,
c=grainData, **plotParams)
return plot
[docs]class Grain(object):
"""
Base class for a grain.
Attributes
----------
grainID : int
ownerMap : defdap.base.Map
coordList : list of tuples
"""
def __init__(self, grainID, ownerMap):
# list of coords stored as tuples (x, y). These are coords in a
# cropped image if crop exists.
self.grainID = grainID
self.ownerMap = ownerMap
self.coordList = []
def __len__(self):
return len(self.coordList)
def __str__(self):
return f"Grain(ID={self.grainID})"
@property
def extremeCoords(self):
"""Coordinates of the bounding box for a grain.
Returns
-------
int, int, int, int
minimum x, minimum y, maximum x, maximum y.
"""
coords = np.array(self.coordList, dtype=int)
x0, y0 = coords.min(axis=0)
xmax, ymax = coords.max(axis=0)
return x0, y0, xmax, ymax
[docs] def centreCoords(self, centreType="box", grainCoords=True):
"""
Calculates the centre of the grain, either as the centre of the
bounding box or the grains centre of mass.
Parameters
----------
centreType : str, optional, {'box', 'com'}
Set how to calculate the centre. Either 'box' for centre of
bounding box or 'com' for centre of mass. Default is 'box'.
grainCoords : bool, optional
If set True the centre is returned in the grain coordinates
otherwise in the map coordinates. Defaults is grain.
Returns
-------
int, int
Coordinates of centre of grain.
"""
x0, y0, xmax, ymax = self.extremeCoords
if centreType == "box":
xCentre = round((xmax + x0) / 2)
yCentre = round((ymax + y0) / 2)
elif centreType == "com":
xCentre, yCentre = np.array(self.coordList).mean(axis=0).round()
else:
raise ValueError("centreType must be box or com")
if grainCoords:
xCentre -= x0
yCentre -= y0
return int(xCentre), int(yCentre)
[docs] def grainOutline(self, bg=np.nan, fg=0):
"""Generate an array of the grain outline.
Parameters
----------
bg : int
Value for points not within grain.
fg : int
Value for points within grain.
Returns
-------
numpy.ndarray
Bounding box for grain with :obj:`~numpy.nan` outside the grain and given number within.
"""
x0, y0, xmax, ymax = self.extremeCoords
# initialise array with nans so area not in grain displays white
outline = np.full((ymax - y0 + 1, xmax - x0 + 1), bg, dtype=int)
for coord in self.coordList:
outline[coord[1] - y0, coord[0] - x0] = fg
return outline
[docs] def plotOutline(self, ax=None, plotScaleBar=False, **kwargs):
"""Plot the outline of the grain.
Parameters
----------
ax : matplotlib.axes.Axes
axis to plot on, if not provided the current active axis is used.
plotScaleBar : bool
plots the scale bar on the grain if true.
kwargs : dict
keyword arguments passed to :func:`defdap.plotting.GrainPlot.addMap`
Returns
-------
defdap.plotting.GrainPlot
"""
plot = plotting.GrainPlot(self, ax=ax)
plot.addMap(self.grainOutline(), **kwargs)
if plotScaleBar:
plot.addScaleBar()
return plot
[docs] def grainData(self, mapData):
"""Extract this grains data from the given map data.
Parameters
----------
mapData : numpy.ndarray
Array of map data. This must be cropped!
Returns
-------
numpy.ndarray
Array containing this grains values from the given map data.
"""
grainData = np.zeros(len(self), dtype=mapData.dtype)
for i, coord in enumerate(self.coordList):
grainData[i] = mapData[coord[1], coord[0]]
return grainData
[docs] def grainMapData(self, mapData=None, grainData=None, bg=np.nan):
"""Extract a single grain map from the given map data.
Parameters
----------
mapData : numpy.ndarray
Array of map data. This must be cropped! Either this or
'grainData' must be supplied and 'grainData' takes precedence.
grainData : numpy.ndarray
Array of data at each point in the grain. Either this or
'mapData' must be supplied and 'grainData' takes precedence.
bg : various, optional
Value to fill the background with. Must be same dtype as
input array.
Returns
-------
numpy.ndarray
Grain map extracted from given data.
"""
if grainData is None:
if mapData is None:
raise ValueError("Either 'mapData' or 'grainData' must "
"be supplied.")
else:
grainData = self.grainData(mapData)
x0, y0, xmax, ymax = self.extremeCoords
grainMapData = np.full((ymax - y0 + 1, xmax - x0 + 1), bg,
dtype=type(grainData[0]))
for coord, data in zip(self.coordList, grainData):
grainMapData[coord[1] - y0, coord[0] - x0] = data
return grainMapData
[docs] def grainMapDataCoarse(self, mapData=None, grainData=None,
kernelSize=2, bg=np.nan):
"""
Create a coarsed data map of this grain only from the given map
data. Data is coarsened using a kernel at each pixel in the
grain using only data in this grain.
Parameters
----------
mapData : numpy.ndarray
Array of map data. This must be cropped! Either this or
'grainData' must be supplied and 'grainData' takes precedence.
grainData : numpy.ndarray
List of data at each point in the grain. Either this or
'mapData' must be supplied and 'grainData' takes precedence.
kernelSize : int, optional
Size of kernel as the number of pixels to dilate by i.e 1
gives a 3x3 kernel.
bg : various, optional
Value to fill the background with. Must be same dtype as
input array.
Returns
-------
numpy.ndarray
Map of this grains coarsened data.
"""
grainMapData = self.grainMapData(mapData=mapData, grainData=grainData)
grainMapDataCoarse = np.full_like(grainMapData, np.nan)
for i, j in np.ndindex(grainMapData.shape):
if np.isnan(grainMapData[i, j]):
grainMapDataCoarse[i, j] = bg
else:
coarseValue = 0
if i - kernelSize >= 0:
yLow = i - kernelSize
else:
yLow = 0
if i + kernelSize + 1 <= grainMapData.shape[0]:
yHigh = i + kernelSize + 1
else:
yHigh = grainMapData.shape[0]
if j - kernelSize >= 0:
xLow = j - kernelSize
else:
xLow = 0
if j + kernelSize + 1 <= grainMapData.shape[1]:
xHigh = j + kernelSize + 1
else:
xHigh = grainMapData.shape[1]
numPoints = 0
for k in range(yLow, yHigh):
for l in range(xLow, xHigh):
if not np.isnan(grainMapData[k, l]):
coarseValue += grainMapData[k, l]
numPoints += 1
if numPoints > 0:
grainMapDataCoarse[i, j] = coarseValue / numPoints
else:
grainMapDataCoarse[i, j] = np.nan
return grainMapDataCoarse
[docs] def plotGrainData(self, mapData=None, grainData=None, **kwargs):
"""
Plot a map of this grain from the given map data.
Parameters
----------
mapData : numpy.ndarray
Array of map data. This must be cropped! Either this or
'grainData' must be supplied and 'grainData' takes precedence.
grainData : numpy.ndarray
List of data at each point in the grain. Either this or
'mapData' must be supplied and 'grainData' takes precedence.
kwargs : dict, optional
Keyword arguments passed to :func:`defdap.plotting.GrainPlot.create`
"""
# Set default plot parameters then update with any input
plotParams = {}
plotParams.update(kwargs)
grainMapData = self.grainMapData(mapData=mapData, grainData=grainData)
plot = GrainPlot.create(self, grainMapData, **plotParams)
return plot