def generate_qc(fn_in, fn_seg, args, path_qc): """ Generate a QC entry allowing to quickly review the segmentation process. """ import spinalcordtoolbox.reports.qc as qc import spinalcordtoolbox.reports.slice as qcslice from spinalcordtoolbox.resample.nipy_resample import resample_file # Resample to fixed resolution (see #2063) tmp_folder = sct.TempFolder() fn_in_r = os.path.join(tmp_folder.path_tmp, 'img_r.nii.gz') # Orient to RPI and retrieve pixel size in IS direction (z) im_fn = Image(fn_in).change_orientation('RPI').save(fn_in_r) resample_file(fn_in_r, fn_in_r, '0.5x0.5x' + str(im_fn.dim[6]), 'mm', 'nn', 0) fn_seg_r = os.path.join(tmp_folder.path_tmp, 'seg_r.nii.gz') Image(fn_seg).change_orientation('RPI').save(fn_seg_r) resample_file(fn_seg_r, fn_seg_r, '0.5x0.5x' + str(im_fn.dim[6]), 'mm', 'nn', 0) qc.add_entry( src=fn_in, process="sct_propseg", args=args, path_qc=path_qc, plane='Axial', qcslice=qcslice.Axial([Image(fn_in_r), Image(fn_seg_r)]), qcslice_operations=[qc.QcImage.listed_seg], qcslice_layout=lambda x: x.mosaic(), )
def generate_qc(fn_in, fn_seg, args, path_qc): """Generate a QC entry allowing to quickly review the segmentation process.""" import spinalcordtoolbox.reports.qc as qc import spinalcordtoolbox.reports.slice as qcslice from spinalcordtoolbox.resample.nipy_resample import resample_file # Resample to fixed resolution (see #2063) tmp_folder = sct.TempFolder() fn_in_r = os.path.join(tmp_folder.path_tmp, 'img_r.nii.gz') # Orient to RPI and retrieve pixel size in IS direction (z) im_fn = Image(fn_in).change_orientation('RPI').save(fn_in_r) resample_file(fn_in_r, fn_in_r, '0.5x0.5x' + str(im_fn.dim[6]), 'mm', 'nn', 0) fn_seg_r = os.path.join(tmp_folder.path_tmp, 'seg_r.nii.gz') Image(fn_seg).change_orientation('RPI').save(fn_seg_r) resample_file(fn_seg_r, fn_seg_r, '0.5x0.5x' + str(im_fn.dim[6]), 'mm', 'nn', 0) # TODO: investigate the following issue further (Julien 2018-12-01): # fn_in_r and fn_seg_r should be in nii.gz format, otherwise qcslice.Axial outputs an memmap instead of # an array. qc.add_entry( src=fn_in, process="sct_deepseg_sc", args=args, path_qc=path_qc, plane='Axial', qcslice=qcslice.Axial([Image(fn_in_r), Image(fn_seg_r)]), qcslice_operations=[qc.QcImage.listed_seg], qcslice_layout=lambda x: x.mosaic(), )
def test_propseg(t2_image, t2_seg_image, tmp_path): param = qc.Params(t2_image.absolutepath, 'sct_propseg', ['-a'], 'Axial', str(tmp_path)) report = qc.QcReport(param, 'Test usage') @qc.QcImage(report, 'none', [qc.QcImage.listed_seg, ], process=param.command) def test(qslice): return qslice.mosaic() test(qcslice.Axial([t2_image, t2_seg_image])) assert os.path.isfile(param.abs_bkg_img_path()) assert os.path.isfile(param.abs_overlay_img_path()) assert os.path.isfile(param.qc_results)
def test_propseg(t2_image, t2_seg_image): param = qc.Params(t2_image, 'sct_propseg', ['-a'], 'Axial', '/tmp') report = qc.QcReport(param, 'Test usage') @qc.QcImage(report, 'none', [ qc.QcImage.listed_seg, ]) def test(qslice): return qslice.mosaic() test(qcslice.Axial(t2_image, t2_seg_image)) assert os.path.isfile(param.abs_bkg_img_path()) assert os.path.isfile(param.abs_overlay_img_path()) assert os.path.isfile(param.qc_results)
def generate_qc(fn_in, fn_seg, args, path_qc): """Generate a QC entry allowing to quickly review the segmentation process.""" import spinalcordtoolbox.reports.qc as qc import spinalcordtoolbox.reports.slice as qcslice qc.add_entry( src=fn_in, process="sct_deepseg_sc", args=args, path_qc=path_qc, plane='Axial', qcslice=qcslice.Axial([Image(fn_in), Image(fn_seg)]), qcslice_operations=[qc.QcImage.listed_seg], qcslice_layout=lambda x: x.mosaic(), )
def generate_qc(fn_in, fn_wm, args, path_qc): """ Generate a QC entry allowing to quickly review the warped template. """ import spinalcordtoolbox.reports.qc as qc import spinalcordtoolbox.reports.slice as qcslice qc.add_entry( src=fn_in, process="sct_warp_template", args=args, path_qc=path_qc, plane='Axial', qcslice=qcslice.Axial([Image(fn_in), Image(fn_wm)]), qcslice_operations=[qc.QcImage.template], qcslice_layout=lambda x: x.mosaic(), )
def generate_qc(fname_data, fname_template2anat, fname_seg, args, path_qc): """ Generate a QC entry allowing to quickly review the straightening process. """ import spinalcordtoolbox.reports.qc as qc import spinalcordtoolbox.reports.slice as qcslice qc.add_entry( src=fname_data, process="sct_register_to_template", args=args, path_qc=path_qc, plane="Axial", qcslice=qcslice.Axial( [Image(fname_data), Image(fname_template2anat), Image(fname_seg)]), qcslice_operations=[qc.QcImage.no_seg_seg], qcslice_layout=lambda x: x.mosaic()[:2], )
def generate_qc(fname_in, fname_gm, fname_wm, param_seg, args, path_qc): """ Generate a QC entry allowing to quickly review the segmentation process. """ import spinalcordtoolbox.reports.qc as qc import spinalcordtoolbox.reports.slice as qcslice im_org = Image(fname_in) im_gm = Image(fname_gm) im_wm = Image(fname_wm) # create simple compound segmentation image for QC purposes if param_seg.type_seg == 'bin': im_wm.data[im_wm.data == 1] = 1 im_gm.data[im_gm.data == 1] = 2 else: # binarize anyway im_wm.data[im_wm.data >= param_seg.thr_bin] = 1 im_wm.data[im_wm.data < param_seg.thr_bin] = 0 im_gm.data[im_gm.data >= param_seg.thr_bin] = 2 im_gm.data[im_gm.data < param_seg.thr_bin] = 0 im_seg = im_gm im_seg.data += im_wm.data s = qcslice.Axial([im_org, im_seg]) qc.add_entry( src=fname_in, process="sct_segment_graymatter", args=args, path_qc=path_qc, plane='Axial', qcslice=s, qcslice_operations=[qc.QcImage.listed_seg], qcslice_layout=lambda x: x.mosaic(), )
def generate_qc(fname_in1, fname_in2=None, fname_seg=None, angle_line=None, args=None, path_qc=None, dataset=None, subject=None, path_img=None, process=None, fps=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). :param fname_in1: str: File name of input image #1 (mandatory) :param fname_in2: str: File name of input image #2 :param fname_seg: str: File name of input segmentation :param angle_line: list: Angle [in rad, wrt. vertical line, must be between -pi and pi] to apply to the line overlaid on the image, for\ each slice, for slice that don't have an angle to display, a nan is expected. To be used for assessing cord orientation. :param args: args from parent function :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 :param fps: float: Number of frames per second for output gif images. Used only for sct_frmi_moco and sct_dmri_moco. :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']: plane = 'Axial' qcslice_type = qcslice.Axial([Image(fname_in1), Image(fname_in2), Image(fname_seg)]) qcslice_operations = [QcImage.no_seg_seg] def qcslice_layout(x): return x.mosaic()[:2] # Rotation visualisation elif process in ['rotation']: plane = 'Axial' qcslice_type = qcslice.Axial([Image(fname_in1), Image(fname_seg)]) qcslice_operations = [QcImage.line_angle] def qcslice_layout(x): return x.mosaic(return_center=True) # Axial orientation, switch between the image and the segmentation elif process in ['sct_propseg', 'sct_deepseg_sc', 'sct_deepseg_gm']: plane = 'Axial' qcslice_type = qcslice.Axial([Image(fname_in1), Image(fname_seg)]) qcslice_operations = [QcImage.listed_seg] def qcslice_layout(x): return x.mosaic() # Axial orientation, switch between the image and the centerline elif process in ['sct_get_centerline']: plane = 'Axial' qcslice_type = qcslice.Axial([Image(fname_in1), Image(fname_seg)]) qcslice_operations = [QcImage.label_centerline] def qcslice_layout(x): return x.mosaic() # Axial orientation, switch between the image and the white matter segmentation (linear interp, in blue) elif process in ['sct_warp_template']: plane = 'Axial' qcslice_type = qcslice.Axial([Image(fname_in1), Image(fname_seg)]) qcslice_operations = [QcImage.template] def qcslice_layout(x): return x.mosaic() # Axial orientation, switch between gif image (before and after motion correction) and grid overlay elif process in ['sct_dmri_moco', 'sct_fmri_moco']: plane = 'Axial' if fname_seg is None: raise Exception("Segmentation is needed to ensure proper cropping around spinal cord.") qcslice_type = qcslice.Axial([Image(fname_in1), Image(fname_in2), Image(fname_seg)]) qcslice_operations = [QcImage.grid] def qcslice_layout(x): return x.mosaics_through_time() # Sagittal orientation, display vertebral labels elif process in ['sct_label_vertebrae']: plane = 'Sagittal' dpi = 100 # bigger picture is needed for this special case, hence reduce dpi qcslice_type = qcslice.Sagittal([Image(fname_in1), Image(fname_seg)], p_resample=None) qcslice_operations = [QcImage.label_vertebrae] def qcslice_layout(x): return x.single() # Sagittal orientation, display posterior labels elif process in ['sct_label_utils']: plane = 'Sagittal' dpi = 100 # bigger picture is needed for this special case, hence reduce dpi # projected_image = projected(Image(fname_seg)) qcslice_type = qcslice.Sagittal([Image(fname_in1), Image(fname_seg)], p_resample=None) qcslice_operations = [QcImage.label_utils] def qcslice_layout(x): return x.single() # Sagittal orientation, display PMJ box elif process in ['sct_detect_pmj']: plane = 'Sagittal' qcslice_type = qcslice.Sagittal([Image(fname_in1), Image(fname_seg)], p_resample=None) qcslice_operations = [QcImage.highlight_pmj] def qcslice_layout(x): return x.single() # Sagittal orientation, static image elif process in ['sct_straighten_spinalcord']: plane = 'Sagittal' dpi = 100 qcslice_type = qcslice.Sagittal([Image(fname_in1), Image(fname_in1)], p_resample=None) qcslice_operations = [QcImage.vertical_line] def qcslice_layout(x): return 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)) add_entry( src=fname_in1, process=process, args=args, path_qc=path_qc, dataset=dataset, subject=subject, plane=plane, path_img=path_img, dpi=dpi, qcslice=qcslice_type, qcslice_operations=qcslice_operations, qcslice_layout=qcslice_layout, stretch_contrast_method='equalized', angle_line=angle_line, fps=fps, )
def generate_qc(fname_in1, fname_in2=None, fname_seg=None, args=None, path_qc=None, dataset=None, subject=None, process=None): """ Generate a QC entry allowing to quickly review results. This function is called by SCT scripts (e.g. sct_propseg). :param fname_in1: str: File name of input image #1 (mandatory) :param fname_in2: str: File name of input image #2 :param fname_seg: str: File name of input segmentation :param args: args from parent function :param path_qc: str: Path to save QC report :param dataset: str: Dataset name :param subject: str: Subject name :param process: str: Name of SCT function. e.g., sct_propseg :return: None """ dpi = 300 # Get QC specifics based on SCT process # Axial orientation, switch between two input images if process in ['sct_register_multimodal', 'sct_register_to_template']: plane = 'Axial' qcslice_type = qcslice.Axial( [Image(fname_in1), Image(fname_in2), Image(fname_seg)]) qcslice_operations = [QcImage.no_seg_seg] qcslice_layout = lambda x: x.mosaic()[:2] # Axial orientation, switch between the image and the segmentation elif process in ['sct_propseg', 'sct_deepseg_sc', 'sct_deepseg_gm']: plane = 'Axial' qcslice_type = qcslice.Axial([Image(fname_in1), Image(fname_seg)]) qcslice_operations = [QcImage.listed_seg] qcslice_layout = lambda x: x.mosaic() # Axial orientation, switch between the image and the white matter segmentation (linear interp, in blue) elif process in ['sct_warp_template']: plane = 'Axial' qcslice_type = qcslice.Axial([Image(fname_in1), Image(fname_seg)]) qcslice_operations = [QcImage.template] qcslice_layout = lambda x: x.mosaic() # Sagittal orientation, display vertebral labels elif process in ['sct_label_vertebrae']: plane = 'Sagittal' dpi = 100 # bigger picture is needed for this special case, hence reduce dpi qcslice_type = qcslice.Sagittal( [Image(fname_in1), Image(fname_seg)], p_resample=None) qcslice_operations = [QcImage.label_vertebrae] qcslice_layout = lambda x: x.single() # Sagittal orientation, display PMJ box elif process in ['sct_detect_pmj']: plane = 'Sagittal' qcslice_type = qcslice.Sagittal( [Image(fname_in1), Image(fname_seg)], p_resample=None) qcslice_operations = [QcImage.highlight_pmj] qcslice_layout = lambda x: x.single() else: raise ValueError("Unrecognized process: {}".format(process)) add_entry( src=fname_in1, process=process, args=args, path_qc=path_qc, dataset=dataset, subject=subject, plane=plane, dpi=dpi, qcslice=qcslice_type, qcslice_operations=qcslice_operations, qcslice_layout=qcslice_layout, stretch_contrast_method='equalized', )
if '-qc' in arguments and not arguments.get('-noqc', False): qc_path = arguments['-qc'] import spinalcordtoolbox.reports.qc as qc import spinalcordtoolbox.reports.slice as qcslice param = qc.Params(fname_input_data, 'sct_propseg', args, 'Axial', qc_path) report = qc.QcReport(param, '') @qc.QcImage(report, 'none', [ qc.QcImage.listed_seg, ]) def test(qslice): return qslice.mosaic() try: test(qcslice.Axial(Image(fname_input_data), Image(fname_seg))) sct.log.info('Sucessfully generated the QC results in %s' % param.qc_results) sct.log.info( 'Use the following command to see the results in a browser:') sct.log.info('sct_qc -folder %s' % qc_path) except: sct.log.warning('Issue when creating QC report.') sct.printv('\nDone! To view results, type:', verbose) sct.printv( "fslview " + fname_input_data + " " + fname_seg + " -l Red -b 0,1 -t 0.7 &\n", verbose, 'info')
def main(): # find all the images of interest and store the mid slice in slice_lst slice_lst = [] for x in os.walk(i_folder): for file in glob.glob( os.path.join(x[0], 'sub' + im_string) ): # prefixe sub: to prevent from fetching warp files print('\nLoading: ' + file) # load data if plane == 'ax': file_seg = glob.glob(os.path.join(x[0], 'sub' + seg_string))[0] # workaround to save some time img, seg = Image(file).change_orientation('RPI'), Image( file_seg).change_orientation('RPI') mid_slice_idx = int(float(img.dim[2]) // 2) nii_mid = nib.nifti1.Nifti1Image(img.data[:, :, mid_slice_idx], affine) nii_mid_seg = nib.nifti1.Nifti1Image( seg.data[:, :, mid_slice_idx], affine) img_mid = Image(img.data[:, :, mid_slice_idx], hdr=nii_mid.header, dim=nii_mid.header.get_data_shape()) seg_mid = Image(seg.data[:, :, mid_slice_idx], hdr=nii_mid_seg.header, dim=nii_mid_seg.header.get_data_shape()) del img, seg qcslice_cur = qcslice.Axial([img_mid, seg_mid]) center_x_lst, center_y_lst = qcslice_cur.get_center( ) # find seg center of mass mid_slice = qcslice_cur.get_slice(qcslice_cur._images[0].data, 0) # get the mid slice # crop image around SC seg mid_slice = qcslice_cur.crop(mid_slice, int(center_x_lst[0]), int(center_y_lst[0]), 30, 30) else: sag_im = Image(file).change_orientation('RSP') if not np.isclose( sag_im.dim[5], sag_im.dim[6]): # in case data is anisotropic sag_im = resample_nib( sag_im.copy(), new_size=[sag_im.dim[4], sag_im.dim[5], sag_im.dim[5]], new_size_type='mm') mid_slice_idx = int(sag_im.dim[0] // 2) mid_slice = sag_im.data[mid_slice_idx, :, :] del sag_im # histogram equalization using CLAHE slice_cur = equalized(mid_slice, winsize) # scale intensities of all slices (ie of all subjects) in a common range of values slice_cur = scale_intensity(slice_cur) # resize all slices with the shape of the first loaded slice if len(slice_lst): slice_cur = resize(slice_cur, slice_size, anti_aliasing=True) else: slice_size = slice_cur.shape slice_lst.append(slice_cur) # create a new Image object containing the samples to display data = np.stack(slice_lst, axis=-1) nii = nib.nifti1.Nifti1Image(data, affine) img = Image(data, hdr=nii.header, dim=nii.header.get_data_shape()) nb_img = img.data.shape[2] nb_items_mosaic = nb_column * nb_row nb_mosaic = np.ceil(float(nb_img) / (nb_items_mosaic)) for i in range(int(nb_mosaic)): if nb_mosaic == 1: fname_out = o_fname else: fname_out = os.path.splitext(o_fname)[0] + '_' + str(i).zfill( 3) + os.path.splitext(o_fname)[1] print('\nCreating: ' + fname_out) # create mosaic idx_end = (i + 1) * nb_items_mosaic if ( i + 1) * nb_items_mosaic <= nb_img else nb_img data_mosaic = img.data[:, :, i * (nb_items_mosaic):idx_end] mosaic = get_mosaic(data_mosaic, nb_column, nb_row) # save mosaic plt.figure() plt.subplot(1, 1, 1) plt.axis("off") plt.imshow(mosaic, interpolation='bilinear', cmap='gray', aspect='equal') plt.savefig(fname_out, dpi=300, bbox_inches='tight', pad_inches=0) plt.close()
def main(): args = get_parameters() print(args) im_string = args.input # i_folder = args.input_folder # seg_string = args.segmentation plane = args.plane nb_column = int(args.col) nb_row = int(args.row) winsize = int(args.winsize_CLAHE) o_fname = args.output # List input folders files = glob.glob(os.path.join(args.input_folder, '**/sub' + im_string), recursive=True) files.sort() # Initialize list that will store each mosaic element slice_lst = [] for file in files: print("Processing ({}/{}): {}".format(files.index(file), len(files), file)) if plane == 'ax': file_seg = add_suffix(file, args.segmentation) # Extract the mid-slice img, seg = Image(file).change_orientation('RPI'), Image(file_seg).change_orientation('RPI') mid_slice_idx = int(float(img.dim[2]) // 2) nii_mid = nib.nifti2.Nifti2Image(img.data[:, :, mid_slice_idx], img.hdr.get_best_affine()) nii_mid_seg = nib.nifti2.Nifti2Image(seg.data[:, :, mid_slice_idx], seg.hdr.get_best_affine()) img_mid = Image(img.data[:, :, mid_slice_idx], hdr=nii_mid.header, dim=nii_mid.header.get_data_shape()) seg_mid = Image(seg.data[:, :, mid_slice_idx], hdr=nii_mid_seg.header, dim=nii_mid_seg.header.get_data_shape()) # Instantiate spinalcordtoolbox.reports.slice.Axial class qcslice_cur = qcslice.Axial([img_mid, seg_mid]) # Find center of mass of the segmentation center_x_lst, center_y_lst = qcslice_cur.get_center() # Select the mid-slice mid_slice = qcslice_cur.get_slice(qcslice_cur._images[0].data, 0) # Crop image around SC seg mid_slice = qcslice_cur.crop(mid_slice, int(center_x_lst[0]), int(center_y_lst[0]), 20, 20) elif plane == 'sag': sag_im = Image(file).change_orientation('RSP') # check if data is not isotropic resolution if not np.isclose(sag_im.dim[5], sag_im.dim[6]): sag_im = resample_nib(sag_im.copy(), new_size=[sag_im.dim[4], sag_im.dim[5], sag_im.dim[5]], new_size_type='mm') mid_slice_idx = int(sag_im.dim[0] // 2) mid_slice = sag_im.data[mid_slice_idx, :, :] del sag_im # Histogram equalization using CLAHE slice_cur = equalized(mid_slice, winsize) # Scale intensities of all slices (ie of all subjects) in a common range of values slice_cur = scale_intensity(slice_cur) # Resize all slices with the shape of the first loaded slice if len(slice_lst): slice_cur = resize(slice_cur, slice_size, anti_aliasing=True) else: slice_size = slice_cur.shape slice_lst.append(slice_cur) # Create a 2d array containing the samples to display data = np.stack(slice_lst, axis=-1) nb_img = data.shape[2] nb_items_mosaic = nb_column * nb_row nb_mosaic = np.ceil(float(nb_img) / nb_items_mosaic) for i in range(int(nb_mosaic)): if nb_mosaic == 1: fname_out = o_fname else: fname_out = os.path.splitext(o_fname)[0] + '_' + str(i).zfill(3) + os.path.splitext(o_fname)[1] # create mosaic idx_end = (i+1)*nb_items_mosaic if (i+1)*nb_items_mosaic <= nb_img else nb_img data_mosaic = data[:, :, i*nb_items_mosaic: idx_end] mosaic = get_mosaic(data_mosaic, nb_column, nb_row) # save mosaic plt.figure() plt.subplot(1, 1, 1) plt.axis("off") plt.imshow(mosaic, interpolation='bilinear', cmap='gray', aspect='equal') plt.savefig(fname_out, dpi=300, bbox_inches='tight', pad_inches=0) plt.close() print('\nCreated: {}'.format(fname_out))