import numpy

r"""
First-order statistics describe the distribution of voxel intensities within the image region defined by the mask
through commonly used and basic metrics.

Let:

:math:\textbf{X} denote the three dimensional image matrix with :math:N voxels

:math:\textbf{P}(i) the first order histogram with :math:N_l discrete intensity levels,
where :math:N_l is defined by the number of levels, calculated based on the binWidth parameter.

:math:p(i) be the normalized first order histogram and equal to :math:\frac{\textbf{P}(i)}{\sum{\textbf{P}(i)}}

- voxelArrayShift [0]: Integer, This amount is added to the gray level intensity in features Energy, Total Energy and
RMS, this is to prevent negative values. __If using CT data, or data normalized with mean 0, consider setting this
parameter to a fixed value (e.g. 2000) that ensures non-negative numbers in the image. Bear in mind however, that
the larger the value, the larger the volume confounding effect will be.__
"""

self.pixelSpacing = inputImage.GetSpacing()
self.voxelArrayShift = kwargs.get('voxelArrayShift', 0)

self.logger.debug('Feature class initialized')

def _moment(self, a, moment=1, axis=0):
r"""
Calculate n-order moment of an array for a given axis
"""

if moment == 1:
return numpy.float64(0.0)
else:
mn = numpy.mean(a, axis, keepdims=True)
s = numpy.power((a - mn), moment)
return numpy.mean(s, axis)

[docs]  def getEnergyFeatureValue(self):
r"""
Calculate the Energy of the image array.

:math:energy = \displaystyle\sum^{N}_{i=1}{(\textbf{X}(i) + c)^2}

Here, :math:c is optional value, defined by voxelArrayShift, which shifts the intensities to prevent negative
values in :math:\textbf{X}. This ensures that voxels with the lowest gray values contribute the least to Energy,
instead of voxels with gray level intensity closest to 0.

Energy is a measure of the magnitude of voxel values in an image. A larger values implies a greater sum of the
squares of these values. This feature is volume-confounded, a larger value of :math:c increases the effect of
volume-confounding.
"""

shiftedParameterArray = self.targetVoxelArray + self.voxelArrayShift

return (numpy.sum(shiftedParameterArray ** 2))

[docs]  def getTotalEnergyFeatureValue(self):
r"""
Calculate the Total Energy of the image array.

:math:total\ energy = V_{voxel}\displaystyle\sum^{N}_{i=1}{(\textbf{X}(i) + c)^2}

Here, :math:c is optional value, defined by voxelArrayShift, which shifts the intensities to prevent negative
values in :math:\textbf{X}. This ensures that voxels with the lowest gray values contribute the least to Energy,
instead of voxels with gray level intensity closest to 0.

Total Energy is the value of Energy feature scaled by the volume of the voxel in cubic mm. This feature is
volume-confounded, a larger value of :math:c increases the effect of volume-confounding.
"""

x, y, z = self.pixelSpacing
cubicMMPerVoxel = x * y * z

return (cubicMMPerVoxel * self.getEnergyFeatureValue())

[docs]  def getEntropyFeatureValue(self):
r"""
Calculate the Entropy of the image array.

:math:entropy = -\displaystyle\sum^{N_l}_{i=1}{p(i)\log_2\big(p(i)+\epsilon\big)}

Here, :math:\epsilon is an arbitrarily small positive number (:math:\approx 2.2\times10^{-16}).

Entropy specifies the uncertainty/randomness in the image values. It measures the average amount of information
required to encode the image values.
"""

eps = numpy.spacing(1)
binEdges = imageoperations.getBinEdges(self.binWidth, self.targetVoxelArray)
bins = numpy.histogram(self.targetVoxelArray, binEdges)[0]
try:
bins = bins + eps
bins = bins / float(bins.sum())
return (-1.0 * numpy.sum(bins * numpy.log2(bins)))
except ZeroDivisionError:
return numpy.core.nan

[docs]  def getMinimumFeatureValue(self):
r"""
Calculate the Minimum Value in the image array.

:math:minimum = \min(\textbf{X})
"""

