Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CSA results on QC report #2306

Merged
merged 11 commits into from
Jun 12, 2019
59 changes: 59 additions & 0 deletions scripts/sct_process_segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from spinalcordtoolbox.aggregate_slicewise import aggregate_per_slice_or_level, save_as_csv, func_wa, func_std, \
_merge_dict
from spinalcordtoolbox.utils import parse_num_list
from spinalcordtoolbox.reports.qc import generate_qc


# TODO: Move this class somewhere else
Expand Down Expand Up @@ -115,6 +116,18 @@ def get_parser():
mandatory=False,
example=['0', '1'],
default_value='1')
parser.add_option(name='-qc',
type_value='folder_creation',
description='The path where the quality control generated content will be saved',
default_value=None)
parser.add_option(name='-qc-dataset',
type_value='str',
description='If provided, this string will be mentioned in the QC report as the dataset the process was run on',
)
parser.add_option(name='-qc-subject',
type_value='str',
description='If provided, this string will be mentioned in the QC report as the subject the process was run on',
)
parser.add_option(name='-v',
type_value='multiple_choice',
description='1: display on, 0: display off (default)',
Expand All @@ -129,6 +142,43 @@ def get_parser():
return parser


def _make_figure(metric):
"""
Make a graph showing CSA and angles per slice.
:param metric: Dictionary of metrics
:return: image object
"""
import tempfile
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure

fname_img = tempfile.NamedTemporaryFile().name + '.png'
z, csa, angle_ap, angle_rl = [], [], [], []
for key, value in metric.items():
z.append(key[0])
csa.append(value['MEAN(area)'])
angle_ap.append(value['MEAN(angle_AP)'])
angle_rl.append(value['MEAN(angle_RL)'])
# Make figure
fig = Figure()
FigureCanvas(fig)
ax = fig.add_subplot(211)
ax.plot(z, csa, 'k')
ax.plot(z, csa, 'k.')
ax.grid(True)
ax.set_ylabel('CSA [$mm^2$]')
ax = fig.add_subplot(212)
ax.plot(z, angle_ap, 'b')
ax.plot(z, angle_ap, 'b.')
ax.plot(z, angle_rl, 'r')
ax.plot(z, angle_rl, 'r.')
ax.grid(True)
ax.set_xlabel('Slice (Inferior-Superior direction)')
ax.set_ylabel('Angle [$deg$]')
fig.savefig(fname_img)
return fname_img


def main(args):
parser = get_parser()
arguments = parser.parse(args)
Expand Down Expand Up @@ -171,6 +221,9 @@ def main(args):
angle_correction = True
elif arguments['-angle-corr'] == '0':
angle_correction = False
path_qc = arguments.get("-qc", None)
qc_dataset = arguments.get("-qc-dataset", None)
qc_subject = arguments.get("-qc-subject", None)

verbose = int(arguments.get('-v'))
sct.init_sct(log_level=verbose, update=True) # Update log level
Expand All @@ -191,6 +244,12 @@ def main(args):
group_funcs=group_funcs)
metrics_agg_merged = _merge_dict(metrics_agg)
save_as_csv(metrics_agg_merged, file_out, fname_in=fname_segmentation, append=append)

# QC report (only show CSA for clarity)
if path_qc is not None:
generate_qc(fname_segmentation, args=args, path_qc=os.path.abspath(path_qc), dataset=qc_dataset,
subject=qc_subject, path_img=_make_figure(metrics_agg_merged), process='sct_process_segmentation')

sct.display_open(file_out)


Expand Down
49 changes: 27 additions & 22 deletions spinalcordtoolbox/reports/qc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import datetime
import io
from string import Template
from shutil import copyfile

warnings.filterwarnings("ignore")

