22
33import csv
44import os
5+ import re
56import warnings
7+ from json import dump as json_dump
68
79import click
810import numpy as np
911from nibabel .filename_parser import splitext_addext
1012from nibabel .loadsave import load as nib_load
1113from nibabel .spatialimages import SpatialImage
1214
15+ from dynamicpet import __version__
1316from dynamicpet .denoise import hypr
1417from dynamicpet .denoise import nesma
1518from dynamicpet .kineticmodel .kineticmodel import KineticModel
@@ -100,19 +103,31 @@ def denoise(
100103) -> None :
101104 """Perform dynamic PET denoising.
102105
103- Outputs will have a '_ <method>' suffix.
106+ Outputs will have a '_desc- <method>_pet ' suffix.
104107
105108 PET: 4-D PET image
106109 """
107110 # load PET
108111 pet_img = petbidsimage_load (pet , json )
109112
110113 if method == "HYPRLR" :
114+ doc = hypr .hypr_lr .__doc__
115+
116+ # parameters not relevant to HYPR-LR
117+ mask = None
118+ window_half_size = None
119+ thresh = None
120+
111121 if fwhm :
112122 res = hypr .hypr_lr (pet_img , fwhm )
113123 else :
114124 raise ValueError ("fwhm must be specified for HYPR-LR" )
115125 elif method == "NESMA" :
126+ doc = nesma .nesma_semiadaptive .__doc__
127+
128+ # parameter not relevant to NESMA
129+ fwhm = None
130+
116131 if mask :
117132 mask_img : SpatialImage = nib_load (mask ) # type: ignore
118133 # check that mask is in the same space as pet
@@ -138,10 +153,37 @@ def denoise(
138153 if outputdir :
139154 os .makedirs (outputdir , exist_ok = True )
140155 bname = os .path .basename (froot )
141- froot = os .path .join (outputdir , bname )
142- output = froot + "_" + method .lower () + ext + addext
156+ # if the input file name follows the PET-BIDS convention, it should end
157+ # with "_pet". Need to move this to the end of the new file name to
158+ # maintain compatibility with the PET-BIDS Derivatives convention.
159+ froot = re .sub ("_pet$" , "" , os .path .join (outputdir , bname ))
160+ output = froot + "_desc-" + method .lower () + "_pet" + ext + addext
161+ output_json = froot + "_desc-" + method .lower () + "_pet.json"
143162 res .to_filename (output )
144163
164+ cmd = (
165+ f"denoise --method { method } "
166+ + (f"--fwhm { fwhm } " if fwhm else "" )
167+ + (f"--mask { mask } " if mask else "" )
168+ + (f"--window_half_size { window_half_size } " if window_half_size else "" )
169+ + (f"--thresh { thresh } " if thresh else "" )
170+ + (f"--outputdir { outputdir } " if outputdir else "" )
171+ + (f"--json { json } " if json else "" )
172+ + pet
173+ )
174+ derivative_json_dict = {
175+ "Description" : (
176+ re .sub (r"\s+" , " " , doc .split ("Args:" )[0 ]).strip () if doc else ""
177+ ),
178+ # "Sources": [pet],
179+ "SoftwareName" : "dynamicpet" ,
180+ "SoftwareVersion" : __version__ ,
181+ "CommandLine" : cmd ,
182+ }
183+
184+ with open (output_json , "w" ) as f :
185+ json_dump (derivative_json_dict , f , indent = 4 )
186+
145187
146188@click .command ()
147189@click .argument ("pet" , type = str )
@@ -200,9 +242,11 @@ def denoise(
200242 ),
201243)
202244@click .option (
203- "--start" , default = None , type = float , help = "Start of time window for model"
245+ "--start" , default = None , type = float , help = "Start of time window for model in min"
246+ )
247+ @click .option (
248+ "--end" , default = None , type = float , help = "End of time window for model in min"
204249)
205- @click .option ("--end" , default = None , type = float , help = "End of time window for model" )
206250@click .option (
207251 "--fwhm" ,
208252 default = None ,
@@ -226,7 +270,7 @@ def denoise(
226270 "'rect' is rectangular integration."
227271 ),
228272)
229- def kineticmodel (
273+ def kineticmodel ( # noqa: C901
230274 pet : str ,
231275 model : str ,
232276 refroi : str | None ,
@@ -242,7 +286,7 @@ def kineticmodel(
242286) -> None :
243287 """Fit a reference tissue model to a dynamic PET image or TACs.
244288
245- Outputs will have a '_km -<model>_kp -<parameter>' suffix.
289+ Outputs will have a '_model -<model>_meas -<parameter>' suffix.
246290
247291 PET: 4-D PET image (can be 3-D if model is SUVR) or a 2-D tabular TACs tsv file
248292 """
@@ -262,16 +306,27 @@ def kineticmodel(
262306 pet_img = pet_img .extract (start , end )
263307 reftac = reftac .extract (start , end )
264308
309+ if fwhm and model not in ["srtmzhou2003" ]:
310+ fwhm = None
311+ warnings .warn (
312+ "--fwhm argument is not relevant for this model, will be ignored" ,
313+ RuntimeWarning ,
314+ stacklevel = 2 ,
315+ )
316+
265317 # fit kinetic model
266318 km : KineticModel
267319 match model :
268320 case "suvr" :
321+ model_abbr = "SUVR"
269322 km = SUVR (reftac , pet_img )
270323 km .fit (mask = petmask_img_mat )
271324 case "srtmlammertsma1996" :
325+ model_abbr = "SRTM"
272326 km = SRTMLammertsma1996 (reftac , pet_img )
273327 km .fit (mask = petmask_img_mat , weight_by = weight_by )
274328 case "srtmzhou2003" :
329+ model_abbr = "SRTM"
275330 km = SRTMZhou2003 (reftac , pet_img )
276331 km .fit (
277332 mask = petmask_img_mat ,
@@ -288,7 +343,13 @@ def kineticmodel(
288343 froot = os .path .join (outputdir , bname )
289344
290345 if isinstance (pet_img , PETBIDSMatrix ):
291- output = froot + "_km-" + model .replace ("." , "" ) + ext
346+ # if the input file name follows the PET-BIDS Derivatives convention,
347+ # it should end with "_tacs". Need to remove this to maintain
348+ # compatibility with the PET-BIDS Derivatives convention.
349+ froot = re .sub ("_tacs$" , "" , froot )
350+
351+ output = froot + "_model-" + model_abbr + "_kinpar" + ext
352+ output_json = froot + "_model-" + model_abbr + "_kinpar.json"
292353 data = np .empty ((len (km .parameters ), pet_img .num_elements ))
293354 for i , param in enumerate (km .parameters .keys ()):
294355 data [i ] = km .get_parameter (param )
@@ -299,16 +360,73 @@ def kineticmodel(
299360 for i , elem in enumerate (pet_img .elem_names ):
300361 tsvwriter .writerow ([elem ] + datat [i ].tolist ())
301362 else :
363+ # if the input file name follows the PET-BIDS convention, it should end
364+ # with "_pet". Need to remove this to maintain compatibility with the
365+ # PET-BIDS Derivatives convention.
366+ froot = re .sub ("_pet$" , "" , froot )
367+
302368 # save estimated parameters as image
303369 for param in km .parameters .keys ():
304370 res_img : SpatialImage = km .get_parameter (param ) # type: ignore
305371 output = (
306- froot + "_km-" + model .replace ("." , "" ) + "_kp-" + param + ext + addext
372+ froot
373+ + "_model-"
374+ + model_abbr
375+ + "_meas-"
376+ + param
377+ + "_mimap"
378+ + ext
379+ + addext
307380 )
308381 res_img .to_filename (output )
309-
310- # also need to save a json PET BIDS derivative file
311- # TODO
382+ output_json = froot + "_model-" + model_abbr + "_mimap.json"
383+
384+ # save json PET BIDS derivative file
385+ inputvalues = [start , end ]
386+ inputvalueslabels = [
387+ "Start of time window for model" ,
388+ "End of time window for model" ,
389+ ]
390+ inputvaluesunits = ["min" , "min" ]
391+
392+ if fwhm :
393+ inputvalues += [fwhm ]
394+ inputvalueslabels += ["Full width at half max" ]
395+ inputvaluesunits += ["mm" ]
396+
397+ cmd = (
398+ f"kineticmodel --model { model } "
399+ + (f"--refroi { refroi } " if refroi else f"--refmask { refmask } " )
400+ + (f"--outputdir { outputdir } " if outputdir else "" )
401+ + (f"--json { json } " if json else "" )
402+ + (f"--petmask { petmask } " if petmask else "" )
403+ + f"--start { start } "
404+ + f"--end { end } "
405+ + (f"--fwhm { fwhm } " if fwhm else "" )
406+ + f"--weight_by { weight_by } "
407+ + f"--integration_type { integration_type } "
408+ + pet
409+ )
410+ doc = km .__class__ .__doc__
411+ derivative_json_dict = {
412+ "Description" : re .sub (r"\s+" , " " , doc ) if doc else "" ,
413+ # "Sources": [pet],
414+ "ModelName" : model_abbr ,
415+ "ReferenceRegion" : refroi if refroi else refmask ,
416+ "AdditionalModelDetails" : (
417+ f"Frame weighting by: { weight_by } . "
418+ + f"Integration type: { integration_type } ." ,
419+ ),
420+ "InputValues" : inputvalues ,
421+ "InputValuesLabels" : inputvalueslabels ,
422+ "InputValuesUnits" : inputvaluesunits ,
423+ "SoftwareName" : "dynamicpet" ,
424+ "SoftwareVersion" : __version__ ,
425+ "CommandLine" : cmd ,
426+ }
427+
428+ with open (output_json , "w" ) as f :
429+ json_dump (derivative_json_dict , f , indent = 4 )
312430
313431
314432def parse_kineticmodel_inputs (
0 commit comments