# -*- coding: utf-8 -*-
"""
.. module:: image_utils
:synopsis: helper module for image data handling
"""
import numpy as np
import dicom
import cv2
import SimpleITK as sitk
import os.path
[docs]def createImageCorners(w, h, pixel2mmX, pixel2mmY):
"""Create corner coordinates for an image.
Top-left corner is supposed to be the (0, 0) corner.
Parameters
----------
w : int
Image width (in *pixel*)
h : int
Image height (in *pixel*)
pixel2mmX, pixel2mmY : float
Number of mm for each pixel in US image, for horizontal and vertical axis (in *mm/pixel*)
Returns
-------
np.ndarray
4 x 4 array of coordinates. Each column is a corner. To (x, y), (z, 1)
are also added to make them ready to be mulitplied by a roto-translation
matrix.
"""
pc = np.array((
(w*pixel2mmX,0,0,1),
(w*pixel2mmX,h*pixel2mmY,0,1),
(0,0,0,1),
(0,h*pixel2mmY,0,1),
)).T
return pc
[docs]def createImageCoords(h, w, pixel2mmY, pixel2mmX):
"""Create all pixel coordinates for an image.
Top-left corner is supposed to be the (0, 0) corner.
Parameters
----------
w : int
Image width (in *pixel*)
h : int
Image height (in *pixel*)
pixel2mmX, pixel2mmY : float
Number of mm for each pixel in US image, for horizontal and vertical axis (in *mm/pixel*)
Returns
-------
np.ndarray
4 x (w * h) array of coordinates. Each column is a point. To (x, y), (z, 1)
are also added to make them ready to be mulitplied by a roto-translation
matrix.
"""
Np = h * w
x = np.linspace(0,w-1,w) * pixel2mmX
y = np.linspace(0,h-1,h) * pixel2mmY
xv, yv = np.meshgrid(x, y)
xv = np.reshape(xv.ravel(), (1,Np))
yv = np.reshape(yv.ravel(), (1,Np))
zv = np.zeros((1,Np))
b = np.ones((1,Np))
p = np.concatenate((xv,yv,zv,b), axis=0) # 4 x Np
return p
[docs]def createCenteredMaskCoords(cx, cy, h, w):
"""Create all pixel coordinates for a centered mask around a point.
Center point is (cx, cy).
Parameters
----------
cx : int
X coordinate for mask center. If None, it will be set as half of the w.
cy : int
Y coordinate for mask center.If None, it will be set as half of the h.
w : int
Mask width. Should be odd.
h : int
Mask height. Should be odd.
Returns
-------
np.ndarray
(w * h) x 2 array of coordinates. Each row is a point.
"""
if cx is None:
cx = (w-1)/2
if cy is None:
cy = (h-1)/2
x = (np.linspace(-(w-1)/2,(w-1)/2,w) + cx).astype(np.int32)
y = (np.linspace(-(h-1)/2,(h-1)/2,h) + cy).astype(np.int32)
xv, yv = np.meshgrid(x, y)
C = np.array([xv.ravel(), yv.ravel()]).T.astype(np.float32)
return C
[docs]def createRandomInMaskCoords(cx, cy, h, w, N):
"""Create random pixel coordinates for a centered mask around a point.
Center point is (cx, cy).
Parameters
----------
cx : int
X coordinate for mask center.
cy : int
Y coordinate for mask center.
w : int
Mask width. Should be odd.
h : int
Mask height. Should be odd.
N : int
Number of coordinates to generate.
Returns
-------
np.ndarray
N x 2 array of coordinates. Each row is a point.
"""
C = createCenteredMaskCoords(cx, cy, h, w)
idx = np.random.randint(h*w, size=N)
Cs = C[idx,:]
return Cs
[docs]def pixelData2grey(D):
"""Convert pixel array to grey values.
Parameters
----------
D : np.ndarray
Pixel array, in format Nch x Nf x Nr x Nc, to convert.
If Nch is 3, then channels are supposed to be R, G, B.
If Nch is 2, then the values are supposed to be grey level and alpha.
If Nch is 1, then the values are supposed to be grey level.
Returns
-------
np.ndarray
Nf x Nr x Nc array of grey level.
"""
d = D.shape
if d[0] < 3:
I = D[0,:]
elif d[0] == 3:
I = rgb2grey(D[0,:], D[1,:], D[2,:])
else:
raise Exception('Image data format not recognized')
return I
[docs]def rgb2grey(R, G, B):
"""Convert RGB channels to grey levels, by using the formula `here <http://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems>`_.
Parameters
----------
R, G, B : np.ndarray
Arrays containing red, green and blue values. R,G, B must have the same dimensions.
Returns
-------
np.ndarray
Array of grey levels, having the same dimensions of either R, G or B.
"""
I = (.2126*R+.7152*G+.0722*B).astype(np.uint8)
return I
[docs]def readDICOM(filePath, method='flattened'):
"""Read DICOM file (containing data for Nc channels, Nf frames, and images of size Nr x Nc).
Parameters
----------
filePath : str
DICOM full file path.
method : str
Pixel array parsing method.
If 'RGB', pixel array is supposed to be 3 x Nf x Nr x Nc. Data for frame i is into [:,i,:,:].
If 'flattened', pixel array is supposed to be Nch x Nf x Nr x Nc. Data for frame i is into [j,k:k+Nch,:,:], where j = floor(Nch*i / Nf), k = (Nch*i) % Nf.
When using 'flattened', pixel array with dimension Nf x Nr x Nc is also supprted (the only stored value is supposed to be a grey level).
Returns
-------
D : np.ndarray
Pixel array reshaped in the standard way Nch x Nf x Nr x Nc as for ``method='RGB'``.
ds : dicom.dataset.FileDataset
Additional parameters in the DICOM file.
"""
ds = dicom.read_file(filePath)
D = ds.pixel_array
if method == 'RGB':
pass
if method == 'flattened':
if len(D.shape) == 4: # first dimension keeps or data channels (RGB, grey-alpha, grey, ...)
N = D.shape[0]
D = np.reshape(D, (D.shape[0]*D.shape[1],D.shape[2],D.shape[3]))[None,:]
D = D[:,::N,:,:]
elif len(D.shape) == 3:
D = D[None,:]
else:
raise Exception('{0}: unknown data format'.format(method))
return D, ds
[docs]def readSITK(filePath):
"""Helper for reading SITK-compatible input file
Parameters
----------
filePath : str
Full file path.
Returns
-------
I : np.ndarray
Pixel array (dimensions depend on input dimensions).
image : sitk.Image
Image object as read by SITK.
"""
# Read image file
reader = sitk.ImageFileReader()
reader.SetFileName(filePath)
image = reader.Execute()
# Convert images to Numpy arrays
I = sitk.GetArrayFromImage(image)
return I, image
def getFileExt(fname):
ext = os.path.splitext(fname)[1][1:]
return ext
[docs]def readImage(img, reader='sitk', **kwargs):
"""Helper for reading image sequence input file.
Parameters
----------
img : mixed
Input data.
If str, it is the file path.
Otherwise, it must be a list where the first element represents array
data, and the second the metadata. This list is just unpacked and
returned in output.
reader : str
The specific reader to be used:
- 'sitk': ``readSITK()`` is called.
- 'pydicom': ``readDICOM()`` is called.
**kwargs : dict
Additional keyword arguments to be passed to the specific reader.
Returns
-------
I : np.ndarray
Pixel array (see specific readers for more details).
metadata : dict
Dictionary containing metadata information. These following are the
available keys. If an item was not able to be retrieved, it is None.
- 'frame_rate': frame rate acquisition (in Hz)
- 'raw_obj': the object as read by the specific reader
"""
if isinstance(img, basestring):
# Get file path and extension
imgFile = img
ext = getFileExt(imgFile)
frame_rate = None
raw_obj = None
if reader == 'sitk':
# Read SITK file
I, image = readSITK(imgFile)
# Get frame rate
if ext == 'dcm':
md_keys = image.GetMetaDataKeys()
cine_rate_key = '0018|0040'
if cine_rate_key in md_keys:
frame_rate = float(image.GetMetaData(cine_rate_key))
else:
raise Exception('Data format unknown')
raw_obj = image
if reader == 'pydicom':
# Read DICOM uncompressed file
I, ds = readDICOM(imgFile, **kwargs)
# Get frame rate
if ext == 'dcm':
if 'CineRate' in ds:
frame_rate = float(ds.CineRate)
else:
raise Exception('pydicom can only read DCM uncompressed files')
raw_obj = ds
# Create metadata dict
metadata = {}
metadata['frame_rate'] = frame_rate
metadata['raw_obj'] = raw_obj
else:
# Data unpacking
I, metadata = img
return I, metadata
[docs]def NCC(I1, I2):
"""Calculate Normalized Cross-Correlation between 2 binary images.
Parameters
----------
I1, I2 : np.ndarray(uint8)
The 2 binary images, same size is required.
Returns
-------
float
NCC.
"""
m1 = np.mean(I1)
s1 = np.std(I1)
m2 = np.mean(I2)
s2 = np.std(I2)
if s1 == 0 or s2 == 0:
NCC = 0.
else:
demI1 = I1 - m1
demI2 = I2 - m2
prodI = demI1 * demI2
prodI *= 1. / (s1 * s2)
NCC = np.mean(prodI)
return NCC
[docs]def CD2(I1, I2):
"""Calculate CD2 similarity measure (logarithm of division of Rayleigh
noises). Images are supposed to be log-compressed.
Parameters
----------
I1, I2 : np.ndarray(uint8)
The 2 binary images, same size is required.
Returns
-------
float
CD2.
"""
#logI1, logI2 = np.log(I1), np.log(I2)
logI1, logI2 = I1.astype(np.float), I2.astype(np.float)
logD = logI1 - logI2
logP = logD - np.log(np.exp(2.*logD) + 1.)
CD2 = logP.sum()
#CD2 = np.exp(logP.sum())
return CD2
[docs]def histogramsSimilarity(H1, H2, meas='bhattacharyya_coef'):
"""Calculate similarity measure between histograms.
Parameters
----------
H1, H2 : np.ndarray
The 2 histograms, same size is required.
dist : str
The kind of measure to compute.
Allowed values: 'bhattacharyya_coef'.
Returns
-------
float
Similarity measure.
"""
if meas == 'bhattacharyya_coef':
B = cv2.compareHist(H1, H2, cv2.cv.CV_COMP_BHATTACHARYYA)
M = 1 - B**2
return M
[docs]def createWhiteMask(frameGray, cx, cy, h, w):
"""Create white mask in a grayscale frame, centered around (cx, cy).
Parameters
----------
frameGray : np.ndarray
frame to copy from for creating a new one with the mask
cx : int
X coordinate for mask center.
cy : int
Y coordinate for mask center.
w : int
Mask width. Should be odd.
h : int
Mask height. Should be odd.
Returns
-------
tuple
First element is the frame containing the white mask, and black around.
Second element is the mask coordinates.
"""
new_mask = np.empty_like(frameGray)
new_mask[:] = 0
pts = createCenteredMaskCoords(round(cx), round(cy), h, w)
a, b = pts[:,0].min(), pts[:,1].min()
c, d = pts[:,0].max(), pts[:,1].max()
new_mask2 = cv2.rectangle(new_mask, (a,b),(c,d), 255, -1)
if new_mask2 is not None:
new_mask = new_mask2
return new_mask, pts
[docs]def findCornersInMask(frameGray, cx, cy, h, w, featureParams):
"""Find Shi-Tomasi corners in a subpart of a frame.
The research mask is centered around (cx, cy).
Parameters
----------
frameGray : np.ndarray
frame to search corners from.
cx : int
X coordinate for mask center.
cy : int
Y coordinate for mask center.
w : int
Mask width. Should be odd.
h : int
Mask height. Should be odd.
featureParams : dict
See **kwargs in ``cv2.cv2.goodFeaturesToTrack()``.
Returns
-------
tuple
First element is a N x 1 x 2 array containing coordinates of N good
corners to track. Second element being the frame contanining the mask.
"""
new_mask, pts = createWhiteMask(frameGray, cx, cy, h, w)
new_p0 = cv2.goodFeaturesToTrack(frameGray, mask=new_mask, **featureParams)
return new_p0, new_mask
[docs]def matchTemplate(SW, T, meas, **kwargs):
"""Execute template match between a template match and a search window,
by using different similarity measures.
Parameters
----------
SW : np.ndarray(H x W)
Search window.
T : np.ndarray(h x w)
Template to search.
meas : mixed
If str, it can be 'bhattacharyya_coef', 'CD2'.
It can also be an OpenCV constant (e.g. cv2.TM_CCORR_NORMED), and
``cv2.matchTemplate()`` will be called instead.
**kwargs : dict
Additional arguments.
For 'bhattacharyya_coef', they are:
- 'nBins': number of bins for histograms calculation.
Returns
-------
np.ndarray(H-h+1 x W-w+1)
Matrix containing similarity measures.
"""
H, W = SW.shape
h, w = T.shape
res = np.zeros((H-h+1,W-w+1))
C = createCenteredMaskCoords(None, None, H-h+1, W-w+1).astype(np.int32)
Cres = C.copy()
C += (w-1)/2, (h-1)/2
if meas in ['bhattacharyya_coef']:
nBins = kwargs['nBins']
I1 = T
H1 = cv2.calcHist([I1],[0],None,[nBins],[0,256])
for i in xrange(C.shape[0]):
c = C[i,:]
I2 = SW[c[1]-(h-1)/2:c[1]+(h-1)/2+1, c[0]-(w-1)/2:c[0]+(w-1)/2+1]
H2 = cv2.calcHist([I2],[0],None,[nBins],[0,256])
res[Cres[i,:]] = histogramsSimilarity(H1, H2, meas=meas, **kwargs)
if meas == 'CD2':
I1 = T
for i in xrange(C.shape[0]):
c = C[i,:]
I2 = SW[c[1]-(h-1)/2:c[1]+(h-1)/2+1, c[0]-(w-1)/2:c[0]+(w-1)/2+1]
c2 = Cres[i,:]
res[c2[1],c2[0]] = CD2(I1, I2)
else:
res = cv2.matchTemplate(SW, T, meas)
return res
def jointHistogram(I1, I2, **kwargs):
# from: http://pythonhosted.org/MedPy/_modules/medpy/metric/image.html
bins = kwargs['bins']
r1 = histogramRange(I1, bins)
r2 = histogramRange(I1, bins)
r = [r1, r2]
hist, xedges, yedges = np.histogram2d(I1.ravel(), I2.ravel(), bins=bins, range=r)
return hist
def marginalHistogram(I, **kwargs):
bins = kwargs['bins']
r = histogramRange(I, bins)
hist, xedges = np.histogram(I.ravel(), bins=bins, range=r)
return hist
def jointEntropy(I1, I2, **kwargs):
hist = jointHistogram(I1, I2, **kwargs)
p = hist.ravel()
H = entropy(p)
return H
def marginalEntropy(I, **kwargs):
hist = marginalHistogram(I, **kwargs)
p = hist.ravel()
H = entropy(p)
return H
def entropy(p):
p = p / float(p.sum())
i = (p > 0)
H = np.sum(-p[i] * np.log2(p[i]))
return H
def histogramRange(I, bins):
I = np.asarray(I)
Imax = I.max()
Imin = I.min()
s = 0.5 * (Imax - Imin) / float(bins - 1)
return (Imin - s, Imax + s)
def MI(I1, I2, **kwargs):
# http://stackoverflow.com/questions/20491028/optimal-way-to-compute-pairwise-mutual-information-using-numpy
H1 = marginalEntropy(I1, **kwargs)
H2 = marginalEntropy(I2, **kwargs)
H12 = jointEntropy(I1, I2, **kwargs)
return H1 + H2 - H12
def NMI(I1, I2, **kwargs):
H1 = marginalEntropy(I1, **kwargs)
H2 = marginalEntropy(I2, **kwargs)
H12 = jointEntropy(I1, I2, **kwargs)
return (H1 + H2) / H12
def SUC(I1, I2, **kwargs):
H1 = marginalEntropy(I1, **kwargs)
H2 = marginalEntropy(I2, **kwargs)
H12 = jointEntropy(I1, I2, **kwargs)
return 2. * (1 - (H12 / (H1 + H2)))