Q-wen2vl/Internvl2.5 | 动态分辨率输入方案解读

作者 | 梦想成真 编辑 | 自动驾驶之心

原文链接:https://zhuanlan.zhihu.com/p/12081484294

点击下方卡片,关注“自动驾驶之心”公众号

戳我-> 领取自动驾驶近15个方向学习路线

>>点击进入→自动驾驶之心大语言模型技术交流群

本文只做学术分享,如有侵权,联系删文

前言

每一个网络都有下采样倍数,那么输入的图像尺寸按理说应该是他的整数倍,能保证刚好被整除。以qwen2vl(vision backbone 下采样 28 倍)为例,动态分辨率核心要考虑三个点

  1. 图像在resize的时候,既需要考虑图像尺寸是 28 的整数倍

  2. 也需要考虑尽可能的保证图像resize不失真,也就是保持宽高比。比如512x512的图像,如果resize 到了128x2048,那么图像就会严重失真。

  3. 其次就是训练的泛化性,推理的时候输入更小/大的图像(尤其视频帧),模型能不能外推。

一个冷知识:mac上显示和实际图像大小可能不一致,猜测这是因为mac显示的时候也做了动态分辨率的resize,保证显示效果。

实际测试发现,mac 上看详情,图像尺寸 1224x926,pil 读入的size是1232x924,size不一致。image save到本地后再看尺寸还是1224x926。

qwen2vl动态分辨率逻辑

qwen 对图像有三层处理逻辑:

# 第一步 resize 
if do_resize:
    resized_height, resized_width = smart_resize(
        height,
        width,
        factor=self.patch_size * self.merge_size,
        min_pixels=self.min_pixels,
        max_pixels=self.max_pixels,
    )
    image = resize(
        image, size=(resized_height, resized_width), resample=resample, input_data_format=input_data_format
    )

# 第二步 rescale 
if do_rescale:
    image = self.rescale(image, scale=rescale_factor, input_data_format=input_data_format)

# 第三步 normalize
if do_normalize:
    image = self.normalize(
        image=image, mean=image_mean, std=image_std, input_data_format=input_data_format
    )

# 第四步 堆叠...

因为qwen2vl vit的后面有一个MLP做的pooling(x2),加上vit本身的降采样(x14),总共图像在 宽、高上会降采样2x14=28倍。

第一步 smart resize

smart resize 分为两步:

1、算宽高 28的整数倍最接近的数值

h_bar = round(height / factor) * factor
w_bar = round(width / factor) * factor

2、统一放缩。这里有两个关键的参数min_pixelsmax_pixels。这两个关键参数用来计量总的像素数,pixels = hxw。如果超过了max_pixels,那么就会统一resize到 min_pixels 和 max_pixels之间。

if h_bar * w_bar > max_pixels:
    beta = math.sqrt((height * width) / max_pixels)
    h_bar = math.floor(height / beta / factor) * factor
    w_bar = math.floor(width / beta / factor) * factor

第二步 rescale

这一步有一个关键的参数,rescale_factor。qwen2vl 默认取 0.00392156862745098(其实就是1/255),得到的结果就是 rescale_factor 逐元素相乘 image。

image = self.rescale(image, scale=rescale_factor, input_data_format=input_data_format)

第三步 normalize

很传统的按照mean,std归一化。

第四步 堆叠 凑时间步

因为qwen的vit最开始的embed方式是一个2x3x3的conv,所以需要把单图copy成2份,比如对于(1, 3, 924, 1232) 的图像就变成了(2, 3, 924, 1232)。

patches = np.tile(patches, (self.temporal_patch_size, 1, 1, 1))

训练泛化性讨论

根据qwen2vl提供的7B叙述,min_pixel是3136,max_pixel是12845056,如何h和w一样大的话,大概可以兼容从 56* 56 到 3584x3584的图像输入。但是对于video的每帧,考虑到多帧情况,最大是16384。并且由于scale到了min_pixels 和 max_pixels之间,所以泛化性不是问题。实际训练中也发现了,调整小max_pixel,对性能影响不大(不过这个也看啥任务)。

internvl2动态分辨率逻辑

总的来说,internvl的逻辑更加复杂一些。以最新的internvl2.5来看,internvl的处理逻辑基本没有变化。相比于qwen的动态分辨率,internvl2的逻辑更加高清一些,所以名字起的也很好,叫dynamic high resolution。

