#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys, os, json, logging, warnings, datetime, io
from string import Template
warnings.filterwarnings("ignore")
import numpy as np
import skimage
import skimage.io
import skimage.exposure
import matplotlib
matplotlib.use('Agg')
import matplotlib.colorbar as colorbar
import matplotlib.colors as color
import matplotlib.pyplot as plt
import sct_utils as sct
logger = logging.getLogger("sct.{}".format(__file__))
[docs]class QcImage(object):
"""
Class used to create a .png file from a 2d image produced by the class "Slice"
"""
_labels_regions = {'PONS': 50, 'MO': 51,
'C1': 1, 'C2': 2, 'C3': 3, 'C4': 4, 'C5': 5, 'C6': 6, 'C7': 7,
'T1': 8, 'T2': 9, 'T3': 10, 'T4': 11, 'T5': 12, 'T6': 13, 'T7': 14, 'T8': 15, 'T9': 16,
'T10': 17, 'T11': 18, 'T12': 19,
'L1': 20, 'L2': 21, 'L3': 22, 'L4': 23, 'L5': 24,
'S1': 25, 'S2': 26, 'S3': 27, 'S4': 28, 'S5': 29,
'Co': 30}
_labels_color = ["#04663c", "#ff0000", "#50ff30",
"#ed1339", "#ffffff", "#e002e8",
"#ffee00", "#00c7ff", "#199f26",
"#563691", "#848545", "#ce2fe1",
"#2142a6", "#3edd76", "#c4c253",
"#e8618a", "#3128a3", "#1a41db",
"#939e41", "#3bec02", "#1c2c79",
"#18584e", "#b49992", "#e9e73a",
"#3b0e6e", "#6e856f", "#637394",
"#36e05b", "#530a1f", "#8179c4",
"#e1320c", "#52a4df", "#000ab5",
"#4a4242", "#0b53a5", "#b49c19",
"#50e7a9", "#bf5a42", "#fa8d8e",
"#83839a", "#320fef", "#82ffbf",
"#360ee7", "#551960", "#11371e",
"#e900c3", "#a21360", "#58a601",
"#811c90", "#235acf", "#49395d",
"#9f89b0", "#e08e08", "#3d2b54",
"#7d0434", "#fb1849", "#14aab4",
"#a22abd", "#d58240", "#ac2aff"]
_seg_colormap = plt.cm.autumn
def __init__(self, qc_report, interpolation, action_list, stretch_contrast=True):
"""
Parameters
----------
qc_report : QcReport
The QC report object
interpolation : str
Type of interpolation used in matplotlib
action_list : list of functions
List of functions that generates a specific type of images
stretch_contrast : adjust image so as to improve contrast
"""
self.qc_report = qc_report
self.interpolation = interpolation
self.action_list = action_list
self._stretch_contrast = stretch_contrast
"""
action_list contain the list of images that has to be generated.
It can be seen as "figures" of matplotlib to be shown
Ex: if 'colorbar' is in the list, the process will generate a color bar in the "img" folder
"""
def listed_seg(self, mask):
img = np.rint(np.ma.masked_where(mask < 1, mask))
fig = plt.imshow(img,
cmap=color.ListedColormap(self._labels_color),
norm=color.Normalize(vmin=0, vmax=len(self._labels_color)),
interpolation=self.interpolation,
alpha=1,
aspect=float(self.aspect_mask))
fig.axes.get_xaxis().set_visible(False)
fig.axes.get_yaxis().set_visible(False)
[docs] def template(self, mask):
"""
Show template statistical atlas
"""
values = mask
values[values<0.5] = 0
color_white = color.colorConverter.to_rgba('white', alpha=0.0)
color_blue = color.colorConverter.to_rgba('blue', alpha=0.7)
color_cyan = color.colorConverter.to_rgba('cyan', alpha=0.8)
cmap = color.LinearSegmentedColormap.from_list('cmap_atlas',
[color_white, color_blue, color_cyan], N=256)
fig = plt.imshow(values,
cmap=cmap,
interpolation=self.interpolation,
aspect=self.aspect_mask)
fig.axes.get_xaxis().set_visible(False)
fig.axes.get_yaxis().set_visible(False)
def no_seg_seg(self, mask):
values = np.ma.masked_equal(np.rint(mask), 0)
fig = plt.imshow(values,
cmap=plt.cm.gray,
interpolation=self.interpolation,
aspect=self.aspect_mask)
fig.axes.get_xaxis().set_visible(False)
fig.axes.get_yaxis().set_visible(False)
def sequential_seg(self, mask):
values = np.ma.masked_equal(np.rint(mask), 0)
fig = plt.imshow(values,
cmap=self._seg_colormap,
interpolation=self.interpolation,
aspect=self.aspect_mask)
fig.axes.get_xaxis().set_visible(False)
fig.axes.get_yaxis().set_visible(False)
def colorbar(self):
fig = plt.figure(figsize=(9, 1.5))
ax = fig.add_axes([0.05, 0.80, 0.9, 0.15])
colorbar.ColorbarBase(ax, cmap=self._seg_colormap, orientation='horizontal')
return '{}_colorbar'.format(self.qc_report.img_base_name)
def __call__(self, func):
"""wrapped function (f).
In this case, it is the "mosaic" or "single" methods of the class "Slice"
Parameters
----------
func : function
The wrapped function
"""
def wrapped_f(sct_slice, *args):
"""
Parameters
----------
sct_slice : spinalcordtoolbox.report.slice:Slice
args : list
Returns
-------
"""
self.qc_report.slice_name = sct_slice.get_name()
# consider only the first 2 slices
aspect_img, self.aspect_mask = sct_slice.aspect()[:2]
self.qc_report.make_content_path()
logger.info('QC: %s with %s slice', func.__name__, sct_slice.get_name())
img, mask = func(sct_slice, *args)
if self._stretch_contrast:
def equalized(a):
"""
Perform histogram equalization using CLAHE
Notes:
- Image value range is preserved
- Workaround for adapthist artifact by padding (#1664)
"""
min_, max_ = a.min(), a.max()
b = (np.float32(a) - min_) / (max_ - min_)
h, w = b.shape
h1 = (h + (8-1))//8*8
w1 = (w + (8-1))//8*8
if h != h1 or w != w1:
b1 = np.zeros((h1, w1), dtype=b.dtype)
b1[:h,:w] = b
b = b1
c = skimage.exposure.equalize_adapthist(b, kernel_size=(8,8))
if h != h1 or w != w1:
c = c[:h,:w]
return np.array(c * (max_ - min_) + min_, dtype=a.dtype)
img = equalized(img)
plt.figure(1)
fig = plt.imshow(img, cmap=plt.cm.gray, interpolation=self.interpolation, aspect=float(aspect_img))
fig.axes.get_xaxis().set_visible(False)
fig.axes.get_yaxis().set_visible(False)
self._save(self.qc_report.qc_params.abs_bkg_img_path())
for action in self.action_list:
logger.debug('Action List %s', action.__name__)
plt.clf()
plt.figure(1)
if self._stretch_contrast and action.__name__ in ("no_seg_seg",):
print("Mask type %s" % mask.dtype)
mask = equalized(mask)
action(self, mask)
self._save(self.qc_report.qc_params.abs_overlay_img_path())
plt.close()
self.qc_report.update_description_file(img.shape)
return wrapped_f
def _save(self, img_path, format='png', bbox_inches='tight', pad_inches=0.00):
""" Save the current figure into an image.
Parameters
----------
img_path : str
path of the folder where the image is saved
format : str
image format
bbox_inches : str
pad_inches : float
"""
logger.debug('Save image %s', img_path)
plt.savefig(img_path,
format=format,
bbox_inches=bbox_inches,
pad_inches=pad_inches,
transparent=True,
dpi=600)
[docs]class Params(object):
"""Parses and stores the variables that will included into the QC details
We derive the value of the contrast and subject name from the `input_file` path,
by splitting it into `[subject]/[contrast]/input_file`
"""
def __init__(self, input_file, command, args, orientation, dest_folder):
"""
Parameters
----------
input_file : str
the input nifti file name
command : str
the command name
args : str
the command's arguments
orientation : str
The anatomical orientation
dest_folder : str
The absolute path of the QC root
"""
abs_input_path = os.path.dirname(os.path.abspath(input_file))
abs_input_path, contrast = os.path.split(abs_input_path)
_, subject = os.path.split(abs_input_path)
if isinstance(args, list):
args = sct.list2cmdline(args)
self.subject = subject
self.cwd = os.getcwd()
self.contrast = contrast
self.command = command
self.args = args
self.orientation = orientation
self.root_folder = dest_folder
self.mod_date = datetime.datetime.strftime(datetime.datetime.now(), '%Y_%m_%d_%H%M%S')
self.qc_results = os.path.join(dest_folder, 'qc_results.json')
self.bkg_img_path = os.path.join(subject, contrast, command, self.mod_date, 'bkg_img.png')
self.overlay_img_path = os.path.join(subject, contrast, command, self.mod_date, 'overlay_img.png')
def abs_bkg_img_path(self):
return os.path.join(self.root_folder, self.bkg_img_path)
def abs_overlay_img_path(self):
return os.path.join(self.root_folder, self.overlay_img_path)
[docs]class QcReport(object):
"""This class generates the quality control report.
It will also setup the folder structure so the report generator only needs to fetch the appropriate files.
"""
def __init__(self, qc_params, usage):
"""
Parameters
----------
tool_name : str
name of the sct tool being used. Is used to name the image file.
qc_params : Params
arguments of the "-param-qc" option in Terminal
cmd_args : list of str
the commands of the process being used to generate the images
usage : str
description of the process
"""
self.tool_name = qc_params.command
self.slice_name = qc_params.orientation
self.qc_params = qc_params
self.usage = usage
import spinalcordtoolbox
pardir = os.path.dirname(os.path.dirname(spinalcordtoolbox.__file__))
self.assets_folder = os.path.join(pardir, 'assets')
self.img_base_name = 'bkg_img'
self.description_base_name = "qc_results"
[docs] def make_content_path(self):
"""Creates the whole directory to contain the QC report
:return: return "root folder of the report" and the "furthest folder path" containing the images
"""
# make a new or update Qc directory
target_img_folder = os.path.dirname(self.qc_params.abs_bkg_img_path())
try:
os.makedirs(target_img_folder)
except OSError as err:
if not os.path.isdir(target_img_folder):
raise err
[docs] def update_description_file(self, dimension):
"""Create the description file with a JSON structure
:param: dimension 2-tuple, the dimension of the image frame (w, h)
"""
output = {
'python': sys.executable,
'cwd': self.qc_params.cwd,
'cmdline': "{} {}".format(self.qc_params.command, self.qc_params.args),
'command': self.qc_params.command,
'subject': self.qc_params.subject,
'contrast': self.qc_params.contrast,
'orientation': self.qc_params.orientation,
'background_img': self.qc_params.bkg_img_path,
'overlay_img': self.qc_params.overlay_img_path,
'dimension': '%dx%d' % dimension,
'moddate': datetime.datetime.now().isoformat(' ')
}
logger.debug('Description file: %s', self.qc_params.qc_results)
results = []
if os.path.isfile(self.qc_params.qc_results):
results = json.load(open(self.qc_params.qc_results, 'r'))
results.append(output)
json.dump(results, open(self.qc_params.qc_results, "w"), indent=2)
self._update_html_assets(results)
def _update_html_assets(self, json_data):
"""Update the html file and assets"""
assets_path = os.path.join(os.path.dirname(__file__), 'assets')
dest_path = self.qc_params.root_folder
with io.open(os.path.join(assets_path, 'index.html')) as template_index:
template = Template(template_index.read())
output = template.substitute(sct_json_data=json.dumps(json_data))
io.open(os.path.join(dest_path, 'index.html'), 'w').write(output)
for path in ['css', 'js', 'imgs', 'fonts']:
src_path = os.path.join(assets_path, '_assets', path)
dest_full_path = os.path.join(dest_path, '_assets', path)
if not os.path.exists(dest_full_path):
os.makedirs(dest_full_path)
for file_ in os.listdir(src_path):
if not os.path.isfile(os.path.join(dest_full_path, file_)):
sct.copy(os.path.join(src_path, file_),
dest_full_path)
[docs]def add_entry(src, process, args, path_qc, plane, background=None, foreground=None,
qcslice=None,
qcslice_operations=[],
qcslice_layout=None,
):
"""
"""
qc_param = Params(src, process, args, plane, path_qc)
report = QcReport(qc_param, '')
if qcslice is not None:
@QcImage(report, 'none', qcslice_operations)
def layout(qslice):
return qcslice_layout(qslice)
layout(qcslice)
else:
report.make_content_path()
def normalized(img):
return np.uint8(skimage.exposure.rescale_intensity(img, out_range=np.uint8))
skimage.io.imsave(qc_param.abs_overlay_img_path(), normalized(foreground))
if background is None:
qc_param.bkg_img_path = qc_param.overlay_img_path
else:
skimage.io.imsave(qc_param.abs_bkg_img_path(), normalized(background))
report.update_description_file(foreground.shape[:2])
sct.printv('Sucessfully generated the QC results in %s' % qc_param.qc_results)
sct.printv('Use the following command to see the results in a browser:')
sct.printv('open file "{}/index.html"'.format(path_qc), type='info')