Download scripts/notebook_conver/cxr_longitudinal_comparison.py from OneScience-Group/Medgemma: direct link, hf CLI and curl.
- Browser
- Download file 6.84 kB
-
https://huggingface.co/OneScience-Group/Medgemma/resolve/main/scripts/notebook_conver/cxr_longitudinal_comparison.py
- Command line
-
hf download hf://OneScience-Group/Medgemma/scripts/notebook_conver/cxr_longitudinal_comparison.py
-
curl -L -o cxr_longitudinal_comparison.py https://huggingface.co/OneScience-Group/Medgemma/resolve/main/scripts/notebook_conver/cxr_longitudinal_comparison.py
6.84 kB
| #!/usr/bin/env python3 | |
| # compare_cxr.py - MedGemma 纵向CXR对比(仿照 c6.py) | |
| # 功能: | |
| # - 比较两张胸部X光片(如治疗前后),生成描述报告 | |
| # - 支持本地模型、指定GPU数量、自定义prompt、图像预处理 | |
| # - 结果保存为文本文件 | |
| import os | |
| import sys | |
| import argparse | |
| import json | |
| import torch | |
| import numpy as np | |
| import skimage | |
| from PIL import Image | |
| from transformers import AutoProcessor, AutoModelForImageTextToText | |
| # ---------- Monkey-patch for torch<2.6 ---------- | |
| import transformers.masking_utils as masking_utils | |
| if torch.__version__ < "2.6": | |
| _orig_causal = masking_utils.create_causal_mask | |
| _orig_sliding = masking_utils.create_sliding_window_causal_mask | |
| def _safe_pop(kw): | |
| kw.pop("or_mask_function", None) | |
| kw.pop("and_mask_function", None) | |
| def _new_causal(*a, **kw): | |
| _safe_pop(kw) | |
| return _orig_causal(*a, **kw) | |
| def _new_sliding(*a, **kw): | |
| _safe_pop(kw) | |
| return _orig_sliding(*a, **kw) | |
| masking_utils.create_causal_mask = _new_causal | |
| masking_utils.create_sliding_window_causal_mask = _new_sliding | |
| def pad_image_to_square(image_array): | |
| """将图像填充为正方形(与训练预处理一致)""" | |
| image_array = skimage.util.img_as_ubyte(image_array) | |
| if image_array.ndim < 3: | |
| image_array = skimage.color.gray2rgb(image_array) | |
| if image_array.shape[2] == 4: | |
| image_array = skimage.color.rgba2rgb(image_array) | |
| h, w = image_array.shape[:2] | |
| if h < w: | |
| dh = w - h | |
| image_array = np.pad(image_array, ((dh // 2, dh - dh // 2), (0, 0), (0, 0))) | |
| elif w < h: | |
| dw = h - w | |
| image_array = np.pad(image_array, ((0, 0), (dw // 2, dw - dw // 2), (0, 0))) | |
| return image_array | |
| def build_default_prompt(): | |
| """默认的纵向CXR对比提示词""" | |
| return ( | |
| "Provide a comparison of these two images and include details from " | |
| "the image which students should take note of when reading longitudinal CXR." | |
| ) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="MedGemma 纵向胸部X光片对比" | |
| ) | |
| parser.add_argument("--model_path", required=True, help="本地模型目录路径") | |
| parser.add_argument("--image1", required=True, help="第一张图像路径(如治疗前)") | |
| parser.add_argument("--image2", required=True, help="第二张图像路径(如治疗后)") | |
| parser.add_argument("--prompt", default=None, | |
| help="自定义对比提示词(不指定则使用默认)") | |
| parser.add_argument("--output_dir", default="./outputs", | |
| help="结果保存目录(默认 ./outputs)") | |
| parser.add_argument("--num_gpus", type=int, default=None, | |
| help="使用的GPU数量(建议在命令行用 CUDA_VISIBLE_DEVICES 控制)") | |
| parser.add_argument("--preprocess", action="store_true", default=False, | |
| help="是否将图像填充为正方形(默认不填充)") | |
| args = parser.parse_args() | |
| os.makedirs(args.output_dir, exist_ok=True) | |
| # GPU 设置(若在脚本内设,需在 import torch 之前,此处仅作提示) | |
| if args.num_gpus and args.num_gpus > 0: | |
| # 实际限制应在运行脚本前通过环境变量设置,这里只打印建议 | |
| print(f"提示:建议在命令行中设置 CUDA_VISIBLE_DEVICES=0,1,... 来限制GPU。" | |
| f"当前脚本不修改可见设备。") | |
| else: | |
| print(f"可用GPU数量: {torch.cuda.device_count()}") | |
| # 加载模型(全本地) | |
| print(f"Loading model from {args.model_path}") | |
| processor = AutoProcessor.from_pretrained( | |
| args.model_path, | |
| trust_remote_code=True, | |
| local_files_only=True | |
| ) | |
| model = AutoModelForImageTextToText.from_pretrained( | |
| args.model_path, | |
| dtype=torch.bfloat16, | |
| device_map="auto", | |
| trust_remote_code=True, | |
| attn_implementation="eager", | |
| local_files_only=True | |
| ) | |
| print("Model loaded.\n") | |
| # 加载图像 | |
| print(f"Loading images:\n {args.image1}\n {args.image2}") | |
| image1 = Image.open(args.image1).convert("RGB") | |
| image2 = Image.open(args.image2).convert("RGB") | |
| print(f"Original sizes: {image1.size}, {image2.size}") | |
| if args.preprocess: | |
| img1_arr = pad_image_to_square(np.array(image1)) | |
| img2_arr = pad_image_to_square(np.array(image2)) | |
| image1 = Image.fromarray(img1_arr) | |
| image2 = Image.fromarray(img2_arr) | |
| print(f"Preprocessed sizes: {image1.size}, {image2.size}") | |
| # 构造提示词 | |
| prompt = args.prompt if args.prompt else build_default_prompt() | |
| print(f"Prompt: {prompt[:100]}...") | |
| # 构造 messages(包含两张图像) | |
| messages = [ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "image", "image": image1}, | |
| {"type": "image", "image": image2}, | |
| {"type": "text", "text": prompt} | |
| ] | |
| } | |
| ] | |
| # 处理输入 | |
| print("Processing inputs...") | |
| inputs = processor.apply_chat_template( | |
| messages, | |
| add_generation_prompt=True, | |
| tokenize=True, | |
| return_tensors="pt", | |
| return_dict=True, | |
| ) | |
| device = model.device | |
| inputs = {k: v.to(device) for k, v in inputs.items()} | |
| if "pixel_values" in inputs: | |
| print(f"Pixel values shape: {inputs['pixel_values'].shape}") | |
| # 推理 | |
| print("Running inference...") | |
| with torch.inference_mode(): | |
| generate_ids = model.generate( | |
| **inputs, | |
| max_new_tokens=600, | |
| do_sample=False, | |
| ) | |
| input_len = inputs["input_ids"].shape[1] | |
| response = processor.batch_decode( | |
| generate_ids[:, input_len:], skip_special_tokens=True | |
| )[0] | |
| # 输出结果 | |
| print("\n" + "=" * 60) | |
| print("MODEL RESPONSE:") | |
| print(response) | |
| print("=" * 60) | |
| # 保存为文本文件 | |
| base1 = os.path.splitext(os.path.basename(args.image1))[0] | |
| base2 = os.path.splitext(os.path.basename(args.image2))[0] | |
| out_txt = os.path.join(args.output_dir, f"compare_{base1}_vs_{base2}.txt") | |
| with open(out_txt, "w", encoding="utf-8") as f: | |
| f.write(f"Image1: {args.image1}\nImage2: {args.image2}\n") | |
| f.write(f"Prompt: {prompt}\n\n") | |
| f.write(f"Response:\n{response}\n") | |
| print(f"Comparison result saved to: {out_txt}") | |
| # 可选保存 JSON | |
| out_json = os.path.join(args.output_dir, f"compare_{base1}_vs_{base2}.json") | |
| result = { | |
| "image1": args.image1, | |
| "image2": args.image2, | |
| "prompt": prompt, | |
| "response": response | |
| } | |
| with open(out_json, "w") as f: | |
| json.dump(result, f, indent=2) | |
| print(f"JSON saved to: {out_json}") | |
| if __name__ == "__main__": | |
| main() | |