b441298791f0052d734143ebbeb7d949.jpeg

代码如下,最重要的就是dynamic_preprocess这个函数。

def load_image(image_file, input_size=448, max_num=12):
    image = Image.open(image_file).convert('RGB')
    # 第一步 transform
    transform = build_transform(input_size=input_size)
    # 第二步 动态分辨率
    images = dynamic_preprocess(image, image_size=input_size, use_thumbnail=True, max_num=max_num)
    pixel_values = [transform(image) for image in images]
    # 第三步 堆叠
    pixel_values = torch.stack(pixel_values)
    return pixel_values

第一步 transform

常规操作,直接绕过

IMAGENET_MEAN = (0.485, 0.456, 0.406)
IMAGENET_STD = (0.229, 0.224, 0.225)

def build_transform(input_size):
    MEAN, STD = IMAGENET_MEAN, IMAGENET_STD
    transform = T.Compose([
        T.Lambda(lambda img: img.convert('RGB') if img.mode != 'RGB' else img),
        T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC),
        T.ToTensor(),
        T.Normalize(mean=MEAN, std=STD)
    ])
    return transform

第二步 dynamic_preprocess

dynamic_preprocess 的默认参数如下,image_size 448是因为internvl需要把图像拆分成patch,训练/测试都是448,use_thumbnail 是指用一个缩略的头图保持整体的全局信息,max_num表示一个patch的最大数目。

dynamic_preprocess(
  image, 
  image_size=448, 
  use_thumbnail=True, 
  max_num=12)

同样是从宽高比下手

aspect_ratio = orig_width / orig_height

他会根据max_num 拆解成35组不同的宽高比,最极限的就是 1:max_num。

[(1, 1), (1, 2), (2, 1), (3, 1), (1, 3), (2, 2), (4, 1), (1, 4), (5, 1), (1, 5), (1, 6), (6, 1), (3, 2), (2, 3), (7, 1), (1, 7), (4, 2), (2, 4), (1, 8), (8, 1), (1, 9), (3, 3), (9, 1), (2, 5), (5, 2), (10, 1), (1, 10), (11, 1), (1, 11), (12, 1), (3, 4), (4, 3), (1, 12), (6, 2), (2, 6)]

然后会通过逻辑代码的对比,找到一个失真最小的宽高比

target_aspect_ratio = find_closest_aspect_ratio(
    aspect_ratio, target_ratios, orig_width, orig_height, image_size)

由于base_size = 448,得到 image最接近的宽高比之后,需要相乘变成最后的图像大小。

target_width = image_size * target_aspect_ratio[0]
target_height = image_size * target_aspect_ratio[1]
blocks = target_aspect_ratio[0] * target_aspect_ratio[1]

比如对于我们输入的图像尺寸是(w, h) = (1224, 926),最合适的宽高比是 (4, 3)。

  • target_width:1792 = 448 * 4

  • target_height:1344 = 448 * 3

接着就到了crop patch了。还是上面的例子 ,internvl会得到没有overlap的crop成 448x448的基础块。当然最后还有一个头图是直接把图像resize到448。

# 第0个patch (0, 0, 448, 448)
# 第1个patch (448, 0, 896, 448)
# 第2个patch (896, 0, 1344, 448)
# 第3个patch (1344, 0, 1792, 448)
# 第4个patch (0, 448, 448, 896)
# 第5个patch (448, 448, 896, 896)
# 第6个patch (896, 448, 1344, 896)
# 第7个patch (1344, 448, 1792, 896)
# 第8个patch (0, 896, 448, 1344)
# 第9个patch (448, 896, 896, 1344)
# 第10个patch (896, 896, 1344, 1344)
# 第11个patch (1344, 896, 1792, 1344)

第三步 堆叠

还是上面这个case,就会得到 pixel_value,尺寸是 。

训练泛化性讨论

不同于qwen 的 整张图 resize,internvl 的crop patch输入是一种sliding window的方式。之前做分割的时候,或者low-level 任务,很多都是sliding window 然后merge。光通过建模方式也无法说qwen的好,还是internvl2.5的动态分辨率效果更好。我的感觉是视觉encoder架构出发,比如vit g的感受野已经很大了,无论哪种方式网络都能看全图像了,不论是patch化还是整张图,所以区分度不是很大,反而qwen2vl的实现更加简单一些。

token 数横向对比

