'''
Preprocessing and device specific IO for the Butterfly iQ.
'''
import skvideo.io.ffprobe
import skvideo.utils
import sys
from skvideo.io import vread
from scipy.signal import find_peaks
from pathlib import Path
import numpy as np
import itkpocus.util
import itk
[docs]def ffprobe_count_frames(filename):
"""
Get metadata by using ffprobe
Checks the output of ffprobe on the desired video file. MetaData is then parsed into a dictionary.
Parameters
----------
filename : string
Path to the video file
Returns
-------
metaDict : dict
Dictionary containing all header-based information
about the passed-in source video.
"""
# check if FFMPEG exists in the path
try:
command = [skvideo._FFMPEG_PATH + "/" + skvideo._FFPROBE_APPLICATION, "-v", "error", "-show_streams", "-count_frames", "-print_format", "xml", filename]
#print(command)
# simply get std output
xml = skvideo.utils.check_output(command)
#print(xml)
d = skvideo.utils.xmltodictparser(xml)["ffprobe"]
d = d["streams"]
#import json
#print json.dumps(d, indent = 4)
#exit(0)
# check type
streamsbytype = {}
if type(d["stream"]) is list:
# go through streams
for stream in d["stream"]:
streamsbytype[stream["@codec_type"].lower()] = stream
else:
streamsbytype[d["stream"]["@codec_type"].lower()] = d["stream"]
return streamsbytype
except AttibuteError as e:
print(e)
return {}
except:
print("Unexpected error:", sys.exc_info()[0])
return {}
[docs]def vread_workaround(fp, ffprobe_count_frames_dict=None):
'''
A workaround for Butterfly .mp4 files that don't correctly set the number of frames. Forces vread to use the @nb_read_frames as the frame count.
Parameters
----------
fp : str
File path to video file to read
ffprobe_count_frames_dict : dict, optional
A dict from ffprobe containing the key '@nb_read_frames'. If not specified, ffprobe will be run on the file first.
Returns
-------
ndarray
Video array in F,H,W,RGB format
'''
ffprobe_count_frames_dict = ffprobe_count_frames_dict if ffprobe_count_frames_dict is not None else ffprobe_count_frames(fp)
return vread(fp, outputdict={'-vframes' : ffprobe_count_frames_dict['video']['@nb_read_frames']})
def _calc_spacing(npvid):
'''
Calculates the physical spacing of an image or video by interpeting the ruler overlay.
Parameters
----------
npvid : ndarray
video or image, if image, H, W, RGB, if video, F, H, W, RGB
Returns
-------
float
'''
if len(npvid.shape) == 4: # it's a video
npimg = npvid[0,:,:,:].squeeze()
else: # it's an image
npimg = npvid
ruler_peaks, ruler_props = find_peaks(npimg[:,-4,0].squeeze(), threshold=10, distance=10)
ruler_dist = np.median(ruler_peaks[1:] - ruler_peaks[:-1])
spacing = 2 / ruler_dist
return spacing
def _calc_crop(npvid):
'''
Calculates the crop region to isolate the ultrasound data.
Parameters
----------
npvid : ndarray
video or image, if image, H, W, RGB, if video, F, H, W, RGB
Returns
-------
ndarray
2x2 array in format [[start_row, end_row], [start_column, end_column]]
'''
if len(npvid.shape) == 4:
npimg = npvid[0,:,:,0].squeeze()
else:
npimg = npvid[:,:,0].squeeze()
# center of image
cr, cc = (np.array(npimg.shape) / 2.0).astype('int')
#sr = int(0.05 * npimg.shape[0])
sr = cr
er = cr
row_cutoff = npimg.shape[1] * 0.25
while sr > 0 and len(np.argwhere(npimg[sr,:])) > row_cutoff:
sr -= 1
sr += 5 # get rid of gray bar
while er < npimg.shape[0]-1 and len(np.argwhere(npimg[er,:])) > row_cutoff:
er += 1
er -= 15 # get rid of angling shadowbox on butterfly
sc = cc
ec = cc
col_baseline = len(np.argwhere(npimg[:, cc]))
while sc > 0 and len(np.argwhere(npimg[sr:er, sc])) > npimg.shape[0] * 0.25:
sc -= 1
while ec < npimg.shape[1]-1 and len(np.argwhere(npimg[sr:er,ec])) > npimg.shape[0] * 0.25:
ec += 1
sc += 15 # get rid of angling shadowbox on butterfly
ec -= 15 # get rid of angling shadowbox on butterfly
return np.array([[sr, er], [sc, ec]], dtype='int')
[docs]def preprocess_video(npvid, framerate=1, version=None):
'''
Preprocesses an ndarray representing a video (FRCRGB)
Parameters
----------
npvid : ndarray
FxRxCx3 (rgb), 0 to 255
version : None
reserved for future use
Returns
-------
img : itk.Image[itk.F,3]
meta : dict
'''
spacing = _calc_spacing(npvid)
spacing = np.array([spacing, spacing, framerate])
crop = _calc_crop(npvid)
img = itkpocus.util.image_from_array(npvid[:,crop[0,0]:crop[0,1], crop[1,0]:crop[1,1],0].astype('float32') / 255.0, spacing=spacing)
return img, {'spacing' : spacing, 'crop' : crop}
[docs]def preprocess_image(npimg, version=None):
'''
Parameters
----------
npimg : ndarray
RxCx3 (rgb), 0 to 255
version : None
reserved for future use
Returns
-------
img : itk.Image[itk.F,2]
meta : dict
'''
spacing = _calc_spacing(npimg)
spacing = np.array([spacing, spacing])
crop = _calc_crop(npimg)
img = itkpocus.util.image_from_array(npimg[crop[0,0]:crop[0,1], crop[1,0]:crop[1,1],0].astype('float32') / 255.0, spacing=spacing)
return img, {'spacing' : spacing, 'crop' : crop}
[docs]def load_and_preprocess_video(fp, version=None):
'''
Loads and preprocesses a Butterfly 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)
'''
vidmeta = ffprobe_count_frames(fp)
npvid_rgb = vread_workaround(fp, vidmeta)
return preprocess_video(npvid_rgb, framerate=itkpocus.util.get_framerate(vidmeta))
[docs]def load_and_preprocess_image(fp, version=None):
'''
Loads and preprocesses a Butterfly 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, 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)