import itk
from scipy.signal import find_peaks
from skimage.morphology import disk
from skimage.morphology import dilation
from skimage.feature import match_template
import skvideo.io
from itkpocus.util import get_framerate
import skimage.filters
import numpy as np
import os
import itkpocus.util
def load_and_preprocess_video(fp, version=None):
'''
Loads and preprocesses a Sonosite video.
Parameters
----------
fp : str
filepath to video (e.g. .mp4)
version : optional
Reserved for future use.
Returns
-------
img : itk.Image[itk.F,3]
Floating point image with physical spacing set (3rd dimension is framerate), origin is at upper-left of image, and intensities are between 0.0 and 1.0
meta : dict
Meta data (includes spacing and crop)
'''
npvid_rgb = skvideo.io.vread(fp)
return preprocess_video(npvid_rgb, framerate=get_framerate(skvideo.io.ffprobe(fp)))
def load_and_preprocess_image(fp, version=None):
'''
Loads and preprocesses a Sonosite image.
Parameters
----------
fp : str
filepath to image
version : optional
Reserved for future use.
Returns
-------
img : itk.Image[itk.F,2]
Floating point image with physical spacing set (3rd dimension is framerate), origin is at upper-left of image, and intensities are between 0.0 and 1.0
meta : dict
Meta data (includes spacing and crop)
'''
npimg_rgb = itk.array_from_image(itk.imread(fp))
return preprocess_image(npimg_rgb)
def _find_spacing(npimg):
'''
Finds the white ticks on the right of the image and calculates their spacing.
Parameters
-----------
npimg (numpy array): 0-255 numpy array MxN
Returns
-------
spacing : float
mm per pixel
'''
rulerpos = [npimg.shape[1]-6, npimg.shape[1]]
rulerimg = npimg[:,rulerpos[0]:rulerpos[1]].copy()
ruler1d = np.max(rulerimg, axis=1)
peaks, properties = find_peaks(ruler1d, height=[240,255], distance=5)
rulerdiff = peaks[1:-1] - peaks[:-2]
if np.std(rulerdiff) >= 2.0:
raise ValueError("Error detecting ruler, peaks not equally-spaced {}".format(peaks))
pixelsize = 5.0 / np.mean(rulerdiff) # in mm, 0.5 cm ruler breaks
return pixelsize
def _normalize(npimg, npimgrgb):
'''
A bunch of pixel-hacking to find the overlay elements in the image. Returns the overlay (necessary)
for cropping later on and the image with the overlay elements in-filled (median filter at overlay pixels).
Returns
-------
npnorm : ndarray
MxN normalized image to be cropped later (median filtered hud elements)
npmasked : ndarray
MxN hud image that is input to cropping algorithm
'''
# try to detect hud elements by their color (ultrasound should be grey) and pure whiteness
npcolor = np.logical_not(np.logical_and(npimgrgb[:,:,0] == npimgrgb[:,:,1], npimgrgb[:,:,1] == npimgrgb[:,:,2]))
npwhite = npimg > 235
npblack = npimg < 10 # tries to get rid of black noise
nphud = np.logical_or(npcolor, npwhite, npblack)
nphud2 = dilation(nphud, disk(4))
npmasked = npimg.copy()
npmasked[nphud2] = 0
# now we have a cropped image, need to get rid of the annotation marks
nphud3 = dilation(nphud, disk(2))
npmed = skimage.filters.median(npimg, disk(4))
npnorm = npimg.copy()
npnorm[nphud3] = npmed[nphud3]
return npnorm, npmasked
def _crop_image(npimg, crop):
'''
Crops an ndarray.
Parameters
----------
npimg : ndarray
MxN
crop : ndarray
2x2 [[ymin, ymax], [xmin, xmax]] where y=row and x=column
Returns
-------
ndarray
'''
return npimg[crop[0,0]:crop[0,1]+1, crop[1,0]:crop[1,1]+1]
def _find_crop(npnorm, npmasked):
'''
A bunch of funkiness with removing the SonoSite overlay. Overlay elements can overlap the ultrasound image
and jpeg compression can add noise. This works by a fixed crop in the y-direction and then a search on the
overlay-removed image to find the x-direction boundaries.
Returns
-------
ndarray
2x2 [[ymin, ymax], [xmin, xmax]] where y=row, x=column
'''
def array_crop(arr, threshold):
c1 = 0
c2 = len(arr)-1
while (c1 <= c2 and arr[c1] < threshold):
c1 += 1
while (c2 > c1 and arr[c2] < threshold):
c2 -= 1
return np.array([c1, c2])
# oy, hard-code cropping, then looking for image intensity in the x-axis
# zooming seems to stretch the image in x, but y is always maxed
# confounding is grey-scale overlay elements and jpeg compression artifacts
crop1 = np.array([[50, 620], [180, 830]])
npcrop1 = _crop_image(npmasked, crop1)
xintensity = np.sum(npcrop1, axis=0)
yintensity = np.sum(npcrop1, axis=1)
xcrop = array_crop(xintensity, 2000)
crop2 = np.array([crop1[0,:], [crop1[1,0] + xcrop[0], crop1[1,1]+xcrop[1]-npcrop1.shape[1]]])
return crop2
[docs]def preprocess_image(npimg, version=None):
'''
Loads and preprocesses a Sonosite image.
Parameters
----------
npimg : ndarray
MxNx3 image array
version : optional
Reserved for future use.
Returns
-------
img : itk.Image[itk.F,2]
Floating point image with physical spacing set, origin is at upper-left of image, and intensities are between 0.0 and 1.0
meta : dict
Meta data (includes spacing and crop)
'''
npimg_bw = npimg[:,:,0].squeeze()
spacing = _find_spacing(npimg_bw)
npnorm, npmasked = _normalize(npimg_bw, npimg)
crop = _find_crop(npnorm, npmasked)
npnormcrop = _crop_image(npnorm, crop) # this is the us image for analysis
img = itk.image_from_array((npnormcrop / 255.0).astype('float32'))
img.SetSpacing(np.array([spacing, spacing]))
return img, {'spacing' : np.array([spacing, spacing]), 'crop' : crop}
[docs]def preprocess_video(npvid, framerate=1, version=None):
'''
Loads and preprocesses a Sonosite video.
Parameters
----------
ndarray : ndarray
FxMxNxRGB
version : optional
Reserved for future use.
Returns
-------
img : itk.Image[itk.F,3]
Floating point image with physical spacing set (3rd dimension is framerate), origin is at upper-left of image, and intensities are between 0.0 and 1.0
meta : dict
Meta data (includes spacing and crop)
'''
frm_cnt = npvid.shape[0]
frm1rgb = npvid[0,:,:,:].squeeze()
frm1 = frm1rgb[:,:,0].squeeze()
spacing = _find_spacing(frm1)
tmpnorm, tmpmasked = _normalize(frm1, frm1rgb)
crop = _find_crop(tmpnorm, tmpmasked)
tmpnormcrop = _crop_image(tmpnorm, crop)
npnormvid = np.zeros([frm_cnt, tmpnormcrop.shape[0], tmpnormcrop.shape[1]])
for i in range(frm_cnt):
frmrgb = npvid[i,:,:,:].squeeze()
frm = frmrgb[:,:,0].squeeze() # just pick a channel for greyscale
frmnorm, frmmasked = _normalize(frm, frmrgb)
npnormvid[i,:,:] = _crop_image(frmnorm, crop)
img = itk.image_from_array((npnormvid / 255.0).astype('float32'))
tmp = np.array([spacing, spacing, framerate])
img.SetSpacing(tmp)
return img, {'spacing' : tmp, 'crop' : crop}
[docs]def load_and_preprocess_image(fp, version=None):
'''
Loads Sonosite .jpg image. Crops to ultrasound data (e.g. removes rulers) and uses a masked median filter to remove any overlayed text (by masking out bright white).
Returns and itk.Image[itk.F,2] with intensities 0 to 1.0 and correct physical spacing.
Parameters
----------
fp : str
filepath
version : None
reserved for future use
Returns
-------
img : itk.Image[it.F,2]
meta : dict
Meta dictionary
'''
return preprocess_image(itk.imread(fp), version)
[docs]def load_and_preprocess_video(fp, version=None):
'''
Loads Sonosite .mp4 video. Crops to ultrasound data (e.g. removes rulers) and uses a masked median filter to remove any overlayed text (by masking out bright white).
Returns and itk.Image[itk.F,3] with intensities 0 to 1.0 and correct physical spacing.
Parameters
----------
fp : str
filepath
version : None
reserved for future use
Returns
-------
img : itk.Image[itk.F,2]
meta : dict
Meta data dictionary
'''
npvid_rgb = skvideo.io.vread(fp)
vidmeta = skvideo.io.ffprobe(fp)
return preprocess_video(npvid_rgb, itkpocus.util.get_framerate(vidmeta), version)