除此之外,我们可以讨论下qwen2vl和internvl2.5对于相同图像的token花费,判断这种image tokenizer的性价比。还是 (w, h) = (1224, 926) 这张图像拿来讨论吧。

qwen2vl

图像的输入是 (2, 3, 924, 1232) ,qwen2vl需要 reshape成 如下格式喂给视觉编码器。reshape 过程太长,忽略。图像最后reshap的尺寸是 (5808, 1176) 。

grid_t * grid_h * grid_w, \
channel * self.temporal_patch_size * self.patch_size * self.patch_size

qwen2vl vision encoder最后一个block的结构是

PatchMerger(
  (ln_q): LayerNorm((1280,), eps=1e-06, elementwise_affine=True)
  (mlp): Sequential(
    (0): Linear(in_features=5120, out_features=5120, bias=True)
    (1): GELU(approximate='none')
    (2): Linear(in_features=5120, out_features=3584, bias=True)
  )
)

最后vision encoder 部分输出1452,3584这样一个 embedding,我们可以简单乘一下 算下这个embedding占用大小 1452x3584=5,203,968

internvl2

internvl 采用了自己研发的 InternVisionModel,最后的特征融合层会把特征转化为 896维度的向量

(mlp1): Sequential(
  (0): LayerNorm((4096,), eps=1e-05, elementwise_affine=True)
  (1): Linear(in_features=4096, out_features=896, bias=True)
  (2): GELU(approximate='none')
  (3): Linear(in_features=896, out_features=896, bias=True)
)

所以,internvl会把13,3,448,448的patch块变成13,256,896的向量,原本448的空间维度首先下采样16倍,变成28,然后28x28的空间维度会一起变成256。所以一张 (w, h) = (1224, 926) 的图像会变成13x256=3328个token,token的维度是896。

当然,vlm还需要 eos 等符号,internvl 是 IMG_START_TOKEN + IMG_CONTEXT_TOKEN * self.num_image_token * num_patches + IMG_END_TOKEN。其他的

这样的标志符我们就忽略计算了,因为这些token很少。

最后的embedding大小是2,981,888。

讨论

对于 (w, h) = (1224, 926) 的图像,按照默认参数,internvl2.5的embedding大小是2,981,888,而qwen2vl是5,203,968,居然更大!这有些反直觉,因为qwen2vl只输入了一张图,但是internvl2.5crop 了12个patch堆叠输入。分析原因发现主要就是qwen vision encoder 输出的channel 维度(3584)太大了,并且internvl系列 patch之间没有overlap,只是多了个一个缩略图的patch额外计算。

但是能不能说qwen2vl就不行呐?qwen2vl可以调整max_pixel,实际在我的case中,我在缩小max_pixel 到1/2,1/4的时候,并没有发现qwen2vl的性能有明显下降,甚至1/2变得更好了一点点...

所以综上,目前来看,条条大路通罗马。对于默认设置,其实internvl2.5需要的image token embedding 更小,但是qwen2vl再调整max_pixel之后也不会造成明显的性能降低,仁者见仁了。peace

① 2025中国国际新能源技术展会

自动驾驶之心联合主办中国国际新能源汽车技术、零部件及服务展会。展会将于2025年2月21日至24日在北京新国展二期举行,展览面积达到2万平方米,预计吸引来自世界各地的400多家参展商和2万名专业观众。作为新能源汽车领域的专业展,它将全面展示新能源汽车行业的最新成果和发展趋势,同期围绕个各关键板块举办论坛,欢迎报名参加。

84e8df0a3585a262d354a5a51a61d702.jpeg

② 国内首个自动驾驶学习社区

『自动驾驶之心知识星球』近4000人的交流社区,已得到大多数自动驾驶公司的认可!涉及30+自动驾驶技术栈学习路线,从0到一带你入门自动驾驶感知端到端自动驾驶世界模型仿真闭环2D/3D检测、语义分割、车道线、BEV感知、Occupancy、多传感器融合、多传感器标定、目标跟踪)、自动驾驶定位建图SLAM、高精地图、局部在线地图)、自动驾驶规划控制/轨迹预测等领域技术方案大模型,更有行业动态和岗位发布!欢迎扫描加入

635cd3775579acc6279958b1720c07e4.png

 ③全网独家视频课程