Expand All @@ -28,6 +29,7 @@
import sct_utils as sct
from spinalcordtoolbox.image import Image
import spinalcordtoolbox.reports.slice as qcslice
from spinalcordtoolbox import __sct_dir__

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -313,7 +315,7 @@ def _save(self, fig, img_path, format='png', bbox_inches='tight', pad_inches=0.0


class Params(object):
"""Parses and stores the variables that will included into the QC details
"""Parses and stores the variables that will be included into the QC details
"""
def __init__(self, input_file, command, args, orientation, dest_folder, dpi=300, dataset=None, subject=None):
"""
Expand Down Expand Up @@ -378,11 +380,7 @@ def __init__(self, qc_params, usage):
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.assets_folder = os.path.join(__sct_dir__, 'assets')
self.img_base_name = 'bkg_img'
self.description_base_name = "qc_results"

Expand Down Expand Up @@ -454,7 +452,7 @@ def _update_html_assets(self, json_data):
dest_full_path)


def add_entry(src, process, args, path_qc, plane, background=None, foreground=None,
def add_entry(src, process, args, path_qc, plane, path_img=None, path_img_overlay=None,
qcslice=None,
qcslice_operations=[],
qcslice_layout=None,
Expand All @@ -470,8 +468,8 @@ def add_entry(src, process, args, path_qc, plane, background=None, foreground=No
:param args:
:param path_qc:
:param plane:
:param background:
:param foreground:
:param path_img: Path to image to display
:param path_img_overlay: Path to image to display on top of path_img (will flip between the two)
:param qcslice: spinalcordtoolbox.reports.slice:Axial
:param qcslice_operations:
:param qcslice_layout:
Expand All @@ -491,20 +489,18 @@ def layout(qslice):
return qcslice_layout(qslice)

layout(qcslice)
else:
elif path_img is not None:
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
report.update_description_file(skimage.io.imread(path_img).shape[:2])
copyfile(path_img, qc_param.abs_bkg_img_path())
if path_img_overlay is not None:
# User specified a second image to overlay
copyfile(path_img_overlay, qc_param.abs_overlay_img_path())
else:
skimage.io.imsave(qc_param.abs_bkg_img_path(), normalized(background))

report.update_description_file(foreground.shape[:2])
# Copy the image both as "overlay" and "path_img_overlay", so it appears static.
# TODO: Leave the possibility in the reports/assets/js files to have static images (instead of having to
# flip between two images).
copyfile(path_img, qc_param.abs_overlay_img_path())

sct.printv('Successfully generated the QC results in %s' % qc_param.qc_results)
sct.printv('Use the following command to see the results in a browser:')
Expand All @@ -521,7 +517,7 @@ def normalized(img):


def generate_qc(fname_in1, fname_in2=None, fname_seg=None, args=None, path_qc=None, dataset=None, subject=None,
process=None):
path_img=None, process=None):
"""
Generate a QC entry allowing to quickly review results. This function is the entry point and is called by SCT
scripts (e.g. sct_propseg).
Expand All @@ -533,11 +529,16 @@ def generate_qc(fname_in1, fname_in2=None, fname_seg=None, args=None, path_qc=No
:param path_qc: str: Path to save QC report
:param dataset: str: Dataset name
:param subject: str: Subject name
:param path_img: dict: Path to image to display (e.g., a graph), instead of computing the image from MRI.
:param process: str: Name of SCT function. e.g., sct_propseg
:return: None
"""
logger.info('\n*** Generate Quality Control (QC) html report ***')
dpi = 300
plane = None
qcslice_type = None
qcslice_operations = None
qcslice_layout = None
# Get QC specifics based on SCT process
# Axial orientation, switch between two input images
if process in ['sct_register_multimodal', 'sct_register_to_template']:
Expand Down Expand Up @@ -570,6 +571,9 @@ def generate_qc(fname_in1, fname_in2=None, fname_seg=None, args=None, path_qc=No
qcslice_type = qcslice.Sagittal([Image(fname_in1), Image(fname_seg)], p_resample=None)
qcslice_operations = [QcImage.highlight_pmj]
qcslice_layout = lambda x: x.single()
# Metric outputs (only graphs)
elif process in ['sct_process_segmentation']:
assert os.path.isfile(path_img)
else:
raise ValueError("Unrecognized process: {}".format(process))

Expand All @@ -581,6 +585,7 @@ def generate_qc(fname_in1, fname_in2=None, fname_seg=None, args=None, path_qc=No
dataset=dataset,
subject=subject,
plane=plane,
path_img=path_img,
dpi=dpi,
qcslice=qcslice_type,
qcslice_operations=qcslice_operations,
Expand Down