#!/usr/bin/env python # This library contains functions for creating part of the # afni_proc.py QC HTML. Specifically, these functions build the # NiiVue-specific parts # # ======================================================================= import os, sys import copy, json from afnipy import afni_util as au # Dictionary for converting an AFNI cbar name (keys) to NiiVue cmap # name(s). Note that NiiVue will split bi-directional pbars into 2 # cmaps (pos first, then neg), so we always make a list of any mapping. afni2nv_cmaps = { 'Plasma' : ['plasma'], 'CET_L17' : ['cet_l17'], 'gray_scale' : ['gray'], 'gray_scale flip' : ['gray'], # special case for ve2a 'GoogleTurbo' : ['turbo'], 'Reds_and_Blues_Inv' : ['afni_reds_inv', 'afni_blues_inv'], } def translate_pbar_dict_afni2nv(pbar_dict): """Take a pbar_dict from @chauffeur_afni and translate pieces for NiiVue Parameters ---------- pbar_dict : dict pbar dictionary output by @chauffeur_afni Returns ------- nv_dict : dict dictionary of ulay, olay, thr and colorizing info for niivue to use """ # start the niivue dict nv_dict = {} # subbricks of ulay, olay and thr, respectively subbb = copy.deepcopy(pbar_dict['subbb']) # ... and prevent negative indices going further here; when # driving in @chauffeur_afni, they just meant 'ignore' and were # treated like 0, if used at all, but here they cause NaNs to appear for i in range(len(subbb)): if subbb[i] < 0 : subbb[i] = 0 # cmap or cmaps in niivue if '=' in pbar_dict['cbar']: # currently, special case of discrete 'one-off' cbar cmap = ['jet'] else: cmap = copy.deepcopy(afni2nv_cmaps[pbar_dict['cbar']]) # show/hide olay button for alignment/flip checks if pbar_dict['pbar_fname'].endswith('_va2t_anat2temp.pbar.jpg') or \ pbar_dict['pbar_fname'].endswith('_ve2a_epi2anat.pbar.jpg') or \ pbar_dict['pbar_fname'].endswith('_warns_flip_0.pbar.jpg') or \ pbar_dict['pbar_fname'].endswith('_warns_flip_1.pbar.jpg') : nv_dict['olay_btn_sh'] = True else: nv_dict['olay_btn_sh'] = False # topval of ulay grayscale cbar nv_dict['cal_max_ulay'] = pbar_dict['ulay_top'] # L/R directionality if pbar_dict['is_left_left'].lower() == "yes" : nv_dict['isRadCon'] = 'false' else: nv_dict['isRadCon'] = 'true' # A/P directionality if pbar_dict['is_left_post'].lower() == "no" : nv_dict['sagNoseLeft'] = 'true' else: nv_dict['sagNoseLeft'] = 'false' # possible 'jump to' coords nv_dict['coor_type'] = pbar_dict['coor_type'] # coors are XYZ or IJK vals, depending on coor_type if nv_dict['coor_type'] == 'SET_DICOM_XYZ' : # NiiVue always uses RAS notation (what AFNI calls LPI) coors_ras = afni_rai_dicom_to_niivue_ras(pbar_dict['coors']) nv_dict['coors_str'] = ', '.join([str(x) for x in coors_ras]) else : nv_dict['coors_str'] = ', '.join([str(x) for x in pbar_dict['coors']]) # is there an overlay to display? nv_dict['see_overlay'] = pbar_dict['see_olay'] if nv_dict['see_overlay'] == '+' : nv_dict['cmap'] = cmap[0] # cbar name(s) if len(cmap) > 1 : nv_dict['cmap_neg'] = cmap[1] # cbar name(s) nv_dict['idx_olay'] = subbb[1] # idx of olay subbrick nv_dict['idx_thr'] = subbb[2] # idx of thr subbrick nv_dict['cal_max_olay'] = pbar_dict['pbar_top'] # cbar max nv_dict['cal_max_thr'] = pbar_dict['vthr'] # thr val # if thresholding, use AFNI-like alpha thresholding; else, don't if float(nv_dict['cal_max_thr']) != 0.0 : nv_dict['modAlpha'] = 2.0 else: nv_dict['modAlpha'] = 0 if pbar_dict['olay_alpha'] != 'No' : # use alpha fade? nv_dict['do_alpha'] = 'true' else: nv_dict['do_alpha'] = 'false' if pbar_dict['olay_boxed'] != 'No' : # use box lines nv_dict['do_boxed'] = 1 else: nv_dict['do_boxed'] = 0 if 'olay_opacity' in pbar_dict : nv_dict['olay_opacity'] = float(pbar_dict['olay_opacity'])/9. else: nv_dict['olay_opacity'] = 1 return nv_dict def set_niivue_ulay(nv_dict): """Use the NiiVue dict to build the NVOBJ.loadVolumes([..]) info for the ulay dset. Parameters ---------- nv_dict : dict dictionary of ulay, olay, thr and colorizing info for niivue to use Returns ------- nv_ulay_txt : str text for the underlay info in loading a NiiVue volume """ nv_ulay_txt = ''' {{ // ulay url:"{ulay}", cal_max: {cal_max_ulay}, }}'''.format( **nv_dict ) return nv_ulay_txt def afni_rai_dicom_to_niivue_ras(A): """The AFNI GUI by default will report in RAI-DICOM convention, where Right/Anterior/Inferior XYZ-coords have negative signs. NiiVue uses RAS+ notation, where Left/Posterior/Inferior are all negative. This function takes an XYZ triplet of numbers A that is RAI-DICOM notation and returns an array B of RAS+ coords. Parameters ---------- A : array/list/tuple An ordered collection of len=3 numbers, assumed to be in RAI-DICOM coordinate notation. Returns ------- B : list A list of len=3 numbers (floats), which should now be in RAS+ coordinate notation. """ N = len(A) if N != 3 : print("** ERROR: ordered collection A should have len=3, not {}" "".format(N)) B = [0.0] * 3 # to store coord values S = [1.0] * 3 # signum values, +/- 1 # coords where signs will flip if A[0] > 0 : S[0] = -1 if A[1] > 0 : S[1] = -1 for i in range(3): B[i] = S[i] * A[i] return B def set_niivue_olay(nv_dict): """Use the NiiVue dict to build the NVOBJ.loadVolumes([..]) info for the olay dset. Parameters ---------- nv_dict : dict dictionary of ulay, olay, thr and colorizing info for niivue to use Returns ------- nv_olay_txt : str text for the overlay (and thr) info in loading a NiiVue volume """ nv_olay_txt = ''', {{ // olay url:"{olay}", frame4D: {idx_olay}, // idx of vol colormap: "{cmap}",'''.format(**nv_dict) if 'cmap_neg' in nv_dict : nv_olay_txt+= ''' colormapNegative: "{cmap_neg}",'''.format(**nv_dict) nv_olay_txt+= ''' cal_min: 0, cal_max: {cal_max_olay}, opacity: {olay_opacity}, }}'''.format(**nv_dict) return nv_olay_txt def set_niivue_thr(nv_dict): """Use the NiiVue dict to build the NVOBJ.loadVolumes([..]) info for the thr dset. Parameters ---------- nv_dict : dict dictionary of ulay, olay, thr and colorizing info for niivue to use Returns ------- nv_thr_txt : str text for the thr info in loading a NiiVue volume """ # thr+olay dsets are same, but might be diff subbricks nv_thr_txt = ''', {{ // thr url:"{olay}", // same dset as olay frame4D: {idx_thr}, // idx of vol colormap: "blue",'''.format(**nv_dict) # doesn't matter, not shown #colormap: "{cmap}",'''.format(**nv_dict) if 'cmap_neg' in nv_dict : nv_thr_txt+= ''' colormapNegative: "blue",'''.format(**nv_dict) # also not shown nv_thr_txt+= ''' cal_min: 0, cal_max: {cal_max_thr}, opacity: 0, }}'''.format(**nv_dict) return nv_thr_txt def set_niivue_then(nv_dict): """Use the NiiVue dict to build the 'then' text after NVOBJ.loadVolumes([..]). Parameters ---------- nv_dict : dict dictionary of ulay, olay, thr and colorizing info for niivue to use Returns ------- nv_then_txt : str text for the 'then' features in loading a NiiVue volume """ ### NB: these colorbarVisible calls could be used, but we turn off ### using the extra NiiVue cbar now, because there is one below ### anyways: #{nobj}.volumes[0].colorbarVisible = false; // no ulay bar #{nobj}.volumes[1].colorbarVisible = true; // yes olay bar #{nobj}.volumes[2].colorbarVisible = false; // no thr bar nv_then_txt = ''' if ( {nobj}.volumes.length > 1 ) {{ {nobj}.volumes[1].alphaThreshold = {do_alpha}; // alpha for olay }} {nobj}.overlayOutlineWidth = {do_boxed}; {nobj}.opts.multiplanarForceRender = true; {nobj}.backgroundMasksOverlays = true;'''.format(**nv_dict) # as of (large set of) NV changes b/t ver=0.38.0 and v=0.38.3, # modulateAlpha behaves differently, so we now (perhaps more # stably anyways) use setModulationImage() only if modAlpha (which # is int/flt) is nonzero if nv_dict['modAlpha'] : nv_then_txt+= ''' {nobj}.setModulationImage( {nobj}.volumes[1].id, {nobj}.volumes[2].id, modulateAlpha = {modAlpha} ); // activate+specify mapping'''.format(**nv_dict) else: # just a comment nv_then_txt+= ''' // no setModulationImage(), bc modulation is not used''' if nv_dict['coor_type'] == "SET_DICOM_XYZ" : nv_then_txt+= ''' {nobj}.scene.crosshairPos = '''.format(**nv_dict) nv_then_txt+= '''{nobj}.mm2frac([{coors_str}]);'''.format(**nv_dict) nv_then_txt+= ''' // jump to XYZ'''.format(**nv_dict) nv_then_txt+= ''' {nobj}.updateGLVolume();'''.format(**nv_dict) return nv_then_txt # ======================================================================= def make_niivue_2dset(qcdir, ulay_name, pbar_dict, olay_name=None, itemid='', path='..', verb=0): """This function creates the mini-html for each dataset(s) that can be viewed in NiiVue. It also creates the text that gets inserted into the primary index.html, where NiiVue will actually open when toggled on. Parameters ---------- qcdir : str name of QC directory (QC_/) ulay_name : str name of ulay dataset pbar_dict : dict pbar (=cmap = cbar) dictionary of items; made by converting @chauffeur_afni's '-pbar_saveim ..' txt file -> JSON and reading it in as a dict. Contains both ulay and olay (if present) information olay_name : str name of olay dataset itemid : str ID of the div that will be activated using this NiiVue instance path : str path to the data (from QC_*/), which will typically be the default verb : str verbosity level whilst processing Returns ------- otxt : str string of the NiiVue canvas to stick into the HTML """ # Make the dsets with paths pdata = path.rstrip('/') if ulay_name[0] == '/' : # the template, which can only be a ulay, could be abs path ulay = ulay_name else: ulay = pdata + '/' + ulay_name if olay_name : olay = pdata + '/' + olay_name # NiiVue canvas ID and object name (*NB: latter can't have '-' chars) nid = 'nvcan_' + itemid nobj = 'nvobj_' + au.rename_label_safely(itemid) # Setup a NiiVue dict for loading and displaying volumes, based on # the AFNI pbar. Translate names of things, and load in various # other useful pieces of information for string generation nv_dict = translate_pbar_dict_afni2nv(pbar_dict) nv_dict['ulay'] = ulay if olay : nv_dict['olay'] = olay nv_dict['nid'] = nid nv_dict['nobj'] = nobj # create pieces of text within NiiVue canvas; now more # complicated, because we won't always just put olay and thr in # and adjust coloring/opacity as necessary---now we include O/T # vols only as needed. These criteria might seem a bit confusing, # but the correct U/O/T volumes should be getting into the correct # 0/1/2 slots this way if nv_dict['ulay'] != nv_dict['olay'] : # we used duplicate ulay and olay for what were really # "olay-only" images; now no more. In such cases, there is no # thr, and the 'olay' one contains more information, so this # condition means that only the olay will be shown, actually nv_ulay_txt = set_niivue_ulay(nv_dict) else: nv_ulay_txt = '' nv_olay_txt = set_niivue_olay(nv_dict) # kind of annoying: remove a comma, add a tab if nv_ulay_txt == '' : nv_olay_txt = ' '*4 + nv_olay_txt[1:] if nv_dict['ulay'] != nv_dict['olay'] and \ float(nv_dict['cal_max_thr']) > 0 : nv_thr_txt = set_niivue_thr(nv_dict) else: nv_thr_txt = '' nv_then_txt = set_niivue_then(nv_dict) # top of mini-html: the typecast part ohtml = ''' ''' ohtml+= '''
 
'''.format( nid=nid ) # for align checks, can have show/hide button if nv_dict['olay_btn_sh'] : ohtml+= ''' '''.format( nid=nid, nobj=nobj ) ohtml+= '''
''' # write out the html to the main QC_*/ dir fname = "{qcdir}/{nid}_container.html".format(qcdir=qcdir, nid=nid) fff = open(fname, 'w') fff.write(ohtml) fff.close() otxt = '''
'''.format(nid=nid) return otxt