def initStream(self, dicomDir, dicomFilePattern, dicomMinSize, **entities): """ Intialize a new DicomToBids stream, watches for Dicoms and streams as BIDS Args: dicomDir: The directory where the scanner will write new DICOM files dicomFilePattern: A regex style pattern of the DICOM filenames to watch for. They should include a {TR} tag with optional formatting. For example filenames like '001_000013_000005.dcm' would have a pattern '001_000013_{TR:06d}.dcm' where the volume number (TR) will be filled in by a 6 digit leading zeros value. dicomMinSize: Minimum size of the file to return (will continue waiting if below this size) entities: BIDS entities (subject, session, task, run, suffix, datatype) that define the particular subject/run of the data to stream """ # TODO - make sure dicomPattern has {TR} in it if 'subject' not in entities.keys(): raise MissingMetadataError("Entities must include 'subject' field") if 'task' not in entities.keys(): raise MissingMetadataError("Entities must include 'task' field") if 'suffix' not in entities.keys(): entities['suffix'] = 'bold' if 'datatype' not in entities.keys(): entities['datatype'] = 'func' self.entities = entities self.dicomDir = dicomDir self.dicomFilePattern = dicomFilePattern # TODO - restrict allowed directories, check that dicomDir is in allowed dir self.dataInterface = DataInterface(dataRemote=False, allowedDirs=self.allowedDirs, allowedFileTypes=['.dcm']) self.dicomStreamId = self.dataInterface.initScannerStream( dicomDir, dicomFilePattern, dicomMinSize) self.nextVol = 0
def __init__(self, dataRemote=False, subjectRemote=False, webUI=None): """ Args: dataRemote: whether file read/write requests will be handled directly by the projectServer or forwarded over websocket RPC to a remote service. subjectRemote: whether subject send/receive feedback will be handled locally within projectServer or forwarded over websocket RPC to a remote service. """ self.dataRemote = dataRemote self.subjectRemote = subjectRemote allowedDirs = None allowedFileTypes = None if dataRemote is False: # Allow all file types and directories for local filesystem access allowedDirs = ['*'] allowedFileTypes = ['*'] # Instantiate the client service instances ProjectRPCService.exposed_DataInterface = DataInterface( dataRemote=dataRemote, allowedDirs=allowedDirs, allowedFileTypes=allowedFileTypes) ProjectRPCService.exposed_BidsInterface = BidsInterface( dataRemote=dataRemote) ProjectRPCService.exposed_SubjectInterface = SubjectInterface( subjectRemote=subjectRemote) ProjectRPCService.exposed_WebDisplayInterface = webUI
def test_readDicom(): dicomImg1 = imgHandler.readDicomFromFile(dicomFile) vol1 = imgHandler.parseDicomVolume(dicomImg1, 64) assert vol1 is not None with open(dicomFile, 'rb') as fp: data = fp.read() dicomImg2 = imgHandler.readDicomFromBuffer(data) vol2 = imgHandler.parseDicomVolume(dicomImg2, 64) assert vol2 is not None assert (vol1 == vol2).all() # if dataInterface is not initialized with allowedDirs or allowedFileTypes it should fail dataInterface = DataInterface() with pytest.raises(ValidationError): dataInterface.initWatch(dicomDir, '*.dcm', 0) # Now allow all dirs and file types dataInterface = DataInterface(allowedDirs=['*'], allowedFileTypes=['*']) dataInterface.initWatch(dicomDir, '*.dcm', 0) dicomImg3 = imgHandler.readRetryDicomFromDataInterface( dataInterface, dicomFile) vol3 = imgHandler.parseDicomVolume(dicomImg3, 64) assert vol3 is not None assert (vol1 == vol3).all() # read in a truncated file, should fail and return None. trucatedDicomFile = os.path.join(dicomDir, test_dicomTruncFile) dicomImg4 = imgHandler.readRetryDicomFromDataInterface( dataInterface, trucatedDicomFile) assert dicomImg4 is None # Test convert to nifti niftiObject = dicomreaders.mosaic_to_nii(dicomImg3) assert niftiObject is not None dataInterface.fileWatcher.__del__() dataInterface.fileWatcher = None # Test anonymization of sensitive patient fields def countUnanonymizedSensitiveAttrs(dicomImg): sensitiveAttrs = 0 for attr in imgHandler.attributesToAnonymize: if hasattr(dicomImg, attr) and getattr(dicomImg, attr) != "": sensitiveAttrs += 1 return sensitiveAttrs dicomImg5 = imgHandler.readDicomFromFile(dicomFile) assert countUnanonymizedSensitiveAttrs(dicomImg5) >= 1 imgHandler.anonymizeDicom(dicomImg5) assert countUnanonymizedSensitiveAttrs(dicomImg5) == 0
def test_localDataInterface(self, dicomTestFilename, bigTestFile): """Test DataInterface when instantiated in local mode""" dataInterface = DataInterface(dataRemote=False, allowedDirs=allowedDirs, allowedFileTypes=allowedFileTypes) runDataInterfaceMethodTests(dataInterface, dicomTestFilename) runLocalFileValidationTests(dataInterface) runReadWriteFileTest(dataInterface, bigTestFile) return
def __init__(self, rpyc_timeout=60, yesToPrompts=False): """ Establishes an RPC connection to a localhost projectServer on a predefined port. The projectServer must be running on the same computer as the script using this interface. """ try: safe_attrs = rpyc.core.protocol.DEFAULT_CONFIG.get('safe_attrs') safe_attrs.add('__format__') rpcConn = rpyc.connect( 'localhost', 12345, config={ "allow_public_attrs": True, "safe_attrs": safe_attrs, "allow_pickle": True, "sync_request_timeout": rpyc_timeout, # "allow_getattr": True, # "allow_setattr": True, # "allow_delattr": True, # "allow_all_attrs": True, }) # Need to provide an override class of DataInstance to return data from getImage self.dataInterface = WrapRpycObject(rpcConn.root.DataInterface) self.subjInterface = WrapRpycObject(rpcConn.root.SubjectInterface) self.bidsInterface = WrapRpycObject(rpcConn.root.BidsInterface) self.exampleInterface = WrapRpycObject( rpcConn.root.ExampleInterface) # WebDisplay is always run within the projectServer (i.e. not a remote service) self.webInterface = rpcConn.root.WebDisplayInterface self.rpcConn = rpcConn except ConnectionRefusedError as err: if yesToPrompts: print( 'Unable to connect to projectServer, continuing using localfiles' ) reply = 'y' else: reply = input( 'Unable to connect to projectServer, continue using localfiles? ' + '(y/n): ') reply.lower().strip() if reply[0] == 'y': # These will be run in the same process as the experiment script self.dataInterface = DataInterface(dataRemote=False, allowedDirs=['*'], allowedFileTypes=['*']) self.subjInterface = SubjectInterface(subjectRemote=False) self.bidsInterface = BidsInterface(dataRemote=False, allowedDirs=['*']) self.exampleInterface = ExampleInterface(dataRemote=False) # Without a webServer (projectServer) the webInterface won't be able to do # anything. Create a stub instance here with ioLoopInst=None so that calls # to it won't thow exceptions. self.webInterface = WebDisplayInterface(ioLoopInst=None) else: raise err
def __init__(self, args, webSocketChannelName='wsData'): """ Uses the WsRemoteService framework to parse connection-related args and establish a connection to a remote projectServer. Instantiates a local version of DataInterface and BidsInterface to handle client requests coming from the projectServer connection. Args: args: Argparse args related to connecting to the remote server. These include "-s <server>", "-u <username>", "-p <password>", "--test", "-i <retry-connection-interval>" webSocketChannelName: The websocket url extension used to connecy and communicate to the remote projectServer, e.g. 'wsData' would connect to 'ws://server:port/wsData' """ self.dataInterface = DataInterface( dataRemote=False, allowedDirs=args.allowedDirs, allowedFileTypes=args.allowedFileTypes) self.bidsInterface = BidsInterface(dataRemote=False) self.wsRemoteService = WsRemoteService(args, webSocketChannelName) self.wsRemoteService.addHandlerClass(DataInterface, self.dataInterface) self.wsRemoteService.addHandlerClass(BidsInterface, self.bidsInterface)
class DicomToBidsStream(): """ A class that watches for DICOM file creation in a specified directory and with a specified file pattern. When DICOM files arrive it converts them to BIDS incrementals and returns the BIDS incremental. This lets a real-time classification script process data directly as BIDS as it arrives from the scanner. """ def __init__(self, allowedDirs=[]): self.allowedDirs = allowedDirs def initStream(self, dicomDir, dicomFilePattern, dicomMinSize, **entities): """ Intialize a new DicomToBids stream, watches for Dicoms and streams as BIDS Args: dicomDir: The directory where the scanner will write new DICOM files dicomFilePattern: A regex style pattern of the DICOM filenames to watch for. They should include a {TR} tag with optional formatting. For example filenames like '001_000013_000005.dcm' would have a pattern '001_000013_{TR:06d}.dcm' where the volume number (TR) will be filled in by a 6 digit leading zeros value. dicomMinSize: Minimum size of the file to return (will continue waiting if below this size) entities: BIDS entities (subject, session, task, run, suffix, datatype) that define the particular subject/run of the data to stream """ # TODO - make sure dicomPattern has {TR} in it if 'subject' not in entities.keys(): raise MissingMetadataError("Entities must include 'subject' field") if 'task' not in entities.keys(): raise MissingMetadataError("Entities must include 'task' field") if 'suffix' not in entities.keys(): entities['suffix'] = 'bold' if 'datatype' not in entities.keys(): entities['datatype'] = 'func' self.entities = entities self.dicomDir = dicomDir self.dicomFilePattern = dicomFilePattern # TODO - restrict allowed directories, check that dicomDir is in allowed dir self.dataInterface = DataInterface(dataRemote=False, allowedDirs=self.allowedDirs, allowedFileTypes=['.dcm']) self.dicomStreamId = self.dataInterface.initScannerStream( dicomDir, dicomFilePattern, dicomMinSize) self.nextVol = 0 def getNumVolumes(self) -> int: """ Return the number of brain volumes in the run, unknowable by this interface ahead of time for a real-time DICOM stream """ raise NotImplementedError( 'getNumVolumes not implemented for DicomBidsStream') def getIncremental(self, volIdx=-1) -> BidsIncremental: """ Get the BIDS incremental for the corresponding DICOM image indicated by the volIdx, where volIdx is equivalent to TR id. VolIdx acts similar to a file_seek pointer. If a volIdx >= 0 is supplied the volume pointer is advanced to that position. If no volIdx or a volIdx < 0 is supplied, then the next image volume after the previous position is returned and the pointer is incremented. Args: volIdx: The volume index (or TR) within the run to retrieve. Returns: BidsIncremental for the matched DICOM for the run/volume """ if volIdx >= 0: # reset the next volume to the user specified volume self.nextVol = volIdx else: # use the default next volume pass # wait for the dicom and create a bidsIncremental dcmImg = self.dataInterface.getImageData(self.dicomStreamId, self.nextVol) dicomMetadata = getDicomMetadata(dcmImg) dicomMetadata.update(self.entities) niftiImage = convertDicomImgToNifti(dcmImg) incremental = BidsIncremental(niftiImage, dicomMetadata) self.nextVol += 1 return incremental