def construct_model_out(m_v, structure, segment_result): ''' Construct ModelOutput protobuf message object using segmentation results. Parameters: m_v - django.db.ModelVersion - Database model entry for model version structure - django.db.Structure - Databse model entry for the structure corresponding to the model version. segment_result - [ndarray] - List of output channel data in ndarray form Returns: model_out - ModelOutput.pb - Protobuf message object for the model output ''' model_out = Model_pb2.ModelOutput() model_out.ModelID = m_v.model_version_id model_out.ProcesserVersion = "{}.{}".format(m_v.major_version, \ m_v.minor_version) model_out.LanguageCode = m_v.language_code structure_pb = structure.model_to_pb() for result in segment_result: out_channel = Model_pb2.ModelOutputChannel() out_channel.Structure.CopyFrom(structure_pb) depth, height, width = result.shape out_channel.Volume.Width = width out_channel.Volume.Height = height out_channel.Volume.Depth = depth result_bytes = result.tobytes() out = BytesIO() with gzip.GzipFile(fileobj=out, mode='w') as file_out: file_out.write(result_bytes) out_channel.Volume.Data = out.getvalue() out_channel.Volume.DataType = Primitives3D_pb2.VolumeData3D.DataTypes.Byte out_channel.Volume.CompressionMethod = 0 model_out.Channels.append(out_channel) return model_out
def pb_to_model(self, pb_str): ''' Converts a protobuf message into attributes for the model. DoubleValues are just stored as their values within the database. Parameters: pb_str - (bytestring) - protobuf message to interpret into django db model Returns: none ''' # Parse protobuf message feedback_pb = Model_pb2.SegmentationFeedback() feedback_pb.ParseFromString(pb_str) assert feedback_pb.IsInitialized(), \ "Protobuf SegmentationFeedback model not initialized after parsing Protobuf input" # Assign class fields self.client_information = feedback_pb.ClientInformation.SoftwareVersion self.segmentation_id = feedback_pb.SegmentationID self.segmentation_accepted = feedback_pb.SegmentationAccepted self.general_comments = feedback_pb.GeneralComments self.general_score = None if feedback_pb.GeneralScore is None else \ feedback_pb.GeneralScore.Value self.save() # Iterate through structure comments and add to database for struc_com in feedback_pb.StructureComments: self.structurecomment_set.create( structure_id=struc_com.StructureID, comments=struc_com.Comments, score=None if struc_com.Score is None else struc_com.Score.Value)
def pb_to_model(self, pb_str): ''' Converts a protobuf message into attributes for the model. DoubleValues are just stored as their values within the database. Parameters: pb_str - (bytestring) - protobuf message to interpret into django db model Returns: none ''' # Parse protobuf message telemetry_pb = Model_pb2.SegmentationTelemetry() telemetry_pb.ParseFromString(pb_str) assert telemetry_pb.IsInitialized(), \ "Protobuf SegmentationTelemetry model not initialized after parsing Protobuf input" # Assign class fields self.client_software_version = telemetry_pb.ClientInformation.SoftwareVersion self.upload_time_ms = telemetry_pb.UploadTimeInMilliseconds self.segmentation_wait_ms = telemetry_pb.SegmentationWaitInMilliseconds self.segmentation_down_ms = telemetry_pb.SegmentationDownloadInMilliseconds self.segmentation_retries = telemetry_pb.NumberOfRetries self.segmentation_model_id = telemetry_pb.ModelID self.segmentation_id = telemetry_pb.SegmentationID self.client_error = telemetry_pb.ClientError self.client_error_info = telemetry_pb.ClientErrorInformation
def get_models(request): ''' Endpoint for API GET Model Collections request. Returns: 1. ModelsCollection protobuf message if "accept" header is "application/x-protobuf" 2. JSON response otherwise ''' # Tries to construct collection of model families, returning an error if failure. try: # Creates protobuf collection object response = Model_pb2.ModelsCollection() # Iterates through all model familys and adds to collection for model_fam in ModelFamily.objects.all(): # Check if pb file exists. if model_fam.pb == '': fname = model_fam.canonical_name fname.replace(' ', '-') pb_msg = model_fam.model_to_pb().SerializeToString() file_io = io.BytesIO(bytes(pb_msg)) model_fam.pb.save(fname + ".pb", File(file_io)) model_fam.save() mf_file = open(model_fam.pb.path, 'rb') file_pb = mf_file.read() mf_file.close() # Check if fields are filled. if model_fam.canonical_name == '': model_fam.pb_to_model(file_pb) model_fam.save() mf_pb = Model_pb2.ModelFamily() mf_pb.ParseFromString(file_pb) response.Models.append(mf_pb) return format_and_send_response(request, response) # On exception, returns invalid request error message pb. except: msg = "Invalid request." details = "The request is not acceptable." return bad_request_helper(request, msg, details, 406)
def post_segmentation(request, model_id): ''' Endpoint for API POST Segmentation Job requests. Enforces protobuf msg input. Parameters: model_id (str) - model_id with which the segmentation job should use. Returns: 1. SegmentationTask protobuf message if "accept" header is "application/x-protobuf" 2. JSON response otherwise ''' # Tries to create a segmentation task entry in the database. seg_job = SegmentationJob() try: # Set model ID seg_job.model_id = model_id # Generate Segmentation ID and 'time_created' using current datetime with TZ support. seg_job.time_field = timezone.now() # Generate file on disk and point database to location request_data = bytes(request.body) # Check valid model input seg_pb = Model_pb2.ModelInput() seg_pb.ParseFromString(request_data) # Save model input file_io = io.BytesIO(request_data) fname = "{}.pb".format("Segmentation_{}".format( seg_job.segmentation_id)) seg_job.model_input.save(fname, File(file_io)) seg_job.save() except: seg_job.delete() msg = "Invalid request." details = "The posted model input is not valid. "+\ "It might have empty fields that are required." return bad_request_helper(request, msg, details, 400) # Start asynchronous segmentation with state machine m_v = ModelVersion.objects.filter(model_version_id=model_id)[0] if m_v.model_type == ModelVersion.ModelVersionType.Phantom: start_phantom_segmentation.delay(model_id, seg_job.segmentation_id) elif m_v.model_type == ModelVersion.ModelVersionType.Pytorch: start_pytorch_segmentation_single_structure.delay( model_id, seg_job.segmentation_id) elif m_v.model_type == ModelVersion.ModelVersionType.Tensorflow: start_phantom_segmentation.delay(model_id, seg_job.segmentation_id) else: start_phantom_segmentation.delay(model_id, seg_job.segmentation_id) # Construct SegmentationTask response. response = seg_job.get_task_response() return format_and_send_response(request, response)
def ping(request): ''' Endpoint for API ping GET requests. Returns either: 1. Protobuf-serialized response if "accept" header is "application/x-protobuf" 2. JSON response otherwise ''' version = MAJOR_VERSION + MINOR_VERSION api_info = Model_pb2.ApiInformation() api_info.Version = version return format_and_send_response(request, api_info)
def test_get_credits_protobuf(self): ''' Test for endpoint: /api/v2/Credits With accept-type "application/x-protobuf" ''' response = self.client.get('/api/v2/Credits',\ **{'HTTP_ACCEPT':'application/x-protobuf'}) self.assertEqual(response.status_code, 200, \ msg='/api/v2/Credits endpoint did not return 200 status code.') response_pb = Model_pb2.Credits() response_pb.ParseFromString(response.content) self.assertEqual(response_pb.TotalCredits, 100000.0, \ msg='/api/v2/Credits endpoint did not return proper Protobuf message.')
def test_api_ping_protobuf(self): ''' Test for endpoint: /api/ping With accept-type "application/x-protobuf" ''' response = self.client.get('/api/ping', \ **{'HTTP_ACCEPT':'application/x-protobuf'}) self.assertEqual(response.status_code, 200, \ msg='/api/ping endpoint did not return 200 status code.') response_pb = Model_pb2.ApiInformation() response_pb.ParseFromString(response.content) self.assertNotEqual(response_pb.Version, None, \ msg='/api/ping endpoint did not return proper Protobuf message.')
def get_segmentation_result(request, model_id, segmentation_id): ''' Endpoint for API GET Segmentation Job result requests. Parameters: model_id (str) - model_id with which the segmentation job should use. segmentation_id (str) - UUID str for the segmentation job Returns: 1. ModelOutput protobuf message if "accept" header is "application/x-protobuf" 2. JSON response otherwise ''' # Database query for segmentation job with segmentation_id parameter. try: seg_job = SegmentationJob.objects.get(model_id=model_id, segmentation_id=segmentation_id) except: msg = "Invalid request." details = "The segmentation job does not exist." return bad_request_helper(request, msg, details, 400) job_status = seg_job.get_job_progress() # Return unfinished bad request if progress isn't 100. if job_status.Progress < 100: msg = "Invalid request." details = "The segmentation job is still processing." return bad_request_helper(request, msg, details, 400) try: # Attempts to read the file as bytestring model_out_path = seg_job.model_output.path f_in = open(model_out_path, 'rb') model_out = f_in.read() f_in.close() if request.headers["accept"] == "application/x-protobuf": seg_job.delete() return HttpResponse(model_out, status=200) # Else response = Model_pb2.ModelOutput() response.ParseFromString(model_out) seg_job.delete() return JsonResponse(json_format.MessageToDict(response), status=200) except: msg = "Invalid request." details = "The segmentation job has encountered an error." return bad_request_helper(request, msg, details, 400)
def get_task_response(self): ''' Generates Segmentation Task response protobuf message from the stored Segmentation Job. Parameters: none Returns: response - Model_pb2.SegmentationTask - Protobuf Segmentation Task object ready for serialization. ''' timestamp = Timestamp() timestamp.FromDatetime(self.time_field) response = Model_pb2.SegmentationTask() response.SegmentationID = str(self.segmentation_id) response.StartTime.seconds = timestamp.seconds response.StartTime.nanos = timestamp.nanos return response
def test_get_models_protobuf(self): ''' Test for endpoint: /api/v2/Model/ With accept-type "application/x-protobuf" ''' response = self.client.get('/api/v2/Model/', \ **{'HTTP_ACCEPT':'application/x-protobuf'}) self.assertEqual(response.status_code, 200, \ msg='/api/v2/Model/ endpoint did not return 200 status code.') response_pb = Model_pb2.ModelsCollection() response_pb.ParseFromString(response.content) self.assertEqual(len(response_pb.Models), 1, \ msg='/api/v2/Model/ endpoint did not return correct JSON message.') self.assertEqual(response_pb.Models[0].CanonicalName, \ GetModelsTestCase.model_family.canonical_name, \ msg='/api/v2/Model/ endpoint did not return correct Phantom model.')
def acquire_model_input(seg_job): ''' Acquires the ModelInput protobuf message object from the corresponding segmentation job. Parameters: seg_job - django.db.SegmentationJob - Django model for a segmentation job. Returns: model_in - ModelInput.pb - Protobuf message object for the model input ''' file_path = open(seg_job.model_input.path, 'rb') file_input = file_path.read() file_path.close() model_in = Model_pb2.ModelInput() model_in.ParseFromString(file_input) return model_in
def bad_request_helper(request, msg, details, status): ''' Helper method for generating bad request responses. Parameters: request - The original request msg - str - Error message details - str - Details regarding the bad request response. status - int - Status code to use in response. Returns: HttpResponse - Model_pb2.BadRequestResponse - If 'accept' type is 'application/x-protobuf' JsonResponse - Model_pb2.BadRequestResponse - Otherwise ''' response = Model_pb2.BadRequestResponse() response.ErrorMessage = msg response.ExceptionDetails = details return format_and_send_response(request, response, status=status)
def model_to_pb(self): ''' Converts the db model into a protobuf message. Parameters: None Returns: structure - protobuf message for a structure. ''' structure = Model_pb2.Structure() structure.Name = self.name structure.Color.R = self.color_r structure.Color.G = self.color_g structure.Color.B = self.color_b structure.Type = self.structure_type structure.FMACode = self.FMA_code structure.InputChannelID = self.input_channel_id structure.StructureID = self.structure_id return structure
def get_job_progress(self): ''' Generates Segmentation Progress response protobuf message from the stored Segmentation Job. Parameters: none Returns: response - Model_pb2.Segmentationprogress - Protobuf Segmentation Progress object. ''' response = Model_pb2.SegmentationProgress() if self.model_output != "": # 100 progress response.Progress = 100 response.Errors = "" response.ErrorCode = 0 return response # Mock response with 0 progress response.Progress = 0 response.Errors = "" response.ErrorCode = 0 return response
def get_credits(request): ''' Endpoint for API GET Credits request. Since this is a research server, mock information will be returned. The user will have plenty of credits. In addition, a message will be returned to indicate that credits will not apply. Returns either: 1. Protobuf-serialized response if "accept" header is "application/x-protobuf" 2. JSON response otherwise ''' total_cred = 100000 display_warning = True message = "Local Research Server - Credits will not apply." language = "English" credits_info = Model_pb2.Credits() credits_info.TotalCredits = total_cred credits_info.DisplayCreditsWarning = display_warning credits_info.CreditsWarningMessage = message credits_info.LanguageCode = language return format_and_send_response(request, credits_info)
def get_vendor_status(request): ''' Endpoint for API GET Vendor Status requests. Since this is a private research server, all information is mocked up. Returns: 1. ModelOutput protobuf message if "accept" header is "application/x-protobuf" 2. JSON response otherwise ''' response = Model_pb2.VendorStatus() response.TotalCredits = 100000 response.LowCreditsWarningMessage = "Local Research Server - Credits will not apply." response.ClientCountryCode = "US" response.SegmentationServiceStatus = Model_pb2.VendorStatus.VendorServiceStatus.Available ip_addr = get_ip_address() response.SegmentationServiceUrl = "http://" + ip_addr + ":8000/api/v2/" response.AvailableSegmentationServiceLocations.append("US") response.VendorName = "Research Server" response.VendorDescriptionHtml = "" response.LanguageCode = "en-US" return format_and_send_response(request, response)
def test_post_job_retrieve_results_json(self): ''' Test for endpoints: /api/v2/Model/{modelId}/segmentation /api/v2/Model/{modelId}/segmentation/{segmentationId} /api/v2/Model/{modelId}/segmentation/{segmentationId}/result Since database objects are not maintained between test cases, 3 end-points are tested here. Retrieve JSON results ''' #---------------------------------------------------------------------- # Testing POST /api/v2/Model/{modelId}/segmentation #---------------------------------------------------------------------- # Post job to temporary server model_id = PostSegmentationTestCase.model_version.model_version_id.replace(" ", "%20") post_response = self.client.post('/api/v2/Model/{}/segmentation'.format(model_id), \ self.seg_job, \ content_type='application/x-protobuf', \ **{'HTTP_ACCEPT':'application/x-protobuf'}) self.assertEqual(post_response.status_code, 200, \ msg='/api/v2/Model/{}/segmentation endpoint did not return 200 status code.'.format(\ model_id)) # Append orphaned segmentation job file path to path list for teardown. db_seg_path = SegmentationJob.objects.all()[0].model_input.path self.path_list.append(db_seg_path) # Parse protobuf message output seg_task = Model_pb2.SegmentationTask() seg_task.ParseFromString(post_response.content) seg_id = seg_task.SegmentationID #---------------------------------------------------------------------- # Testing /api/v2/Model/{modelId}/segmentation/{segmentationId} #---------------------------------------------------------------------- # Get progress from temporary server. Since the test client is running with eager Celery # scheduling, it should always return 100% progress. get_response = self.client.get('/api/v2/Model/{}/segmentation/{}'.format( \ model_id, seg_id), \ **{'HTTP_ACCEPT':'application/x-protobuf'}) self.assertEqual(get_response.status_code, 200, \ msg='/api/v2/Model/{}/segmentation/{} endpoint did not return 200 status code.'.format(\ model_id, seg_id)) # Parse protobuf message output progress = Model_pb2.SegmentationProgress() progress.ParseFromString(get_response.content) self.assertEqual(progress.Progress, 100, \ msg='/api/v2/Model/{}/segmentation/{} endpoint did not return 100% progress.'.format(\ model_id, seg_id)) # Append orphaned segmentation job file path to path list for teardown. db_results_path = SegmentationJob.objects.all()[0].model_output.path self.path_list.append(db_results_path) #---------------------------------------------------------------------- # Testing /api/v2/Model/{modelId}/segmentation/{segmentationId}/result #---------------------------------------------------------------------- seg_response = self.client.get('/api/v2/Model/{}/segmentation/{}/result'.format( \ model_id, seg_id), \ **{'HTTP_ACCEPT':'application/json'}) self.assertEqual(seg_response.status_code, 200, \ msg='/api/v2/Model/{}/segmentation/{}/result endpoint did not return 200 status code.'\ .format(model_id, seg_id)) seg_result = seg_response.json() self.assertEqual(seg_result['ModelID'], model_id.replace("%20"," "), \ msg='/api/v2/Model/{}/segmentation/{}/result endpoint did not fetch correct result.'\ .format(model_id, seg_id))
def pb_to_model(self, pb_str): ''' Converts a protobuf message into attributes for the model. DoubleValues are just stored as their values within the database. Parameters: pb_str - (bytestring) - protobuf message to interpret into django db model Returns: none ''' model_family = Model_pb2.ModelFamily() model_family.ParseFromString(pb_str) # Protobuf top-level fields self.canonical_name = model_family.CanonicalName self.language_code = model_family.LanguageCode # Model family description self.name = model_family.FamilyDescription.Name self.description = model_family.FamilyDescription.Description self.clinical_domain = model_family.FamilyDescription.ClinicalDomain self.anatomical_region = model_family.FamilyDescription.AnatomicalRegion self.primary_structure = model_family.FamilyDescription.PrimaryStructure self.modalities = model_family.FamilyDescription.Modalities self.gender = model_family.FamilyDescription.Constraints.Gender # Model family constraints body parts examined for body_part in model_family.FamilyDescription.Constraints.BodyPartsExamined: self.bodypartexamined_set.create(part_type=body_part) # Model channel descriptions for model_channel in model_family.FamilyDescription.InputChannels: self.modelchanneldescription_set.create( channel_id=model_channel.ChannelID, spacing_min_x=model_channel.Constraints. SpacingMinInMillimeters.X, spacing_min_y=model_channel.Constraints. SpacingMinInMillimeters.Y, spacing_min_z=model_channel.Constraints. SpacingMinInMillimeters.Z, spacing_max_x=model_channel.Constraints. SpacingMaxInMillimeters.X, spacing_max_y=model_channel.Constraints. SpacingMaxInMillimeters.Y, spacing_max_z=model_channel.Constraints. SpacingMaxInMillimeters.Z, dimensions_min_x=model_channel.Constraints. DimensionsMinInPixels.X, dimensions_min_y=model_channel.Constraints. DimensionsMinInPixels.Y, dimensions_min_z=model_channel.Constraints. DimensionsMinInPixels.Z, dimensions_max_x=model_channel.Constraints. DimensionsMaxInPixels.X, dimensions_max_y=model_channel.Constraints. DimensionsMaxInPixels.Y, dimensions_max_z=model_channel.Constraints. DimensionsMaxInPixels.Z, req_original=model_channel.Constraints.OriginalDataRequired, is_axial=model_channel.Constraints.IsAxial, accepted_modalities_pb=model_channel.Constraints. AcceptedModalities) # Model Versions for model_version in model_family.ModelVersions: # Create model version in db db_mv = self.modelversion_set.create( model_version_id=model_version.ID, model_version_desc=model_version.VersionDescription, created_time=model_version.CreatedOn.ToDatetime().replace( tzinfo=pytz.utc), credits_req=model_version.NumberOfCreditsRequired, major_version=model_version.MajorVersion, minor_version=model_version.MinorVersion, language_code=model_version.LanguageCode) # Iterative add structures for struc in model_version.Structures: db_mv.structure_set.create( name=struc.Name, color_r=struc.Color.R, color_g=struc.Color.G, color_b=struc.Color.B, structure_type=struc.Type, FMA_code=struc.FMACode, input_channel_id=struc.InputChannelID, structure_id=struc.StructureID)
def model_to_pb(self, filename=None): # Not implemented ''' Converts the db model into a protobuf message. Parameters: None Returns: m_f - protobuf message for a model family. ''' m_f = Model_pb2.ModelFamily() # Protobuf top-level fields m_f.CanonicalName = self.canonical_name m_f.LanguageCode = self.language_code # Family Description fields m_f.FamilyDescription.Name = self.name m_f.FamilyDescription.Description = self.description m_f.FamilyDescription.ClinicalDomain = self.clinical_domain m_f.FamilyDescription.AnatomicalRegion = self.anatomical_region m_f.FamilyDescription.PrimaryStructure = self.primary_structure m_f.FamilyDescription.Modalities = self.modalities # Family Description Input Channels for chan_desc in self.modelchanneldescription_set.all(): model_channel = Model_pb2.ModelChannelDescription() model_channel.ChannelID = chan_desc.channel_id for mode in ast.literal_eval(chan_desc.accepted_modalities_pb): model_channel.Constraints.AcceptedModalities.append(mode) model_channel.Constraints.OriginalDataRequired = chan_desc.req_original model_channel.Constraints.IsAxial = chan_desc.is_axial model_channel.Constraints.SpacingMinInMillimeters.X = chan_desc.spacing_min_x model_channel.Constraints.SpacingMinInMillimeters.Y = chan_desc.spacing_min_y model_channel.Constraints.SpacingMinInMillimeters.Z = chan_desc.spacing_min_z model_channel.Constraints.SpacingMaxInMillimeters.X = chan_desc.spacing_max_x model_channel.Constraints.SpacingMaxInMillimeters.Y = chan_desc.spacing_max_y model_channel.Constraints.SpacingMaxInMillimeters.Z = chan_desc.spacing_max_z model_channel.Constraints.DimensionsMinInPixels.X = chan_desc.dimensions_min_x model_channel.Constraints.DimensionsMinInPixels.Y = chan_desc.dimensions_min_y model_channel.Constraints.DimensionsMinInPixels.Z = chan_desc.dimensions_min_z model_channel.Constraints.DimensionsMaxInPixels.X = chan_desc.dimensions_max_x model_channel.Constraints.DimensionsMaxInPixels.Y = chan_desc.dimensions_max_y model_channel.Constraints.DimensionsMaxInPixels.Z = chan_desc.dimensions_max_z m_f.FamilyDescription.InputChannels.append(model_channel) # Family Description Constraint fields m_f.FamilyDescription.Constraints.Gender = self.gender for bpe in self.bodypartexamined_set.all(): m_f.FamilyDescription.Constraints.BodyPartsExamined.append( bpe.part_type) # Model Version fields for m_v in self.modelversion_set.all(): model_version = Model_pb2.ModelVersion() model_version.ID = m_v.model_version_id model_version.VersionDescription = m_v.model_version_desc timestamp = Timestamp() timestamp.FromDatetime(m_v.created_time) model_version.CreatedOn.seconds = timestamp.seconds model_version.CreatedOn.nanos = timestamp.nanos model_version.NumberOfCreditsRequired = m_v.credits_req model_version.MajorVersion = m_v.major_version model_version.MinorVersion = m_v.minor_version model_version.LanguageCode = m_v.language_code for m_struct in m_v.structure_set.all(): model_structure = Model_pb2.Structure() model_structure.Name = m_struct.name model_structure.Color.R = m_struct.color_r model_structure.Color.G = m_struct.color_g model_structure.Color.B = m_struct.color_b model_structure.Type = m_struct.structure_type model_structure.FMACode = m_struct.FMA_code model_structure.InputChannelID = m_struct.input_channel_id model_structure.StructureID = m_struct.structure_id model_version.Structures.append(model_structure) m_f.ModelVersions.append(model_version) return m_f