sdk/python/foundation-models/healthcare-ai/medimageinsight/classification_demo/MedImageInsight.py (195 lines of code) (raw):

import urllib.request import json import os import ssl import numpy as np import mlflow.pyfunc import pandas as pd from .demo_utils import get_files_path, get_text from tqdm import tqdm class medimageinsight_package: """ The medimageinsight_package class is responsible for deploying MedImageInsight using either an online endpoint or the MLflow architecture. By using this class, you can easily deploy MedImageInsight and choose the deployment method that best suits your needs. Whether you want to deploy the model as an online endpoint or utilize the MLflow architecture, this class provides a convenient interface to handle the deployment process. The core function of this class is to efficiently generate image and text embeddings with MedImageInsight. Overall, the medimageinsight_package class provides a template to deploy the MedImageInsight model either as an online endpoint or using the MLflow architecture. """ def __init__( self, option="run_from_endpoint", endpoint_url=None, endpoint_key=None, model_version=None, mlflow_model_path=None, ): """ Initializes the MedImageInsight package. Parameters: - option (str): The option for deployment. Default is 'run_local'. - endpoint_url (str): The URL of the endpoint. Required if option is 'run_from_endpoint'. - endpoint_key (str): The key to invoke the endpoint. Required if option is 'run_from_endpoint'. - model_version (str): The version of the model. Required if option is 'run_from_endpoint'. - mlflow_model_path (str): The path to the MLFlow model. Required if option is 'run_local'. """ self.option = option if self.option == "run_from_endpoint": self.endpoint_url = endpoint_url self.endpoint_key = endpoint_key self.model_version = model_version if not self.endpoint_key: raise Exception("A key should be provided to invoke the endpoint") self.headers = { "Content-Type": "application/json", "Authorization": ("Bearer " + self.endpoint_key), } if self.model_version is not None: self.headers["azureml-model-deployment"] = (self.model_version,) elif self.option == "run_local": self.mlflow_model_path = mlflow_model_path if not self.mlflow_model_path: raise Exception("A path to the mlflow model should be provided") self.mlflow_model = mlflow.pyfunc.load_model(self.mlflow_model_path) def allowSelfSignedHttps(allowed): """ Bypasses the server certificate verification on the client side. Parameters: - allowed (bool): Whether to allow self-signed HTTPS certificates. """ if ( allowed and not os.environ.get("PYTHONHTTPSVERIFY", "") and getattr(ssl, "_create_unverified_context", None) ): ssl._create_default_https_context = ssl._create_unverified_context def generate_embeddings(self, data): """ Runs inference to generate embeddings on the model. Parameters (Must provide one of the following): - data (dict): - 'image': The path or data of the image(s). - 'text': The text data. Returns: - embeddings_dict (dict): A dictionary where each key is the name, and the value is another dictionary containing 'image_feature' and/or 'text_feature'. """ embeddings_dict = {} # Determine the appropriate function to call based on the option if self.option == "run_from_endpoint": run_function = self.run_from_endpoint elif self.option == "run_local": run_function = self.run_from_mlflow else: raise ValueError( f"Invalid option '{self.option}'. Expected 'run_from_endpoint' or 'run_local'." ) # Flags to check if image and/or text data are provided has_image = data.get("image") is not None has_text = data.get("text") is not None params = data["params"] if "params" in data else None # Generate embeddings based on provided data if has_image and has_text: embedding_dict, scale_factor = run_function( image=data["image"], text=data["text"], params=params ) for name, feat in embedding_dict.items(): embeddings_dict.setdefault(name, {})["image_feature"] = feat[ "image_feature" ] embeddings_dict.setdefault(name, {})["text_feature"] = feat[ "image_feature" ] else: if has_image: image_embedding_dict, scale_factor = run_function( image=data["image"], params=params ) for name, img_feat in image_embedding_dict.items(): embeddings_dict.setdefault(name, {})["image_feature"] = img_feat[ "image_feature" ] if has_text: text_embedding_dict, scale_factor = run_function(text=data["text"]) for name, txt_feat in text_embedding_dict.items(): embeddings_dict.setdefault(name, {})["text_feature"] = txt_feat[ "text_feature" ] return embeddings_dict, scale_factor def run_from_mlflow(self, image=None, text=None, params=None): """ Run inference with the MLflow model. Parameters: - image (str): The path to the image data. - text (str): The path to the text data. - params (dict): Additional parameters for prediction. - image_standardization_jpeg_compression_ratio (int): The JPEG compression ratio for the model input, default: 75. - image_standardization_image_size (int): The image size for MedImageInsight model input, default: 512. Returns: - embeddings_dict (dict): A dictionary where each key is the name, and the value is another dictionary containing 'image_feature' and/or 'text_feature'. """ embeddings_dict = {} if params is None: params = {} data_dict = {} # Collect image data into a dictionary if image is not None: # Assuming get_files_path returns a dictionary {name: {'file': image_data, 'index': index}} images_data = get_files_path(image) for name, data in images_data.items(): data_dict.setdefault(name, {})["image"] = data["file"] # Collect text data into a dictionary if text is not None: # Assuming get_text returns a dictionary {name: {'text': text_data, 'index': index}} texts_data = get_text(text) for name, data in texts_data.items(): data_dict.setdefault(name, {})["text"] = data["text"] # Ensure that image and text names match if both are provided if image is not None and text is not None: assert set(images_data.keys()) == set( texts_data.keys() ), "Image and text names do not match" print("--------Start Generating Image and Text Features--------") elif image is not None: print("--------Start Generating Image Features--------") elif text is not None: print("--------Start Generating Text Features--------") else: raise ValueError("At least one of 'image' or 'text' must be provided.") # Process each item in data_dict for name, data in tqdm(data_dict.items(), total=len(data_dict)): df = pd.DataFrame( {"image": [data.get("image", "")], "text": [data.get("text", "")]} ) result = self.mlflow_model.predict(df, params=params) embeddings_dict[name] = {} if "image_features" in result: embeddings_dict[name]["image_feature"] = np.array( result["image_features"][0] ) if "text_features" in result: embeddings_dict[name]["text_feature"] = np.array( result["text_features"][0] ) if "scaling_factor" in result: scaling_factor = np.array(result["scaling_factor"][0]) else: scaling_factor = None if image is not None: print("--------Finished All Image Features Generation!!--------") if text is not None: print("--------Finished All Text Features Prediction!!--------") return embeddings_dict, scaling_factor def run_from_endpoint(self, image=None, text=None, params=None): """ Deploys the endpoint. Parameters: - image (str): The path to the image data. - text (str): The path to the text data. - params (dict): Additional parameters for prediction. - image_standardization_jpeg_compression_ratio (int): The JPEG compression ratio for the model input, default: 75. - image_standardization_image_size (int): The image size for MedImageInsight model input, default: 512. Returns: - embeddings_dict (dict): A dictionary where each key is the name, and the value is another dictionary containing 'image_feature' and/or 'text_feature'. """ embeddings_dict = {} if params is None: params = {} data_dict = {} # Collect image data into a dictionary if image is not None: images_data = get_files_path(image) for name, data in images_data.items(): data_dict.setdefault(name, {})["image"] = data["file"] # Collect text data into a dictionary if text is not None: texts_data = get_text(text) for name, data in texts_data.items(): data_dict.setdefault(name, {})["text"] = data["text"] # Ensure that image and text names match if both are provided if image is not None and text is not None: assert set(images_data.keys()) == set( texts_data.keys() ), "Image and text names do not match" print("--------Start Generating Image and Text Features--------") elif image is not None: print("--------Start Generating Image Features--------") elif text is not None: print("--------Start Generating Text Features--------") else: raise ValueError("At least one of 'image' or 'text' must be provided.") # Process each item in data_dict scaling_factor = None for name, data in tqdm(data_dict.items(), total=len(data_dict)): data_list = [data.get("image", ""), data.get("text", "")] request_data = { "input_data": { "columns": ["image", "text"], "index": [0], "data": [data_list], }, "params": params, } body = str.encode(json.dumps(request_data)) req = urllib.request.Request(self.endpoint_url, body, self.headers) try: response = urllib.request.urlopen(req) result = response.read() feature_json = json.loads(result) embeddings_dict[name] = {} for subj in feature_json: if "image_features" in subj: embeddings_dict[name]["image_feature"] = np.array( subj["image_features"] ) if "text_features" in subj: embeddings_dict[name]["text_feature"] = np.array( subj["text_features"] ) if "scaling_factor" in subj and scaling_factor is None: scaling_factor = np.array(subj["scaling_factor"]) except urllib.error.HTTPError as error: print( "The embedding generation request failed with status code: " + str(error.code) ) print(error.info()) print(error.read().decode("utf8", "ignore")) if image is not None: print("--------Finished All Image Features Generation!!--------") if text is not None: print("--------Finished All Text Features Generation!!--------") return embeddings_dict, scaling_factor