char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Xuan-Son Nguyenmtmd: propagate const to preproc class (#28310)de8656bd9

char/aliceai-t5-moe
71.2 KiB1737 linesraw
1#include "mtmd-image.h"
2
3#include <algorithm>
4#include <cmath>
5#include <vector>
6
7void mtmd_image_preproc_out::append(const clip_hparams & hparams, const clip_image_u8 & img, bool normalized) {
8    clip_image_f32 dst;
9    dst.from_u8(img);
10    if (normalized) {
11        dst.normalize(hparams.image_mean, hparams.image_std);
12    }
13    entries.push_back(std::move(dst));
14}
15
16void mtmd_image_preproc_out::append(const clip_hparams & hparams, const std::vector<clip_image_u8> & imgs, bool normalized) {
17    for (const auto & img : imgs) {
18        append(hparams, img, normalized);
19    }
20}
21
22void mtmd_image_preproc_out::append(const clip_hparams & hparams, clip_image_f32 & img, bool normalized) {
23    if (normalized) {
24        img.normalize(hparams.image_mean, hparams.image_std);
25    }
26    entries.push_back(std::move(img));
27}
28
29void mtmd_image_preproc_out::append_overview(const clip_hparams & hparams, const clip_image_u8 & img, bool normalized) {
30    overview.from_u8(img);
31    if (normalized) {
32        overview.normalize(hparams.image_mean, hparams.image_std);
33    }
34}
35
36// set of tools to manipulate images
37// in the future, we can have HW acceleration by allowing this struct to access 3rd party lib like imagick or opencv
38struct img_tool {
39    static void resize(
40            const clip_image_u8 & src,
41            clip_image_u8 & dst,
42            const clip_image_size & target_resolution,
43            resize_algo algo,
44            pad_style padding = PAD_CEIL,
45            std::array<uint8_t, 3> pad_color = {0, 0, 0}) {
46        dst.set_size(target_resolution, src.is_placeholder());
47
48        if (src.is_placeholder()) {
49            // no-op for placeholder image, just set the size and return
50            return;
51        }
52
53        if (dst.get_size() == src.get_size()) {
54            // no resize needed, simple copy
55            dst.cpy_buf(src.get_ro_buf());
56            return;
57        }
58
59        if (padding == PAD_NONE) {
60            // direct resize
61            resize_pillow(src, dst, target_resolution.width, target_resolution.height, algo);
62        } else {
63            // resize with padding
64            clip_image_u8 resized_image;
65            float scale_w = static_cast<float>(target_resolution.width) / src.get_size().width;
66            float scale_h = static_cast<float>(target_resolution.height) / src.get_size().height;
67            float scale = std::min(scale_w, scale_h);
68
69            int new_width, new_height;
70            if (padding == PAD_NEAREST) {
71                new_width  = std::min(static_cast<int>(std::round(src.get_size().width * scale)), target_resolution.width);
72                new_height = std::min(static_cast<int>(std::round(src.get_size().height * scale)), target_resolution.height);
73            } else {
74                new_width  = std::min(static_cast<int>(std::ceil(src.get_size().width * scale)), target_resolution.width);
75                new_height = std::min(static_cast<int>(std::ceil(src.get_size().height * scale)), target_resolution.height);
76            }
77
78            resize_pillow(src, resized_image, new_width, new_height, algo);
79
80            // fill dst with pad_color
81            fill(dst, pad_color);
82
83            int offset_x, offset_y;
84            if (padding == PAD_NEAREST) {
85                offset_x = static_cast<int>(std::round((target_resolution.width  - new_width)  / 2.0f));
86                offset_y = static_cast<int>(std::round((target_resolution.height - new_height) / 2.0f));
87            } else {
88                offset_x = (target_resolution.width  - new_width)  / 2;
89                offset_y = (target_resolution.height - new_height) / 2;
90            }
91            composite(dst, resized_image, offset_x, offset_y);
92        }
93    }
94
95    static void crop(const clip_image_u8 & image, clip_image_u8 & dst, int x, int y, int w, int h) {
96        GGML_ASSERT(x >= 0 && y >= 0 && w > 0 && h > 0);
97        GGML_ASSERT(x + w <= image.get_size().width && y + h <= image.get_size().height);
98        dst.set_size({w, h}, image.is_placeholder());
99
100        if (image.is_placeholder()) {
101            // no-op for placeholder image, just set the size and return
102            return;
103        }
104
105        for (int i = 0; i < h; ++i) {
106            for (int j = 0; j < w; ++j) {
107                dst.set_pixel(j, i, image.get_pixel(x + j, y + i));
108            }
109        }
110    }
111
112    struct calc_size_opt {
113        int align_size = 1;
114        int min_pixels = 0;   // 0 = disabled
115        int max_pixels = 0;   // 0 = disabled
116        // applied before min/max_pixels, so min_pixels can push an edge back above longest_edge
117        int longest_edge = 0; // 0 = disabled
118    };
119
120    // calculate the size of the **resized** image, while preserving the aspect ratio and
121    // aligning to the nearest multiple of align_size ("smart_resize" in transformers code)
122    static clip_image_size calc_size_preserved_ratio(const clip_image_size & inp_size, const calc_size_opt & opts) {
123        GGML_ASSERT(opts.align_size > 0);
124        const int width  = inp_size.width;
125        const int height = inp_size.height;
126        if (width <= 0 || height <= 0) {
127            return {0, 0};
128        }
129
130        auto round_by_factor = [f = opts.align_size](float x) { return static_cast<int>(std::round(x / static_cast<float>(f))) * f; };
131        auto ceil_by_factor  = [f = opts.align_size](float x) { return static_cast<int>(std::ceil(x / static_cast<float>(f))) * f; };
132        auto floor_by_factor = [f = opts.align_size](float x) { return static_cast<int>(std::floor(x / static_cast<float>(f))) * f; };
133
134        int w_bar, h_bar;
135        if (opts.longest_edge > 0) {
136            const float scale = std::min(static_cast<float>(opts.longest_edge) / width,
137                                          static_cast<float>(opts.longest_edge) / height);
138            w_bar = ceil_by_factor(width  * scale);
139            h_bar = ceil_by_factor(height * scale);
140        } else {
141            // always align up first
142            w_bar = std::max(opts.align_size, round_by_factor(width));
143            h_bar = std::max(opts.align_size, round_by_factor(height));
144        }
145
146        if (opts.max_pixels > 0 && h_bar * w_bar > opts.max_pixels) {
147            const auto beta = std::sqrt(static_cast<float>(height) * width / opts.max_pixels);
148            h_bar = std::max(opts.align_size, floor_by_factor(height / beta));
149            w_bar = std::max(opts.align_size, floor_by_factor(width  / beta));
150        } else if (opts.min_pixels > 0 && h_bar * w_bar < opts.min_pixels) {
151            const auto beta = std::sqrt(static_cast<float>(opts.min_pixels) / (static_cast<float>(height) * width));
152            h_bar = ceil_by_factor(height * beta);
153            w_bar = ceil_by_factor(width * beta);
154        }
155
156        return {w_bar, h_bar};
157    }
158
159    // draw src image into dst image at offset (offset_x, offset_y)
160    static void composite(clip_image_u8 & dst, const clip_image_u8 & src, int offset_x, int offset_y) {
161        if (src.is_placeholder()) {
162            // no-op for placeholder image
163            return;
164        }
165
166        const auto src_size = src.get_size();
167        const auto dst_size = dst.get_size();
168        for (int y = 0; y < src_size.height; ++y) {
169            for (int x = 0; x < src_size.width; ++x) {
170                int dx = x + offset_x;
171                int dy = y + offset_y;
172                // skip pixels that would be out of bounds in the destination
173                if (dx < 0 || dy < 0 || dx >= dst_size.width || dy >= dst_size.height) {
174                    continue;
175                }
176                dst.set_pixel(dx, dy, src.get_pixel(x, y));
177            }
178        }
179    }
180
181    // fill the image with a solid color
182    static void fill(clip_image_u8 & img, const std::array<uint8_t, 3> & color) {
183        if (img.is_placeholder()) {
184            // no-op for placeholder image
185            return;
186        }
187
188        const auto size = img.get_size();
189        for (int y = 0; y < size.height; ++y) {
190            for (int x = 0; x < size.width; ++x) {
191                img.set_pixel(x, y, color);
192            }
193        }
194    }
195
196private:
197    // Pillow-compatible separable resampling (Bilinear, Bicubic and Lanczos)
198    // Adapted from https://github.com/python-pillow/Pillow/blob/main/src/libImaging/Resample.c
199    //
200    // Key properties:
201    // 1. Separable filtering: horizontal pass followed by vertical pass
202    // 2. Pre-computes normalized filter coefficients for each output pixel
203    // 3. Fixed-point integer arithmetic (22 fractional bits) for speed and determinism
204    static bool resize_pillow(
205            const clip_image_u8 & img,
206            clip_image_u8 & dst,
207            int target_width,
208            int target_height,
209            resize_algo algo) {
210        // Fixed-point precision: 22 bits = 32 (int32_t) - 8 (uint8_t pixels) - 2 (headroom for accumulation)
211        // This allows encoding fractional weights as integers: weight * 2^22
212        const int PRECISION_BITS = 32 - 8 - 2;
213
214        // Filter support radius
215        double filter_support;
216        switch (algo) {
217            case RESIZE_ALGO_BILINEAR: filter_support = 1.0; break;
218            case RESIZE_ALGO_BICUBIC:  filter_support = 2.0; break;
219            case RESIZE_ALGO_LANCZOS:  filter_support = 3.0; break;
220            default:
221                throw std::runtime_error("Unsupported resize algorithm");
222        }
223
224        // Returns filter weight for distance x from pixel center
225        // Note: for bicubic, Pillow uses a = -0.5 while GGML/PyTorch use a = -0.75
226        auto resample_filter = [algo](double x) -> double {
227            if (algo == RESIZE_ALGO_LANCZOS) {
228                if (-3.0 <= x && x < 3.0) {
229                    auto sinc = [](double v) {
230                        if (v == 0.0) {
231                            return 1.0;
232                        }
233                        const double pi_v = v * 3.141592653589793238462643383279502884;
234                        return std::sin(pi_v) / pi_v;
235                    };
236                    return sinc(x) * sinc(x / 3.0);
237                }
238                return 0.0;
239            }
240
241            if (x < 0.0) {
242                x = -x;
243            }
244
245            if (algo == RESIZE_ALGO_BILINEAR) {
246                return x < 1.0 ? 1.0 - x : 0.0;
247            }
248
249            constexpr double a = -0.5;
250            if (x < 1.0) {
251                return ((a + 2.0) * x - (a + 3.0)) * x * x + 1;
252            }
253            if (x < 2.0) {
254                return (((x - 5) * x + 8) * x - 4) * a;
255            }
256            return 0.0;  // Zero outside [-2, 2]
257        };
258
259        // Clipping function for 8-bit values
260        auto clip8 = [](int val) -> uint8_t {
261            if (val < 0) return 0;
262            if (val > 255) return 255;
263            return static_cast<uint8_t>(val);
264        };
265
266        // Precompute filter coefficients for ONE dimension (horizontal or vertical)
267        //
268        // Parameters:
269        //   inSize  - Number of pixels in input dimension (e.g., src_width or src_height)
270        //   outSize - Number of pixels in output dimension (e.g., target_width or target_height)
271        //   bounds  - [OUTPUT] Array of size outSize*2 storing input pixel ranges:
272        //             bounds[xx*2+0] = first input pixel index for output pixel xx (xmin)
273        //             bounds[xx*2+1] = number of input pixels for output pixel xx (xcnt)
274        //   weights - [OUTPUT] Array of size outSize*ksize storing fixed-point filter weights:
275        //             kk[xx*ksize + x] = weight for input pixel x contributing to output pixel xx
276        //
277        // Returns: kernel size (ksize) - number of input pixels that contribute to each output pixel
278        auto precompute_weights = [&](int inSize, int outSize,
279                                     std::vector<int> & bounds, std::vector<int32_t> & weights) -> int {
280            GGML_ASSERT(inSize > 0 && outSize > 0);
281            double support, scale, filterscale;
282            double center, ww, ss;
283            int xx, x, ksize, xmin, xmax;
284
285            // Calculate scaling factor: ratio of input range to output size
286            filterscale = scale = static_cast<double>(inSize) / outSize;
287            // For upsampling (scale < 1), keep filterscale = 1 to maintain filter sharpness
288            // For downsampling (scale > 1), widen filter to prevent aliasing
289            if (filterscale < 1.0) {
290                filterscale = 1.0;
291            }
292
293            // Determine filter support radius and kernel size
294            support = filter_support * filterscale;  // Widen filter when downsampling
295            ksize = static_cast<int>(std::ceil(support)) * 2 + 1;  // Total pixels in kernel
296
297            std::vector<double> pre_weights(outSize * ksize);  // Temporary weights
298            bounds.resize(outSize * 2);
299
300
301            // For each output pixel, compute its filter coefficients
302            for (xx = 0; xx < outSize; xx++) {
303                // Calculate the center position in input space (pixel-center convention: +0.5)
304                center = (xx + 0.5) * scale;
305                ww = 0.0;  // Sum of weights for normalization
306                ss = 1.0 / filterscale;  // Scale factor for filter function
307
308                // Determine the range of input pixels that contribute to this output pixel
309                xmin = static_cast<int>(center - support + 0.5);
310                if (xmin < 0) {
311                    xmin = 0;
312                }
313
314                xmax = static_cast<int>(center + support + 0.5);
315                if (xmax > inSize) {
316                    xmax = inSize;
317                }
318
319                xmax -= xmin;
320
321                // Compute filter weights for each contributing input pixel
322                for (x = 0; x < xmax; x++) {
323                    // Distance from input pixel center to output pixel center in input space
324                    double w = resample_filter((x + xmin - center + 0.5) * ss);
325                    pre_weights[xx * ksize + x] = w;
326                    ww += w;  // Accumulate for normalization
327                }
328
329                // Normalize weights to sum to 1.0 (preserves brightness)
330                for (x = 0; x < xmax; x++) {
331                    if (ww != 0.0) {
332                        pre_weights[xx * ksize + x] /= ww;
333                    }
334                }
335
336                // Zero-pad remaining kernel positions
337                for (; x < ksize; x++) {
338                    pre_weights[xx * ksize + x] = 0;
339                }
340
341                // Store input pixel range for this output pixel
342                bounds[xx * 2 + 0] = xmin;
343                bounds[xx * 2 + 1] = xmax;
344            }
345
346            // Convert floating-point coefficients to fixed-point integers
347            // Formula: int32 = round(float * 2^PRECISION_BITS)
348            weights.resize(outSize * ksize);
349
350            const double fxp_scale = std::ldexp(1.0, PRECISION_BITS); // 1.0 * 2^PRECISION_BITS
351
352            for (int i = 0; i < outSize * ksize; i++) {
353                // Pillow adds +/- 0.5 then truncates toward zero; std::round would round twice
354                const double rounded = pre_weights[i] * fxp_scale + (pre_weights[i] < 0 ? -0.5 : 0.5);
355                weights[i] = static_cast<int32_t>(rounded);
356            }
357
358            return ksize;
359        };
360
361        // Horizontal resampling pass
362        // Resizes width from src to out_nx, preserving height
363        auto resample_horizontal = [&](const uint8_t * src, int in_nx, int in_ny,
364                                       int out_nx,
365                                       int ksize, const std::vector<int> & bounds, const std::vector<int32_t> & weights) {
366            std::vector<uint8_t> out((size_t) out_nx * in_ny * 3);
367
368            // Process each row independently
369            for (int yy = 0; yy < in_ny; yy++) {
370                const uint8_t * src_row = src + (size_t) yy * in_nx * 3;
371                uint8_t * dst_row = out.data() + (size_t) yy * out_nx * 3;
372
373                // For each output pixel in this row
374                for (int xx = 0; xx < out_nx; xx++) {
375                    const int xmin = bounds[xx * 2 + 0];  // First input pixel index
376                    const int xcnt = bounds[xx * 2 + 1];  // Number of input pixels
377                    const int32_t * k = &weights[xx * ksize];
378                    const uint8_t * p = src_row + (size_t) xmin * 3;
379
380                    // Accumulators for RGB channels, with rounding bias (0.5 in fixed-point)
381                    int32_t ss0 = 1 << (PRECISION_BITS - 1);
382                    int32_t ss1 = 1 << (PRECISION_BITS - 1);
383                    int32_t ss2 = 1 << (PRECISION_BITS - 1);
384
385                    // Convolve: sum weighted input pixels
386                    for (int x = 0; x < xcnt; x++) {
387                        ss0 += p[0] * k[x];
388                        ss1 += p[1] * k[x];
389                        ss2 += p[2] * k[x];
390                        p += 3;
391                    }
392
393                    // Convert back from fixed-point (divide by 2^PRECISION_BITS) and clamp to [0,255]
394                    dst_row[xx * 3 + 0] = clip8(ss0 >> PRECISION_BITS);
395                    dst_row[xx * 3 + 1] = clip8(ss1 >> PRECISION_BITS);
396                    dst_row[xx * 3 + 2] = clip8(ss2 >> PRECISION_BITS);
397                }
398            }
399
400            return out;
401        };
402
403        // Vertical resampling pass
404        // Resizes height from src to out_ny, preserving width
405        // Accumulates whole rows at once (contiguous access, auto-vectorizes well)
406        auto resample_vertical = [&](const uint8_t * src, int in_nx,
407                                     int out_ny,
408                                     int ksize, const std::vector<int> & bounds, const std::vector<int32_t> & weight) {
409            const size_t row_elems = (size_t) in_nx * 3;
410            std::vector<uint8_t> out(row_elems * out_ny);
411            std::vector<int32_t> acc(row_elems);
412
413            // For each output row
414            for (int yy = 0; yy < out_ny; yy++) {
415                const int ymin = bounds[yy * 2 + 0];  // First input row index
416                const int ycnt = bounds[yy * 2 + 1];  // Number of input rows
417                const int32_t * k = &weight[yy * ksize];
418
419                // Rounding bias (0.5 in fixed-point)
420                std::fill(acc.begin(), acc.end(), 1 << (PRECISION_BITS - 1));
421
422                // Convolve: accumulate each weighted input row
423                for (int y = 0; y < ycnt; y++) {
424                    const uint8_t * src_row = src + (size_t) (ymin + y) * row_elems;
425                    const int32_t w = k[y];
426                    for (size_t i = 0; i < row_elems; i++) {
427                        acc[i] += src_row[i] * w;
428                    }
429                }
430
431                // Convert back from fixed-point and clamp to [0,255]
432                uint8_t * dst_row = out.data() + (size_t) yy * row_elems;
433                for (size_t i = 0; i < row_elems; i++) {
434                    dst_row[i] = clip8(acc[i] >> PRECISION_BITS);
435                }
436            }
437
438            return out;
439        };
440
441        // Main resampling logic using separable two-pass approach
442        const int src_width  = img.get_size().width;
443        const int src_height = img.get_size().height;
444
445        bool need_horizontal = (target_width != src_width);
446        bool need_vertical = (target_height != src_height);
447
448        // Precompute filter coefficients for both dimensions
449        std::vector<int> bounds_horiz, bounds_vert;
450        std::vector<int32_t> weights_horiz, weights_vert;
451        int ksize_horiz = 0, ksize_vert = 0;
452
453        if (need_horizontal) {
454            ksize_horiz = precompute_weights(src_width, target_width, bounds_horiz, weights_horiz);
455        }
456
457        if (need_vertical) {
458            ksize_vert = precompute_weights(src_height, target_height, bounds_vert, weights_vert);
459        }
460
461        // Perform two-pass resampling
462        const uint8_t * src = img.get_ro_buf().data();
463        if (need_horizontal && need_vertical) {
464            auto temp = resample_horizontal(src, src_width, src_height, target_width, ksize_horiz, bounds_horiz, weights_horiz);
465            dst.set_size({target_width, target_height}, false);
466            dst.cpy_buf(resample_vertical(temp.data(), target_width, target_height, ksize_vert, bounds_vert, weights_vert));
467        } else if (need_horizontal) {
468            dst.set_size({target_width, src_height}, false);
469            dst.cpy_buf(resample_horizontal(src, src_width, src_height, target_width, ksize_horiz, bounds_horiz, weights_horiz));
470        } else if (need_vertical) {
471            dst.set_size({src_width, target_height}, false);
472            dst.cpy_buf(resample_vertical(src, src_width, target_height, ksize_vert, bounds_vert, weights_vert));
473        } else {
474            // No resizing needed - direct copy
475            dst.set_size(img.get_size(), false);
476            dst.cpy_buf(img.get_ro_buf());
477        }
478
479        return true;
480    }
481};
482
483
484//
485// mtmd_image_preprocessor_llava_uhd
486//
487
488mtmd_image_preproc_out mtmd_image_preprocessor_llava_uhd::preprocess(const clip_image_u8 & img) const {
489    const clip_image_size original_size = img.get_size();
490    auto const inst = get_slice_instructions(original_size);
491    auto sliced = slice_image(img, inst);
492
493    mtmd_image_preproc_out output;
494    output.append_overview(hparams, sliced.overview, true);
495    output.append(hparams, sliced.slices, true);
496    output.grid_x = inst.grid_size.width;
497    output.grid_y = inst.grid_size.height;
498
499    return output;
500}
501
502mtmd_image_preprocessor_llava_uhd::slice_instructions mtmd_image_preprocessor_llava_uhd::get_slice_instructions(const clip_image_size & original_size) const {
503    mtmd_image_preprocessor_llava_uhd::slice_instructions res;
504    // align slices by patch_size * n_merge so an integer number of merger output tokens fits per slice
505    const int n_merge         = hparams.n_merge;
506    const int patch_size      = hparams.patch_size * n_merge;
507    const int slice_size      = hparams.image_size;
508    const int original_width  = original_size.width;
509    const int original_height = original_size.height;
510
511    const bool has_slices    = original_size.width > slice_size || original_size.height > slice_size;
512    const bool has_pinpoints = !hparams.image_res_candidates.empty();
513
514    if (!has_slices) {
515        // skip slicing logic
516        res.overview_size = clip_image_size{slice_size, slice_size};
517        res.refined_size  = clip_image_size{0, 0};
518        res.grid_size     = clip_image_size{0, 0};
519
520        return res;
521    }
522
523    if (has_pinpoints) {
524        // has pinpoints, use them to calculate the grid size (e.g. llava-1.6)
525        auto refine_size = select_best_resolution(
526            original_size,
527            hparams.image_res_candidates);
528        res.overview_size         = clip_image_size{slice_size, slice_size};
529        res.refined_size          = refine_size;
530        res.grid_size             = clip_image_size{0, 0};
531
532        LOG_DBG("%s: using pinpoints for slicing\n", __func__);
533        LOG_DBG("%s: original size: %d x %d, overview size: %d x %d, refined size: %d x %d\n",
534                __func__, original_width, original_height,
535                res.overview_size.width, res.overview_size.height,
536                res.refined_size.width,  res.refined_size.height);
537
538        for (int y = 0; y < refine_size.height; y += slice_size) {
539            for (int x = 0; x < refine_size.width; x += slice_size) {
540                slice_coordinates slice;
541                slice.x = x;
542                slice.y = y;
543                slice.size.width  = std::min(slice_size, refine_size.width  - x);
544                slice.size.height = std::min(slice_size, refine_size.height - y);
545                res.slices.push_back(slice);
546                LOG_DBG("%s: slice %d: x=%d, y=%d, size=%dx%d\n",
547                        __func__, (int)res.slices.size() - 1,
548                        slice.x, slice.y, slice.size.width, slice.size.height);
549            }
550        }
551
552        res.grid_size.height = refine_size.height / slice_size;
553        res.grid_size.width  = refine_size.width  / slice_size;
554        LOG_DBG("%s: grid size: %d x %d\n", __func__, res.grid_size.width, res.grid_size.height);
555
556        return res;
557    }
558
559    // no pinpoints, dynamically calculate the grid size (e.g. minicpmv)
560
561    auto best_size    = get_best_resize(original_size, slice_size, patch_size, !has_slices);
562    res.overview_size = best_size;
563
564    {
565        const int max_slice_nums = 9; // TODO: this is only used by minicpmv, maybe remove it
566        const float log_ratio = log((float)original_width / original_height);
567        const float ratio = (float)original_width * original_height / (slice_size * slice_size);
568        const int multiple = fmin(ceil(ratio), max_slice_nums);
569
570        auto best_grid   = get_best_grid(max_slice_nums, multiple, log_ratio);
571        auto refine_size = get_refine_size(original_size, best_grid, slice_size, patch_size, true);
572        res.grid_size    = best_grid;
573        res.refined_size = refine_size;
574
575        LOG_DBG("%s: original size: %d x %d, overview size: %d x %d, refined size: %d x %d, grid size: %d x %d\n",
576                __func__, original_width, original_height,
577                res.overview_size.width, res.overview_size.height,
578                res.refined_size.width, res.refined_size.height,
579                res.grid_size.width, res.grid_size.height);
580
581        int width  = refine_size.width;
582        int height = refine_size.height;
583        int grid_x = int(width  / best_grid.width);
584        int grid_y = int(height / best_grid.height);
585        for (int patches_y = 0,                    ic = 0;
586                patches_y < refine_size.height && ic < best_grid.height;
587                patches_y += grid_y,              ic += 1) {
588            for (int patches_x = 0,                   jc = 0;
589                    patches_x < refine_size.width && jc < best_grid.width;
590                    patches_x += grid_x,             jc += 1) {
591                slice_coordinates slice;
592                slice.x = patches_x;
593                slice.y = patches_y;
594                slice.size.width  = grid_x;
595                slice.size.height = grid_y;
596                res.slices.push_back(slice);
597                LOG_DBG("%s: slice %d: x=%d, y=%d, size=%dx%d\n",
598                        __func__, (int)res.slices.size() - 1,
599                        slice.x, slice.y, slice.size.width, slice.size.height);
600            }
601        }
602    }
603
604    return res;
605}
606
607mtmd_image_preprocessor_llava_uhd::slice_output mtmd_image_preprocessor_llava_uhd::slice_image(const clip_image_u8 & img, const mtmd_image_preprocessor_llava_uhd::slice_instructions & inst) const {
608    slice_output output;
609
610    // resize to overview size
611    img_tool::resize(img, output.overview, inst.overview_size, hparams.image_resize_algo_ov,
612                        hparams.image_pad_ov, hparams.image_pad_color_ov);
613
614    if (inst.slices.empty()) {
615        // no slices, just return the overview image
616        return output;
617    }
618
619    // resize to refined size
620    clip_image_u8 refined_img;
621    img_tool::resize(img, refined_img, inst.refined_size, hparams.image_resize_algo_rf,
622                        hparams.image_pad_rf, hparams.image_pad_color_rf);
623
624    // create slices
625    for (const auto & slice : inst.slices) {
626        int x = slice.x;
627        int y = slice.y;
628        int w = slice.size.width;
629        int h = slice.size.height;
630
631        clip_image_u8 img_slice;
632        img_tool::crop(refined_img, img_slice, x, y, w, h);
633        output.slices.push_back(std::move(img_slice));
634    }
635
636    return output;
637}
638
639clip_image_size mtmd_image_preprocessor_llava_uhd::get_best_resize(const clip_image_size & original_size, int scale_resolution, int patch_size, bool allow_upscale) const {
640    int width  = original_size.width;
641    int height = original_size.height;
642    if ((width * height > scale_resolution * scale_resolution) || allow_upscale) {
643        float r = static_cast<float>(width) / height;
644        height  = static_cast<int>(scale_resolution / std::sqrt(r));
645        width   = static_cast<int>(height * r);
646    }
647    clip_image_size res;
648    res.width  = ensure_divide(width,  patch_size);
649    res.height = ensure_divide(height, patch_size);
650    return res;
651}
652
653clip_image_size mtmd_image_preprocessor_llava_uhd::resize_maintain_aspect_ratio(const clip_image_size & orig, const clip_image_size & target_max) const {
654    float scale_width  = static_cast<float>(target_max.width)  / orig.width;
655    float scale_height = static_cast<float>(target_max.height) / orig.height;
656    float scale = std::min(scale_width, scale_height);
657    return clip_image_size{
658        static_cast<int>(orig.width  * scale),
659        static_cast<int>(orig.height * scale),
660    };
661}
662
663clip_image_size mtmd_image_preprocessor_llava_uhd::select_best_resolution(const clip_image_size & original_size, const std::vector<clip_image_size> & possible_resolutions) const {
664    clip_image_size best_fit;
665    int min_wasted_area = std::numeric_limits<int>::max();
666    int max_effective_resolution = 0;
667
668    for (const clip_image_size & candidate : possible_resolutions) {
669        auto target_size = resize_maintain_aspect_ratio(original_size, candidate);
670        int effective_resolution = std::min(
671            target_size.width * target_size.height,
672            original_size.width * original_size.height);
673        int wasted_area = (candidate.width * candidate.height) - effective_resolution;
674
675        if (effective_resolution > max_effective_resolution || (effective_resolution == max_effective_resolution && wasted_area < min_wasted_area)) {
676            max_effective_resolution = effective_resolution;
677            min_wasted_area = wasted_area;
678            best_fit = candidate;
679        }
680
681        LOG_DBG("%s: candidate: %d x %d, target: %d x %d, wasted: %d, effective: %d\n", __func__, candidate.width, candidate.height, target_size.width, target_size.height, wasted_area, effective_resolution);
682    }
683
684    return best_fit;
685}
686
687int mtmd_image_preprocessor_llava_uhd::ensure_divide(int length, int patch_size) const {
688    return std::max(static_cast<int>(std::round(static_cast<float>(length) / patch_size) * patch_size), patch_size);
689}
690
691clip_image_size mtmd_image_preprocessor_llava_uhd::get_refine_size(const clip_image_size & original_size, const clip_image_size & grid, int scale_resolution, int patch_size, bool allow_upscale) const {
692    int width  = original_size.width;
693    int height = original_size.height;
694    int grid_x = grid.width;
695    int grid_y = grid.height;
696
697    int refine_width  = ensure_divide(width, grid_x);
698    int refine_height = ensure_divide(height, grid_y);
699
700    clip_image_size grid_size;
701    grid_size.width  = refine_width  / grid_x;
702    grid_size.height = refine_height / grid_y;
703
704    auto best_grid_size  = get_best_resize(grid_size, scale_resolution, patch_size, allow_upscale);
705    int best_grid_width  = best_grid_size.width;
706    int best_grid_height = best_grid_size.height;
707
708    clip_image_size refine_size;
709    refine_size.width  = best_grid_width  * grid_x;
710    refine_size.height = best_grid_height * grid_y;
711    return refine_size;
712}
713
714clip_image_size mtmd_image_preprocessor_llava_uhd::get_best_grid(const int max_slice_nums, const int multiple, const float log_ratio) const {
715    std::vector<int> candidate_split_grids_nums;
716    for (int i : {multiple - 1, multiple, multiple + 1}) {
717        if (i == 1 || i > max_slice_nums) {
718            continue;
719        }
720        candidate_split_grids_nums.push_back(i);
721    }
722
723    std::vector<clip_image_size> candidate_grids;
724    for (int split_grids_nums : candidate_split_grids_nums) {
725        int m = 1;
726        while (m <= split_grids_nums) {
727            if (split_grids_nums % m == 0) {
728                candidate_grids.push_back(clip_image_size{m, split_grids_nums / m});
729            }
730            ++m;
731        }
732    }
733
734    clip_image_size best_grid{1, 1};
735    float min_error = std::numeric_limits<float>::infinity();
736    for (const auto& grid : candidate_grids) {
737        float error = std::abs(log_ratio - std::log(1.0 * grid.width / grid.height));
738        if (error < min_error) {
739            best_grid = grid;
740            min_error = error;
741        }
742    }
743    return best_grid;
744}
745
746//
747// mtmd_image_preprocessor_fixed_size
748//
749
750mtmd_image_preproc_out mtmd_image_preprocessor_fixed_size::preprocess(const clip_image_u8 & img) const {
751    clip_image_u8 resized_image;
752    int sz = hparams.image_size;
753    img_tool::resize(img, resized_image, {sz, sz},
754                        hparams.image_resize_algo,
755                        hparams.image_resize_pad,
756                        hparams.image_pad_color);
757    mtmd_image_preproc_out output;
758    output.append(hparams, resized_image, true);
759    return output;
760}
761
762//
763// mtmd_image_preprocessor_dyn_size
764//
765
766mtmd_image_preproc_out mtmd_image_preprocessor_dyn_size::preprocess(const clip_image_u8 & img) const {
767    GGML_ASSERT(hparams.image_min_pixels > 0 && hparams.image_max_pixels > 0);
768    clip_image_u8 resized_image;
769    const clip_image_size original_size = img.get_size();
770    // the original pixtral model doesn't have n_merge
771    const int cur_merge = hparams.n_merge;
772    const clip_image_size target_size = img_tool::calc_size_preserved_ratio(
773        original_size,
774        {
775            /* align_size   */ hparams.patch_size * cur_merge,
776            /* min_pixels   */ hparams.image_min_pixels,
777            /* max_pixels   */ hparams.image_max_pixels,
778            /* longest_edge */ 0,
779        });
780    img_tool::resize(img, resized_image, target_size,
781                        hparams.image_resize_algo,
782                        hparams.image_resize_pad,
783                        hparams.image_pad_color);
784    mtmd_image_preproc_out output;
785    output.append(hparams, resized_image, true);
786    return output;
787}
788
789//
790// mtmd_image_preprocessor_longest_edge
791//
792
793mtmd_image_preproc_out mtmd_image_preprocessor_longest_edge::preprocess(const clip_image_u8 & img) const {
794    GGML_ASSERT(hparams.image_longest_edge > 0);
795    clip_image_u8 resized_image;
796    const clip_image_size original_size = img.get_size();
797    // the original pixtral model doesn't have n_merge
798    const int cur_merge = hparams.n_merge == 0 ? 1 : hparams.n_merge;
799    const clip_image_size target_size = img_tool::calc_size_preserved_ratio(
800        original_size,
801        {
802            /* align_size   */ hparams.patch_size * cur_merge,
803            /* min_pixels   */ std::max(0, hparams.image_min_pixels),
804            /* max_pixels   */ std::max(0, hparams.image_max_pixels),
805            /* longest_edge */ hparams.image_longest_edge,
806        });
807    img_tool::resize(img, resized_image, target_size,
808                        hparams.image_resize_algo,
809                        hparams.image_resize_pad,
810                        hparams.image_pad_color);
811    mtmd_image_preproc_out output;
812    output.append(hparams, resized_image, true);
813    return output;
814}
815
816//
817// mtmd_image_preprocessor_minicpmv
818//
819
820mtmd_image_preprocessor_llava_uhd::slice_instructions mtmd_image_preprocessor_minicpmv::get_slice_instructions(const clip_image_size & original_size) const {
821    if (hparams.n_merge == 2) {
822        const int   slice_size = hparams.image_size;
823        const float ratio      = (float)original_size.width * original_size.height / (slice_size * slice_size);
824        if (ratio <= 1.0f) {
825            mtmd_image_preprocessor_llava_uhd::slice_instructions inst;
826            const int patch_size = hparams.patch_size * hparams.n_merge;
827            inst.overview_size = get_best_resize(original_size, slice_size, patch_size, true);
828            inst.refined_size  = clip_image_size{0, 0};
829            inst.grid_size     = clip_image_size{0, 0};
830            return inst;
831        }
832    }
833    return mtmd_image_preprocessor_llava_uhd::get_slice_instructions(original_size);
834}
835
836//
837// mtmd_image_preprocessor_lfm2
838//
839
840mtmd_image_preproc_out mtmd_image_preprocessor_lfm2::preprocess(const clip_image_u8 & img) const {
841    auto const inst = get_slice_instructions(img.get_size());
842    if (!inst.slices.empty()) {
843        return mtmd_image_preprocessor_llava_uhd::preprocess(img);
844    }
845
846    // single tile: no thumbnail
847    // note: not using output.overview here because it will emit <|img_thumbnail|> token, which we don't want in this case
848    auto sliced = slice_image(img, inst);
849    mtmd_image_preproc_out output;
850    output.append(hparams, sliced.overview, true);
851    return output;
852}
853
854bool mtmd_image_preprocessor_lfm2::should_tile(
855        const clip_hparams & hparams,
856        const clip_image_size & original_size) {
857    const int align_size = hparams.patch_size * hparams.n_merge;
858
859    const auto round_by_factor = [align_size](float x) {
860        // see https://github.com/ggml-org/llama.cpp/pull/27057#discussion_r3796264887
861        return static_cast<int>(std::nearbyint(static_cast<double>(x) / align_size)) * align_size;
862    };
863
864    const int h_bar = std::max(hparams.patch_size, round_by_factor(original_size.height));
865    const int w_bar = std::max(hparams.patch_size, round_by_factor(original_size.width));
866
867    return static_cast<double>(h_bar) * static_cast<double>(w_bar) >
868           static_cast<double>(hparams.image_max_pixels) * max_pixels_tolerance;
869}
870
871mtmd_image_preprocessor_llava_uhd::slice_instructions mtmd_image_preprocessor_lfm2::get_slice_instructions(const clip_image_size & original_size) const {
872    mtmd_image_preprocessor_llava_uhd::slice_instructions inst;
873    const int align_size = hparams.patch_size * hparams.n_merge;
874    inst.overview_size = img_tool::calc_size_preserved_ratio(
875                            original_size,
876                            { align_size, hparams.image_min_pixels, hparams.image_max_pixels, 0 });
877
878    const bool needs_tiling = should_tile(hparams, original_size);
879
880    if (!needs_tiling) {
881        inst.refined_size = clip_image_size{0, 0};
882        inst.grid_size    = clip_image_size{0, 0};
883        return inst;
884    }
885
886    const clip_image_size grid = get_grid_layout(original_size.height, original_size.width);
887
888    inst.grid_size    = grid;
889    inst.refined_size = clip_image_size{tile_size * grid.width, tile_size * grid.height};
890
891    LOG_DBG("%s: original size: %d x %d, overview size: %d x %d, refined size: %d x %d, grid size: %d x %d\n",
892            __func__,
893            original_size.width, original_size.height,
894            inst.overview_size.width, inst.overview_size.height,
895            inst.refined_size.width, inst.refined_size.height,
896            grid.width, grid.height);
897
898    for (int row = 0; row < grid.height; row++) {
899        for (int col = 0; col < grid.width; col++) {
900            mtmd_image_preprocessor_llava_uhd::slice_coordinates slice;
901            slice.x    = col * tile_size;
902            slice.y    = row * tile_size;
903            slice.size = clip_image_size{tile_size, tile_size};
904            inst.slices.push_back(slice);
905            LOG_DBG("%s: slice %d: x=%d, y=%d, size=%d x %d\n",
906                    __func__, (int)inst.slices.size() - 1,
907                    slice.x, slice.y, slice.size.width, slice.size.height);
908        }
909    }
910
911    return inst;
912}
913
914clip_image_size mtmd_image_preprocessor_lfm2::find_closest_aspect_ratio(
915        float aspect_ratio,
916        const std::vector<clip_image_size> & target_ratios,
917        int width, int height) const {
918    float best_ratio_diff = std::numeric_limits<float>::max();
919    clip_image_size best_ratio = {1, 1};
920    const float area = static_cast<float>(width * height);
921
922    for (const auto & ratio : target_ratios) {
923        const float target_aspect_ratio = static_cast<float>(ratio.width) / ratio.height;
924        const float ratio_diff = std::abs(aspect_ratio - target_aspect_ratio);
925        if (ratio_diff < best_ratio_diff) {
926            best_ratio_diff = ratio_diff;
927            best_ratio = ratio;
928        } else if (ratio_diff == best_ratio_diff) {
929            const float target_area = static_cast<float>(tile_size * tile_size * ratio.width * ratio.height);
930            if (area > 0.5f * target_area) {
931                best_ratio = ratio;
932            }
933        }
934    }
935    return best_ratio;
936}
937
938std::vector<clip_image_size> mtmd_image_preprocessor_lfm2::get_target_ratios() const {
939    std::vector<clip_image_size> ratios;
940    for (int n = min_tiles; n <= max_tiles; n++) {
941        for (int w = 1; w <= n; w++) {
942            for (int h = 1; h <= n; h++) {
943                if (w * h >= min_tiles && w * h <= max_tiles) {
944                    bool found = false;
945                    for (const auto & r : ratios) {
946                        if (r.width == w && r.height == h) {
947                            found = true;
948                            break;
949                        }
950                    }
951                    if (!found) {
952                        ratios.push_back({w, h});
953                    }
954                }
955            }
956        }
957    }
958    std::sort(ratios.begin(), ratios.end(), [](const clip_image_size & a, const clip_image_size & b) {
959        return a.width * a.height < b.width * b.height;
960    });
961    return ratios;
962}
963
964clip_image_size mtmd_image_preprocessor_lfm2::get_grid_layout(int height, int width) const {
965    const float aspect_ratio = static_cast<float>(width) / height;
966    const auto ratios = get_target_ratios();
967    return find_closest_aspect_ratio(aspect_ratio, ratios, width, height);
968}
969
970//
971// mtmd_image_preprocessor_idefics3
972//
973
974mtmd_image_preproc_out mtmd_image_preprocessor_idefics3::preprocess(const clip_image_u8 & img) const {
975    // The refined size has two steps:
976    // 1. Resize w/ aspect-ratio preserving such that the longer side is
977    //      the preprocessor longest size
978    // 2. Resize w/out preserving aspect ratio such that both sides are
979    //      multiples of image_size (always rounding up)
980    //
981    // CITE: https://github.com/huggingface/transformers/blob/main/src/transformers/models/idefics3/image_processing_idefics3.py#L737
982    const clip_image_size original_size = img.get_size();
983
984    // old gguf files have no preprocessor longest size, custom token limits also need the generic size below
985    if (hparams.image_longest_edge > 0 && hparams.image_min_pixels <= 0 && hparams.image_max_pixels <= 0) {
986        const int    tile_size    = hparams.image_size;
987        const int    longest_edge = hparams.image_longest_edge;
988        const double aspect_ratio = (double) original_size.width / original_size.height;
989
990        clip_image_size resized_size;
991        if (original_size.width >= original_size.height) {
992            resized_size.width   = longest_edge;
993            resized_size.height  = (int) (longest_edge / aspect_ratio);
994            resized_size.height += resized_size.height % 2;
995        } else {
996            resized_size.height  = longest_edge;
997            resized_size.width   = (int) (longest_edge * aspect_ratio);
998            resized_size.width  += resized_size.width % 2;
999        }
1000
1001        const int grid_x = (resized_size.width  + tile_size - 1) / tile_size;
1002        const int grid_y = (resized_size.height + tile_size - 1) / tile_size;
1003        const clip_image_size refined_size = clip_image_size{grid_x * tile_size, grid_y * tile_size};
1004
1005        clip_image_u8 resized_img;
1006        img_tool::resize(img, resized_img, resized_size, hparams.image_resize_algo, PAD_NONE);
1007
1008        clip_image_u8 refined_img;
1009        img_tool::resize(resized_img, refined_img, refined_size, hparams.image_resize_algo, PAD_NONE);
1010
1011        clip_image_u8 overview;
1012        img_tool::resize(refined_img, overview, {tile_size, tile_size}, hparams.image_resize_algo, PAD_NONE);
1013
1014        std::vector<clip_image_u8> slices;
1015        for (int y = 0; y < grid_y; y++) {
1016            for (int x = 0; x < grid_x; x++) {
1017                clip_image_u8 slice;
1018                img_tool::crop(refined_img, slice, x * tile_size, y * tile_size, tile_size, tile_size);
1019                slices.push_back(std::move(slice));
1020            }
1021        }
1022
1023        LOG_DBG("%s: grid size: %d x %d (%d tiles) + overview\n", __func__, grid_x, grid_y, grid_x * grid_y);
1024
1025        mtmd_image_preproc_out output;
1026        output.append_overview(hparams, overview, true);
1027        output.append(hparams, slices, true);
1028        output.grid_x = grid_x;
1029        output.grid_y = grid_y;
1030        return output;
1031    }
1032
1033    const clip_image_size refined_size = img_tool::calc_size_preserved_ratio(
1034        original_size,
1035        { hparams.image_size, std::max(0, hparams.image_min_pixels), std::max(0, hparams.image_max_pixels), hparams.image_longest_edge });
1036    // LOG_INF("%s: original size: %d x %d, refined size: %d x %d\n",
1037    //         __func__, original_size.width, original_size.height,
1038    //         refined_size.width, refined_size.height);
1039
1040    mtmd_image_preprocessor_llava_uhd::slice_instructions instructions;
1041    instructions.overview_size = clip_image_size{hparams.image_size, hparams.image_size};
1042    instructions.refined_size = refined_size;
1043    instructions.grid_size = clip_image_size{
1044        static_cast<int>(std::ceil(static_cast<float>(refined_size.width) / hparams.image_size)),
1045        static_cast<int>(std::ceil(static_cast<float>(refined_size.height) / hparams.image_size)),
1046    };
1047    for (int y = 0; y < refined_size.height; y += hparams.image_size) {
1048        for (int x = 0; x < refined_size.width; x += hparams.image_size) {
1049            // LOG_INF("%s: adding slice at x=%d, y=%d\n", __func__, x, y);
1050            instructions.slices.push_back(mtmd_image_preprocessor_llava_uhd::slice_coordinates{
1051                /* x    */x,
1052                /* y    */y,
1053                /* size */clip_image_size{
1054                    std::min(hparams.image_size, refined_size.width - x),
1055                    std::min(hparams.image_size, refined_size.height - y)
1056                }
1057            });
1058        }
1059    }
1060    auto sliced = slice_image(img, instructions);
1061
1062    mtmd_image_preproc_out output;
1063    output.append_overview(hparams, sliced.overview, true);
1064    output.append(hparams, sliced.slices, true);
1065    output.grid_x = instructions.grid_size.width;
1066    output.grid_y = instructions.grid_size.height;
1067    return output;
1068}
1069
1070//
1071// mtmd_image_preprocessor_internvl
1072//
1073
1074mtmd_image_preproc_out mtmd_image_preprocessor_internvl::preprocess(const clip_image_u8 & img) const {
1075    GGML_ASSERT(!hparams.image_res_candidates.empty());
1076    const clip_image_size original_size = img.get_size();
1077    auto const inst = get_slice_instructions(original_size);
1078    auto sliced = slice_image(img, inst);
1079
1080    mtmd_image_preproc_out output;
1081    // InternVL: slices first, then overview
1082    output.append(hparams, sliced.slices, true);
1083    output.append_overview(hparams, sliced.overview, true);
1084    output.grid_x = inst.grid_size.width;
1085    output.grid_y = inst.grid_size.height;
1086    return output;
1087}
1088
1089//
1090// mtmd_image_preprocessor_deepseekocr
1091//
1092
1093std::vector<clip_image_size> mtmd_image_preprocessor_deepseekocr::get_target_ratios() const {
1094    std::vector<clip_image_size> ratios;
1095    for (int n = min_tiles; n <= max_tiles; n++) {
1096        for (int w = 1; w <= n; w++) {
1097            for (int h = 1; h <= n; h++) {
1098                if (w * h < min_tiles || w * h > max_tiles) {
1099                    continue;
1100                }
1101                bool found = false;
1102                for (const auto & r : ratios) {
1103                    if (r.width == w && r.height == h) {
1104                        found = true;
1105                        break;
1106                    }
1107                }
1108                if (!found) {
1109                    ratios.push_back({ w, h });
1110                }
1111            }
1112        }
1113    }
1114    std::sort(ratios.begin(), ratios.end(), [](const clip_image_size & a, const clip_image_size & b) {
1115        return a.width * a.height < b.width * b.height;
1116    });
1117    return ratios;
1118}
1119
1120clip_image_size mtmd_image_preprocessor_deepseekocr::find_closest_aspect_ratio(
1121    float                                aspect_ratio,
1122    const std::vector<clip_image_size> & target_ratios,
1123    int                                  width,
1124    int                                  height) const {
1125    float           best_ratio_diff = std::numeric_limits<float>::max();
1126    clip_image_size best_ratio      = { 1, 1 };
1127    const float     area            = static_cast<float>(width * height);
1128
1129    for (const auto & ratio : target_ratios) {
1130        const float target_aspect_ratio = static_cast<float>(ratio.width) / ratio.height;
1131        const float ratio_diff          = std::abs(aspect_ratio - target_aspect_ratio);
1132        if (ratio_diff < best_ratio_diff) {
1133            best_ratio_diff = ratio_diff;
1134            best_ratio      = ratio;
1135        } else if (ratio_diff == best_ratio_diff) {
1136            const float target_area = static_cast<float>(tile_size * tile_size * ratio.width * ratio.height);
1137            if (area > 0.5f * target_area) {
1138                best_ratio = ratio;
1139            }
1140        }
1141    }
1142    return best_ratio;
1143}
1144
1145//
1146// DeepSeek-V4-Flash-Vision (deepseek4v)
1147//
1148// port of load_image / safe_resize / solve_resize_ratio / grid_tokens from inference/image_processor.py
1149// the resize solver picks the largest target size (multiple of patch_size) whose LLM token block fits max_n_token
1150//
1151
1152// ref: grid_tokens()
1153mtmd_image_preprocessor_deepseek4v::grid_info mtmd_image_preprocessor_deepseek4v::grid_tokens(int best_height, int best_width, int patch_size, int r) {
1154    grid_info g;
1155    g.n_llm_h = ((best_height / patch_size) + r - 1) / r;
1156    g.n_llm_w = ((best_width  / patch_size) + r - 1) / r;
1157    g.n_tokens = dsv4_get_block_layout(g.n_llm_w, g.n_llm_h, 0).n_out;
1158    return g;
1159}
1160
1161// ref: solve_resize_ratio()
1162void mtmd_image_preprocessor_deepseek4v::solve_resize_ratio(int height, int width, int p, int r, int max_n_token,
1163                                                            int & best_height, int & best_width) {
1164    const double ratio   = (double) height / width;
1165    const double max_w_f = std::sqrt((max_n_token - 2) / ratio + 0.25) - 0.5;
1166    const double max_h_f = max_w_f * ratio;
1167    if (max_w_f < 1.0) {
1168        const int max_w = 1;
1169        int max_h = (max_n_token - 2) / (max_w + 1);
1170        if (max_h % 2 == 1) {
1171            max_h -= 1;
1172        }
1173        best_width  = max_w * p * r;
1174        best_height = max_h * p * r;
1175    } else if (max_h_f < 2.0) {
1176        const int max_h = 2;
1177        // guard tiny budgets; cannot be hit with the current lower bound on max_n_token
1178        const int max_w = std::max(((max_n_token - 2) / max_h) - 1, 2);
1179        best_width  = max_w * p * r;
1180        best_height = max_h * p * r;
1181    } else {
1182        const int max_w_i = (int) std::floor(max_w_f);
1183        int max_h_i = (int) std::floor(max_h_f);
1184        if (max_h_i % 2 == 1) {
1185            max_h_i -= 1;
1186        }
1187        const double beta = std::min(
1188            (double) max_w_i * p * r / width,
1189            (double) max_h_i * p * r / height);
1190        best_width  = (int) std::floor(width  * beta / p) * p;
1191        best_height = (int) std::floor(height * beta / p) * p;
1192    }
1193}
1194
1195// ref: safe_resize()
1196void mtmd_image_preprocessor_deepseek4v::safe_resize(int height, int width, int & best_height, int & best_width,
1197                                                     int p, int r, int max_n_token) {
1198    max_n_token -= 4 - 1; // reserve room for the position-dependent lead pads (COMPRESS_PAD_TO - 1)
1199    grid_info g = grid_tokens(best_height, best_width, p, r);
1200    int budget = max_n_token;
1201    while (g.n_tokens > max_n_token) {
1202        solve_resize_ratio(height, width, p, r, budget, best_height, best_width);
1203        g = grid_tokens(best_height, best_width, p, r);
1204        budget -= 1;
1205    }
1206}
1207
1208// ref: load_image()
1209mtmd_image_preproc_out mtmd_image_preprocessor_deepseek4v::preprocess(const clip_image_u8 & img) const {
1210    mtmd_image_preproc_out out;
1211
1212    const int p           = hparams.patch_size;
1213    const int r           = hparams.n_merge;
1214    const int max_n_token = hparams.dsv4_max_n_token;
1215    const int max_wh      = hparams.dsv4_max_wh_ratio;
1216
1217    const clip_image_size orig = img.get_size();
1218    int width  = orig.width;
1219    int height = orig.height;
1220    if (max_wh > 0 && width > height * max_wh) {
1221        width = height * max_wh;
1222    }
1223    if (hparams.image_min_pixels > 0 && width * height > 0
1224            && width * height < hparams.image_min_pixels) {
1225        const double up = std::sqrt((double) hparams.image_min_pixels / ((double) width * height));
1226        width  = (int) (width  * up);
1227        height = (int) (height * up);
1228    }
1229    int best_width  = CLIP_ALIGN(width,  p);
1230    int best_height = CLIP_ALIGN(height, p);
1231    safe_resize(height, width, best_height, best_width, p, r, max_n_token);
1232
1233    clip_image_u8 resized;
1234    if (max_wh > 0 && orig.width >= max_wh * orig.height) {
1235        // extreme aspect ratio: plain stretch resize, no padding
1236        img_tool::resize(img, resized, {best_width, best_height}, hparams.image_resize_algo, PAD_NONE);
1237    } else {
1238        // aspect-preserving resize + centered padding (PIL ImageOps.pad)
1239        img_tool::resize(img, resized, {best_width, best_height}, hparams.image_resize_algo,
1240                         PAD_NEAREST, hparams.image_pad_color);
1241    }
1242
1243    out.append(hparams, resized);
1244    return out;
1245}
1246
1247mtmd_image_preproc_out mtmd_image_preprocessor_deepseekocr::preprocess(const clip_image_u8 & img) const {
1248    mtmd_image_preproc_out output;
1249    int grid_w = 0;
1250    int grid_h = 0;
1251    const auto img_size = img.get_size();
1252
1253    // global view: aspect-preserving fit-and-pad to base_size
1254    clip_image_u8 padded;
1255    img_tool::resize(img, padded,
1256                    { base_size, base_size },
1257                    RESIZE_ALGO_BICUBIC,
1258                    PAD_NEAREST,
1259                    hparams.image_pad_color);
1260    output.append_overview(hparams, padded, true);
1261    output.overview.add_viewsep = true;
1262
1263    // if this condition doesn't hold, the output is overview only, no tiles
1264    if (img_size.width > tile_size || img_size.height > tile_size) {
1265        const float           aspect_ratio  = static_cast<float>(img_size.width) / img_size.height;
1266        const auto            target_ratios = get_target_ratios();
1267        const clip_image_size grid =
1268            find_closest_aspect_ratio(aspect_ratio, target_ratios, img_size.width, img_size.height);
1269        grid_w = grid.width;
1270        grid_h = grid.height;
1271
1272        clip_image_u8 refined;
1273        img_tool::resize(img, refined, { tile_size * grid_w, tile_size * grid_h }, RESIZE_ALGO_BICUBIC,
1274                         PAD_NONE);
1275
1276        for (int row = 0; row < grid_h; row++) {
1277            if (fuse_row) {
1278                // concat all tiles in this row into a single image, along the H axis
1279                // output image size: w = tile_size, h = tile_size * grid_w
1280                // this is to ensure the whole row is always processed together
1281                clip_image_u8 row_img;
1282                row_img.set_size({tile_size, tile_size * grid_w}, false);
1283                for (int col = 0; col < grid_w; col++) {
1284                    for (int py = 0; py < tile_size; py++) {
1285                        for (int px = 0; px < tile_size; px++) {
1286                            row_img.set_pixel(px, col * tile_size + py,
1287                                              refined.get_pixel(col * tile_size + px, row * tile_size + py));
1288                        }
1289                    }
1290                }
1291                output.append(hparams, row_img, true);
1292            } else {
1293                for (int col = 0; col < grid_w; col++) {
1294                    clip_image_u8 tile;
1295                    img_tool::crop(refined, tile, col * tile_size, row * tile_size, tile_size, tile_size);
1296                    output.append(hparams, tile, true);
1297                }
1298            }
1299        }
1300        if (fuse_row) {
1301            grid_w = 1; // each fused row is one image; a single output column
1302        }
1303    }
1304
1305    LOG_DBG("%s: grid size: %d x %d (%d tiles) + global view\n", __func__, grid_w, grid_h, grid_w * grid_h);
1306    LOG_DBG("%s: overview size: %d x %d\n", __func__, padded.get_size().width, padded.get_size().height);
1307
1308    output.grid_x = grid_w;
1309    output.grid_y = grid_h;
1310    return output;
1311}
1312
1313//
1314// mtmd_image_preprocessor_step3vl
1315//
1316
1317void mtmd_image_preprocessor_step3vl::img_u8_resize_bilinear_to_f32(
1318        const clip_image_u8 & src,
1319        clip_image_f32 & dst,
1320        int target_width,
1321        int target_height,
1322        const float mean[3],
1323        const float std[3]) const {
1324    const auto src_size = src.get_size();
1325    if (src_size.width == target_width && src_size.height == target_height) {
1326        dst.from_u8(src);
1327        dst.normalize(mean, std);
1328        return;
1329    }
1330
1331    dst.set_size({target_width, target_height}, false, false);
1332
1333    if (src.is_placeholder()) {
1334        // no-op for placeholder image, just set the size and return
1335        return;
1336    }
1337
1338    const float scale_x = static_cast<float>(src_size.width)  / target_width;
1339    const float scale_y = static_cast<float>(src_size.height) / target_height;
1340
1341    std::vector<float> local_buf((size_t) 3 * (size_t) target_width * (size_t) target_height);
1342
1343    for (int y = 0; y < target_height; ++y) {
1344        const float src_y = (static_cast<float>(y) + 0.5f) * scale_y - 0.5f;
1345        const int y0_floor = static_cast<int>(std::floor(src_y));
1346        const int y0 = std::max(0, std::min(y0_floor,     src_size.height - 1));
1347        const int y1 = std::max(0, std::min(y0_floor + 1, src_size.height - 1));
1348        const float ly = src_y - y0_floor;
1349
1350        for (int x = 0; x < target_width; ++x) {
1351            const float src_x = (static_cast<float>(x) + 0.5f) * scale_x - 0.5f;
1352            const int x0_floor = static_cast<int>(std::floor(src_x));
1353            const int x0 = std::max(0, std::min(x0_floor,     src_size.width - 1));
1354            const int x1 = std::max(0, std::min(x0_floor + 1, src_size.width - 1));
1355            const float lx = src_x - x0_floor;
1356
1357            const auto p00 = src.get_pixel(x0, y0);
1358            const auto p01 = src.get_pixel(x1, y0);
1359            const auto p10 = src.get_pixel(x0, y1);
1360            const auto p11 = src.get_pixel(x1, y1);
1361
1362            const size_t idx_dst = (size_t) 3 * ((size_t) y * (size_t) target_width + (size_t) x);
1363            for (int c = 0; c < 3; ++c) {
1364                const float v00 = (static_cast<float>(p00[c]) / 255.0f - mean[c]) / std[c];
1365                const float v01 = (static_cast<float>(p01[c]) / 255.0f - mean[c]) / std[c];
1366                const float v10 = (static_cast<float>(p10[c]) / 255.0f - mean[c]) / std[c];
1367                const float v11 = (static_cast<float>(p11[c]) / 255.0f - mean[c]) / std[c];
1368
1369                const float top = v00 + (v01 - v00) * lx;
1370                const float bot = v10 + (v11 - v10) * lx;
1371                local_buf[idx_dst + c] = top + (bot - top) * ly;
1372            }
1373        }
1374    }
1375    dst.cpy_buf(local_buf);
1376}
1377
1378int mtmd_image_preprocessor_step3vl::get_image_longest_edge(const clip_hparams & params) {
1379    return params.image_longest_edge > 0 ? params.image_longest_edge : default_image_longest_edge;
1380}
1381
1382int mtmd_image_preprocessor_step3vl::determine_window_size(const clip_hparams & params, int longer, int shorter) {
1383    const int image_size = params.image_size;
1384    const int crop_size  = default_image_crop_size;
1385    const float aspect_ratio = static_cast<float>(longer) / shorter;
1386
1387    if (longer <= image_size) {
1388        return aspect_ratio > small_aspect_ratio_limit ? shorter : 0;
1389    }
1390
1391    return aspect_ratio > wide_aspect_ratio_limit ? std::min(shorter, crop_size) : crop_size;
1392}
1393
1394int mtmd_image_preprocessor_step3vl::calc_crop_extent(int length, int window_size) {
1395    const float ratio = static_cast<float>(length) / window_size;
1396    if (ratio < 1.0f) {
1397        return length;
1398    }
1399
1400    const float decimal = ratio - std::floor(ratio);
1401    const int rounded = decimal > crop_rounding_threshold
1402        ? static_cast<int>(std::floor(ratio)) + 1
1403        : static_cast<int>(std::floor(ratio));
1404    return window_size * rounded;
1405}
1406
1407std::vector<int> mtmd_image_preprocessor_step3vl::calc_grid(int length, int window_size) {
1408    const int n = length <= window_size
1409        ? 1
1410        : static_cast<int>(std::ceil(static_cast<float>(length - window_size) / window_size + 1.0f));
1411    std::vector<int> starts(n);
1412
1413    for (int i = 0; i < n; ++i) {
1414        starts[i] = window_size * i;
1415    }
1416
1417    if (n > 1 && starts.back() + window_size > length) {
1418        starts.back() = length - window_size;
1419    }
1420
1421    return starts;
1422}
1423
1424clip_image_u8 mtmd_image_preprocessor_step3vl::prepare_image(const clip_image_u8 & img, const clip_hparams & params) {
1425    clip_image_u8 resized = img;
1426    const auto img_size = img.get_size();
1427    const float aspect_ratio = img_size.height > 0 ? static_cast<float>(img_size.width) / img_size.height : 1.0f;
1428    if (std::min(img_size.width, img_size.height) < 32 &&
1429        (aspect_ratio > wide_aspect_ratio_limit ||
1430         aspect_ratio < 1.0f / wide_aspect_ratio_limit)) {
1431        const int square_size = std::max(img_size.width, img_size.height);
1432        clip_image_u8 padded;
1433        padded.set_size({square_size, square_size}, false);
1434        img_tool::fill(padded, {0, 0, 0});
1435        img_tool::composite(padded, img, 0, 0);
1436        resized = std::move(padded);
1437    }
1438
1439    const int max_image_size = get_image_longest_edge(params);
1440    const auto resized_size = resized.get_size();
1441    if (std::max(resized_size.width, resized_size.height) > max_image_size) {
1442        const float scale = static_cast<float>(max_image_size) / std::max(resized_size.width, resized_size.height);
1443        const clip_image_size new_size = {
1444            std::max(1, static_cast<int>(std::floor(resized_size.width  * scale))),
1445            std::max(1, static_cast<int>(std::floor(resized_size.height * scale))),
1446        };
1447        clip_image_u8 scaled;
1448        img_tool::resize(resized, scaled, new_size, RESIZE_ALGO_BILINEAR, PAD_NONE);
1449        resized = std::move(scaled);
1450    }
1451
1452    return resized;
1453}
1454
1455clip_image_u8 mtmd_image_preprocessor_step3vl::crop_with_black_padding(const clip_image_u8 & image, int x, int y, int w, int h) {
1456    clip_image_u8 dst;
1457    dst.set_size({w, h}, false);
1458    img_tool::fill(dst, {0, 0, 0});
1459
1460    const auto img_size = image.get_size();
1461    const int src_x0 = std::max(0, x);
1462    const int src_y0 = std::max(0, y);
1463    const int src_x1 = std::min(img_size.width,  x + w);
1464    const int src_y1 = std::min(img_size.height, y + h);
1465
1466    if (src_x0 >= src_x1 || src_y0 >= src_y1) {
1467        return dst;
1468    }
1469
1470    const int dst_x0 = src_x0 - x;
1471    const int dst_y0 = src_y0 - y;
1472
1473    for (int yy = 0; yy < src_y1 - src_y0; ++yy) {
1474        for (int xx = 0; xx < src_x1 - src_x0; ++xx) {
1475            dst.set_pixel(dst_x0 + xx, dst_y0 + yy, image.get_pixel(src_x0 + xx, src_y0 + yy));
1476        }
1477    }
1478
1479    return dst;
1480}
1481
1482mtmd_image_preprocessor_step3vl::slice_instructions mtmd_image_preprocessor_step3vl::build_slice_instructions(
1483        const clip_hparams & params,
1484        const clip_image_size & prepared_size) {
1485    slice_instructions instructions;
1486    instructions.overview_size = prepared_size;
1487
1488    const int window_size = determine_window_size(
1489        params,
1490        std::max(prepared_size.width, prepared_size.height),
1491        std::min(prepared_size.width, prepared_size.height));
1492    if (window_size <= 0) {
1493        instructions.refined_size = clip_image_size{0, 0};
1494        instructions.grid_size    = clip_image_size{0, 0};
1495        return instructions;
1496    }
1497
1498    const int crop_width  = calc_crop_extent(prepared_size.width,  window_size);
1499    const int crop_height = calc_crop_extent(prepared_size.height, window_size);
1500    instructions.refined_size = clip_image_size{crop_width, crop_height};
1501
1502    const auto xs = calc_grid(crop_width,  window_size);
1503    const auto ys = calc_grid(crop_height, window_size);
1504    instructions.grid_size = clip_image_size{
1505        static_cast<int>(xs.size()),
1506        static_cast<int>(ys.size()),
1507    };
1508
1509    for (int y : ys) {
1510        for (int x : xs) {
1511            instructions.slices.push_back(slice_coordinates{
1512                /* x    */ x,
1513                /* y    */ y,
1514                /* size */ clip_image_size{window_size, window_size},
1515            });
1516        }
1517    }
1518
1519    return instructions;
1520}
1521
1522mtmd_image_preproc_out mtmd_image_preprocessor_step3vl::preprocess(const clip_image_u8 & img) const {
1523    clip_image_u8 prepared = prepare_image(img, hparams);
1524    const auto instructions = build_slice_instructions(hparams, prepared.get_size());
1525
1526    mtmd_image_preproc_out output;
1527    // overview (normalized f32, already includes mean/std)
1528    img_u8_resize_bilinear_to_f32(
1529        prepared,
1530        output.overview,
1531        hparams.image_size,
1532        hparams.image_size,
1533        hparams.image_mean,
1534        hparams.image_std);
1535
1536    if (instructions.slices.empty()) {
1537        output.grid_x = 0;
1538        output.grid_y = 0;
1539        return output;
1540    }
1541
1542    clip_image_u8 img_for_crop = prepared;
1543    const auto prepared_size = prepared.get_size();
1544    if (instructions.refined_size.width != prepared_size.width || instructions.refined_size.height != prepared_size.height) {
1545        clip_image_u8 refined;
1546        img_tool::resize(prepared, refined, instructions.refined_size, RESIZE_ALGO_BILINEAR, PAD_NONE);
1547        img_for_crop = std::move(refined);
1548    }
1549
1550    const int crop_size = default_image_crop_size;
1551    for (const auto & slice : instructions.slices) {
1552        // If the requested patch extends past the source image, pad the out-of-bounds area with black.
1553        clip_image_u8 patch = crop_with_black_padding(img_for_crop, slice.x, slice.y, slice.size.width, slice.size.height);
1554
1555        clip_image_f32 patch_f32;
1556        img_u8_resize_bilinear_to_f32(
1557            patch,
1558            patch_f32,
1559            crop_size,
1560            crop_size,
1561            hparams.image_mean,
1562            hparams.image_std);
1563        output.append(hparams, patch_f32, false);
1564    }
1565
1566    output.grid_x = instructions.grid_size.width;
1567    output.grid_y = instructions.grid_size.height;
1568
1569    return output;
1570}
1571
1572//
1573// mtmd_image_preprocessor_youtuvl
1574//
1575
1576mtmd_image_preproc_out mtmd_image_preprocessor_youtuvl::preprocess(const clip_image_u8 & img) const {
1577    const int patch_size = hparams.patch_size;   // typically 16
1578    const int merge_size = hparams.n_merge;      // typically 2
1579    const int align_size = patch_size * merge_size;  // 32
1580
1581    const int max_num_patches = hparams.image_max_pixels > 0 ?
1582        hparams.image_max_pixels / (patch_size * patch_size) : 256;
1583
1584    // Linear search for optimal scale to fit within max_num_patches
1585    const auto img_size = img.get_size();
1586    float scale = 1.0f;
1587    int target_height = img_size.height;
1588    int target_width  = img_size.width;
1589
1590    auto get_scaled_image_size = [align_size](float scale, int size) -> int {
1591        float scaled_size = size * scale;
1592        // Round up to nearest multiple of align_size
1593        int aligned = static_cast<int>(std::ceil(scaled_size / align_size)) * align_size;
1594        // Ensure at least one patch
1595        return std::max(align_size, aligned);
1596    };
1597
1598    // Linear search with 0.02 step size
1599    while (scale > 0.0f) {
1600        target_height = get_scaled_image_size(scale, img_size.height);
1601        target_width  = get_scaled_image_size(scale, img_size.width);
1602
1603        int num_patches_h = target_height / patch_size;
1604        int num_patches_w = target_width / patch_size;
1605        int num_patches = num_patches_h * num_patches_w;
1606
1607        if (num_patches > max_num_patches) {
1608            scale -= 0.02f;
1609        } else {
1610            break;
1611        }
1612    }
1613
1614    clip_image_size new_size = {target_width, target_height};
1615
1616    // Resize the image
1617    clip_image_u8 resized;
1618    img_tool::resize(img, resized, new_size, hparams.image_resize_algo, hparams.image_resize_pad);
1619
1620    mtmd_image_preproc_out output;
1621    output.append(hparams, resized, true);
1622    return output;
1623}
1624
1625mtmd_image_preproc_out mtmd_image_preprocessor_granite::preprocess(const clip_image_u8 & img) const {
1626    GGML_ASSERT(!hparams.image_res_candidates.empty());
1627
1628    const clip_image_size orig_size = img.get_size();
1629    const int             tile_size = hparams.image_size;
1630    GGML_ASSERT(tile_size > 0);
1631
1632    // llava-next always encodes an overview plus a grid of tiles, even for small images
1633    const clip_image_size refined_size = select_best_resolution(orig_size, hparams.image_res_candidates);
1634    const int             grid_x       = refined_size.width  / tile_size;
1635    const int             grid_y       = refined_size.height / tile_size;
1636
1637    // the tiles are stacked on the Y axis, a big grid overflows the stacked image height
1638    GGML_ASSERT(grid_x >= 0 && grid_x <= 1024 && grid_y >= 0 && grid_y <= 1024);
1639
1640    clip_image_u8 overview;
1641    img_tool::resize(img, overview, {tile_size, tile_size}, hparams.image_resize_algo_ov,
1642                        hparams.image_pad_ov, hparams.image_pad_color_ov);
1643
1644    clip_image_u8 refined;
1645    img_tool::resize(img, refined, refined_size, hparams.image_resize_algo_rf,
1646                        hparams.image_pad_rf, hparams.image_pad_color_rf);
1647
1648    // stack the overview and the tiles on the Y axis, so the whole grid goes through one graph
1649    clip_image_u8 stacked;
1650    stacked.set_size({tile_size, tile_size * (1 + grid_x * grid_y)}, false);
1651    auto copy_tile = [&](const clip_image_u8 & src, int src_x, int src_y, int dst_idx) {
1652        for (int py = 0; py < tile_size; py++) {
1653            for (int px = 0; px < tile_size; px++) {
1654                stacked.set_pixel(px, dst_idx * tile_size + py, src.get_pixel(src_x + px, src_y + py));
1655            }
1656        }
1657    };
1658    copy_tile(overview, 0, 0, 0);
1659    for (int ty = 0; ty < grid_y; ty++) {
1660        for (int tx = 0; tx < grid_x; tx++) {
1661            copy_tile(refined, tx * tile_size, ty * tile_size, 1 + ty * grid_x + tx);
1662        }
1663    }
1664
1665    LOG_DBG("%s: grid size: %d x %d (%d tiles) + overview\n", __func__, grid_x, grid_y, grid_x * grid_y);
1666
1667    mtmd_image_preproc_out output;
1668    output.append(hparams, stacked, true);
1669    auto & entry = output.entries.back();
1670    entry.anyres.grid_x  = grid_x;
1671    entry.anyres.grid_y  = grid_y;
1672    entry.anyres.orig_nx = orig_size.width;
1673    entry.anyres.orig_ny = orig_size.height;
1674    return output;
1675}
1676
1677//
1678// mtmd_image_preprocessor_muse_glimmer
1679//
1680
1681// Replicates transformers' get_aspect_ratio_preserving_size
1682static clip_image_size muse_glimmer_grid_size(int img_w, int img_h, int patch_hw, int max_tokens) {
1683    double i_nph = (double) img_h / patch_hw;
1684    double i_npw = (double) img_w / patch_hw;
1685    const double ratio = i_nph > 0.0 ? i_npw / i_nph : 1.0;
1686    if (i_nph * i_npw > (double) max_tokens) {
1687        i_nph = std::sqrt((double) max_tokens / ratio);
1688        i_npw = i_nph * ratio;
1689    }
1690    const int hs[2] = { (int) std::floor(i_nph), (int) std::ceil(i_nph) };
1691    const int ws[2] = { (int) std::floor(i_npw), (int) std::ceil(i_npw) };
1692    const double target_ar = (double) img_h / (double) img_w;
1693    int    best_nph = -1;
1694    int    best_npw = -1;
1695    double best_d   = 0.0;
1696    for (int a = 0; a < 2; ++a) {
1697        for (int b = 0; b < 2; ++b) {
1698            const int nph = hs[a];
1699            const int npw = ws[b];
1700            if (nph < 1 || npw < 1 || nph * npw > max_tokens) {
1701                continue;
1702            }
1703            const double d = std::fabs((double) nph / (double) npw - target_ar);
1704            const int n_tokens      = nph * npw;
1705            const int best_n_tokens = best_nph * best_npw;
1706            if (best_nph < 0 || d < best_d || (d == best_d && n_tokens > best_n_tokens)) {
1707                best_nph = nph;
1708                best_npw = npw;
1709                best_d   = d;
1710            }
1711        }
1712    }
1713    if (best_nph < 0) { // no candidate fit under the cap: round and clamp
1714        best_nph = std::max(1, (int) std::lround(i_nph));
1715        best_npw = std::max(1, (int) std::lround(i_npw));
1716    }
1717    return clip_image_size{ best_npw * patch_hw, best_nph * patch_hw };
1718}
1719
1720mtmd_image_preproc_out mtmd_image_preprocessor_muse_glimmer::preprocess(const clip_image_u8 & img) const {
1721    const int patch_hw   = hparams.patch_size * hparams.n_merge;
1722    const int patch_area = hparams.patch_size * hparams.patch_size * hparams.n_merge * hparams.n_merge;
1723    GGML_ASSERT(patch_area > 0 && hparams.image_max_pixels > 0);
1724    const int max_tokens = hparams.image_max_pixels / patch_area;
1725
1726    const clip_image_size original_size = img.get_size();
1727    const clip_image_size target_size   = muse_glimmer_grid_size(
1728        original_size.width, original_size.height, patch_hw, max_tokens);
1729
1730    // PIL resizes directly to (target_w, target_h) -- a stretch, no padding.
1731    clip_image_u8 resized_image;
1732    img_tool::resize(img, resized_image, target_size, hparams.image_resize_algo, PAD_NONE);
1733
1734    mtmd_image_preproc_out output;
1735    output.append(hparams, resized_image, true);
1736    return output;
1737}