return (numpy.min(self.targetVoxelArray))

[docs]  def get10PercentileFeatureValue(self):
r"""
Calculate the 10\ :sup:th percentile in the image array.
"""

return (numpy.percentile(self.targetVoxelArray, 10))

[docs]  def get90PercentileFeatureValue(self):
r"""
Calculate the 90\ :sup:th percentile in the image array.
"""

return (numpy.percentile(self.targetVoxelArray, 90))

[docs]  def getMaximumFeatureValue(self):
r"""
Calculate the Maximum Value in the image array.

:math:maximum = \max(\textbf{X})
"""

return (numpy.max(self.targetVoxelArray))

[docs]  def getMeanFeatureValue(self):
r"""
Calculate the Mean Value for the image array.

:math:mean = \frac{1}{N}\displaystyle\sum^{N}_{i=1}{\textbf{X}(i)}
"""

return (numpy.mean(self.targetVoxelArray))

[docs]  def getMedianFeatureValue(self):
r"""
Calculate the Median Value for the image array.
"""

return (numpy.median(self.targetVoxelArray))

[docs]  def getInterquartileRangeFeatureValue(self):
r"""
Calculate the interquartile range of the image array.

:math:interquartile\ range = \textbf{P}_{75} - \textbf{P}_{25}, where :math:\textbf{P}_{25} and
:math:\textbf{P}_{75} are the 25\ :sup:th and 75\ :sup:th percentile of the image array, respectively.
"""

return numpy.percentile(self.targetVoxelArray, 75) - numpy.percentile(self.targetVoxelArray, 25)

[docs]  def getRangeFeatureValue(self):
r"""
Calculate the Range of Values in the image array.

:math:range = \max(\textbf{X}) - \min(\textbf{X})
"""

return (numpy.max(self.targetVoxelArray) - numpy.min(self.targetVoxelArray))

[docs]  def getMeanAbsoluteDeviationFeatureValue(self):
r"""
Calculate the Mean Absolute Deviation for the image array.

:math:mean\ absolute\ deviation = \frac{1}{N}\displaystyle\sum^{N}_{i=1}{|\textbf{X}(i)-\bar{X}|}

Mean Absolute Deviation is the mean distance of all intensity values from the Mean Value of the image array.
"""

return (numpy.mean(numpy.absolute((numpy.mean(self.targetVoxelArray) - self.targetVoxelArray))))

[docs]  def getRobustMeanAbsoluteDeviationFeatureValue(self):
r"""
Calculate the Robust Mean Absolute Deviation for the image array.

:math:robust\ mean\ absolute\ deviation = \frac{1}{N_{10-90}}\displaystyle\sum^{N_{10-90}}_{i=1}{|\textbf{X}_{10-90}(i)-\bar{X}_{10-90}|}

Robust Mean Absolute Deviation is the mean distance of all intensity values
from the Mean Value calculated on the subset of image array with gray levels in between, or equal
to the 10\ :sup:th and 90\ :sup:th percentile.
"""

prcnt10 = self.get10PercentileFeatureValue()
prcnt90 = self.get90PercentileFeatureValue()
percentileArray = self.targetVoxelArray[(self.targetVoxelArray >= prcnt10) * (self.targetVoxelArray <= prcnt90)]

return numpy.mean(numpy.absolute(percentileArray - numpy.mean(percentileArray)))

[docs]  def getRootMeanSquaredFeatureValue(self):
r"""
Calculate the Root Mean Squared of the image array.

:math:RMS = \sqrt{\frac{1}{N}\sum^{N}_{i=1}{(\textbf{X}(i) + c)^2}}

Here, :math:c is optional value, defined by voxelArrayShift, which shifts the intensities to prevent negative
values in :math:\textbf{X}. This ensures that voxels with the lowest gray values contribute the least to RMS,
instead of voxels with gray level intensity closest to 0.

RMS is the square-root of the mean of all the squared intensity values. It is another measure of the magnitude of
the image values. This feature is volume-confounded, a larger value of :math:c increases the effect of
volume-confounding.
"""

