在交易图表上通过资源驱动的双三次插值图像缩放技术创建动态 MQL5 图形界面·进阶篇
(2/3)· 最近邻太糙、双线性发虚?用资源位图加双三次插值让交易图表界面既清晰又可交互
◍ 把资源图按图表比例塞进MT5
想在MT5图表上叠加一张自定义图片(比如标注支撑区的底图),核心是先读资源、再按图表像素尺寸重算缩放,避免拉变形。下面这段函数直接从编译资源里读像素,并依据图表宽高比决定是「宽撑满」还是「高撑满」。 资源读取失败是最常见的坑:ResourceReadImage 若返回 false,多半是资源名没在 #resource 里正确声明,或图片格式不被终端支持。此时函数直接返回 false 并打印错误,调用方应当中断绘制而不是继续跑缩放逻辑。 缩放分支里用 image_aspect_ratio 与 chart_aspect_ratio 比较:图片更宽就令 scaled_image_width = 图表宽,高度按比值反推;反之高度撑满。若你开了 LimitToOriginalSize,还会用 MathMin 把尺寸钳在原始像素内,再二次校正一边以保持比例——这对高清屏上不想让logo被放糊的场景很实用。 调试时留意 PrintFormat 打出的三组数据:Original / Chart / Scaled。比如原图 800x600、图表 1200x700,则 chart 比值为1.714、image 为1.333,走「高撑满」分支,缩放后宽约933、高700,开限制则回退到 800x600。开MT5按F4编译挂到图表,看日志就能验证这套计算。
class="type">bool DisplayImageOnChart() { class=class="str">"cmt">// Load the original image from the resource class="type">uint image_pixels[]; class=class="str">"cmt">//--- Declare an array to store image pixel data class="type">uint original_image_width, original_image_height; class=class="str">"cmt">//--- Declare variables for original image dimensions if (!ResourceReadImage(ORIGINAL_IMAGE_RESOURCE, image_pixels, original_image_width, original_image_height)) { class=class="str">"cmt">//--- Read the image resource into the pixel array Print("Error: Failed to read original image data from resource."); class=class="str">"cmt">//--- Log an error if reading the image fails class="kw">return false; class=class="str">"cmt">//--- Return false to indicate failure } class=class="str">"cmt">// Get chart dimensions class="type">int chart_pixel_width = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS); class=class="str">"cmt">//--- Retrieve the chart width in pixels class="type">int chart_pixel_height = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); class=class="str">"cmt">//--- Retrieve the chart height in pixels class=class="str">"cmt">// Calculate scaled dimensions while preserving aspect ratio class="type">class="kw">double image_aspect_ratio = (class="type">class="kw">double)original_image_width / original_image_height; class=class="str">"cmt">//--- Calculate the aspect ratio of the original image class="type">class="kw">double chart_aspect_ratio = (class="type">class="kw">double)chart_pixel_width / chart_pixel_height; class=class="str">"cmt">//--- Calculate the aspect ratio of the chart class="type">int scaled_image_width, scaled_image_height; class=class="str">"cmt">//--- Declare variables for scaled image dimensions if (image_aspect_ratio > chart_aspect_ratio) { class=class="str">"cmt">//--- Check if the image is wider relative to the chart scaled_image_width = chart_pixel_width; class=class="str">"cmt">//--- Set scaled width to match chart width scaled_image_height = (class="type">int)(chart_pixel_width / image_aspect_ratio); class=class="str">"cmt">//--- Calculate scaled height to maintain aspect ratio } else { scaled_image_height = chart_pixel_height; class=class="str">"cmt">//--- Set scaled height to match chart height scaled_image_width = (class="type">int)(chart_pixel_height * image_aspect_ratio); class=class="str">"cmt">//--- Calculate scaled width to maintain aspect ratio } class=class="str">"cmt">// Limit scaling to original size if enabled if (LimitToOriginalSize) { class=class="str">"cmt">//--- Check if the user has enabled limiting to original size scaled_image_width = MathMin(scaled_image_width, (class="type">int)original_image_width); class=class="str">"cmt">//--- Restrict width to original width scaled_image_height = MathMin(scaled_image_height, (class="type">int)original_image_height); class=class="str">"cmt">//--- Restrict height to original height class=class="str">"cmt">// Recalculate one dimension to maintain aspect ratio if (scaled_image_width < scaled_image_height * image_aspect_ratio) { class=class="str">"cmt">//--- Check if width is the limiting factor scaled_image_height = (class="type">int)(scaled_image_width / image_aspect_ratio); class=class="str">"cmt">//--- Adjust height to maintain aspect ratio } else { scaled_image_width = (class="type">int)(scaled_image_height * image_aspect_ratio); class=class="str">"cmt">//--- Adjust width to maintain aspect ratio } } class=class="str">"cmt">// Log dimensions for debugging PrintFormat( "Original: %dx%d, Chart: %dx%d, Scaled: %dx%d", original_image_width, original_image_height, chart_pixel_width, chart_pixel_height,
「双三次插值缩放像素阵列的实现细节」
把图表截图做无损放大时,直接拉伸像素会糊成马赛克;双三次插值靠周围 4×4 邻域加权,能在 MT5 里拿到更顺滑的缩略图。下面这段 ScaleImage 就是干这事的核心:先按目标宽高开新数组,再逐点映射回原图坐标做插值。 缩放映射的数学很直白——目标点 (x,y) 对应原图坐标 original_x = x * original_width / new_width,original_y 同理。new_width 若设为原宽 2 倍,则每个目标像素只跨越原图 0.5 个像素间距,插值权重决定它偏向哪个邻居。 BicubicInterpolateComponent 里那四个 weights_x 系数是关键:t 为分数位移,weight[1] 在 t=0 时等于 1、两侧随距离立方衰减。若把 -0.5 改成 -0.75(Catmull-Rom 之外更锐的核),边缘可能更清晰但噪点也会被放大,外汇 tick 图缩放时尤其要留意。 跑完双层循环后,代码用 ArrayResize 把原 pixels 改成新尺寸,再 ArrayCopy 覆写回去。你在 EA 里调用前,最好先 Print 原宽高和 new_width*new_height,确认数组没越界——MT5 对 uint[] 越界不会抛异常,只会静默截断。
class="type">void ScaleImage( class="type">uint &pixels[], class="type">int original_width, class="type">int original_height, class="type">int new_width, class="type">int new_height ) { class="type">uint scaled_pixels[]; class=class="str">"cmt">//--- 声明存放缩放后像素的数组 ArrayResize(scaled_pixels, new_width * new_height); class=class="str">"cmt">//--- 按目标尺寸重设数组大小 for (class="type">int y = class="num">0; y < new_height; y++) { class=class="str">"cmt">//--- 遍历缩放图每一行 for (class="type">int x = class="num">0; x < new_width; x++) { class=class="str">"cmt">//--- 遍历缩放图每一列 class=class="str">"cmt">// 映射回原图坐标 class="type">class="kw">double original_x = (class="type">class="kw">double)x * original_width / new_width; class=class="str">"cmt">//--- 计算对应原图 x 坐标 class="type">class="kw">double original_y = (class="type">class="kw">double)y * original_height / new_height; class=class="str">"cmt">//--- 计算对应原图 y 坐标 class=class="str">"cmt">// 应用双三次插值 class="type">uint pixel = BicubicInterpolate(pixels, original_width, original_height, original_x, original_y); class=class="str">"cmt">//--- 插值得到像素值 scaled_pixels[y * new_width + x] = pixel; class=class="str">"cmt">//--- 存入缩放数组 } } ArrayResize(pixels, new_width * new_height); class=class="str">"cmt">//--- 把原数组改为新尺寸 ArrayCopy(pixels, scaled_pixels); class=class="str">"cmt">//--- 将缩放结果拷回原数组 } class="type">class="kw">double BicubicInterpolateComponent(class="type">uchar &components[], class="type">class="kw">double fractional_x, class="type">class="kw">double fractional_y) { class="type">class="kw">double weights_x[class="num">4]; class=class="str">"cmt">//--- 声明 x 方向权重数组 class="type">class="kw">double t = fractional_x; class=class="str">"cmt">//--- 记录 x 方向小数部分 weights_x[class="num">0] = (-class="num">0.5 * t * t * t + t * t - class="num">0.5 * t); class=class="str">"cmt">//--- x-class="num">1 位置权重 weights_x[class="num">1] = (class="num">1.5 * t * t * t - class="num">2.5 * t * t + class="num">1); class=class="str">"cmt">//--- x 位置权重 weights_x[class="num">2] = (-class="num">1.5 * t * t * t + class="num">2 * t * t + class="num">0.5 * t); class=class="str">"cmt">//--- x+class="num">1 位置权重 weights_x[class="num">3] = (class="num">0.5 * t * t * t - class="num">0.5 * t * t); class=class="str">"cmt">//--- x+class="num">2 位置权重 class="type">class="kw">double y_values[class="num">4]; class=class="str">"cmt">//--- 声明中间 y 值数组 for (class="type">int j = class="num">0; j < class="num">4; j++) { class=class="str">"cmt">//--- 遍历邻域四行 y_values[j] =
双立方插值的Y轴权重与像素拆包
上面这段把双立方插值收了尾:在X方向对每个Y行算出中间值后,再针对Y方向做四次多项式加权。Y权重用的仍是Catmull-Rom基函数,系数直接写死在表达式里——比如 weights_y[1] 的 1.5*t*t*t - 2.5*t*t + 1,就是中心采样点权重,t趋近0时它接近1,两侧点权重趋近0。 最终结果用 MathMax(0, MathMin(255, result)) 夹到0~255,这是ARGB单通道的有效区间,越界会直接咬死在边界而不是回绕。外汇或贵金属图表做自定义指标重采样时,这类夹值能避免渲染出噪点,但高频缩放下仍可能偏色,属正常误差。 GetArgb 函数把32位像素按位移拆成四个8位通道:alpha拿右移24位后的低8位,red是16位,green是8位,blue直接取低8位。注意MT5里 uint 像素是ABGR内存序,拆出来后若拿去画Canvas要确认端序,否则红蓝会反。 BicubicInterpolate 入口先取整和小数:x0=(int)x 丢掉小数部分,fractional_x = x - x0 留作权重计算的t。这一步若x为负或越界不会在此处理,调用前需自己保证坐标在 [1, width-2] 区间内,否则取 components 会越界读脏数据。
weights_x[class="num">0] * components[j * class="num">4 + class="num">0] + weights_x[class="num">1] * components[j * class="num">4 + class="num">1] + weights_x[class="num">2] * components[j * class="num">4 + class="num">2] + weights_x[class="num">3] * components[j * class="num">4 + class="num">3]; class=class="str">"cmt">//--- Perform interpolation in x for each y } class=class="str">"cmt">// Calculate cubic interpolation weights for y class="type">class="kw">double weights_y[class="num">4]; class=class="str">"cmt">//--- Declare an array for y interpolation weights t = fractional_y; class=class="str">"cmt">//--- Store the fractional y value weights_y[class="num">0] = (-class="num">0.5 * t * t * t + t * t - class="num">0.5 * t); class=class="str">"cmt">//--- Calculate weight for y-class="num">1 weights_y[class="num">1] = (class="num">1.5 * t * t * t - class="num">2.5 * t * t + class="num">1); class=class="str">"cmt">//--- Calculate weight for y weights_y[class="num">2] = (-class="num">1.5 * t * t * t + class="num">2 * t * t + class="num">0.5 * t); class=class="str">"cmt">//--- Calculate weight for y+class="num">1 weights_y[class="num">3] = (class="num">0.5 * t * t * t - class="num">0.5 * t * t); class=class="str">"cmt">//--- Calculate weight for y+class="num">2 class=class="str">"cmt">// Interpolate in y class="type">class="kw">double result = weights_y[class="num">0] * y_values[class="num">0] + weights_y[class="num">1] * y_values[class="num">1] + weights_y[class="num">2] * y_values[class="num">2] + weights_y[class="num">3] * y_values[class="num">3]; class=class="str">"cmt">//--- Perform interpolation in y to get the final value class=class="str">"cmt">// Clamp the result to valid class="type">class="kw">color range [class="num">0, class="num">255] class="kw">return MathMax(class="num">0, MathMin(class="num">255, result)); class=class="str">"cmt">//--- Clamp the interpolated value to the valid class="type">class="kw">color range } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Extract ARGB components from a pixel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GetArgb(class="type">uint pixel, class="type">uchar &alpha, class="type">uchar &red, class="type">uchar &green, class="type">uchar &blue) { alpha = (class="type">uchar)((pixel >> class="num">24) & 0xFF); class=class="str">"cmt">//--- Extract the alpha channel from the pixel red = (class="type">uchar)((pixel >> class="num">16) & 0xFF); class=class="str">"cmt">//--- Extract the red channel from the pixel green = (class="type">uchar)((pixel >> class="num">8) & 0xFF); class=class="str">"cmt">//--- Extract the green channel from the pixel blue = (class="type">uchar)(pixel & 0xFF); class=class="str">"cmt">//--- Extract the blue channel from the pixel } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Perform bicubic interpolation for a single pixel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">uint BicubicInterpolate( class="type">uint &pixels[], class="type">int width, class="type">int height, class="type">class="kw">double x, class="type">class="kw">double y ) { class=class="str">"cmt">// Get integer and fractional parts class="type">int x0 = (class="type">int)x; class=class="str">"cmt">//--- Extract the integer part of the x-coordinate class="type">int y0 = (class="type">int)y; class=class="str">"cmt">//--- Extract the integer part of the y-coordinate class="type">class="kw">double fractional_x = x - x0; class=class="str">"cmt">//--- Calculate the fractional part of the x-coordinate
◍ 双三次插值的邻域取点与分量合成
在 MT5 里做图表图像缩放,双三次插值不会只盯一个像素,而是取目标坐标周围的 4x4 共 16 个点。代码里先用 x0、y0 算出整数基点,再用 fractional_x / fractional_y 保留小数部分,供后续曲线加权。 邻域索引做了夹取处理:MathMin(MathMax(x0+i,0),width-1) 保证越界时落在 [0, width-1] 内,避免读到图像外的脏内存。循环 i 从 -1 到 2,正好覆盖左上加右下共 4 行 4 列。
- 个像素取出后,通过 GetArgb 拆成 alpha / red / green / blue 四个长度为 16 的数组。每个分量各自跑一遍 BicubicInterpolateComponent,再强转 uchar——这意味着单个通道的插值结果若算到 255.6 也会被截断,视觉上可能出现轻微色偏。
| 最后用位运算 (alpha<<24) | (red<<16) | (green<<8) | blue 拼回一个 uint 像素值。调用 ScaleImage 后,用 '::ScaledImage'+计数器 生成唯一资源名交给 ResourceCreate,COLOR_FORMAT_ARGB_NORMALIZE 要求输入分量已归一化到 0–255。外汇与贵金属图表资源重绘属高频操作,实盘加载自定义资源存在内存与刷新延迟风险,建议先在策略测试器验证。 |
|---|
class="type">class="kw">double fractional_y = y - y0; class=class="str">"cmt">//--- Calculate the fractional part of the y-coordinate class=class="str">"cmt">// Define 4x4 neighborhood class="type">int x_indices[class="num">4], y_indices[class="num">4]; class=class="str">"cmt">//--- Declare arrays for x and y indices for (class="type">int i = -class="num">1; i <= class="num">2; i++) { class=class="str">"cmt">//--- Iterate over the 4x4 neighborhood x_indices[i + class="num">1] = MathMin(MathMax(x0 + i, class="num">0), width - class="num">1); class=class="str">"cmt">//--- Calculate clamped x-index y_indices[i + class="num">1] = MathMin(MathMax(y0 + i, class="num">0), height - class="num">1); class=class="str">"cmt">//--- Calculate clamped y-index } class=class="str">"cmt">// Get class="num">16 pixels in the 4x4 neighborhood class="type">uint neighborhood_pixels[class="num">16]; class=class="str">"cmt">//--- Declare an array for the 4x4 neighborhood pixels for (class="type">int j = class="num">0; j < class="num">4; j++) { class=class="str">"cmt">//--- Iterate over rows of the neighborhood for (class="type">int i = class="num">0; i < class="num">4; i++) { class=class="str">"cmt">//--- Iterate over columns of the neighborhood neighborhood_pixels[j * class="num">4 + i] = pixels[y_indices[j] * width + x_indices[i]]; class=class="str">"cmt">//--- Store the pixel value } } class=class="str">"cmt">// Extract ARGB components class="type">uchar alpha_components[class="num">16], red_components[class="num">16], green_components[class="num">16], blue_components[class="num">16]; class=class="str">"cmt">//--- Declare arrays for ARGB components for (class="type">int i = class="num">0; i < class="num">16; i++) { class=class="str">"cmt">//--- Iterate over the neighborhood pixels GetArgb( neighborhood_pixels[i], alpha_components[i], red_components[i], green_components[i], blue_components[i] ); class=class="str">"cmt">//--- Extract ARGB components for each pixel } class=class="str">"cmt">// Perform bicubic interpolation for each component class="type">uchar alpha_out = (class="type">uchar)BicubicInterpolateComponent(alpha_components, fractional_x, fractional_y); class=class="str">"cmt">//--- Interpolate the alpha component class="type">uchar red_out = (class="type">uchar)BicubicInterpolateComponent(red_components, fractional_x, fractional_y); class=class="str">"cmt">//--- Interpolate the red component class="type">uchar green_out = (class="type">uchar)BicubicInterpolateComponent(green_components, fractional_x, fractional_y); class=class="str">"cmt">//--- Interpolate the green component class="type">uchar blue_out = (class="type">uchar)BicubicInterpolateComponent(blue_components, fractional_x, fractional_y); class=class="str">"cmt">//--- Interpolate the blue component class=class="str">"cmt">// Combine components into a single pixel class="kw">return (alpha_out << class="num">24) | (red_out << class="num">16) | (green_out << class="num">8) | blue_out; class=class="str">"cmt">//--- Combine ARGB components into a single pixel value } class=class="str">"cmt">// Scale the image class="kw">using bicubic interpolation ScaleImage(image_pixels, original_image_width, original_image_height, scaled_image_width, scaled_image_height); class=class="str">"cmt">//--- Scale the image to the calculated dimensions class=class="str">"cmt">// Create a unique resource name for the scaled image class="type">class="kw">string scaled_resource_name = "::ScaledImage" + IntegerToString(scaled_resource_counter++); class=class="str">"cmt">//--- Generate a unique resource name class="kw">using the counter class=class="str">"cmt">// Create a new resource with the scaled image if (!ResourceCreate( scaled_resource_name, image_pixels, scaled_image_width, scaled_image_height, class="num">0, class="num">0, scaled_image_width, COLOR_FORMAT_ARGB_NORMALIZE )) { class=class="str">"cmt">//--- Create a new resource for the scaled image
「锚点偏移与位图标签的落地写法」
资源创建失败时直接返回 false 并打日志,是这套全图覆盖逻辑里最该先锁死的动作;否则后续坐标计算全建立在空资源上,MT5 图表只会静默空白。 定位策略分两条路:CenterImageDynamically 为 true 时,用图表像素宽高减去缩放图宽高再除 2,得到 x_offset 与 y_offset,图像居中。false 时走 AnchorCorner 枚举,TOP_LEFT 直接用用户输入偏移,TOP_RIGHT 需从 chart_pixel_width 扣掉图宽和偏移,BOTTOM 两组同理扣 chart_pixel_height,default 回退到左上角。 真正画图靠 CreateFullChartImage:先用 ObjectFind(0, object_name) 判存在,小于 0 才 ObjectCreate 一个 OBJ_BITMAP_LABEL;随后 ObjectSetString 把资源名写进 OBJPROP_BMPFILE。开 MT5 把这段代码塞进 EA,改 AnchorCorner 看四角贴图差异,能立刻验证你的偏移公式有没有算反。
Print("Error: Failed to create resource for scaled image: ", scaled_resource_name); class=class="str">"cmt">//--- Log an error if resource creation fails class="kw">return false; class=class="str">"cmt">//--- Return false to indicate failure } class=class="str">"cmt">// Determine image position based on user input class="type">int x_offset, y_offset; class=class="str">"cmt">//--- Declare variables for x and y offsets if (CenterImageDynamically) { class=class="str">"cmt">//--- Check if the user wants to center the image dynamically x_offset = (chart_pixel_width - scaled_image_width) / class="num">2; class=class="str">"cmt">//--- Calculate horizontal offset to center the image y_offset = (chart_pixel_height - scaled_image_height) / class="num">2; class=class="str">"cmt">//--- Calculate vertical offset to center the image } else { class=class="str">"cmt">// Set base position based on chosen anchor corner class="kw">switch (AnchorCorner) { class=class="str">"cmt">//--- Select the anchor corner based on user input case TOP_LEFT: class=class="str">"cmt">//--- Handle Top-Left corner x_offset = XOffsetFromCorner; class=class="str">"cmt">//--- Use user-defined x-offset from top-left y_offset = YOffsetFromCorner; class=class="str">"cmt">//--- Use user-defined y-offset from top-left class="kw">break; case TOP_RIGHT: class=class="str">"cmt">//--- Handle Top-Right corner x_offset = chart_pixel_width - scaled_image_width - XOffsetFromCorner; class=class="str">"cmt">//--- Calculate x-offset from right edge y_offset = YOffsetFromCorner; class=class="str">"cmt">//--- Use user-defined y-offset from top class="kw">break; case BOTTOM_LEFT: class=class="str">"cmt">//--- Handle Bottom-Left corner x_offset = XOffsetFromCorner; class=class="str">"cmt">//--- Use user-defined x-offset from left y_offset = chart_pixel_height - scaled_image_height - YOffsetFromCorner; class=class="str">"cmt">//--- Calculate y-offset from bottom class="kw">break; case BOTTOM_RIGHT: class=class="str">"cmt">//--- Handle Bottom-Right corner x_offset = chart_pixel_width - scaled_image_width - XOffsetFromCorner; class=class="str">"cmt">//--- Calculate x-offset from right edge y_offset = chart_pixel_height - scaled_image_height - YOffsetFromCorner; class=class="str">"cmt">//--- Calculate y-offset from bottom class="kw">break; class="kw">default: class=class="str">"cmt">//--- Handle unexpected case x_offset = XOffsetFromCorner; class=class="str">"cmt">//--- Default to top-left x-offset y_offset = YOffsetFromCorner; class=class="str">"cmt">//--- Default to top-left y-offset } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create and position the chart image object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateFullChartImage( class="type">class="kw">string object_name, class="type">class="kw">string resource_name, class="type">int x_size, class="type">int y_size, class="type">int x_offset, class="type">int y_offset, class="type">bool is_background ) { class=class="str">"cmt">// Create the bitmap label object if it doesn&class="macro">#x27;t exist if (ObjectFind(class="num">0, object_name) < class="num">0) { class=class="str">"cmt">//--- Check if the object already exists ObjectCreate(class="num">0, object_name, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0); class=class="str">"cmt">//--- Create a new bitmap label object } class=class="str">"cmt">// Set object properties ObjectSetString(class="num">0, object_name, OBJPROP_BMPFILE, resource_name); class=class="str">"cmt">//--- Set the resource file for the bitmap