1
2# Fleming-VL-8B Multi-Modal Inference Script
3
4# This script demonstrates three inference modes:
5# 1. Single image inference
6# 2. Video inference (frame-by-frame)
7# 3. 3D medical image (CT/MRI) inference from .npy files
8
9# Model: UbiquantAI/Fleming-VL-8B
10# Based on: InternVL_chat-1.2 template
11
12
13from transformers import AutoTokenizer, AutoModel
14from torchvision.transforms.functional import InterpolationMode
15from decord import VideoReader, cpu
16from PIL import Image
17import torchvision.transforms as T
18import numpy as np
19import torch
20import os
21
22
23# ============================================================================
24# Configuration
25# ============================================================================
26
27MODEL_PATH = "UbiquantAI/Fleming-VL-38B"
28
29# Prompt template for reasoning-based responses
30REASONING_PROMPT = (
31 "A conversation between User and Assistant. The user asks a question, "
32 "and the Assistant solves it. The assistant first thinks about the "
33 "reasoning process in the mind and then provides the user a concise "
34 "final answer in a short word or phrase. The reasoning process and "
35 "answer are enclosed within <think> </think> and <answer> </answer> "
36 "tags, respectively, i.e., <think> reasoning process here </think>"
37 "<answer> answer here </answer>"
38)
39
40IMAGENET_MEAN = (0.485, 0.456, 0.406)
41IMAGENET_STD = (0.229, 0.224, 0.225)
42
43
44# ============================================================================
45# Image Preprocessing Functions
46# ============================================================================
47
48def build_transform(input_size):
49 """Build image transformation pipeline."""
50 MEAN, STD = IMAGENET_MEAN, IMAGENET_STD
51 transform = T.Compose([
52 T.Lambda(lambda img: img.convert('RGB') if img.mode != 'RGB' else img),
53 T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC),
54 T.ToTensor(),
55 T.Normalize(mean=MEAN, std=STD)
56 ])
57 return transform
58
59
60def find_closest_aspect_ratio(aspect_ratio, target_ratios, width, height, image_size):
61 """Find the closest aspect ratio from target ratios."""
62 best_ratio_diff = float('inf')
63 best_ratio = (1, 1)
64 area = width * height
65 for ratio in target_ratios:
66 target_aspect_ratio = ratio[0] / ratio[1]
67 ratio_diff = abs(aspect_ratio - target_aspect_ratio)
68 if ratio_diff < best_ratio_diff:
69 best_ratio_diff = ratio_diff
70 best_ratio = ratio
71 elif ratio_diff == best_ratio_diff:
72 if area > 0.5 * image_size * image_size * ratio[0] * ratio[1]:
73 best_ratio = ratio
74 return best_ratio
75
76
77def dynamic_preprocess(image, min_num=1, max_num=12, image_size=448, use_thumbnail=False):
78 """
79 Dynamically preprocess image by splitting into tiles based on aspect ratio.
80
81 Args:
82 image: PIL Image
83 min_num: Minimum number of tiles
84 max_num: Maximum number of tiles
85 image_size: Size of each tile
86 use_thumbnail: Whether to add a thumbnail image
87
88 Returns:
89 List of preprocessed PIL Images
90 """
91 orig_width, orig_height = image.size
92 aspect_ratio = orig_width / orig_height
93
94 # Calculate possible tile configurations
95 target_ratios = set(
96 (i, j) for n in range(min_num, max_num + 1)
97 for i in range(1, n + 1)
98 for j in range(1, n + 1)
99 if i * j <= max_num and i * j >= min_num
100 )
101 target_ratios = sorted(target_ratios, key=lambda x: x[0] * x[1])
102
103 # Find the closest aspect ratio to the target
104 target_aspect_ratio = find_closest_aspect_ratio(
105 aspect_ratio, target_ratios, orig_width, orig_height, image_size
106 )
107
108 # Calculate target dimensions
109 target_width = image_size * target_aspect_ratio[0]
110 target_height = image_size * target_aspect_ratio[1]
111 blocks = target_aspect_ratio[0] * target_aspect_ratio[1]
112
113 # Resize and split the image
114 resized_img = image.resize((target_width, target_height))
115 processed_images = []
116 for i in range(blocks):
117 box = (
118 (i % (target_width // image_size)) * image_size,
119 (i // (target_width // image_size)) * image_size,
120 ((i % (target_width // image_size)) + 1) * image_size,
121 ((i // (target_width // image_size)) + 1) * image_size
122 )
123 split_img = resized_img.crop(box)
124 processed_images.append(split_img)
125
126 assert len(processed_images) == blocks
127
128 # Add thumbnail if requested
129 if use_thumbnail and len(processed_images) != 1:
130 thumbnail_img = image.resize((image_size, image_size))
131 processed_images.append(thumbnail_img)
132
133 return processed_images
134
135
136# ============================================================================
137# Utility Functions
138# ============================================================================
139
140def load_model(model_path, use_flash_attn=True):
141 """
142 Load the vision-language model and tokenizer.
143
144 Args:
145 model_path: Path to the pretrained model
146 use_flash_attn: Whether to use flash attention (default: True)
147
148 Returns:
149 tuple: (model, tokenizer)
150 """
151 model = AutoModel.from_pretrained(
152 model_path,
153 torch_dtype=torch.bfloat16,
154 low_cpu_mem_usage=True,
155 use_flash_attn=use_flash_attn,
156 trust_remote_code=True
157 ).eval().cuda()
158
159 tokenizer = AutoTokenizer.from_pretrained(
160 model_path,
161 trust_remote_code=True,
162 use_fast=False
163 )
164
165 return model, tokenizer
166
167
168# ============================================================================
169# Image Inference
170# ============================================================================
171
172def inference_single_image(model, tokenizer, image_path, question,
173 prompt=REASONING_PROMPT, input_size=448, max_num=12):
174 """
175 Perform inference on a single image.
176
177 Args:
178 model: Loaded vision-language model
179 tokenizer: Loaded tokenizer
180 image_path: Path to the input image
181 question: Question to ask about the image
182 prompt: System prompt template
183 input_size: Input image size (default: 448)
184 max_num: Maximum number of tiles (default: 12)
185
186 Returns:
187 str: Model response
188 """
189 # Load and preprocess image using InternVL's dynamic preprocessing
190 image = Image.open(image_path).convert('RGB')
191 transform = build_transform(input_size=input_size)
192 images = dynamic_preprocess(image, image_size=input_size, use_thumbnail=True, max_num=max_num)
193 pixel_values = [transform(img) for img in images]
194 pixel_values = torch.stack(pixel_values).to(torch.bfloat16).cuda()
195
196 # Prepare question with prompt and image token
197 full_question = f"{prompt}\n<image>\n{question}"
198 # print("###",full_question)
199
200 # Generate response
201 generation_config = dict(max_new_tokens=2048, do_sample=False)
202 response = model.chat(tokenizer, pixel_values, full_question, generation_config)
203
204 return response
205
206
207# ============================================================================
208# Video Inference
209# ============================================================================
210
211def get_frame_indices(bound, fps, max_frame, first_idx=0, num_segments=32):
212 """
213 Calculate evenly distributed frame indices for video sampling.
214
215 Args:
216 bound: Tuple of (start_time, end_time) in seconds, or None for full video
217 fps: Frames per second of the video
218 max_frame: Maximum frame index
219 first_idx: First frame index to consider
220 num_segments: Number of frames to sample
221
222 Returns:
223 np.array: Array of frame indices
224 """
225 if bound:
226 start, end = bound[0], bound[1]
227 else:
228 start, end = -100000, 100000
229
230 start_idx = max(first_idx, round(start * fps))
231 end_idx = min(round(end * fps), max_frame)
232 seg_size = float(end_idx - start_idx) / num_segments
233
234 frame_indices = np.array([
235 int(start_idx + (seg_size / 2) + np.round(seg_size * idx))
236 for idx in range(num_segments)
237 ])
238
239 return frame_indices
240
241
242def load_video(video_path, bound=None, input_size=448, max_num=1, num_segments=32):
243 """
244 Load and preprocess video frames.
245
246 Args:
247 video_path: Path to the video file
248 bound: Time boundary tuple (start, end) in seconds
249 input_size: Input image size (default: 448)
250 max_num: Maximum number of tiles per frame (default: 1)
251 num_segments: Number of frames to extract
252
253 Returns:
254 tuple: (pixel_values tensor, list of num_patches per frame)
255 """
256 vr = VideoReader(video_path, ctx=cpu(0), num_threads=1)
257 max_frame = len(vr) - 1
258 fps = float(vr.get_avg_fps())
259
260 pixel_values_list = []
261 num_patches_list = []
262 transform = build_transform(input_size=input_size)
263
264 frame_indices = get_frame_indices(bound, fps, max_frame, first_idx=0, num_segments=num_segments)
265
266 for frame_index in frame_indices:
267 # Extract and preprocess frame
268 img = Image.fromarray(vr[frame_index].asnumpy()).convert('RGB')
269 img = dynamic_preprocess(img, image_size=input_size, use_thumbnail=True, max_num=max_num)
270 pixel_values = [transform(tile) for tile in img]
271 pixel_values = torch.stack(pixel_values)
272 num_patches_list.append(pixel_values.shape[0])
273 pixel_values_list.append(pixel_values)
274
275 pixel_values = torch.cat(pixel_values_list)
276 return pixel_values, num_patches_list
277
278
279def inference_video(model, tokenizer, video_path, video_duration, question,
280 prompt=REASONING_PROMPT, input_size=448, max_num=1):
281 """
282 Perform inference on a video by sampling frames.
283
284 Args:
285 model: Loaded vision-language model
286 tokenizer: Loaded tokenizer
287 video_path: Path to the video file
288 video_duration: Duration of video in seconds
289 question: Question to ask about the video
290 prompt: System prompt template
291 input_size: Input image size (default: 448)
292 max_num: Maximum number of tiles per frame (default: 1)
293
294 Returns:
295 str: Model response
296 """
297 # Sample frames from video (1 frame per second)
298 num_segments = int(video_duration)
299 pixel_values, num_patches_list = load_video(
300 video_path, bound=None, input_size=input_size,
301 max_num=max_num, num_segments=num_segments
302 )
303 pixel_values = pixel_values.to(torch.bfloat16).cuda()
304
305 # Create image token prefix for all frames
306 video_prefix = ''.join([f'<image>\n' for _ in range(len(num_patches_list))])
307
308 # Prepare question with prompt and image tokens
309 full_question = f"{prompt}\n{video_prefix}{question}"
310
311 # Generate response
312 generation_config = dict(max_new_tokens=1024, do_sample=False)
313 response, history = model.chat(
314 tokenizer,
315 pixel_values,
316 full_question,
317 generation_config,
318 num_patches_list=num_patches_list,
319 history=None,
320 return_history=True
321 )
322
323 return response
324
325
326# ============================================================================
327# 3D Medical Image (NPY) Inference
328# ============================================================================
329
330def normalize_image(image):
331 """
332 Normalize image array to 0-255 range.
333
334 Args:
335 image: NumPy array of image data
336
337 Returns:
338 np.array: Normalized image as uint8
339 """
340 img_min = np.min(image)
341 img_max = np.max(image)
342
343 if img_max - img_min == 0:
344 return np.zeros_like(image, dtype=np.uint8)
345
346 return ((image - img_min) / (img_max - img_min) * 255).astype(np.uint8)
347
348
349def convert_npy_to_images(npy_path, input_size=448, max_num=1, num_slices=11):
350 """
351 Convert 3D medical image (.npy) to multiple 2D RGB images.
352
353 Expected input shape: (32, 256, 256) or (1, 32, 256, 256)
354 Extracts evenly distributed slices and converts to RGB format.
355
356 Args:
357 npy_path: Path to the .npy file
358 input_size: Input image size (default: 448)
359 max_num: Maximum number of tiles per slice (default: 1)
360 num_slices: Number of slices to extract (default: 11)
361
362 Returns:
363 tuple: (pixel_values tensor, list of num_patches per slice) or False if error
364 """
365 try:
366 # Load .npy file
367 data = np.load(npy_path)
368
369 # Handle shape (1, 32, 256, 256) -> (32, 256, 256)
370 if data.ndim == 4 and data.shape[0] == 1:
371 data = data[0]
372
373 # Validate shape
374 if data.shape != (32, 256, 256):
375 print(f"Warning: {npy_path} has shape {data.shape}, expected (32, 256, 256), skipping")
376 return False
377
378 # Select evenly distributed slices from 32 slices
379 indices = np.linspace(0, 31, num_slices, dtype=int)
380
381 transform = build_transform(input_size=input_size)
382 pixel_values_list = []
383 num_patches_list = []
384
385 # Process each selected slice
386 for idx in indices:
387 # Get slice
388 slice_img = data[idx]
389
390 # Normalize to 0-255
391 normalized = normalize_image(slice_img)
392
393 # Convert grayscale to RGB by stacking
394 rgb_img = np.stack([normalized, normalized, normalized], axis=-1)
395
396 # Convert to PIL Image
397 img = Image.fromarray(rgb_img)
398
399 # Preprocess with InternVL's dynamic preprocessing
400 img = dynamic_preprocess(img, image_size=input_size, use_thumbnail=True, max_num=max_num)
401 pixel_values = [transform(tile) for tile in img]
402 pixel_values = torch.stack(pixel_values)
403 num_patches_list.append(pixel_values.shape[0])
404 pixel_values_list.append(pixel_values)
405
406 pixel_values = torch.cat(pixel_values_list)
407 return pixel_values, num_patches_list
408
409 except Exception as e:
410 print(f"Error processing {npy_path}: {str(e)}")
411 return False
412
413
414def inference_3d_medical_image(model, tokenizer, npy_path, question,
415 prompt=REASONING_PROMPT, input_size=448, max_num=1):
416 """
417 Perform inference on 3D medical images stored as .npy files.
418
419 Args:
420 model: Loaded vision-language model
421 tokenizer: Loaded tokenizer
422 npy_path: Path to the .npy file (shape: 32x256x256)
423 question: Question to ask about the image
424 prompt: System prompt template
425 input_size: Input image size (default: 448)
426 max_num: Maximum number of tiles per slice (default: 1)
427
428 Returns:
429 str: Model response or None if error
430 """
431 # Convert 3D volume to multiple 2D slices
432 result = convert_npy_to_images(npy_path, input_size=input_size, max_num=max_num)
433
434 if result is False:
435 return None
436
437 pixel_values, num_patches_list = result
438 pixel_values = pixel_values.to(torch.bfloat16).cuda()
439
440 # Create image token prefix for all slices
441 image_prefix = ''.join([f'<image>\n' for _ in range(len(num_patches_list))])
442
443 # Prepare question with prompt and image tokens
444 full_question = f"{prompt}\n{image_prefix}{question}"
445
446 # Generate response
447 generation_config = dict(max_new_tokens=1024, do_sample=False)
448 response, history = model.chat(
449 tokenizer,
450 pixel_values,
451 full_question,
452 generation_config,
453 num_patches_list=num_patches_list,
454 history=None,
455 return_history=True
456 )
457
458 return response
459
460
461# ============================================================================
462# Main Execution Examples
463# ============================================================================
464
465def main():
466 """
467 Main function demonstrating all three inference modes.
468 """
469
470 # ========================================================================
471 # Example 1: Single Image Inference
472 # ========================================================================
473 print("\n" + "="*80)
474 print("EXAMPLE 1: Single Image Inference")
475 print("="*80)
476
477 image_path = "./resource/1.jpg"
478 question = ' What type of abnormality is present in this image?'
479
480 model, tokenizer = load_model(MODEL_PATH, use_flash_attn=True)
481 response = inference_single_image(model, tokenizer, image_path, question)
482
483 print(f"\nUser: {question}")
484 print(f"Assistant: {response}")
485
486 # Clean up GPU memory
487 del model, tokenizer
488 torch.cuda.empty_cache()
489
490 # ========================================================================
491 # Example 2: Video Inference
492 # ========================================================================
493 print("\n" + "="*80)
494 print("EXAMPLE 2: Video Inference")
495 print("="*80)
496
497 video_path = "./resource/video.mp4"
498 video_duration = 6 # seconds
499 question = "Please describe the video."
500
501 model, tokenizer = load_model(MODEL_PATH, use_flash_attn=False)
502 response = inference_video(model, tokenizer, video_path, video_duration, question)
503
504 print(f"\nUser: {question}")
505 print(f"Assistant: {response}")
506
507 # Clean up GPU memory
508 del model, tokenizer
509 torch.cuda.empty_cache()
510
511 # ========================================================================
512 # Example 3: 3D Medical Image Inference
513 # ========================================================================
514 print("\n" + "="*80)
515 print("EXAMPLE 3: 3D Medical Image Inference")
516 print("="*80)
517
518 npy_path = "./resource/test.npy"
519 question = "What device is observed on the chest wall?"
520
521 # Example cases:
522 # Case 1: /path/to/test_1016_d_2.npy
523 # Question: "Where is the largest lymph node observed?"
524 # Answer: "Right hilar region."
525 #
526 # Case 2: /path/to/test_1031_a_2.npy
527 # Question: "What device is observed on the chest wall?"
528 # Answer: "Pacemaker."
529
530 model, tokenizer = load_model(MODEL_PATH, use_flash_attn=False)
531 response = inference_3d_medical_image(model, tokenizer, npy_path, question)
532
533 if response:
534 print(f"\nUser: {question}")
535 print(f"Assistant: {response}")
536 else:
537 print("\nError: Failed to process 3D medical image")
538
539 # Clean up GPU memory
540 del model, tokenizer
541 torch.cuda.empty_cache()
542
543
544if __name__ == "__main__":
545 main()
546