shiftedParameterArray = self.targetVoxelArray + self.voxelArrayShift
return (numpy.sqrt((numpy.sum(shiftedParameterArray ** 2)) / float(shiftedParameterArray.size)))

[docs]  def getStandardDeviationFeatureValue(self):
r"""
Calculate the Standard Deviation of the image array.

:math:standard\ deviation = \sqrt{\frac{1}{N}\sum^{N}_{i=1}{(\textbf{X}(i)-\bar{X})^2}}

Standard Deviation measures the amount of variation or dispersion from the Mean Value.
"""

return (numpy.std(self.targetVoxelArray))

[docs]  def getSkewnessFeatureValue(self, axis=0):
r"""
Calculate the Skewness of the image array.

:math:skewness = \displaystyle\frac{\mu_3}{\sigma^3}
= \frac{\frac{1}{N}\sum^{N}_{i=1}{(\textbf{X}(i)-\bar{X})^3}}
{\left(\sqrt{\frac{1}{N}\sum^{N}_{i=1}{(\textbf{X}(i)-\bar{X})^2}}\right)^3}

Where :math:\mu_3 is the 3\ :sup:rd central moment.

Skewness measures the asymmetry of the distribution of values about the Mean value. Depending on where the tail is
elongated and the mass of the distribution is concentrated, this value can be positive or negative.

https://en.wikipedia.org/wiki/Skewness
"""

m2 = self._moment(self.targetVoxelArray, 2, axis)
m3 = self._moment(self.targetVoxelArray, 3, axis)

if (m2 == 0): return numpy.core.nan

return m3 / m2 ** 1.5

[docs]  def getKurtosisFeatureValue(self, axis=0):
r"""
Calculate the Kurtosis of the image array.

:math:kurtosis = \displaystyle\frac{\mu_4}{\sigma^4}
= \frac{\frac{1}{N}\sum^{N}_{i=1}{(\textbf{X}(i)-\bar{X})^4}}
{\left(\frac{1}{N}\sum^{N}_{i=1}{(\textbf{X}(i)-\bar{X}})^2\right)^2}

Where :math:\mu_4 is the 4\ :sup:th central moment.

Kurtosis is a measure of the 'peakedness' of the distribution of values in the image ROI. A higher kurtosis implies
that the mass of the distribution is concentrated towards the tail(s) rather than towards the mean. A lower kurtosis
implies the reverse: that the mass of the distribution is concentrated towards a spike near the Mean value.

https://en.wikipedia.org/wiki/Kurtosis
"""

m2 = self._moment(self.targetVoxelArray, 2, axis)
m4 = self._moment(self.targetVoxelArray, 4, axis)

if (m2 == 0): return numpy.core.nan

return m4 / m2 ** 2.0

[docs]  def getVarianceFeatureValue(self):
r"""
Calculate the Variance in the image array.

:math:variance = \sigma^2 = \frac{1}{N}\displaystyle\sum^{N}_{i=1}{(\textbf{X}(i)-\bar{X})^2}

Variance is the the mean of the squared distances of each intensity value from the Mean value. This is a measure of
"""

return (numpy.std(self.targetVoxelArray) ** 2)

[docs]  def getUniformityFeatureValue(self):
r"""
Calculate the Uniformity of the image array.

:math:uniformity = \displaystyle\sum^{N_l}_{i=1}{p(i)^2}

Uniformity is a measure of the sum of the squares of each intensity value. This is a measure of the heterogeneity of
the image array, where a greater uniformity implies a greater heterogeneity or a greater range of discrete intensity
values.
"""

eps = numpy.spacing(1)
binEdges = imageoperations.getBinEdges(self.binWidth, self.targetVoxelArray)
bins = numpy.histogram(self.targetVoxelArray, binEdges)[0]
try:
bins = bins / (float(bins.sum() + eps))
return (numpy.sum(bins ** 2))
except ZeroDivisionError:
return numpy.core.nan