端到端自动驾驶、仿真测试、自动驾驶C++、BEV感知、BEV模型部署、BEV目标跟踪、毫米波雷达视觉融合多传感器标定多传感器融合多模态3D目标检测车道线检测轨迹预测在线高精地图世界模型点云3D目标检测目标跟踪Occupancy、CUDA与TensorRT模型部署大模型与自动驾驶NeRF语义分割自动驾驶仿真、传感器部署、决策规划、轨迹预测等多个方向学习视频(扫码即可学习

8dd80c98cab5f69fc88ea61e13725092.png

网页端官网:www.zdjszx.com

④【自动驾驶之心】全平台矩阵

e0da1550d8ede95fa4fcd7c41a41b4e0.png

<think>好的,我现在需要帮助用户解决关于STM32与OV7725摄像头和ILI9341液晶屏接口的问题。首先,用户提供了详细的硬件连接信息,我需要根据这些信息来编写相应的驱动代码。 首先处理摄像头部分。OV7725使用的是模拟I2C,所以需要配置相应的GPIO引脚作为SCL和SDA。用户提到的OV7725_SIO_C连接到PC6,SIO_D到PC7。模拟I2C的关键在于正确设置GPIO的模式(推挽输出、开漏输出),以及实现起始信号、停止信号、数据读写和应答检测。接下来是FIFO相关的引脚,比如RRST、RCLK、OE等,需要配置这些引脚为输出模式,控制FIFO的读写操作。特别是RCLK的时钟信号,可能需要使用定时器产生脉冲来读取数据。此外,VSYNC连接到PE5或PC3,需要配置为输入模式,检测帧同步信号,以确定何时开始读取一帧数据。数据线D0-D7连接到PB8-PB15,需要配置为输入模式,并设置为上拉输入,确保数据稳定。 然后是液晶屏部分,ILI9341通过FSMC接口连接,使用8080时序。FSMC的配置需要设置正确的控制信号和地址线。用户提供的ILI9341_DC连接到PE2(FSMC_A23),这意味着数据/命令选择线通过地址线A23来控制。当A23为高电平时,访问数据寄存器;低电平时访问命令寄存器。需要配置FSMC的存储块(BANK1),设置时序参数,比如地址建立时间、数据保持时间等。此外,复位引脚和背光引脚需要配置为GPIO输出,控制液晶屏的复位和背光开关。 在代码实现方面,摄像头部分需要初始化各个GPIO,编写模拟I2C的读写函数来配置OV7725的寄存器。例如,设置图像分辨率、输出格式等。然后配置VSYNC中断,当检测到帧同步信号时,触发读取FIFO中的数据。使用定时器产生RCLK时钟信号,逐个读取像素数据。液晶屏部分则需要初始化FSMC接口,编写函数来发送命令和数据,以及绘制像素点的函数,将摄像头采集的图像数据显示在屏幕上。 需要注意的问题包括:模拟I2C的时序是否正确,FIFO的读写指针复位操作是否在正确的时间执行,VSYNC中断的响应时间是否足够快以避免数据丢失,FSMC的时序配置是否匹配ILI9341的要求,以及数据线的连接是否正确。此外,可能还需要处理DMA传输来优化数据读取和显示的速度,减少CPU负担。 接下来,用户可能会遇到的问题包括如何优化图像传输速度、如何处理图像数据格式转换(如从YUV到RGB565),如何实现双缓冲避免屏幕撕裂,或者如何调试I2C通信中的问题。此外,还可能涉及图像处理算法在STM32上的实现,比如边缘检测或图像压缩等。</think>根据提供的硬件连接信息,这里给出STM32驱动OV7725摄像头和ILI9341液晶屏的核心代码实现: ### 一、OV7725摄像头驱动 ```c /* GPIO配置 */ void OV7725_GPIO_Init(void) { // I2C引脚配置(模拟) GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // SCL(PC6) SDA(PC7) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStruct); // 控制信号引脚配置 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); // OE(PA3), RRST(PA2) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_Init(GPIOA, &GPIO_InitStruct); // RCLK(PC5), WRST(PC4) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_Init(GPIOC, &GPIO_InitStruct); // WEN(PD3) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3; GPIO_Init(GPIOD, &GPIO_InitStruct); } /* 模拟I2C写寄存器 */ void OV7725_WriteReg(uint8_t reg, uint8_t data) { I2C_Start(); I2C_SendByte(0x42); // 7位地址+写位 I2C_WaitAck(); I2C_SendByte(reg); I2C_WaitAck(); I2C_SendByte(data); I2C_WaitAck(); I2C_Stop(); } /* FIFO读取初始化 */ void FIFO_ReadInit(void) { OV7725_RRST_LOW(); // 复位读指针 OV7725_RCLK_LOW(); delay_us(1); OV7725_RRST_HIGH(); } /* 读取一帧数据 */ void OV7725_ReadFrame(uint16_t *buffer) { while(OV7725_VSYNC_READ()); // 等待VSYNC上升沿 FIFO_ReadInit(); for(int i=0; i<320*240; i++) // QVGA分辨率 { OV7725_RCLK_HIGH(); delay_ns(500); buffer[i] = GPIOC->IDR & 0xFF; // 读取数据总线 OV7725_RCLK_LOW(); delay_ns(500); } } ``` ### 二、ILI9341液晶驱动 ```c /* FSMC初始化 */ void FSMC_Init(void) { FSMC_NORSRAMInitTypeDef FSMC_InitStruct; FSMC_NORSRAMTimingInitTypeDef Timing; // 时序配置 Timing.FSMC_AddressSetupTime = 1; Timing.FSMC_AddressHoldTime = 0; Timing.FSMC_DataSetupTime = 5; Timing.FSMC_BusTurnAroundDuration = 0; Timing.FSMC_CLKDivision = 0; Timing.FSMC_DataLatency = 0; Timing.FSMC_AccessMode = FSMC_AccessMode_A; // FSMC配置 FSMC_InitStruct.FSMC_Bank = FSMC_Bank1_NORSRAM4; FSMC_InitStruct.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; FSMC_InitStruct.FSMC_MemoryType = FSMC_MemoryType_SRAM; FSMC_InitStruct.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b; FSMC_InitStruct.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable; FSMC_InitStruct.FSMC_AsynchronousWait = FSMC_AsynchronousWait_Disable; FSMC_InitStruct.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low; FSMC_InitStruct.FSMC_WrapMode = FSMC_WrapMode_Disable; FSMC_InitStruct.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState; FSMC_InitStruct.FSMC_WriteOperation = FSMC_WriteOperation_Enable; FSMC_InitStruct.FSMC_WaitSignal = FSMC_WaitSignal_Disable; FSMC_InitStruct.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable; FSMC_InitStruct.FSMC_WriteBurst = FSMC_WriteBurst_Disable; FSMC_InitStruct.FSMC_ReadWriteTimingStruct = &Timing; FSMC_InitStruct.FSMC_WriteTimingStruct = &Timing; FSMC_NORSRAMInit(&FSMC_InitStruct); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE); } /* 写命令函数 */ void LCD_WriteCmd(uint16_t cmd) { *(__IO uint16_t *)0x6C000000 = cmd; // A23=0 对应命令地址 } /* 写数据函数 */ void LCD_WriteData(uint16_t data) { *(__IO uint16_t *)0x6D000000 = data; // A23=1 对应数据地址 } /* 显示图像 */ void LCD_ShowImage(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t *img) { LCD_SetWindow(x, y, x+width-1, y+height-1); LCD_WriteCmd(0x2C); // 内存写命令 for(int i=0; i<height; i++) { for(int j=0; j<width; j++) { LCD_WriteData(*img++); } } } ``` ### 三、系统整合 ```c int main(void) { // 硬件初始化 OV7725_Init(); LCD_Init(); // 摄像头配置 OV7725_WriteReg(0x12, 0x80); // 复位摄像头 delay_ms(100); OV7725_WriteReg(0x12, 0x00); // 退出复位 // ...其他寄存器配置 // 显示循环 uint16_t frameBuffer[320*240]; while(1) { OV7725_ReadFrame(frameBuffer); LCD_ShowImage(0, 0, 320, 240, frameBuffer); } } ``` ### 代码说明: 1. **OV7725驱动**: - 使用PC6/PC7模拟I2C协议配置摄像头寄存器 - 通过PA2/PA3等控制FIFO状态 - VSYNC中断触发帧读取 - RCLK时钟信号控制数据读取节奏 2. **ILI9341驱动**: - 利用FSMC硬件接口实现8080时序 - 地址线A23作为命令/数据选择线 - 使用DMA加速图像数据传输(可选优化) 3. **系统整合**: - 主循环不断读取摄像头数据并刷新屏幕 - 使用双缓冲机制可避免画面撕裂(需增加缓冲区)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值