Skip to content

Commit

Permalink
Merge branch 'master' into readme-tweak-rendering-python-example
Browse files Browse the repository at this point in the history
  • Loading branch information
brad-t-moore authored Aug 12, 2021
2 parents cfb6c6a + 658c9f0 commit 54901d6
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 26 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ITK Point-of-Care Ultrasound (ITKPOCUS)

![](doc/_static/Clarius%20Phantom.gif)
<img src="docs/_static/Clarius Phantom.gif" />

## About
ITK Point-of-Care Ultrasound (ITKPOCUS) is an open source (Apache 2.0) collection of software libraries for the preprocessing and streaming of point-of-care ultrasound (POCUS) devices in order to support image processing and AI. It currently has levels of support for Clarius, Butterfly, Sonivate, Sonoque, and Interson probes.
Expand Down
3 changes: 2 additions & 1 deletion docs/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
_*
_build

Binary file added docs/_static/Clarius Phantom.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/itkpocus-screenshot.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions docs/itkpocus.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ itkpocus.sonoque module
:undoc-members:
:show-inheritance:

itkpocus.sonosite module
------------------------

.. automodule:: itkpocus.sonosite
:members:
:undoc-members:
:show-inheritance:

itkpocus.util module
--------------------

Expand Down
8 changes: 8 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
itkpocus
========

.. toctree::
:maxdepth: 4

itkpocus
tests
6 changes: 5 additions & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ nbformat==5.1.3
nbsphinx==0.8.6
nbsphinx-link==1.3.0
Sphinx==4.1.0

sk-video
scipy
matplotlib
scikit-image
jupyterlab
21 changes: 21 additions & 0 deletions docs/tests.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
tests package
=============

Submodules
----------

tests.test\_butterfly module
----------------------------

.. automodule:: tests.test_butterfly
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

.. automodule:: tests
:members:
:undoc-members:
:show-inheritance:
44 changes: 23 additions & 21 deletions itkpocus/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
# ITKPOCUS (ITK Point-of-Care Ultrasound) Python Library
ITK Point-of-Care Ultrasound (ITKPOCUS) is an open source (Apache 2.0) collection of software libraries for the preprocessing and streaming of point-of-care ultrasound (POCUS) devices in order to support image processing and AI. It currently has levels of support for Clarius, Butterfly, Sonivate, Sonoque, and Interson probes.

## Documentation
1. Example Jupyter Notebooks
2. Readthedocs
The [itkpocus](https://pypi.org/project/itk-pocus/) Python package supports removing overlays, automatic cropping, and determining the physical dimensions of ultrasound video and images.

## Installation
1. Install <https://www.ffmpeg.org/>
2. Add ffmpeg binary directory to your PATH
1. Note: errors in this step will result in exceptions from _skvideo_
3. Activate your virtual environment
4. `pip install itk-pocus`
See the [ITKPOCUS](https://github.com/KitwareMedical/ITKPOCUS) Github repo for source code, examples of streaming ultrasound video (Clarius, Interson, Sonivate) using OpenIGTLink, and our Roadmap.

ITKPOCUS is developed by [Kitware, Inc.](https://www.kitware.com) in collaboration with Duke University. This effort was sponsored by the U.S. Government under Other Transactions Number W81XWH-15-9-0001/W81XWH-19-9-0015 (MTEC 19-08-MuLTI-0079).

## Documentation
[https://itkpocus.readthedocs.io/en/latest/](https://itkpocus.readthedocs.io/en/latest/)

## Usage
The scripts provided convert manufacturer video files to ITK Image objects. They may also remove overlays from the ultrasound image and set the physical dimension of the image by processing the overlay ruler (when applicable).
The scripts provided convert manufacturer image and video files to ITK Image objects. They may also remove overlays from the ultrasound image and set the physical dimension of the image by processing the overlay ruler (when applicable).

```python
import itkpocus.clarius as clarius
import matplotlib.pyplot as plt
import itk
import matplotlib.pyplot as plt
import itkpocus.sonoque as sonoque
import numpy as np

img_fp=PATH_TO_FILE
video_fp=PATH_TO_FILE
fp = '../tests/data/sonoque_axial-lateral-resolution-2020.dcm'
orig_img = itk.imread(fp)
new_img, meta = sonoque.load_and_preprocess_image(fp)
```
![](../docs/_static/itkpocus-screenshot.jpg)

img, meta = load_and_preprocess_image(img_fp)
plt.imshow(img)
print(img, meta)
## Installation
1. Install <https://www.ffmpeg.org/>
2. Add ffmpeg binary directory to your PATH
1. Note: errors in this step will result in exceptions from _skvideo_
3. Activate your virtual environment
4. `pip install itk-pocus`

vid, vid_meta = load_and_preprocess_video(video_fp)
plt.imshow(itk.array_from_image(vid)[0,:,:]) # plot first frame
print(vid, vid_meta)
```
2 changes: 1 addition & 1 deletion itkpocus/itkpocus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
'''

__version__ = '0.1.rc1'
__version__ = '0.1rc2'

2 changes: 1 addition & 1 deletion itkpocus/itkpocus/clarius.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,4 @@ def load_and_preprocess_video(fp, version=None):
meta : dict
Meta data (includes spacing and crop)
'''
return preprocess_video(skvideo.io.vread(fp))
return preprocess_video(skvideo.io.vread(fp), framerate=get_framerate(skvideo.io.ffprobe(fp)))
229 changes: 229 additions & 0 deletions itkpocus/itkpocus/sonosite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
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

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

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}

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 (npnormvid / 255.0).astype('float32'), {'spacing' : tmp, 'crop' : crop}
4 changes: 4 additions & 0 deletions itkpocus/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ classifiers = [
description-file = "README.md"
requires = [
"itk>=5.2.0.post2",
"scikit-image",
"scipy",
"sk-video",
"numpy"
]
requires-python = ">=3.7"
dist-name = "itk-pocus"

0 comments on commit 54901d6

Please sign in to comment.