• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

saitoha / libsixel / 19918707358

04 Dec 2025 05:12AM UTC coverage: 38.402% (-4.0%) from 42.395%
19918707358

push

github

saitoha
tests: fix meson msys dll lookup

9738 of 38220 branches covered (25.48%)

12841 of 33438 relevant lines covered (38.4%)

782420.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.67
/src/assessment.c
1
/*
2
 * SPDX-License-Identifier: MIT
3
 *
4
 * Copyright (c) 2025 libsixel developers. See `AUTHORS`.
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
7
 * this software and associated documentation files (the "Software"), to deal in
8
 * the Software without restriction, including without limitation the rights to
9
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10
 * the Software, and to permit persons to whom the Software is furnished to do so,
11
 * subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in all
14
 * copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 */
23

24
/*
25
 * assessment.c - High-speed image quality evaluator ported from Python.
26
 *
27
 *  +-------------------------------------------------------------+
28
 *  |                        PIPELINE MAP                         |
29
 *  +---------------------+------------------+--------------------+
30
 *  | image loading (RGB) | metric kernels   | JSON emit          |
31
 *  |  (libsixel loader)  |  (MS-SSIM, etc.) | (stdout + files)   |
32
 *  +---------------------+------------------+--------------------+
33
 *
34
 *  ASCII flow for the metrics modules:
35
 *
36
 *        +-----------+      +---------------+      +-------------+
37
 *        |   Luma    | ---> |  Spectral +   | ---> |  Composite  |
38
 *        |  Stack    |      |  Spatial      |      |   Report    |
39
 *        +-----------+      +---------------+      +-------------+
40
 *              |                    |                      |
41
 *              |                    |                      +--> JSON writer
42
 *              |                    +--> FFT / histogram engines
43
 *              +--> MS-SSIM / PSNR / GMSD
44
 *
45
 *  Every function carries intentionally verbose comments so that future
46
 *  maintainers can follow the numerical steps without cross-referencing the
47
 *  removed Python original.
48
 */
49

50
#define _POSIX_C_SOURCE 200809L
51
#define _XOPEN_SOURCE 700
52

53
#include "config.h"
54

55
#include <ctype.h>
56
#include <errno.h>
57
#include <limits.h>
58
#include <math.h>
59
#include <setjmp.h>
60
#include <stdbool.h>
61
#include <stdarg.h>
62
#include <stdint.h>
63
#include <stdio.h>
64
#include <stdlib.h>
65
#include <string.h>
66

67
#include <sixel.h>
68

69
#include "assessment.h"
70
#include "encoder.h"
71
#include "compat_stub.h"
72

73
#if defined(_WIN32)
74
#include <io.h>
75
#include <windows.h>
76
#else
77
#include <unistd.h>
78
#endif
79

80
#if defined(__APPLE__)
81
#include <mach-o/dyld.h>
82
#endif
83

84
#if defined(__linux__)
85
#include <sys/stat.h>
86
#include <sys/types.h>
87
#endif
88

89
#if defined(HAVE_ONNXRUNTIME)
90
#include "onnxruntime_c_api.h"
91
#endif
92

93
#ifndef SIXEL_MODEL_DIR
94
#define SIXEL_MODEL_DIR ""
95
#endif
96

97
#if !defined(PATH_MAX)
98
#define PATH_MAX 4096
99
#endif
100

101
#if defined(_WIN32)
102
#define SIXEL_PATH_SEP '\\'
103
#define SIXEL_PATH_LIST_SEP ';'
104
#else
105
#define SIXEL_PATH_SEP '/'
106
#define SIXEL_PATH_LIST_SEP ':'
107
#endif
108

109
#define SIXEL_LOCAL_MODELS_SEG1 ".."
110
#define SIXEL_LOCAL_MODELS_SEG2 "models"
111
#define SIXEL_LOCAL_MODELS_SEG3 "lpips"
112

113
#ifndef M_PI
114
#define M_PI 3.14159265358979323846
115
#endif
116

117
enum { SIXEL_ASSESSMENT_RGB_CHANNELS = 3 };
118

119
typedef struct sixel_assessment_float_buffer {
120
    size_t length;
121
    float *values;
122
} sixel_assessment_float_buffer_t;
123

124
typedef struct sixel_assessment_complex {
125
    double re;
126
    double im;
127
} sixel_assessment_complex_t;
128

129
typedef struct sixel_assessment_metrics {
130
    float ms_ssim;
131
    float high_freq_out;
132
    float high_freq_ref;
133
    float high_freq_delta;
134
    float stripe_ref;
135
    float stripe_out;
136
    float stripe_rel;
137
    float band_run_rel;
138
    float band_grad_rel;
139
    float clip_l_ref;
140
    float clip_r_ref;
141
    float clip_g_ref;
142
    float clip_b_ref;
143
    float clip_l_out;
144
    float clip_r_out;
145
    float clip_g_out;
146
    float clip_b_out;
147
    float clip_l_rel;
148
    float clip_r_rel;
149
    float clip_g_rel;
150
    float clip_b_rel;
151
    float delta_chroma_mean;
152
    float delta_e00_mean;
153
    float gmsd_value;
154
    float psnr_y;
155
    float lpips_alex;
156
} sixel_assessment_metrics_t;
157

158
typedef struct sixel_assessment_capture {
159
    sixel_frame_t *frame;
160
} sixel_assessment_capture_t;
161

162
static sixel_assessment_t *g_assessment_context = NULL;
163
static sixel_assessment_t *g_active_encode_assessment = NULL;
164

165
typedef struct assessment_stage_descriptor {
166
    sixel_assessment_stage_t id;
167
    char const *label;
168
} assessment_stage_descriptor_t;
169

170
static assessment_stage_descriptor_t const g_stage_descriptors[] = {
171
    {SIXEL_ASSESSMENT_STAGE_IMAGE_CHUNK, "ImageRead"},
172
    {SIXEL_ASSESSMENT_STAGE_IMAGE_DECODE, "ImageDecode"},
173
    {SIXEL_ASSESSMENT_STAGE_SCALE, "Scale"},
174
    {SIXEL_ASSESSMENT_STAGE_CROP, "Crop"},
175
    {SIXEL_ASSESSMENT_STAGE_COLORSPACE, "ColorConvert"},
176
    {SIXEL_ASSESSMENT_STAGE_PALETTE_HISTOGRAM, "PaletteHistogram"},
177
    {SIXEL_ASSESSMENT_STAGE_PALETTE_SOLVE, "PaletteSolve"},
178
    {SIXEL_ASSESSMENT_STAGE_PALETTE_APPLY, "PaletteApply"},
179
    {SIXEL_ASSESSMENT_STAGE_ENCODE, "Encode"},
180
    {SIXEL_ASSESSMENT_STAGE_ENCODE_PREPARE, "EncodePrepare"},
181
    {SIXEL_ASSESSMENT_STAGE_ENCODE_CLASSIFY, "EncodeClassify"},
182
    {SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE, "EncodeCompose"},
183
    {SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE_SCAN, "EncodeComposeScan"},
184
    {SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE_QUEUE, "EncodeComposeQueue"},
185
    {SIXEL_ASSESSMENT_STAGE_ENCODE_EMIT, "EncodeEmit"},
186
    {SIXEL_ASSESSMENT_STAGE_OUTPUT, "Output"}
187
};
188

189
/*
190
 * Only top-level stages contribute to the total so nested probes do not
191
 * inflate the reported wall time by double counting their parents.
192
 */
193
static unsigned char const g_stage_counts_toward_total[] = {
194
    1, /* ImageRead */
195
    1, /* ImageDecode */
196
    1, /* Scale */
197
    1, /* Crop */
198
    1, /* ColorConvert */
199
    1, /* PaletteHistogram */
200
    1, /* PaletteSolve */
201
    1, /* PaletteApply */
202
    1, /* Encode */
203
    0, /* EncodePrepare */
204
    0, /* EncodeClassify */
205
    0, /* EncodeCompose */
206
    0, /* EncodeComposeScan */
207
    0, /* EncodeComposeQueue */
208
    0, /* EncodeEmit */
209
    1  /* Output */
210
};
211

212
static int g_encode_parallel_threads = 1;
213
static int g_encode_substages_visible = 1;
214

215
static int
216
sixel_assessment_stage_is_encode_detail(sixel_assessment_stage_t stage)
×
217
{
218
    switch (stage) {
×
219
    case SIXEL_ASSESSMENT_STAGE_ENCODE_PREPARE:
220
    case SIXEL_ASSESSMENT_STAGE_ENCODE_CLASSIFY:
221
    case SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE:
222
    case SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE_SCAN:
223
    case SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE_QUEUE:
224
    case SIXEL_ASSESSMENT_STAGE_ENCODE_EMIT:
225
        return 1;
226
    default:
227
        break;
228
    }
229
    return 0;
230
}
231

232
static int
233
sixel_assessment_stage_should_emit(int stage_index)
×
234
{
235
    sixel_assessment_stage_t stage;
×
236

237
    stage = g_stage_descriptors[stage_index].id;
×
238
    if (!g_encode_substages_visible &&
×
239
            sixel_assessment_stage_is_encode_detail(stage)) {
×
240
        return 0;
×
241
    }
242
    return 1;
243
}
244

245
static void
246
assessment_reset_stage_bookkeeping(sixel_assessment_t *assessment)
3✔
247
{
248
    if (assessment == NULL) {
3!
249
        return;
250
    }
251
    assessment->active_stage = SIXEL_ASSESSMENT_STAGE_NONE;
3✔
252
    assessment->stage_started_at = 0.0;
3✔
253
    assessment->stage_active = 0;
3✔
254
    memset(assessment->stage_durations, 0,
3✔
255
           sizeof(assessment->stage_durations));
256
    memset(assessment->stage_bytes, 0, sizeof(assessment->stage_bytes));
3✔
257
    assessment->output_bytes_written = 0u;
3✔
258
    assessment->encode_output_time_pending = 0.0;
3✔
259
}
260

261
static void
262
assessment_guess_format(sixel_assessment_t *assessment)
6✔
263
{
264
    char const *loader;
6✔
265
    char const *extension;
6✔
266
    size_t len;
6✔
267
    size_t index;
6✔
268

269
    if (assessment == NULL) {
6!
270
        return;
271
    }
272
    if (assessment->format_name[0] != '\0') {
6✔
273
        return;
274
    }
275
    loader = assessment->loader_name;
3✔
276
    if (loader[0] != '\0') {
3!
277
        if (strcmp(loader, "libpng") == 0) {
3!
278
            (void)sixel_compat_strcpy(assessment->format_name,
×
279
                                      sizeof(assessment->format_name),
280
                                      "png");
281
            return;
×
282
        } else if (strcmp(loader, "libjpeg") == 0) {
3!
283
            (void)sixel_compat_strcpy(assessment->format_name,
×
284
                                      sizeof(assessment->format_name),
285
                                      "jpeg");
286
            return;
×
287
        } else if (strcmp(loader, "wic") == 0) {
3!
288
            (void)sixel_compat_strcpy(assessment->format_name,
×
289
                                      sizeof(assessment->format_name),
290
                                      "wic");
291
            return;
×
292
        } else if (strcmp(loader, "builtin") == 0) {
3!
293
            (void)sixel_compat_strcpy(assessment->format_name,
3✔
294
                                      sizeof(assessment->format_name),
295
                                      "builtin");
296
            return;
3✔
297
        }
298
    }
299
    extension = strrchr(assessment->input_path, '.');
×
300
    if (extension != NULL && extension[1] != '\0') {
×
301
        len = strlen(extension + 1);
×
302
        if (len >= sizeof(assessment->format_name)) {
×
303
            len = sizeof(assessment->format_name) - 1;
304
        }
305
        for (index = 0; index < len; ++index) {
×
306
            assessment->format_name[index] =
×
307
                (char)tolower((unsigned char)extension[1 + index]);
×
308
        }
309
        assessment->format_name[len] = '\0';
×
310
    } else {
311
        (void)sixel_compat_strcpy(assessment->format_name,
×
312
                                  sizeof(assessment->format_name),
313
                                  "unknown");
314
    }
315
}
1!
316

317
static int
318
assessment_escape_json(char const *input,
9✔
319
                       char *output,
320
                       size_t output_size)
321
{
322
    size_t index;
9✔
323
    size_t written;
9✔
324
    unsigned char ch;
9✔
325
    int n;
9✔
326

327
    if (output == NULL || output_size == 0) {
9!
328
        return -1;
329
    }
330
    written = 0;
9✔
331
    if (input == NULL) {
9!
332
        output[0] = '\0';
×
333
        return 0;
×
334
    }
335
    for (index = 0; input[index] != '\0'; ++index) {
207✔
336
        ch = (unsigned char)input[index];
198✔
337
        if (ch == '"' || ch == '\\') {
198!
338
            if (written + 2 >= output_size) {
×
339
                return -1;
340
            }
341
            output[written++] = '\\';
×
342
            output[written++] = (char)ch;
×
343
        } else if (ch <= 0x1F) {
198!
344
            if (written + 6 >= output_size) {
×
345
                return -1;
346
            }
347
            n = snprintf(output + written, output_size - written,
×
348
                         "\\u%04x", ch);
349
            if (n < 0 || (size_t)n >= output_size - written) {
×
350
                return -1;
351
            }
352
            written += (size_t)n;
×
353
        } else {
354
            if (written + 1 >= output_size) {
198!
355
                return -1;
356
            }
357
            output[written++] = (char)ch;
198✔
358
        }
359
    }
360
    if (written >= output_size) {
9!
361
        return -1;
362
    }
363
    output[written] = '\0';
9✔
364
    return 0;
9✔
365
}
366

367
static char const *
368
assessment_pixelformat_name(int pixelformat)
3✔
369
{
370
    switch (pixelformat) {
3!
371
    case SIXEL_PIXELFORMAT_PAL1:
372
        return "PAL1";
373
    case SIXEL_PIXELFORMAT_PAL2:
×
374
        return "PAL2";
×
375
    case SIXEL_PIXELFORMAT_PAL4:
×
376
        return "PAL4";
×
377
    case SIXEL_PIXELFORMAT_PAL8:
×
378
        return "PAL8";
×
379
    case SIXEL_PIXELFORMAT_RGB555:
×
380
        return "RGB555";
×
381
    case SIXEL_PIXELFORMAT_RGB565:
×
382
        return "RGB565";
×
383
    case SIXEL_PIXELFORMAT_RGB888:
3✔
384
        return "RGB888";
3✔
385
    case SIXEL_PIXELFORMAT_BGR555:
×
386
        return "BGR555";
×
387
    case SIXEL_PIXELFORMAT_BGR565:
×
388
        return "BGR565";
×
389
    case SIXEL_PIXELFORMAT_BGR888:
×
390
        return "BGR888";
×
391
    case SIXEL_PIXELFORMAT_G1:
×
392
        return "G1";
×
393
    case SIXEL_PIXELFORMAT_G2:
×
394
        return "G2";
×
395
    case SIXEL_PIXELFORMAT_G4:
×
396
        return "G4";
×
397
    case SIXEL_PIXELFORMAT_G8:
×
398
        return "G8";
×
399
    default:
400
        break;
×
401
    }
402
    return "unknown";
×
403
}
404

405
static char const *
406
assessment_colorspace_name(int colorspace)
3✔
407
{
408
    switch (colorspace) {
3!
409
    case SIXEL_COLORSPACE_GAMMA:
410
        return "gamma";
411
    case SIXEL_COLORSPACE_LINEAR:
×
412
        return "linear";
×
413
    case SIXEL_COLORSPACE_SMPTEC:
×
414
        return "smpte-c";
×
415
    default:
416
        break;
×
417
    }
418
    return "unknown";
×
419
}
420

421
SIXELAPI void
422
sixel_assessment_stage_transition(sixel_assessment_t *assessment,
27✔
423
                                  sixel_assessment_stage_t stage)
424
{
425
    double now;
27✔
426
    double elapsed;
27✔
427
    sixel_assessment_stage_t previous_stage;
27✔
428
    double total_pending;
27✔
429

430
    if (assessment == NULL) {
27!
431
        return;
432
    }
433
    if ((assessment->sections_mask &
27!
434
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
435
        return;
436
    }
437
    if (stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
438
            stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
439
        return;
440
    }
441
    now = sixel_assessment_timer_now();
×
442
    previous_stage = assessment->active_stage;
×
443
    if (assessment->stage_active &&
×
444
            previous_stage > SIXEL_ASSESSMENT_STAGE_NONE &&
×
445
            previous_stage < SIXEL_ASSESSMENT_STAGE_COUNT) {
446
        elapsed = now - assessment->stage_started_at;
×
447
        if (previous_stage != SIXEL_ASSESSMENT_STAGE_OUTPUT) {
×
448
            /* Output spans rely on explicit fn_write() timing. */
449
            assessment->stage_durations[previous_stage] += elapsed;
×
450
        }
451
        if (previous_stage == SIXEL_ASSESSMENT_STAGE_ENCODE &&
×
452
                (assessment->encode_output_time_pending > 0.0 ||
×
453
                 assessment->encode_palette_time_pending > 0.0)) {
×
454
            /*
455
             * Rebalance the encode slot so that we only keep the pure
456
             * encoder core time.  The write spans move to the output
457
             * bucket, and the palette spans return to the palette
458
             * bucket.  The ASCII map shows how the same wall-clock
459
             * region is split across the stages.
460
             *
461
             *     encode wall time
462
             *     +----------+-----------+------------------+
463
             *     | palette  | encode    | write callback ->|
464
             *     | (return) | core stay | (output bucket) |
465
             *     +----------+-----------+------------------+
466
             */
467
            total_pending = assessment->encode_output_time_pending +
×
468
                assessment->encode_palette_time_pending;
×
469
            assessment->stage_durations[previous_stage] -= total_pending;
×
470
            if (assessment->stage_durations[previous_stage] < 0.0) {
×
471
                assessment->stage_durations[previous_stage] = 0.0;
×
472
            }
473
            assessment->encode_output_time_pending = 0.0;
×
474
            assessment->encode_palette_time_pending = 0.0;
×
475
            if (g_active_encode_assessment == assessment) {
×
476
                g_active_encode_assessment = NULL;
×
477
            }
478
        }
479
    }
480
    assessment->active_stage = stage;
×
481
    assessment->stage_started_at = now;
×
482
    assessment->stage_active = 1;
×
483
    if (stage == SIXEL_ASSESSMENT_STAGE_ENCODE) {
×
484
        assessment->encode_output_time_pending = 0.0;
×
485
        assessment->encode_palette_time_pending = 0.0;
×
486
        g_active_encode_assessment = assessment;
×
487
    } else if (g_active_encode_assessment == assessment) {
×
488
        g_active_encode_assessment = NULL;
×
489
    }
490
}
1!
491

492
SIXELAPI void
493
sixel_assessment_stage_finish(sixel_assessment_t *assessment)
6✔
494
{
495
    double now;
6✔
496
    double elapsed;
6✔
497
    sixel_assessment_stage_t finished_stage;
6✔
498
    double total_pending;
6✔
499

500
    if (assessment == NULL) {
6!
501
        return;
502
    }
503
    if ((assessment->sections_mask &
6!
504
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
505
        assessment->stage_active = 0;
6✔
506
        assessment->active_stage = SIXEL_ASSESSMENT_STAGE_NONE;
6✔
507
        assessment->stage_started_at = 0.0;
6✔
508
        assessment->encode_output_time_pending = 0.0;
6✔
509
        if (g_active_encode_assessment == assessment) {
6!
510
            g_active_encode_assessment = NULL;
×
511
        }
512
        return;
6✔
513
    }
514
    if (!assessment->stage_active ||
×
515
            assessment->active_stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
516
            assessment->active_stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
517
        assessment->stage_active = 0;
×
518
        assessment->active_stage = SIXEL_ASSESSMENT_STAGE_NONE;
×
519
        assessment->stage_started_at = 0.0;
×
520
        assessment->encode_output_time_pending = 0.0;
×
521
        return;
×
522
    }
523
    now = sixel_assessment_timer_now();
×
524
    elapsed = now - assessment->stage_started_at;
×
525
    finished_stage = assessment->active_stage;
×
526
    if (finished_stage != SIXEL_ASSESSMENT_STAGE_OUTPUT) {
×
527
        /* Output spans rely on explicit fn_write() timing. */
528
        assessment->stage_durations[finished_stage] += elapsed;
×
529
    }
530
    if (finished_stage == SIXEL_ASSESSMENT_STAGE_ENCODE &&
×
531
            (assessment->encode_output_time_pending > 0.0 ||
×
532
             assessment->encode_palette_time_pending > 0.0)) {
×
533
        /* Mirror the transition logic for the final leg. */
534
        total_pending = assessment->encode_output_time_pending +
×
535
            assessment->encode_palette_time_pending;
×
536
        assessment->stage_durations[finished_stage] -= total_pending;
×
537
        if (assessment->stage_durations[finished_stage] < 0.0) {
×
538
            assessment->stage_durations[finished_stage] = 0.0;
×
539
        }
540
        assessment->encode_output_time_pending = 0.0;
×
541
        assessment->encode_palette_time_pending = 0.0;
×
542
        if (g_active_encode_assessment == assessment) {
×
543
            g_active_encode_assessment = NULL;
×
544
        }
545
    }
546
    assessment->stage_active = 0;
×
547
    assessment->active_stage = SIXEL_ASSESSMENT_STAGE_NONE;
×
548
    assessment->stage_started_at = 0.0;
×
549
}
1!
550

551
SIXELAPI void
552
sixel_assessment_record_stage_duration(sixel_assessment_t *assessment,
×
553
                                       sixel_assessment_stage_t stage,
554
                                       double duration)
555
{
556
    if (assessment == NULL) {
×
557
        return;
558
    }
559
    if ((assessment->sections_mask &
×
560
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
561
        return;
562
    }
563
    if (stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
564
            stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
565
        return;
566
    }
567
    if (duration < 0.0) {
×
568
        duration = 0.0;
569
    }
570
    assessment->stage_durations[stage] += duration;
×
571
}
572

573
SIXELAPI void
574
sixel_assessment_record_loader(sixel_assessment_t *assessment,
3✔
575
                               char const *path,
576
                               char const *loader_name,
577
                               size_t input_bytes)
578
{
579
    unsigned int mask;
3✔
580

581
    if (assessment == NULL) {
3!
582
        return;
583
    }
584
    mask = assessment->sections_mask;
3✔
585
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
3!
586
                 SIXEL_ASSESSMENT_SECTION_PERFORMANCE)) == 0u) {
587
        return;
588
    }
589
    if ((mask & SIXEL_ASSESSMENT_SECTION_BASIC) != 0u) {
3!
590
        if (path != NULL) {
3!
591
            (void)snprintf(assessment->input_path,
3✔
592
                           sizeof(assessment->input_path),
593
                           "%s",
594
                           path);
595
        } else {
596
            assessment->input_path[0] = '\0';
×
597
        }
598
        if (loader_name != NULL) {
3!
599
            (void)snprintf(assessment->loader_name,
3✔
600
                           sizeof(assessment->loader_name),
601
                           "%s",
602
                           loader_name);
603
        } else {
604
            assessment->loader_name[0] = '\0';
×
605
        }
606
        assessment->input_bytes = input_bytes;
3✔
607
        assessment_guess_format(assessment);
3✔
608
    }
609
    if ((mask & SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
3!
610
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_IMAGE_CHUNK] +=
×
611
            input_bytes;
612
    }
613
}
1!
614

615
SIXELAPI void
616
sixel_assessment_record_source_frame(sixel_assessment_t *assessment,
3✔
617
                                     sixel_frame_t *frame)
618
{
619
    int width;
3✔
620
    int height;
3✔
621
    int pixelformat;
3✔
622
    int colorspace;
3✔
623
    int depth;
3✔
624
    size_t bytes;
3✔
625
    unsigned int mask;
3✔
626

627
    if (assessment == NULL || frame == NULL) {
3!
628
        return;
629
    }
630
    mask = assessment->sections_mask;
3✔
631
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
3!
632
                 SIXEL_ASSESSMENT_SECTION_SIZE |
633
                 SIXEL_ASSESSMENT_SECTION_PERFORMANCE)) == 0u) {
634
        return;
635
    }
636
    width = sixel_frame_get_width(frame);
3✔
637
    height = sixel_frame_get_height(frame);
3✔
638
    pixelformat = sixel_frame_get_pixelformat(frame);
3✔
639
    colorspace = sixel_frame_get_colorspace(frame);
3✔
640
    depth = sixel_helper_compute_depth(pixelformat);
3✔
641
    if (depth <= 0 || width <= 0 || height <= 0) {
3!
642
        bytes = 0u;
643
    } else {
644
        bytes = (size_t)width;
3✔
645
        bytes *= (size_t)height;
3✔
646
        bytes *= (size_t)depth;
3✔
647
    }
648
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
3!
649
                 SIXEL_ASSESSMENT_SECTION_SIZE)) != 0u) {
650
        assessment->input_pixelformat = pixelformat;
3✔
651
        assessment->input_colorspace = colorspace;
3✔
652
        assessment->source_pixels_bytes = bytes;
3✔
653
    }
654
    if ((mask & SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
3!
655
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_IMAGE_DECODE] +=
×
656
            bytes;
657
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_SCALE] += bytes;
×
658
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_CROP] += bytes;
×
659
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_COLORSPACE] += bytes;
×
660
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_PALETTE_HISTOGRAM] +=
×
661
            bytes;
662
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_PALETTE_SOLVE] +=
×
663
            bytes;
664
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_PALETTE_APPLY] +=
×
665
            bytes;
666
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_ENCODE] += bytes;
×
667
    }
668
}
1!
669

670
SIXELAPI void
671
sixel_assessment_record_quantized_capture(
3✔
672
    sixel_assessment_t *assessment,
673
    sixel_encoder_t *encoder)
674
{
675
    unsigned int mask;
3✔
676

677
    if (assessment == NULL || encoder == NULL) {
3!
678
        return;
679
    }
680
    mask = assessment->sections_mask;
3✔
681
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
3!
682
                 SIXEL_ASSESSMENT_SECTION_SIZE |
683
                 SIXEL_ASSESSMENT_SECTION_QUALITY)) == 0u) {
684
        return;
685
    }
686
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
3!
687
                 SIXEL_ASSESSMENT_SECTION_SIZE)) == 0u &&
×
688
            (assessment->view_mask & SIXEL_ASSESSMENT_VIEW_QUANTIZED) == 0u) {
×
689
        return;
690
    }
691
    assessment->quantized_pixels_bytes = encoder->capture_pixel_bytes;
3✔
692
    assessment->palette_bytes = encoder->capture_palette_size;
3✔
693
    assessment->palette_colors = encoder->capture_ncolors;
3✔
694
    if (assessment->quantized_pixels_bytes == 0 &&
3!
695
            assessment->palette_colors == 0) {
696
        assessment->palette_bytes = 0;
×
697
    }
698
}
1!
699

700
SIXELAPI void
701
sixel_assessment_record_output_size(sixel_assessment_t *assessment,
3✔
702
                                    size_t output_bytes)
703
{
704
    if (assessment == NULL) {
3!
705
        return;
706
    }
707
    if (output_bytes == 0u && assessment->output_bytes_written > 0u) {
3!
708
        output_bytes = assessment->output_bytes_written;
709
    }
710
    assessment->output_bytes = output_bytes;
3✔
711
    if ((assessment->sections_mask &
3!
712
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
713
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_OUTPUT] +=
×
714
            output_bytes;
715
    }
716
}
717

718
SIXELAPI void
719
sixel_assessment_record_output_write(sixel_assessment_t *assessment,
72✔
720
                                     size_t bytes,
721
                                     double duration)
722
{
723
    if (assessment == NULL) {
72!
724
        return;
725
    }
726
    if (bytes > 0u) {
72!
727
        assessment->output_bytes_written += bytes;
72✔
728
    }
729
    if (duration > 0.0 &&
72!
730
            (assessment->sections_mask &
72!
731
             SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
732
        /* The output bucket collects every fn_write() span verbatim. */
733
        assessment->stage_durations[SIXEL_ASSESSMENT_STAGE_OUTPUT] += duration;
×
734
        if (assessment->active_stage == SIXEL_ASSESSMENT_STAGE_ENCODE) {
×
735
            assessment->encode_output_time_pending += duration;
×
736
        }
737
    }
738
}
739

740
SIXELAPI int
741
sixel_assessment_palette_probe_enabled(void)
255✔
742
{
743
    return g_active_encode_assessment != NULL;
255✔
744
}
745

746
SIXELAPI void
747
sixel_assessment_record_palette_apply_span(double duration)
×
748
{
749
    sixel_assessment_t *assessment;
×
750

751
    if (duration <= 0.0) {
×
752
        return;
753
    }
754
    assessment = g_active_encode_assessment;
×
755
    if (assessment == NULL) {
×
756
        return;
757
    }
758
    if ((assessment->sections_mask &
×
759
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
760
        return;
761
    }
762
    /*
763
     * Palette work performed inside sixel_encode() belongs to the
764
     * palette bucket even though the call happens while the encode
765
     * stage is active.  The time goes straight to the palette stage
766
     * while we stash the same span for the later encode rebalance.
767
     */
768
    assessment->stage_durations[SIXEL_ASSESSMENT_STAGE_PALETTE_APPLY] +=
×
769
        duration;
770
    assessment->encode_palette_time_pending += duration;
×
771
}
×
772

773
SIXELAPI int
774
sixel_assessment_encode_probe_enabled(void)
1,416✔
775
{
776
    sixel_assessment_t *assessment;
1,416✔
777

778
    assessment = g_active_encode_assessment;
1,416✔
779
    if (assessment == NULL) {
1,416!
780
        return 0;
781
    }
782
    if ((assessment->sections_mask &
×
783
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
784
        return 0;
×
785
    }
786
    return 1;
787
}
788

789
SIXELAPI void
790
sixel_assessment_set_encode_parallelism(int threads)
1,416✔
791
{
792
    if (threads < 1) {
1,416!
793
        threads = 1;
×
794
    }
795
    g_encode_parallel_threads = threads;
1,416✔
796
    if (threads > 1) {
1,416!
797
        g_encode_substages_visible = 0;
×
798
    } else {
799
        g_encode_substages_visible = 1;
1,416✔
800
    }
801
}
1,416✔
802

803
SIXELAPI void
804
sixel_assessment_record_encode_span(sixel_assessment_stage_t stage,
×
805
                                    double duration)
806
{
807
    sixel_assessment_t *assessment;
×
808

809
    if (duration <= 0.0) {
×
810
        return;
811
    }
812
    if (stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
813
            stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
814
        return;
815
    }
816
    assessment = g_active_encode_assessment;
×
817
    if (assessment == NULL) {
×
818
        return;
819
    }
820
    if ((assessment->sections_mask &
×
821
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
822
        return;
823
    }
824
    assessment->stage_durations[stage] += duration;
×
825
}
×
826

827
SIXELAPI void
828
sixel_assessment_record_encode_work(sixel_assessment_stage_t stage,
×
829
                                    double amount)
830
{
831
    sixel_assessment_t *assessment;
×
832

833
    if (amount <= 0.0) {
×
834
        return;
835
    }
836
    if (stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
837
            stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
838
        return;
839
    }
840
    assessment = g_active_encode_assessment;
×
841
    if (assessment == NULL) {
×
842
        return;
843
    }
844
    if ((assessment->sections_mask &
×
845
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
846
        return;
847
    }
848
    assessment->stage_bytes[stage] += amount;
×
849
}
×
850

851
SIXELAPI void
852
sixel_assessment_select_sections(sixel_assessment_t *assessment,
3✔
853
                                 unsigned int sections)
854
{
855
    unsigned int section_mask;
3✔
856
    unsigned int view_mask;
3✔
857

858
    if (assessment == NULL) {
3!
859
        return;
860
    }
861
    section_mask = sections & SIXEL_ASSESSMENT_SECTION_MASK;
3✔
862
    if ((section_mask & SIXEL_ASSESSMENT_SECTION_QUALITY) == 0u) {
3!
863
        view_mask = SIXEL_ASSESSMENT_VIEW_ENCODED;
864
    } else if ((sections & SIXEL_ASSESSMENT_VIEW_MASK) != 0u) {
×
865
        view_mask = SIXEL_ASSESSMENT_VIEW_QUANTIZED;
866
    } else {
867
        view_mask = SIXEL_ASSESSMENT_VIEW_ENCODED;
3✔
868
    }
869
    assessment->sections_mask = section_mask;
3✔
870
    assessment->view_mask = view_mask;
3✔
871
    if ((section_mask & SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
3!
872
        assessment->stage_active = 0;
3✔
873
        if (g_active_encode_assessment == assessment) {
3!
874
            g_active_encode_assessment = NULL;
×
875
        }
876
    }
877
}
1!
878

879
SIXELAPI void
880
sixel_assessment_attach_encoder(sixel_assessment_t *assessment,
3✔
881
                                sixel_encoder_t *encoder)
882
{
883
    if (encoder == NULL) {
3!
884
        return;
885
    }
886
    encoder->assessment_observer = assessment;
3✔
887
}
888

889
static int assessment_resolve_executable_dir(char const *argv0,
890
                                            char *buffer,
891
                                            size_t size);
892
static void align_frame_pixels(float **ref_pixels,
893
                               int *ref_width,
894
                               int *ref_height,
895
                               float **out_pixels,
896
                               int *out_width,
897
                               int *out_height);
898

899
static void
900
assessment_fail(SIXELSTATUS status, char const *message)
×
901
{
902
    sixel_assessment_t *ctx;
×
903
    size_t length;
×
904

905
    ctx = g_assessment_context;
×
906
    if (ctx != NULL) {
×
907
        ctx->last_error = status;
×
908
        if (message != NULL) {
×
909
            length = strlen(message);
×
910
            if (length >= sizeof(ctx->error_message)) {
×
911
                length = sizeof(ctx->error_message) - 1u;
912
            }
913
            memcpy(ctx->error_message, message, length);
×
914
            ctx->error_message[length] = '\0';
×
915
        } else {
916
            ctx->error_message[0] = '\0';
×
917
        }
918
        longjmp(ctx->bailout, 1);
×
919
    }
920
    if (message != NULL) {
×
921
        fprintf(stderr, "%s\n", message);
×
922
    } else {
923
        fprintf(stderr, "assessment failure\n");
×
924
    }
925
    abort();
×
926
}
927

928
/*
929
 * Memory helpers
930
 */
931
static void *xmalloc(size_t size)
×
932
{
933
    void *ptr;
×
934
    ptr = malloc(size);
×
935
    if (ptr == NULL) {
×
936
        assessment_fail(SIXEL_BAD_ALLOCATION,
×
937
                       "malloc failed while building assessment state");
938
    }
939
    return ptr;
×
940
}
941

942
static void *xcalloc(size_t nmemb, size_t size)
×
943
{
944
    void *ptr;
×
945
    ptr = calloc(nmemb, size);
×
946
    if (ptr == NULL) {
×
947
        assessment_fail(SIXEL_BAD_ALLOCATION,
×
948
                       "calloc failed while building assessment state");
949
    }
950
    return ptr;
×
951
}
952

953
/*
954
 * Loader bridge (libsixel -> float RGB)
955
 */
956
static SIXELSTATUS copy_frame_to_rgb(sixel_frame_t *frame,
×
957
                                     unsigned char **pixels,
958
                                     int *width,
959
                                     int *height)
960
{
961
    SIXELSTATUS status;
×
962
    int frame_width;
×
963
    int frame_height;
×
964
    int pixelformat;
×
965
    size_t size;
×
966
    unsigned char *buffer;
×
967
    int normalized_format;
×
968

969
    frame_width = sixel_frame_get_width(frame);
×
970
    frame_height = sixel_frame_get_height(frame);
×
971
    status = sixel_frame_strip_alpha(frame, NULL);
×
972
    if (SIXEL_FAILED(status)) {
×
973
        return status;
974
    }
975
    pixelformat = sixel_frame_get_pixelformat(frame);
×
976
    size = (size_t)frame_width * (size_t)frame_height * 3u;
×
977
    /*
978
     * Use malloc here because the loader's allocator enforces a strict
979
     * allocation ceiling that can reject large frames even on hosts with
980
     * sufficient RAM available.
981
     */
982
    buffer = (unsigned char *)malloc(size);
×
983
    if (buffer == NULL) {
×
984
        return SIXEL_BAD_ALLOCATION;
985
    }
986
    if (pixelformat == SIXEL_PIXELFORMAT_RGB888) {
×
987
        memcpy(buffer, sixel_frame_get_pixels(frame), size);
×
988
    } else {
989
        normalized_format = pixelformat;
×
990
        status = sixel_helper_normalize_pixelformat(
×
991
            buffer,
992
            &normalized_format,
993
            sixel_frame_get_pixels(frame),
×
994
            pixelformat,
995
            frame_width,
996
            frame_height);
997
        if (SIXEL_FAILED(status)) {
×
998
            free(buffer);
×
999
            return status;
×
1000
        }
1001
    }
1002
    *pixels = buffer;
×
1003
    *width = frame_width;
×
1004
    *height = frame_height;
×
1005
    return SIXEL_OK;
×
1006
}
1007

1008
static SIXELSTATUS
1009
frame_to_rgb_float(sixel_frame_t *frame,
×
1010
                   float **pixels_out,
1011
                   int *width_out,
1012
                   int *height_out)
1013
{
1014
    SIXELSTATUS status;
×
1015
    unsigned char *pixels;
×
1016
    int width;
×
1017
    int height;
×
1018
    float *converted;
×
1019
    size_t count;
×
1020
    size_t index;
×
1021

1022
    status = SIXEL_FALSE;
×
1023
    pixels = NULL;
×
1024
    width = 0;
×
1025
    height = 0;
×
1026
    converted = NULL;
×
1027
    count = 0;
×
1028
    index = 0;
×
1029

1030
    if (frame == NULL || pixels_out == NULL || width_out == NULL ||
×
1031
            height_out == NULL) {
×
1032
        return SIXEL_BAD_ARGUMENT;
1033
    }
1034

1035
    *pixels_out = NULL;
×
1036
    *width_out = 0;
×
1037
    *height_out = 0;
×
1038

1039
    status = copy_frame_to_rgb(frame, &pixels, &width, &height);
×
1040
    if (SIXEL_FAILED(status)) {
×
1041
        goto cleanup;
×
1042
    }
1043
    count = (size_t)width * (size_t)height *
×
1044
            (size_t)SIXEL_ASSESSMENT_RGB_CHANNELS;
1045
    converted = (float *)xmalloc(count * sizeof(float));
×
1046
    for (index = 0; index < count; ++index) {
×
1047
        converted[index] = pixels[index] / 255.0f;
×
1048
    }
1049
    *pixels_out = converted;
×
1050
    *width_out = width;
×
1051
    *height_out = height;
×
1052
    converted = NULL;
×
1053
    status = SIXEL_OK;
×
1054

1055
cleanup:
×
1056
    if (pixels != NULL) {
×
1057
        free(pixels);
×
1058
    }
1059
    if (converted != NULL) {
×
1060
        free(converted);
1061
    }
1062
    return status;
1063
}
1064

1065
/*
1066
 * Path discovery helpers (shared by CLI + LPIPS bridge)
1067
 */
1068
#if defined(HAVE_ONNXRUNTIME) || \
1069
    (!defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__))
1070
static int path_accessible(char const *path)
1071
{
1072
#if defined(_WIN32)
1073
    int rc;
1074

1075
    rc = _access(path, 4);
1076
    return rc == 0;
1077
#else
1078
    return access(path, R_OK) == 0;
1079
#endif
1080
}
1081

1082
static int
1083
join_path(char const *dir,
1084
          char const *leaf,
1085
          char *buffer,
1086
          size_t size)
1087
{
1088
    size_t dir_len;
1089
    size_t leaf_len;
1090
    int need_sep;
1091
    size_t total;
1092

1093
    dir_len = strlen(dir);
1094
    leaf_len = strlen(leaf);
1095
    need_sep = 0;
1096
    if (leaf_len > 0 && (leaf[0] == '/' || leaf[0] == '\\')) {
1097
        dir_len = 0;
1098
    } else if (dir_len > 0 && dir[dir_len - 1] != SIXEL_PATH_SEP) {
1099
        need_sep = 1;
1100
    }
1101
    total = dir_len + need_sep + leaf_len + 1u;
1102
    if (total > size) {
1103
        return -1;
1104
    }
1105
    if (dir_len > 0) {
1106
        memcpy(buffer, dir, dir_len);
1107
    }
1108
    if (need_sep) {
1109
        buffer[dir_len] = SIXEL_PATH_SEP;
1110
        ++dir_len;
1111
    }
1112
    if (leaf_len > 0) {
1113
        memcpy(buffer + dir_len, leaf, leaf_len);
1114
        dir_len += leaf_len;
1115
    }
1116
    buffer[dir_len] = '\0';
1117
    return 0;
1118
}
1119
#endif
1120

1121
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__)
1122
static int
1123
resolve_from_path_env(char const *name,
1124
                      char *buffer,
1125
                      size_t size)
1126
{
1127
    char const *env;
1128
    char const *cursor;
1129
    char const *separator;
1130
    size_t chunk_len;
1131

1132
    env = getenv("PATH");
1133
    if (env == NULL || *env == '\0') {
1134
        return -1;
1135
    }
1136
    cursor = env;
1137
    while (*cursor != '\0') {
1138
        separator = strchr(cursor, SIXEL_PATH_LIST_SEP);
1139
        if (separator == NULL) {
1140
            chunk_len = strlen(cursor);
1141
        } else {
1142
            chunk_len = (size_t)(separator - cursor);
1143
        }
1144
        if (chunk_len >= size) {
1145
            return -1;
1146
        }
1147
        memcpy(buffer, cursor, chunk_len);
1148
        buffer[chunk_len] = '\0';
1149
        if (join_path(buffer, name, buffer, size) != 0) {
1150
            return -1;
1151
        }
1152
        if (path_accessible(buffer)) {
1153
            return 0;
1154
        }
1155
        if (separator == NULL) {
1156
            break;
1157
        }
1158
        cursor = separator + 1;
1159
    }
1160
    return -1;
1161
}
1162
#endif
1163

1164
static int
1165
assessment_resolve_executable_dir(char const *argv0,
×
1166
                                  char *buffer,
1167
                                  size_t size)
1168
{
1169
    char candidate[PATH_MAX];
×
1170
    size_t length;
×
1171
    char *slash;
×
1172
#if defined(_WIN32)
1173
    DWORD written;
1174
#elif defined(__APPLE__)
1175
    uint32_t bufsize;
1176
#elif defined(__linux__)
1177
    ssize_t count;
×
1178
#endif
1179

1180
    candidate[0] = '\0';
×
1181
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
1182
    (void)argv0;
×
1183
#endif
1184
#if defined(_WIN32)
1185
    written = GetModuleFileNameA(NULL, candidate, (DWORD)sizeof(candidate));
1186
    if (written == 0 || written >= sizeof(candidate)) {
1187
        return -1;
1188
    }
1189
#elif defined(__APPLE__)
1190
    bufsize = (uint32_t)sizeof(candidate);
1191
    if (_NSGetExecutablePath(candidate, &bufsize) != 0) {
×
1192
        return -1;
1193
    }
1194
#elif defined(__linux__)
1195
    count = readlink("/proc/self/exe", candidate, sizeof(candidate) - 1u);
×
1196
    if (count < 0 || count >= (ssize_t)sizeof(candidate)) {
×
1197
        return -1;
1198
    }
1199
    candidate[count] = '\0';
×
1200
#else
1201
    if (argv0 == NULL) {
1202
        return -1;
1203
    }
1204
    if (strchr(argv0, '/') != NULL || strchr(argv0, '\\') != NULL) {
1205
        if (strlen(argv0) >= sizeof(candidate)) {
1206
            return -1;
1207
        }
1208
        (void)sixel_compat_strcpy(candidate,
1209
                                  sizeof(candidate),
1210
                                  argv0);
1211
    } else if (resolve_from_path_env(argv0, candidate,
1212
                                     sizeof(candidate)) != 0) {
1213
        return -1;
1214
    }
1215
    {
1216
        char *resolved;
1217

1218
        resolved = realpath(candidate, NULL);
1219
        if (resolved == NULL) {
1220
            return -1;
1221
        }
1222
        if (strlen(resolved) >= sizeof(candidate)) {
1223
            free(resolved);
1224
            return -1;
1225
        }
1226
        (void)sixel_compat_strcpy(candidate,
1227
                                  sizeof(candidate),
1228
                                  resolved);
1229
        free(resolved);
1230
    }
1231
#endif
1232
#if defined(_WIN32)
1233
    {
1234
        char *resolved;
1235

1236
        resolved = _fullpath(NULL, candidate, 0u);
1237
        if (resolved != NULL) {
1238
            if (strlen(resolved) < sizeof(candidate)) {
1239
                (void)sixel_compat_strcpy(candidate,
1240
                                          sizeof(candidate),
1241
                                          resolved);
1242
            }
1243
            free(resolved);
1244
        }
1245
    }
1246
#endif
1247
    length = strlen(candidate);
×
1248
    if (length == 0) {
×
1249
        return -1;
1250
    }
1251
    slash = strrchr(candidate, '/');
×
1252
#if defined(_WIN32)
1253
    if (slash == NULL) {
1254
        slash = strrchr(candidate, '\\');
1255
    }
1256
#endif
1257
    if (slash == NULL) {
×
1258
        return -1;
1259
    }
1260
    *slash = '\0';
×
1261
    if (strlen(candidate) + 1u > size) {
×
1262
        return -1;
1263
    }
1264
    (void)sixel_compat_strcpy(buffer, size, candidate);
×
1265
    return 0;
×
1266
}
1267

1268
#if defined(HAVE_ONNXRUNTIME)
1269
/*
1270
 * LPIPS helper plumbing (model discovery + tensor formatting)
1271
 */
1272
typedef struct image_f32 {
1273
    int width;
1274
    int height;
1275
    float *nchw;
1276
} image_f32_t;
1277

1278
static const OrtApi *g_lpips_api = NULL;
1279

1280
static int build_local_model_path(char const *binary_dir,
1281
                                  char const *name,
1282
                                  char *buffer,
1283
                                  size_t size)
1284
{
1285
    char stage1[PATH_MAX];
1286
    char stage2[PATH_MAX];
1287
    char stage3[PATH_MAX];
1288

1289
    if (binary_dir == NULL || binary_dir[0] == '\0') {
1290
        return (-1);
1291
    }
1292

1293
    if (join_path(binary_dir, SIXEL_LOCAL_MODELS_SEG1,
1294
                  stage1, sizeof(stage1)) != 0) {
1295
        return (-1);
1296
    }
1297
    if (join_path(stage1, SIXEL_LOCAL_MODELS_SEG2,
1298
                  stage2, sizeof(stage2)) != 0) {
1299
        return (-1);
1300
    }
1301
    if (join_path(stage2, SIXEL_LOCAL_MODELS_SEG3,
1302
                  stage3, sizeof(stage3)) != 0) {
1303
        return (-1);
1304
    }
1305
    if (join_path(stage3, name, buffer, size) != 0) {
1306
        return (-1);
1307
    }
1308
    return (0);
1309
}
1310

1311
static int find_model(char const *binary_dir,
1312
                      char const *override_dir,
1313
                      char const *name,
1314
                      char *buffer,
1315
                      size_t size)
1316
{
1317
    char env_root[PATH_MAX];
1318
    char install_root[PATH_MAX];
1319
    char binary_parent_path[PATH_MAX];
1320
    char const *env_dir;
1321

1322
    env_dir = getenv("LIBSIXEL_MODEL_DIR");
1323
    if (env_dir != NULL && env_dir[0] != '\0') {
1324
        if (join_path(env_dir, SIXEL_LOCAL_MODELS_SEG3,
1325
                      env_root, sizeof(env_root)) == 0) {
1326
            if (join_path(env_root, name, buffer, size) == 0) {
1327
                if (path_accessible(buffer)) {
1328
                    return (0);
1329
                }
1330
            }
1331
        }
1332
    }
1333
    if (override_dir != NULL && override_dir[0] != '\0') {
1334
        if (join_path(override_dir, name, buffer, size) == 0) {
1335
            if (path_accessible(buffer)) {
1336
                return (0);
1337
            }
1338
        }
1339
    }
1340
    if (SIXEL_MODEL_DIR[0] != '\0') {
1341
        /* checking ${packagedatadir}/models */
1342
        if (join_path(SIXEL_MODEL_DIR, SIXEL_LOCAL_MODELS_SEG3,
1343
                      install_root, sizeof(install_root)) == 0) {
1344
            if (join_path(install_root, name, buffer, size) == 0) {
1345
                if (path_accessible(buffer)) {
1346
                    return (0);
1347
                }
1348
            }
1349
        }
1350
    }
1351

1352
    /*
1353
     * Try ../models/lpips and ../../models/lpips so staged binaries in
1354
     * converters/ or tools/ still find the ONNX assets.
1355
     */
1356
    if (binary_dir != NULL && binary_dir[0] != '\0') {
1357
        /* checking ../models/lpips */
1358
        if (build_local_model_path(binary_dir, name, buffer, size) == 0) {
1359
            if (path_accessible(buffer)) {
1360
                return (0);
1361
            }
1362
        }
1363
    }
1364
    if (binary_dir != NULL && binary_dir[0] != '\0') {
1365
        /* checking ../../models/lpips */
1366
        if (join_path(binary_dir, SIXEL_LOCAL_MODELS_SEG1,
1367
                      binary_parent_path, sizeof(binary_parent_path)) == 0) {
1368
            if (build_local_model_path(binary_parent_path, name, buffer, size) == 0) {
1369
                if (path_accessible(buffer)) {
1370
                    return (0);
1371
                }
1372
            }
1373
        }
1374
    }
1375
    return (-1);
1376
}
1377

1378
static int
1379
ensure_lpips_models(sixel_assessment_t *assessment)
1380
{
1381
    if (assessment->lpips_models_ready) {
1382
        return 0;
1383
    }
1384
    if (find_model(assessment->binary_dir,
1385
                   assessment->model_dir_state > 0
1386
                       ? assessment->model_dir
1387
                       : NULL,
1388
                   "lpips_diff.onnx",
1389
                   assessment->diff_model_path,
1390
                   sizeof(assessment->diff_model_path)) != 0) {
1391
        fprintf(stderr,
1392
                "Warning: lpips_diff.onnx not found.\n");
1393
        return -1;
1394
    }
1395
    if (find_model(assessment->binary_dir,
1396
                   assessment->model_dir_state > 0
1397
                       ? assessment->model_dir
1398
                       : NULL,
1399
                   "lpips_feature.onnx",
1400
                   assessment->feat_model_path,
1401
                   sizeof(assessment->feat_model_path)) != 0) {
1402
        fprintf(stderr,
1403
                "Warning: lpips_feature.onnx not found.\n");
1404
        return -1;
1405
    }
1406
    assessment->lpips_models_ready = 1;
1407
    return 0;
1408
}
1409

1410
static void
1411
free_image_f32(image_f32_t *image)
1412
{
1413
    if (image->nchw != NULL) {
1414
        free(image->nchw);
1415
        image->nchw = NULL;
1416
    }
1417
}
1418

1419
static int
1420
convert_pixels_to_nchw(float const *src_pixels,
1421
                       int width,
1422
                       int height,
1423
                       image_f32_t *dst)
1424
{
1425
    size_t plane_size;
1426
    size_t index;
1427
    float *buffer;
1428

1429
    if (src_pixels == NULL || dst == NULL) {
1430
        return -1;
1431
    }
1432
    if (width <= 0 || height <= 0) {
1433
        return -1;
1434
    }
1435
    plane_size = (size_t)width * (size_t)height;
1436
    buffer = (float *)malloc(plane_size *
1437
                             (size_t)SIXEL_ASSESSMENT_RGB_CHANNELS *
1438
                             sizeof(float));
1439
    if (buffer == NULL) {
1440
        return -1;
1441
    }
1442
    for (index = 0; index < plane_size; ++index) {
1443
        buffer[plane_size * 0u + index] =
1444
            src_pixels[index * 3u + 0u] * 2.0f - 1.0f;
1445
        buffer[plane_size * 1u + index] =
1446
            src_pixels[index * 3u + 1u] * 2.0f - 1.0f;
1447
        buffer[plane_size * 2u + index] =
1448
            src_pixels[index * 3u + 2u] * 2.0f - 1.0f;
1449
    }
1450
    dst->width = width;
1451
    dst->height = height;
1452
    dst->nchw = buffer;
1453
    return 0;
1454
}
1455

1456
static float *
1457
bilinear_resize_nchw3(float const *src,
1458
                      int src_width,
1459
                      int src_height,
1460
                      int dst_width,
1461
                      int dst_height)
1462
{
1463
    float *dst;
1464
    int channel;
1465
    int y;
1466
    int x;
1467
    float scale_y;
1468
    float scale_x;
1469
    float fy;
1470
    float fx;
1471
    int y0;
1472
    int x0;
1473
    float wy;
1474
    float wx;
1475
    size_t src_stride;
1476
    size_t dst_index;
1477

1478
    dst = (float *)malloc((size_t)3 * (size_t)dst_height *
1479
                          (size_t)dst_width * sizeof(float));
1480
    if (dst == NULL) {
1481
        return NULL;
1482
    }
1483
    src_stride = (size_t)src_width * (size_t)src_height;
1484
    for (channel = 0; channel < 3; ++channel) {
1485
        for (y = 0; y < dst_height; ++y) {
1486
            scale_y = (float)src_height / (float)dst_height;
1487
            fy = (float)y * scale_y;
1488
            y0 = (int)fy;
1489
            if (y0 >= src_height - 1) {
1490
                y0 = src_height - 2;
1491
            }
1492
            wy = fy - (float)y0;
1493
            for (x = 0; x < dst_width; ++x) {
1494
                scale_x = (float)src_width / (float)dst_width;
1495
                fx = (float)x * scale_x;
1496
                x0 = (int)fx;
1497
                if (x0 >= src_width - 1) {
1498
                    x0 = src_width - 2;
1499
                }
1500
                wx = fx - (float)x0;
1501
                dst_index = (size_t)channel * (size_t)dst_width *
1502
                            (size_t)dst_height +
1503
                            (size_t)y * (size_t)dst_width + (size_t)x;
1504
                dst[dst_index] =
1505
                    (1.0f - wx) * (1.0f - wy) *
1506
                        src[(size_t)channel * src_stride +
1507
                            (size_t)y0 * (size_t)src_width + (size_t)x0] +
1508
                    wx * (1.0f - wy) *
1509
                        src[(size_t)channel * src_stride +
1510
                            (size_t)y0 * (size_t)src_width +
1511
                            (size_t)(x0 + 1)] +
1512
                    (1.0f - wx) * wy *
1513
                        src[(size_t)channel * src_stride +
1514
                            (size_t)(y0 + 1) * (size_t)src_width +
1515
                            (size_t)x0] +
1516
                    wx * wy *
1517
                        src[(size_t)channel * src_stride +
1518
                            (size_t)(y0 + 1) * (size_t)src_width +
1519
                            (size_t)(x0 + 1)];
1520
            }
1521
        }
1522
    }
1523
    return dst;
1524
}
1525

1526
static int
1527
ort_status_to_error(OrtStatus *status)
1528
{
1529
    char const *message;
1530

1531
    if (status == NULL) {
1532
        return 0;
1533
    }
1534
    message = g_lpips_api->GetErrorMessage(status);
1535
    fprintf(stderr,
1536
            "ONNX Runtime error: %s\n",
1537
            message != NULL ? message : "(null)");
1538
    g_lpips_api->ReleaseStatus(status);
1539
    return -1;
1540
}
1541

1542
static void
1543
get_first_input_shape(OrtSession *session,
1544
                      int64_t *dims,
1545
                      size_t *rank)
1546
{
1547
    OrtTypeInfo *type_info;
1548
    OrtTensorTypeAndShapeInfo const *shape_info;
1549

1550
    type_info = NULL;
1551
    shape_info = NULL;
1552
    if (ort_status_to_error(g_lpips_api->SessionGetInputTypeInfo(
1553
            session, 0, &type_info)) != 0) {
1554
        return;
1555
    }
1556
    if (ort_status_to_error(g_lpips_api->CastTypeInfoToTensorInfo(
1557
            type_info, &shape_info)) != 0) {
1558
        g_lpips_api->ReleaseTypeInfo(type_info);
1559
        return;
1560
    }
1561
    if (ort_status_to_error(g_lpips_api->GetDimensionsCount(
1562
            shape_info, rank)) != 0) {
1563
        g_lpips_api->ReleaseTypeInfo(type_info);
1564
        return;
1565
    }
1566
    (void)ort_status_to_error(g_lpips_api->GetDimensions(
1567
        shape_info, dims, *rank));
1568
    g_lpips_api->ReleaseTypeInfo(type_info);
1569
}
1570

1571
static int
1572
tail_index(char const *name)
1573
{
1574
    int length;
1575
    int index;
1576

1577
    length = (int)strlen(name);
1578
    index = length - 1;
1579
    while (index >= 0 && isdigit((unsigned char)name[index])) {
1580
        --index;
1581
    }
1582
    if (index == length - 1) {
1583
        return -1;
1584
    }
1585
    return atoi(name + index + 1);
1586
}
1587

1588
static int
1589
run_lpips(char const *diff_model,
1590
          char const *feat_model,
1591
          image_f32_t *image_a,
1592
          image_f32_t *image_b,
1593
          float *result_out)
1594
{
1595
    OrtEnv *env;
1596
    OrtAllocator *allocator;
1597
    OrtSessionOptions *options;
1598
    OrtSession *diff_session;
1599
    OrtSession *feat_session;
1600
    OrtMemoryInfo *memory_info;
1601
    OrtValue *tensor_a;
1602
    OrtValue *tensor_b;
1603
    OrtValue **features_a;
1604
    OrtValue **features_b;
1605
    OrtValue const **diff_values;
1606
    OrtValue *diff_outputs[1];
1607
    char *feat_input_name;
1608
    char **feat_output_names;
1609
    char **diff_input_names;
1610
    char *diff_output_name;
1611
    int64_t feat_dims[8];
1612
    size_t feat_rank;
1613
    size_t feat_outputs;
1614
    size_t diff_inputs;
1615
    int target_width;
1616
    int target_height;
1617
    float *resized_a;
1618
    float *resized_b;
1619
    float const *tensor_data_a;
1620
    float const *tensor_data_b;
1621
    size_t plane_size;
1622
    size_t i;
1623
    int64_t tensor_shape[4];
1624
    OrtStatus *status;
1625
    int rc;
1626

1627
    env = NULL;
1628
    allocator = NULL;
1629
    options = NULL;
1630
    diff_session = NULL;
1631
    feat_session = NULL;
1632
    memory_info = NULL;
1633
    tensor_a = NULL;
1634
    tensor_b = NULL;
1635
    features_a = NULL;
1636
    features_b = NULL;
1637
    diff_values = NULL;
1638
    diff_outputs[0] = NULL;
1639
    feat_input_name = NULL;
1640
    feat_output_names = NULL;
1641
    diff_input_names = NULL;
1642
    diff_output_name = NULL;
1643
    target_width = image_a->width;
1644
    target_height = image_a->height;
1645
    resized_a = NULL;
1646
    resized_b = NULL;
1647
    tensor_data_a = image_a->nchw;
1648
    tensor_data_b = image_b->nchw;
1649
    feat_rank = 0;
1650
    feat_outputs = 0;
1651
    diff_inputs = 0;
1652
    status = NULL;
1653
    rc = -1;
1654
    *result_out = NAN;
1655

1656
    g_lpips_api = OrtGetApiBase()->GetApi(ORT_API_VERSION);
1657
    if (g_lpips_api == NULL) {
1658
        fprintf(stderr, "ONNX Runtime API unavailable.\n");
1659
        goto cleanup;
1660
    }
1661

1662
    status = g_lpips_api->CreateEnv(ORT_LOGGING_LEVEL_WARNING,
1663
                                    "lpips",
1664
                                    &env);
1665
    if (ort_status_to_error(status) != 0) {
1666
        goto cleanup;
1667
    }
1668
    status = g_lpips_api->GetAllocatorWithDefaultOptions(&allocator);
1669
    if (ort_status_to_error(status) != 0) {
1670
        goto cleanup;
1671
    }
1672
    status = g_lpips_api->CreateSessionOptions(&options);
1673
    if (ort_status_to_error(status) != 0) {
1674
        goto cleanup;
1675
    }
1676
    status = g_lpips_api->CreateSession(env, diff_model, options,
1677
                                        &diff_session);
1678
    if (ort_status_to_error(status) != 0) {
1679
        goto cleanup;
1680
    }
1681
    status = g_lpips_api->CreateSession(env, feat_model, options,
1682
                                        &feat_session);
1683
    if (ort_status_to_error(status) != 0) {
1684
        goto cleanup;
1685
    }
1686

1687
    get_first_input_shape(feat_session, feat_dims, &feat_rank);
1688
    if (feat_rank >= 4 && feat_dims[3] > 0) {
1689
        target_width = (int)feat_dims[3];
1690
    }
1691
    if (feat_rank >= 4 && feat_dims[2] > 0) {
1692
        target_height = (int)feat_dims[2];
1693
    }
1694

1695
    if (image_a->width != target_width ||
1696
        image_a->height != target_height) {
1697
        resized_a = bilinear_resize_nchw3(image_a->nchw,
1698
                                          image_a->width,
1699
                                          image_a->height,
1700
                                          target_width,
1701
                                          target_height);
1702
        if (resized_a == NULL) {
1703
            fprintf(stderr,
1704
                    "Warning: unable to resize LPIPS reference tensor.\n");
1705
            goto cleanup;
1706
        }
1707
        tensor_data_a = resized_a;
1708
    }
1709
    if (image_b->width != target_width ||
1710
        image_b->height != target_height) {
1711
        resized_b = bilinear_resize_nchw3(image_b->nchw,
1712
                                          image_b->width,
1713
                                          image_b->height,
1714
                                          target_width,
1715
                                          target_height);
1716
        if (resized_b == NULL) {
1717
            fprintf(stderr,
1718
                    "Warning: unable to resize LPIPS output tensor.\n");
1719
            goto cleanup;
1720
        }
1721
        tensor_data_b = resized_b;
1722
    }
1723

1724
    plane_size = (size_t)target_width * (size_t)target_height;
1725
    tensor_shape[0] = 1;
1726
    tensor_shape[1] = 3;
1727
    tensor_shape[2] = target_height;
1728
    tensor_shape[3] = target_width;
1729

1730
    status = g_lpips_api->CreateCpuMemoryInfo(OrtArenaAllocator,
1731
                                              OrtMemTypeDefault,
1732
                                              &memory_info);
1733
    if (ort_status_to_error(status) != 0) {
1734
        goto cleanup;
1735
    }
1736
    status = g_lpips_api->CreateTensorWithDataAsOrtValue(
1737
        memory_info,
1738
        (void *)tensor_data_a,
1739
        plane_size * 3u * sizeof(float),
1740
        tensor_shape,
1741
        4,
1742
        ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,
1743
        &tensor_a);
1744
    if (ort_status_to_error(status) != 0) {
1745
        goto cleanup;
1746
    }
1747
    status = g_lpips_api->CreateTensorWithDataAsOrtValue(
1748
        memory_info,
1749
        (void *)tensor_data_b,
1750
        plane_size * 3u * sizeof(float),
1751
        tensor_shape,
1752
        4,
1753
        ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,
1754
        &tensor_b);
1755
    if (ort_status_to_error(status) != 0) {
1756
        goto cleanup;
1757
    }
1758

1759
    status = g_lpips_api->SessionGetInputName(feat_session,
1760
                                              0,
1761
                                              allocator,
1762
                                              &feat_input_name);
1763
    if (ort_status_to_error(status) != 0) {
1764
        goto cleanup;
1765
    }
1766
    status = g_lpips_api->SessionGetOutputCount(feat_session,
1767
                                                &feat_outputs);
1768
    if (ort_status_to_error(status) != 0) {
1769
        goto cleanup;
1770
    }
1771
    feat_output_names = (char **)calloc(feat_outputs, sizeof(char *));
1772
    features_a = (OrtValue **)calloc(feat_outputs, sizeof(OrtValue *));
1773
    features_b = (OrtValue **)calloc(feat_outputs, sizeof(OrtValue *));
1774
    if (feat_output_names == NULL ||
1775
        features_a == NULL ||
1776
        features_b == NULL) {
1777
        fprintf(stderr,
1778
                "Warning: out of memory while preparing LPIPS features.\n");
1779
        goto cleanup;
1780
    }
1781
    for (i = 0; i < feat_outputs; ++i) {
1782
        status = g_lpips_api->SessionGetOutputName(feat_session,
1783
                                                   i,
1784
                                                   allocator,
1785
                                                   &feat_output_names[i]);
1786
        if (ort_status_to_error(status) != 0) {
1787
            goto cleanup;
1788
        }
1789
    }
1790
    status = g_lpips_api->Run(feat_session,
1791
                              NULL,
1792
                              (char const *const *)&feat_input_name,
1793
                              (OrtValue const *const *)&tensor_a,
1794
                              1,
1795
                              (char const *const *)feat_output_names,
1796
                              feat_outputs,
1797
                              features_a);
1798
    if (ort_status_to_error(status) != 0) {
1799
        goto cleanup;
1800
    }
1801
    status = g_lpips_api->Run(feat_session,
1802
                              NULL,
1803
                              (char const *const *)&feat_input_name,
1804
                              (OrtValue const *const *)&tensor_b,
1805
                              1,
1806
                              (char const *const *)feat_output_names,
1807
                              feat_outputs,
1808
                              features_b);
1809
    if (ort_status_to_error(status) != 0) {
1810
        goto cleanup;
1811
    }
1812

1813
    status = g_lpips_api->SessionGetInputCount(diff_session,
1814
                                               &diff_inputs);
1815
    if (ort_status_to_error(status) != 0) {
1816
        goto cleanup;
1817
    }
1818
    diff_input_names = (char **)calloc(diff_inputs, sizeof(char *));
1819
    diff_values = (OrtValue const **)calloc(diff_inputs,
1820
                                            sizeof(OrtValue *));
1821
    if (diff_input_names == NULL || diff_values == NULL) {
1822
        fprintf(stderr,
1823
                "Warning: out of memory while preparing LPIPS diff inputs.\n");
1824
        goto cleanup;
1825
    }
1826
    for (i = 0; i < diff_inputs; ++i) {
1827
        status = g_lpips_api->SessionGetInputName(diff_session,
1828
                                                  i,
1829
                                                  allocator,
1830
                                                  &diff_input_names[i]);
1831
        if (ort_status_to_error(status) != 0) {
1832
            goto cleanup;
1833
        }
1834
        if (diff_input_names[i] == NULL) {
1835
            continue;
1836
        }
1837
        if (strncmp(diff_input_names[i], "feat_x_", 7) == 0) {
1838
            int index;
1839

1840
            index = tail_index(diff_input_names[i]);
1841
            if (index >= 0 && (size_t)index < feat_outputs) {
1842
                diff_values[i] = features_a[index];
1843
            }
1844
        } else if (strncmp(diff_input_names[i], "feat_y_", 7) == 0) {
1845
            int index;
1846

1847
            index = tail_index(diff_input_names[i]);
1848
            if (index >= 0 && (size_t)index < feat_outputs) {
1849
                diff_values[i] = features_b[index];
1850
            }
1851
        }
1852
    }
1853

1854
    status = g_lpips_api->SessionGetOutputName(diff_session,
1855
                                               0,
1856
                                               allocator,
1857
                                               &diff_output_name);
1858
    if (ort_status_to_error(status) != 0) {
1859
        goto cleanup;
1860
    }
1861
    status = g_lpips_api->Run(diff_session,
1862
                              NULL,
1863
                              (char const *const *)diff_input_names,
1864
                              diff_values,
1865
                              diff_inputs,
1866
                              (char const *const *)&diff_output_name,
1867
                              1,
1868
                              diff_outputs);
1869
    if (ort_status_to_error(status) != 0) {
1870
        goto cleanup;
1871
    }
1872

1873
    if (diff_outputs[0] != NULL) {
1874
        float *result_data;
1875

1876
        result_data = NULL;
1877
        status = g_lpips_api->GetTensorMutableData(diff_outputs[0],
1878
                                                   (void **)&result_data);
1879
        if (ort_status_to_error(status) != 0) {
1880
            goto cleanup;
1881
        }
1882
        if (result_data != NULL) {
1883
            *result_out = result_data[0];
1884
            rc = 0;
1885
        }
1886
    }
1887

1888
cleanup:
1889
    if (diff_outputs[0] != NULL) {
1890
        g_lpips_api->ReleaseValue(diff_outputs[0]);
1891
    }
1892
    /*
1893
     * Clean up ORT-managed string buffers with explicit status release.
1894
     *
1895
     * We always release the temporary OrtStatus objects to prevent
1896
     * resource leaks when ONNX Runtime reports cleanup diagnostics.
1897
     */
1898
    if (diff_output_name != NULL) {
1899
        status = g_lpips_api->AllocatorFree(allocator, diff_output_name);
1900
        if (status != NULL) {
1901
            g_lpips_api->ReleaseStatus(status);
1902
        }
1903
    }
1904
    if (diff_input_names != NULL) {
1905
        for (i = 0; i < diff_inputs; ++i) {
1906
            if (diff_input_names[i] != NULL) {
1907
                status = g_lpips_api->AllocatorFree(allocator,
1908
                                                    diff_input_names[i]);
1909
                if (status != NULL) {
1910
                    g_lpips_api->ReleaseStatus(status);
1911
                }
1912
            }
1913
        }
1914
        free(diff_input_names);
1915
    }
1916
    if (diff_values != NULL) {
1917
        free(diff_values);
1918
    }
1919
    if (feat_output_names != NULL) {
1920
        for (i = 0; i < feat_outputs; ++i) {
1921
            if (feat_output_names[i] != NULL) {
1922
                status = g_lpips_api->AllocatorFree(allocator,
1923
                                                    feat_output_names[i]);
1924
                if (status != NULL) {
1925
                    g_lpips_api->ReleaseStatus(status);
1926
                }
1927
            }
1928
        }
1929
        free(feat_output_names);
1930
    }
1931
    if (features_a != NULL) {
1932
        for (i = 0; i < feat_outputs; ++i) {
1933
            if (features_a[i] != NULL) {
1934
                g_lpips_api->ReleaseValue(features_a[i]);
1935
            }
1936
        }
1937
        free(features_a);
1938
    }
1939
    if (features_b != NULL) {
1940
        for (i = 0; i < feat_outputs; ++i) {
1941
            if (features_b[i] != NULL) {
1942
                g_lpips_api->ReleaseValue(features_b[i]);
1943
            }
1944
        }
1945
        free(features_b);
1946
    }
1947
    if (feat_input_name != NULL) {
1948
        status = g_lpips_api->AllocatorFree(allocator, feat_input_name);
1949
        if (status != NULL) {
1950
            g_lpips_api->ReleaseStatus(status);
1951
        }
1952
    }
1953
    if (tensor_a != NULL) {
1954
        g_lpips_api->ReleaseValue(tensor_a);
1955
    }
1956
    if (tensor_b != NULL) {
1957
        g_lpips_api->ReleaseValue(tensor_b);
1958
    }
1959
    if (memory_info != NULL) {
1960
        g_lpips_api->ReleaseMemoryInfo(memory_info);
1961
    }
1962
    if (feat_session != NULL) {
1963
        g_lpips_api->ReleaseSession(feat_session);
1964
    }
1965
    if (diff_session != NULL) {
1966
        g_lpips_api->ReleaseSession(diff_session);
1967
    }
1968
    if (options != NULL) {
1969
        g_lpips_api->ReleaseSessionOptions(options);
1970
    }
1971
    if (env != NULL) {
1972
        g_lpips_api->ReleaseEnv(env);
1973
    }
1974
    if (resized_a != NULL) {
1975
        free(resized_a);
1976
    }
1977
    if (resized_b != NULL) {
1978
        free(resized_b);
1979
    }
1980
    return rc;
1981
}
1982
#endif /* HAVE_ONNXRUNTIME */
1983

1984
/*
1985
 * Array math helpers
1986
 */
1987
static sixel_assessment_float_buffer_t
1988
float_buffer_create(size_t length)
×
1989
{
1990
    sixel_assessment_float_buffer_t buf;
×
1991
    buf.length = length;
×
1992
    buf.values = (float *)xcalloc(length, sizeof(float));
×
1993
    return buf;
×
1994
}
1995

1996
static void
1997
float_buffer_free(sixel_assessment_float_buffer_t *buf)
×
1998
{
1999
    if (buf->values != NULL) {
×
2000
        free(buf->values);
×
2001
        buf->values = NULL;
×
2002
        buf->length = 0;
×
2003
    }
2004
}
×
2005

2006
static float
2007
clamp_float(float v, float min_v, float max_v)
2008
{
2009
    float result;
2010
    result = v;
×
2011
    if (result < min_v) {
×
2012
        result = min_v;
2013
    }
2014
    if (result > max_v) {
×
2015
        result = max_v;
2016
    }
2017
    return result;
2018
}
2019

2020
/*
2021
 * Luma conversion and resizing utilities
2022
 */
2023
static sixel_assessment_float_buffer_t
2024
pixels_to_luma709(const float *pixels, int width, int height)
×
2025
{
2026
    sixel_assessment_float_buffer_t buf;
×
2027
    size_t total;
×
2028
    size_t i;
×
2029
    float r;
×
2030
    float g;
×
2031
    float b;
×
2032

2033
    total = (size_t)width * (size_t)height;
×
2034
    buf = float_buffer_create(total);
×
2035
    for (i = 0; i < total; ++i) {
×
2036
        r = pixels[i * 3 + 0];
×
2037
        g = pixels[i * 3 + 1];
×
2038
        b = pixels[i * 3 + 2];
×
2039
        buf.values[i] = 0.2126f * r + 0.7152f * g + 0.0722f * b;
×
2040
    }
2041
    return buf;
×
2042
}
2043

2044
static sixel_assessment_float_buffer_t
2045
pixels_channel(const float *pixels, int width, int height, int channel)
×
2046
{
2047
    sixel_assessment_float_buffer_t buf;
×
2048
    size_t total;
×
2049
    size_t i;
×
2050
    float value;
×
2051

2052
    total = (size_t)width * (size_t)height;
×
2053
    buf = float_buffer_create(total);
×
2054
    for (i = 0; i < total; ++i) {
×
2055
        value = pixels[i * 3 + channel];
×
2056
        buf.values[i] = value;
×
2057
    }
2058
    return buf;
×
2059
}
2060
/*
2061
 * Gaussian kernel and separable convolution
2062
 */
2063
static sixel_assessment_float_buffer_t gaussian_kernel1d(int size, float sigma)
×
2064
{
2065
    sixel_assessment_float_buffer_t kernel;
×
2066
    int i;
×
2067
    float mean;
×
2068
    float sum;
×
2069
    float x;
×
2070
    float value;
×
2071

2072
    kernel = float_buffer_create((size_t)size);
×
2073
    mean = ((float)size - 1.0f) * 0.5f;
×
2074
    sum = 0.0f;
×
2075
    for (i = 0; i < size; ++i) {
×
2076
        x = (float)i - mean;
×
2077
        value = expf(-0.5f * (x / sigma) * (x / sigma));
×
2078
        kernel.values[i] = value;
×
2079
        sum += value;
×
2080
    }
2081
    if (sum > 0.0f) {
×
2082
        for (i = 0; i < size; ++i) {
×
2083
            kernel.values[i] /= sum;
×
2084
        }
2085
    }
2086
    return kernel;
×
2087
}
2088

2089
static sixel_assessment_float_buffer_t
2090
separable_conv2d(const sixel_assessment_float_buffer_t *img, int width,
×
2091
                 int height, const sixel_assessment_float_buffer_t *kernel)
2092
{
2093
    sixel_assessment_float_buffer_t tmp;
×
2094
    sixel_assessment_float_buffer_t out;
×
2095
    int pad;
×
2096
    int x;
×
2097
    int y;
×
2098
    int k;
×
2099
    int kernel_size;
×
2100
    float acc;
×
2101
    int px;
×
2102
    int py;
×
2103
    int idx;
×
2104
    float sample;
×
2105

2106
    pad = (int)kernel->length / 2;
×
2107
    kernel_size = (int)kernel->length;
×
2108
    tmp = float_buffer_create((size_t)width * (size_t)height);
×
2109
    out = float_buffer_create((size_t)width * (size_t)height);
×
2110

2111
    for (y = 0; y < height; ++y) {
×
2112
        for (x = 0; x < width; ++x) {
×
2113
            acc = 0.0f;
2114
            for (k = 0; k < kernel_size; ++k) {
×
2115
                px = x + k - pad;
×
2116
                if (px < 0) {
×
2117
                    px = -px;
2118
                }
2119
                if (px >= width) {
×
2120
                    px = width - (px - width) - 1;
×
2121
                    if (px < 0) {
×
2122
                        px = 0;
2123
                    }
2124
                }
2125
                idx = y * width + px;
×
2126
                sample = img->values[idx];
×
2127
                acc += kernel->values[k] * sample;
×
2128
            }
2129
            tmp.values[y * width + x] = acc;
×
2130
        }
2131
    }
2132

2133
    for (y = 0; y < height; ++y) {
×
2134
        for (x = 0; x < width; ++x) {
×
2135
            acc = 0.0f;
2136
            for (k = 0; k < kernel_size; ++k) {
×
2137
                py = y + k - pad;
×
2138
                if (py < 0) {
×
2139
                    py = -py;
2140
                }
2141
                if (py >= height) {
×
2142
                    py = height - (py - height) - 1;
×
2143
                    if (py < 0) {
×
2144
                        py = 0;
2145
                    }
2146
                }
2147
                idx = py * width + x;
×
2148
                sample = tmp.values[idx];
×
2149
                acc += kernel->values[k] * sample;
×
2150
            }
2151
            out.values[y * width + x] = acc;
×
2152
        }
2153
    }
2154

2155
    float_buffer_free(&tmp);
×
2156
    return out;
×
2157
}
2158

2159
/*
2160
 * SSIM and MS-SSIM computation
2161
 */
2162
static float ssim_luma(
×
2163
    const sixel_assessment_float_buffer_t *x,
2164
    const sixel_assessment_float_buffer_t *y,
2165
    int width,
2166
    int height,
2167
    float k1,
2168
    float k2,
2169
    int win_size,
2170
    float sigma)
2171
{
2172
    sixel_assessment_float_buffer_t kernel;
×
2173
    sixel_assessment_float_buffer_t mu_x;
×
2174
    sixel_assessment_float_buffer_t mu_y;
×
2175
    sixel_assessment_float_buffer_t mu_x2;
×
2176
    sixel_assessment_float_buffer_t mu_y2;
×
2177
    sixel_assessment_float_buffer_t mu_xy;
×
2178
    sixel_assessment_float_buffer_t sigma_x2;
×
2179
    sixel_assessment_float_buffer_t sigma_y2;
×
2180
    sixel_assessment_float_buffer_t sigma_xy;
×
2181
    float C1;
×
2182
    float C2;
×
2183
    size_t total;
×
2184
    size_t i;
×
2185
    float mean;
×
2186
    float numerator;
×
2187
    float denominator;
×
2188
    float value;
×
2189
    sixel_assessment_float_buffer_t x_sq;
×
2190
    sixel_assessment_float_buffer_t y_sq;
×
2191
    sixel_assessment_float_buffer_t xy_buf;
×
2192

2193
    kernel = gaussian_kernel1d(win_size, sigma);
×
2194
    mu_x = separable_conv2d(x, width, height, &kernel);
×
2195
    mu_y = separable_conv2d(y, width, height, &kernel);
×
2196

2197
    total = (size_t)width * (size_t)height;
×
2198
    mu_x2 = float_buffer_create(total);
×
2199
    mu_y2 = float_buffer_create(total);
×
2200
    mu_xy = float_buffer_create(total);
×
2201
    sigma_x2 = float_buffer_create(total);
×
2202
    sigma_y2 = float_buffer_create(total);
×
2203
    sigma_xy = float_buffer_create(total);
×
2204
    x_sq = float_buffer_create(total);
×
2205
    y_sq = float_buffer_create(total);
×
2206
    xy_buf = float_buffer_create(total);
×
2207

2208
    for (i = 0; i < total; ++i) {
×
2209
        mu_x2.values[i] = mu_x.values[i] * mu_x.values[i];
×
2210
        mu_y2.values[i] = mu_y.values[i] * mu_y.values[i];
×
2211
        mu_xy.values[i] = mu_x.values[i] * mu_y.values[i];
×
2212
        x_sq.values[i] = x->values[i] * x->values[i];
×
2213
        y_sq.values[i] = y->values[i] * y->values[i];
×
2214
        xy_buf.values[i] = x->values[i] * y->values[i];
×
2215
    }
2216

2217
    float_buffer_free(&sigma_x2);
×
2218
    float_buffer_free(&sigma_y2);
×
2219
    float_buffer_free(&sigma_xy);
×
2220
    sigma_x2 = separable_conv2d(&x_sq, width, height, &kernel);
×
2221
    sigma_y2 = separable_conv2d(&y_sq, width, height, &kernel);
×
2222
    sigma_xy = separable_conv2d(&xy_buf, width, height, &kernel);
×
2223

2224
    for (i = 0; i < total; ++i) {
×
2225
        sigma_x2.values[i] -= mu_x2.values[i];
×
2226
        sigma_y2.values[i] -= mu_y2.values[i];
×
2227
        sigma_xy.values[i] -= mu_xy.values[i];
×
2228
    }
2229

2230
    C1 = (k1 * 1.0f) * (k1 * 1.0f);
×
2231
    C2 = (k2 * 1.0f) * (k2 * 1.0f);
×
2232

2233
    mean = 0.0f;
×
2234
    for (i = 0; i < total; ++i) {
×
2235
        numerator = (2.0f * mu_xy.values[i] + C1) *
×
2236
                    (2.0f * sigma_xy.values[i] + C2);
×
2237
        denominator = (mu_x2.values[i] + mu_y2.values[i] + C1) *
×
2238
                      (sigma_x2.values[i] + sigma_y2.values[i] + C2);
×
2239
        if (denominator != 0.0f) {
×
2240
            value = numerator / (denominator + 1e-12f);
×
2241
        } else {
2242
            value = 0.0f;
2243
        }
2244
        mean += value;
×
2245
    }
2246
    mean /= (float)total;
×
2247
    mean = clamp_float(mean, 0.0f, 1.0f);
×
2248

2249
    float_buffer_free(&kernel);
×
2250
    float_buffer_free(&mu_x);
×
2251
    float_buffer_free(&mu_y);
×
2252
    float_buffer_free(&mu_x2);
×
2253
    float_buffer_free(&mu_y2);
×
2254
    float_buffer_free(&mu_xy);
×
2255
    float_buffer_free(&sigma_x2);
×
2256
    float_buffer_free(&sigma_y2);
×
2257
    float_buffer_free(&sigma_xy);
×
2258
    float_buffer_free(&x_sq);
×
2259
    float_buffer_free(&y_sq);
×
2260
    float_buffer_free(&xy_buf);
×
2261

2262
    return mean;
×
2263
}
2264

2265
static sixel_assessment_float_buffer_t
2266
downsample2(const sixel_assessment_float_buffer_t *img, int width, int height,
×
2267
            int *new_width, int *new_height)
2268
{
2269
    int h2;
×
2270
    int w2;
×
2271
    int y;
×
2272
    int x;
×
2273
    sixel_assessment_float_buffer_t out;
×
2274
    float sum;
×
2275
    int idx0;
×
2276
    int idx1;
×
2277
    int idx2;
×
2278
    int idx3;
×
2279

2280
    h2 = height / 2;
×
2281
    w2 = width / 2;
×
2282
    out = float_buffer_create((size_t)h2 * (size_t)w2);
×
2283
    for (y = 0; y < h2; ++y) {
×
2284
        for (x = 0; x < w2; ++x) {
×
2285
            sum = 0.0f;
×
2286
            idx0 = (2 * y) * width + (2 * x);
×
2287
            idx1 = (2 * y + 1) * width + (2 * x);
×
2288
            idx2 = (2 * y) * width + (2 * x + 1);
×
2289
            idx3 = (2 * y + 1) * width + (2 * x + 1);
×
2290
            sum += img->values[idx0];
×
2291
            sum += img->values[idx1];
×
2292
            sum += img->values[idx2];
×
2293
            sum += img->values[idx3];
×
2294
            out.values[y * w2 + x] = sum * 0.25f;
×
2295
        }
2296
    }
2297
    *new_width = w2;
×
2298
    *new_height = h2;
×
2299
    return out;
×
2300
}
2301

2302
static float
2303
ms_ssim_luma(const sixel_assessment_float_buffer_t *ref,
×
2304
             const sixel_assessment_float_buffer_t *out,
2305
             int width,
2306
             int height)
2307
{
2308
    static const float weights[5] = {
×
2309
        0.0448f, 0.2856f, 0.3001f, 0.2363f, 0.1333f
2310
    };
2311
    sixel_assessment_float_buffer_t cur_ref;
×
2312
    sixel_assessment_float_buffer_t cur_out;
×
2313
    sixel_assessment_float_buffer_t next_ref;
×
2314
    sixel_assessment_float_buffer_t next_out;
×
2315
    int cur_width;
×
2316
    int cur_height;
×
2317
    float weighted_sum;
×
2318
    float weight_total;
×
2319
    int level;
×
2320
    float ssim_value;
×
2321
    int next_width;
×
2322
    int next_height;
×
2323

2324
    cur_ref = float_buffer_create((size_t)width * (size_t)height);
×
2325
    cur_out = float_buffer_create((size_t)width * (size_t)height);
×
2326
    memcpy(cur_ref.values, ref->values,
×
2327
           sizeof(float) * (size_t)width * (size_t)height);
2328
    memcpy(cur_out.values, out->values,
×
2329
           sizeof(float) * (size_t)width * (size_t)height);
2330
    cur_width = width;
×
2331
    cur_height = height;
×
2332
    weighted_sum = 0.0f;
×
2333
    weight_total = 0.0f;
×
2334

2335
    for (level = 0; level < 5; ++level) {
×
2336
        ssim_value = ssim_luma(&cur_ref, &cur_out, cur_width, cur_height,
×
2337
                               0.01f, 0.03f, 11, 1.5f);
2338
        weighted_sum += ssim_value * weights[level];
×
2339
        weight_total += weights[level];
×
2340
        if (level < 4) {
×
2341
            next_ref = downsample2(&cur_ref, cur_width, cur_height,
×
2342
                                   &next_width, &next_height);
2343
            next_out = downsample2(&cur_out, cur_width, cur_height,
×
2344
                                   &next_width, &next_height);
2345
            float_buffer_free(&cur_ref);
×
2346
            float_buffer_free(&cur_out);
×
2347
            cur_ref = next_ref;
×
2348
            cur_out = next_out;
×
2349
            cur_width = next_width;
×
2350
            cur_height = next_height;
×
2351
        }
2352
    }
2353

2354
    float_buffer_free(&cur_ref);
×
2355
    float_buffer_free(&cur_out);
×
2356

2357
    if (weight_total > 0.0f) {
×
2358
        return weighted_sum / weight_total;
×
2359
    }
2360
    return 0.0f;
2361
}
2362
/*
2363
 * FFT helpers for spectral metrics
2364
 */
2365
static int
2366
next_power_of_two(int value)
×
2367
{
2368
    int n;
×
2369
    n = 1;
×
2370
    while (n < value) {
×
2371
        n <<= 1;
×
2372
    }
2373
    return n;
×
2374
}
2375

2376
static void
2377
fft_bit_reverse(sixel_assessment_complex_t *data, int n)
×
2378
{
2379
    int i;
×
2380
    int j;
×
2381
    int bit;
×
2382
    sixel_assessment_complex_t tmp;
×
2383

2384
    j = 0;
×
2385
    for (i = 0; i < n; ++i) {
×
2386
        if (i < j) {
×
2387
            tmp = data[i];
×
2388
            data[i] = data[j];
×
2389
            data[j] = tmp;
×
2390
        }
2391
        bit = n >> 1;
×
2392
        while ((j & bit) != 0) {
×
2393
            j &= ~bit;
×
2394
            bit >>= 1;
×
2395
        }
2396
        j |= bit;
×
2397
    }
2398
}
×
2399

2400
static void
2401
fft_cooley_tukey(sixel_assessment_complex_t *data, int n, int inverse)
×
2402
{
2403
    int len;
×
2404
    double angle;
×
2405
    sixel_assessment_complex_t wlen;
×
2406
    int half;
×
2407
    int i;
×
2408
    sixel_assessment_complex_t w;
×
2409
    int j;
×
2410
    sixel_assessment_complex_t u;
×
2411
    sixel_assessment_complex_t v;
×
2412
    double tmp_re;
×
2413
    double tmp_im;
×
2414

2415
    fft_bit_reverse(data, n);
×
2416
    for (len = 2; len <= n; len <<= 1) {
×
2417

2418
        angle = 2.0 * M_PI / (double)len;
×
2419
        if (inverse) {
×
2420
            angle = -angle;
×
2421
        }
2422
        wlen.re = cos(angle);
×
2423
        wlen.im = sin(angle);
×
2424
        half = len >> 1;
×
2425
        for (i = 0; i < n; i += len) {
×
2426
            w.re = 1.0;
2427
            w.im = 0.0;
2428
            for (j = 0; j < half; ++j) {
×
2429
                u = data[i + j];
×
2430
                v.re = data[i + j + half].re * w.re -
×
2431
                       data[i + j + half].im * w.im;
×
2432
                v.im = data[i + j + half].re * w.im +
×
2433
                       data[i + j + half].im * w.re;
×
2434
                data[i + j].re = u.re + v.re;
×
2435
                data[i + j].im = u.im + v.im;
×
2436
                data[i + j + half].re = u.re - v.re;
×
2437
                data[i + j + half].im = u.im - v.im;
×
2438
                tmp_re = w.re * wlen.re - w.im * wlen.im;
×
2439
                tmp_im = w.re * wlen.im + w.im * wlen.re;
×
2440
                w.re = tmp_re;
×
2441
                w.im = tmp_im;
×
2442
            }
2443
        }
2444
    }
2445
    if (inverse) {
×
2446
        for (i = 0; i < n; ++i) {
×
2447
            data[i].re /= n;
×
2448
            data[i].im /= n;
×
2449
        }
2450
    }
2451
}
×
2452

2453
static void
2454
fft2d(sixel_assessment_float_buffer_t *input, int width, int height,
×
2455
      sixel_assessment_complex_t *output, int out_width, int out_height)
2456
{
2457
    int padded_width;
×
2458
    int padded_height;
×
2459
    int y;
×
2460
    int x;
×
2461
    sixel_assessment_complex_t *row;
×
2462
    sixel_assessment_complex_t *col;
×
2463
    sixel_assessment_complex_t value;
×
2464

2465
    padded_width = out_width;
×
2466
    padded_height = out_height;
×
2467
    row = (sixel_assessment_complex_t *)xmalloc(
×
2468
        sizeof(sixel_assessment_complex_t) * (size_t)padded_width);
×
2469
    col = (sixel_assessment_complex_t *)xmalloc(
×
2470
        sizeof(sixel_assessment_complex_t) * (size_t)padded_height);
×
2471

2472
    for (y = 0; y < padded_height; ++y) {
×
2473
        for (x = 0; x < padded_width; ++x) {
×
2474
            if (y < height && x < width) {
×
2475
                value.re = input->values[y * width + x];
×
2476
                value.im = 0.0;
×
2477
            } else {
2478
                value.re = 0.0;
2479
                value.im = 0.0;
2480
            }
2481
            output[y * padded_width + x] = value;
×
2482
        }
2483
    }
2484

2485
    for (y = 0; y < padded_height; ++y) {
×
2486
        for (x = 0; x < padded_width; ++x) {
×
2487
            row[x] = output[y * padded_width + x];
×
2488
        }
2489
        fft_cooley_tukey(row, padded_width, 0);
×
2490
        for (x = 0; x < padded_width; ++x) {
×
2491
            output[y * padded_width + x] = row[x];
×
2492
        }
2493
    }
2494

2495
    for (x = 0; x < padded_width; ++x) {
×
2496
        for (y = 0; y < padded_height; ++y) {
×
2497
            col[y] = output[y * padded_width + x];
×
2498
        }
2499
        fft_cooley_tukey(col, padded_height, 0);
×
2500
        for (y = 0; y < padded_height; ++y) {
×
2501
            output[y * padded_width + x] = col[y];
×
2502
        }
2503
    }
2504

2505
    free(row);
×
2506
    free(col);
×
2507
}
×
2508

2509
static void
2510
fft_shift(sixel_assessment_complex_t *data, int width, int height)
×
2511
{
2512
    int half_w;
×
2513
    int half_h;
×
2514
    int y;
×
2515
    int x;
×
2516
    int nx;
×
2517
    int ny;
×
2518
    sixel_assessment_complex_t tmp;
×
2519

2520
    half_w = width / 2;
×
2521
    half_h = height / 2;
×
2522
    for (y = 0; y < height; ++y) {
×
2523
        for (x = 0; x < width; ++x) {
×
2524
            nx = (x + half_w) % width;
×
2525
            ny = (y + half_h) % height;
×
2526
            if (ny > y || (ny == y && nx > x)) {
×
2527
                continue;
×
2528
            }
2529
            tmp = data[y * width + x];
×
2530
            data[y * width + x] = data[ny * width + nx];
×
2531
            data[ny * width + nx] = tmp;
×
2532
        }
2533
    }
2534
}
×
2535
static float
2536
high_frequency_ratio(const sixel_assessment_float_buffer_t *img,
×
2537
                     int width, int height, float cutoff)
2538
{
2539
    int padded_width;
×
2540
    int padded_height;
×
2541
    sixel_assessment_complex_t *freq;
×
2542
    size_t total;
×
2543
    sixel_assessment_float_buffer_t centered;
×
2544
    double hi_sum;
×
2545
    double total_sum;
×
2546
    int y;
×
2547
    int x;
×
2548
    double cy;
×
2549
    double cx;
×
2550
    double mean;
×
2551
    size_t i;
×
2552
    double dy;
×
2553
    double dx;
×
2554
    double r;
×
2555
    double norm;
×
2556
    double power;
×
2557

2558
    padded_width = next_power_of_two(width);
×
2559
    padded_height = next_power_of_two(height);
×
2560
    total = (size_t)padded_width * (size_t)padded_height;
×
2561
    freq = (sixel_assessment_complex_t *)xmalloc(
×
2562
        total * sizeof(sixel_assessment_complex_t));
2563
    centered = float_buffer_create((size_t)width * (size_t)height);
×
2564

2565
    for (y = 0; y < height; ++y) {
×
2566
        for (x = 0; x < width; ++x) {
×
2567
            centered.values[y * width + x] = img->values[y * width + x];
×
2568
        }
2569
    }
2570
    mean = 0.0;
2571
    for (i = 0; i < (size_t)width * (size_t)height; ++i) {
×
2572
        mean += centered.values[i];
×
2573
    }
2574
    mean /= (double)((size_t)width * (size_t)height);
×
2575
    for (i = 0; i < (size_t)width * (size_t)height; ++i) {
×
2576
        centered.values[i] -= (float)mean;
×
2577
    }
2578

2579
    fft2d(&centered, width, height, freq, padded_width, padded_height);
×
2580
    fft_shift(freq, padded_width, padded_height);
×
2581

2582
    hi_sum = 0.0;
×
2583
    total_sum = 0.0;
×
2584
    cy = padded_height / 2.0;
×
2585
    cx = padded_width / 2.0;
×
2586

2587
    for (y = 0; y < padded_height; ++y) {
×
2588
        for (x = 0; x < padded_width; ++x) {
×
2589
            dy = (double)y - cy;
×
2590
            dx = (double)x - cx;
×
2591
            r = sqrt(dy * dy + dx * dx);
×
2592
            norm = r / (0.5 * sqrt((double)padded_height *
×
2593
                                   (double)padded_height +
2594
                                   (double)padded_width *
×
2595
                                   (double)padded_width));
2596
            power = freq[y * padded_width + x].re *
×
2597
                    freq[y * padded_width + x].re +
2598
                    freq[y * padded_width + x].im *
×
2599
                    freq[y * padded_width + x].im;
2600
            total_sum += power;
×
2601
            if (norm >= cutoff) {
×
2602
                hi_sum += power;
×
2603
            }
2604
        }
2605
    }
2606

2607
    free(freq);
×
2608
    float_buffer_free(&centered);
×
2609

2610
    if (total_sum <= 0.0) {
×
2611
        return 0.0f;
2612
    }
2613
    return (float)(hi_sum / total_sum);
×
2614
}
2615

2616
static float
2617
stripe_score(const sixel_assessment_float_buffer_t *img, int width, int height,
×
2618
             int bins)
2619
{
2620
    int padded_width;
×
2621
    int padded_height;
×
2622
    sixel_assessment_complex_t *freq;
×
2623
    sixel_assessment_float_buffer_t centered;
×
2624
    double cy;
×
2625
    double cx;
×
2626
    double rmin;
×
2627
    double *hist;
×
2628
    int y;
×
2629
    int x;
×
2630
    double mean_val;
×
2631
    double max_val;
×
2632
    double mean;
×
2633
    size_t i;
×
2634
    double dy;
×
2635
    double dx;
×
2636
    double r;
×
2637
    double ang;
×
2638
    int index;
×
2639
    double power;
×
2640

2641
    padded_width = next_power_of_two(width);
×
2642
    padded_height = next_power_of_two(height);
×
2643
    freq = (sixel_assessment_complex_t *)xmalloc(
×
2644
        sizeof(sixel_assessment_complex_t) *
2645
        (size_t)padded_width * (size_t)padded_height);
×
2646
    centered = float_buffer_create((size_t)width * (size_t)height);
×
2647
    for (y = 0; y < height; ++y) {
×
2648
        for (x = 0; x < width; ++x) {
×
2649
            centered.values[y * width + x] = img->values[y * width + x];
×
2650
        }
2651
    }
2652
    mean = 0.0;
2653
    for (i = 0; i < (size_t)width * (size_t)height; ++i) {
×
2654
        mean += centered.values[i];
×
2655
    }
2656
    mean /= (double)((size_t)width * (size_t)height);
×
2657
    for (i = 0; i < (size_t)width * (size_t)height; ++i) {
×
2658
        centered.values[i] -= (float)mean;
×
2659
    }
2660

2661
    fft2d(&centered, width, height, freq, padded_width, padded_height);
×
2662
    fft_shift(freq, padded_width, padded_height);
×
2663

2664
    hist = (double *)xcalloc((size_t)bins, sizeof(double));
×
2665
    cy = padded_height / 2.0;
×
2666
    cx = padded_width / 2.0;
×
2667
    rmin = 0.05 * (double)(width > height ? width : height);
×
2668

2669
    for (y = 0; y < padded_height; ++y) {
×
2670
        for (x = 0; x < padded_width; ++x) {
×
2671
            dy = (double)y - cy;
×
2672
            dx = (double)x - cx;
×
2673
            r = sqrt(dy * dy + dx * dx);
×
2674
            if (r < rmin) {
×
2675
                continue;
×
2676
            }
2677
            ang = atan2(dy, dx);
×
2678
            if (ang < 0.0) {
×
2679
                ang += M_PI;
×
2680
            }
2681
            index = (int)(ang / M_PI * bins);
×
2682
            if (index < 0) {
×
2683
                index = 0;
2684
            }
2685
            if (index >= bins) {
×
2686
                index = bins - 1;
×
2687
            }
2688
            power = freq[y * padded_width + x].re *
×
2689
                    freq[y * padded_width + x].re +
2690
                    freq[y * padded_width + x].im *
×
2691
                    freq[y * padded_width + x].im;
2692
            hist[index] += power;
×
2693
        }
2694
    }
2695

2696
    mean_val = 0.0;
2697
    for (x = 0; x < bins; ++x) {
×
2698
        mean_val += hist[x];
×
2699
    }
2700
    mean_val = mean_val / (double)bins + 1e-12;
×
2701

2702
    max_val = hist[0];
×
2703
    for (x = 1; x < bins; ++x) {
×
2704
        if (hist[x] > max_val) {
×
2705
            max_val = hist[x];
2706
        }
2707
    }
2708

2709
    free(hist);
×
2710
    free(freq);
×
2711
    float_buffer_free(&centered);
×
2712

2713
    return (float)(max_val / mean_val);
×
2714
}
2715
/*
2716
 * Banding metrics (run-length and gradient-based)
2717
 */
2718
static float
2719
banding_index_runlen(const sixel_assessment_float_buffer_t *img,
×
2720
                     int width, int height, int levels)
2721
{
2722
    int y;
×
2723
    int x;
×
2724
    double total_runs;
×
2725
    double total_segments;
×
2726
    int prev;
×
2727
    int run_len;
×
2728
    int segs;
×
2729
    int runs_sum;
×
2730
    int value;
×
2731

2732
    total_runs = 0.0;
×
2733
    total_segments = 0.0;
×
2734
    for (y = 0; y < height; ++y) {
×
2735
        prev = (int)clamp_float(img->values[y * width] * (levels - 1) + 0.5f,
×
2736
                                0.0f, (float)(levels - 1));
×
2737
        run_len = 1;
×
2738
        segs = 0;
×
2739
        runs_sum = 0;
×
2740
        for (x = 1; x < width; ++x) {
×
2741
            value = (int)clamp_float(
×
2742
                img->values[y * width + x] * (levels - 1) + 0.5f,
×
2743
                0.0f, (float)(levels - 1));
2744
            if (value == prev) {
×
2745
                run_len += 1;
×
2746
            } else {
2747
                runs_sum += run_len;
×
2748
                segs += 1;
×
2749
                run_len = 1;
×
2750
                prev = value;
×
2751
            }
2752
        }
2753
        runs_sum += run_len;
×
2754
        segs += 1;
×
2755
        total_runs += (double)runs_sum / (double)segs;
×
2756
        total_segments += 1.0;
×
2757
    }
2758
    if (total_segments <= 0.0) {
×
2759
        return 0.0f;
2760
    }
2761
    return (float)((total_runs / total_segments) / (double)width);
×
2762
}
2763

2764
static sixel_assessment_float_buffer_t
2765
gaussian_blur(const sixel_assessment_float_buffer_t *img, int width,
×
2766
              int height, float sigma, int ksize)
2767
{
2768
    sixel_assessment_float_buffer_t kernel;
×
2769
    sixel_assessment_float_buffer_t blurred;
×
2770

2771
    kernel = gaussian_kernel1d(ksize, sigma);
×
2772
    blurred = separable_conv2d(img, width, height, &kernel);
×
2773
    float_buffer_free(&kernel);
×
2774
    return blurred;
×
2775
}
2776

2777
static void
2778
finite_diff(const sixel_assessment_float_buffer_t *img,
×
2779
            int width,
2780
            int height,
2781
            sixel_assessment_float_buffer_t *dx,
2782
            sixel_assessment_float_buffer_t *dy)
2783
{
2784
    int x;
×
2785
    int y;
×
2786
    int xm1;
×
2787
    int xp1;
×
2788
    int ym1;
×
2789
    int yp1;
×
2790
    float vxm1;
×
2791
    float vxp1;
×
2792
    float vym1;
×
2793
    float vyp1;
×
2794

2795
    *dx = float_buffer_create((size_t)width * (size_t)height);
×
2796
    *dy = float_buffer_create((size_t)width * (size_t)height);
×
2797

2798
    for (y = 0; y < height; ++y) {
×
2799
        for (x = 0; x < width; ++x) {
×
2800
            xm1 = x - 1;
×
2801
            xp1 = x + 1;
×
2802
            ym1 = y - 1;
×
2803
            yp1 = y + 1;
×
2804
            if (xm1 < 0) {
×
2805
                xm1 = 0;
2806
            }
2807
            if (xp1 >= width) {
×
2808
                xp1 = width - 1;
×
2809
            }
2810
            if (ym1 < 0) {
×
2811
                ym1 = 0;
2812
            }
2813
            if (yp1 >= height) {
×
2814
                yp1 = height - 1;
×
2815
            }
2816
            vxm1 = img->values[y * width + xm1];
×
2817
            vxp1 = img->values[y * width + xp1];
×
2818
            vym1 = img->values[ym1 * width + x];
×
2819
            vyp1 = img->values[yp1 * width + x];
×
2820
            dx->values[y * width + x] = (vxp1 - vxm1) * 0.5f;
×
2821
            dy->values[y * width + x] = (vyp1 - vym1) * 0.5f;
×
2822
        }
2823
    }
2824
}
×
2825

2826
static int
2827
compare_floats(const void *a, const void *b)
×
2828
{
2829
    float fa;
×
2830
    float fb;
×
2831
    fa = *(const float *)a;
×
2832
    fb = *(const float *)b;
×
2833
    if (fa < fb) {
×
2834
        return -1;
2835
    }
2836
    if (fa > fb) {
×
2837
        return 1;
×
2838
    }
2839
    return 0;
2840
}
2841

2842
static float
2843
banding_index_gradient(const sixel_assessment_float_buffer_t *img,
×
2844
                       int width, int height)
2845
{
2846
    sixel_assessment_float_buffer_t blurred;
×
2847
    sixel_assessment_float_buffer_t dx;
×
2848
    sixel_assessment_float_buffer_t dy;
×
2849
    sixel_assessment_float_buffer_t grad;
×
2850
    size_t total;
×
2851
    float *sorted;
×
2852
    double g99;
×
2853
    int bins;
×
2854
    double *hist;
×
2855
    double *centers;
×
2856
    int half;
×
2857
    int i;
×
2858
    double sum_hist;
×
2859
    double sum_residual;
×
2860
    double zero_thresh;
×
2861
    double zero_mass;
×
2862
    double sum_x;
×
2863
    double sum_y;
×
2864
    double sum_xx;
×
2865
    double sum_xy;
×
2866
    int count;
×
2867
    double slope;
×
2868
    double intercept;
×
2869
    double gx;
×
2870
    double gy;
×
2871
    size_t idx_pos;
×
2872
    double value;
×
2873
    int bin_index;
×
2874
    double xval;
×
2875
    double yval;
×
2876
    double denom;
×
2877
    double env;
×
2878
    double resid;
×
2879

2880
    blurred = gaussian_blur(img, width, height, 1.0f, 7);
×
2881
    finite_diff(&blurred, width, height, &dx, &dy);
×
2882
    grad = float_buffer_create((size_t)width * (size_t)height);
×
2883

2884
    total = (size_t)width * (size_t)height;
×
2885
    for (i = 0; (size_t)i < total; ++i) {
×
2886
        gx = dx.values[i];
×
2887
        gy = dy.values[i];
×
2888
        grad.values[i] = (float)sqrt(gx * gx + gy * gy);
×
2889
    }
2890

2891
    sorted = (float *)xmalloc(total * sizeof(float));
×
2892
    memcpy(sorted, grad.values, total * sizeof(float));
×
2893
    qsort(sorted, total, sizeof(float), compare_floats);
×
2894
    if (total == 0) {
×
2895
        g99 = 0.0;
2896
    } else {
2897
        idx_pos = (size_t)((double)(total - 1) * 0.995);
×
2898
        g99 = sorted[idx_pos];
×
2899
    }
2900
    g99 += 1e-9;
×
2901
    free(sorted);
×
2902

2903
    bins = 128;
×
2904
    hist = (double *)xcalloc((size_t)bins, sizeof(double));
×
2905
    centers = (double *)xmalloc((size_t)bins * sizeof(double));
×
2906
    for (i = 0; i < bins; ++i) {
×
2907
        centers[i] = ((double)i + 0.5) * (g99 / (double)bins);
×
2908
    }
2909

2910
    for (i = 0; (size_t)i < total; ++i) {
×
2911
        if (grad.values[i] > (float)g99) {
×
2912
            grad.values[i] = (float)g99;
×
2913
        }
2914
        value = grad.values[i];
×
2915
        bin_index = (int)(value / g99 * bins);
×
2916
        if (bin_index < 0) {
×
2917
            bin_index = 0;
2918
        }
2919
        if (bin_index >= bins) {
×
2920
            bin_index = bins - 1;
2921
        }
2922
        hist[bin_index] += 1.0;
×
2923
    }
2924

2925
    for (i = 0; i < bins; ++i) {
×
2926
        hist[i] += 1e-12;
×
2927
    }
2928

2929
    half = bins / 2;
2930
    sum_x = 0.0;
2931
    sum_y = 0.0;
2932
    sum_xx = 0.0;
2933
    sum_xy = 0.0;
2934
    count = bins - half;
2935
    for (i = half; i < bins; ++i) {
×
2936
        xval = centers[i];
×
2937
        yval = log(hist[i]);
×
2938
        sum_x += xval;
×
2939
        sum_y += yval;
×
2940
        sum_xx += xval * xval;
×
2941
        sum_xy += xval * yval;
×
2942
    }
2943
    slope = 0.0;
×
2944
    intercept = 0.0;
×
2945
    if (count > 1) {
×
2946
        denom = (double)count * sum_xx - sum_x * sum_x;
×
2947
        if (fabs(denom) > 1e-12) {
×
2948
            slope = ((double)count * sum_xy - sum_x * sum_y) / denom;
×
2949
            intercept = (sum_y - slope * sum_x) / (double)count;
×
2950
        }
2951
    }
2952

2953
    sum_hist = 0.0;
×
2954
    sum_residual = 0.0;
×
2955
    for (i = 0; i < bins; ++i) {
×
2956
        env = exp(intercept + slope * centers[i]);
×
2957
        resid = hist[i] - env;
×
2958
        if (resid < 0.0) {
×
2959
            resid = 0.0;
2960
        }
2961
        sum_hist += hist[i];
×
2962
        sum_residual += resid;
×
2963
    }
2964

2965
    zero_thresh = 0.01 * g99;
×
2966
    zero_mass = 0.0;
×
2967
    if (total > 0) {
×
2968
        for (i = 0; (size_t)i < total; ++i) {
×
2969
            if (grad.values[i] <= (float)zero_thresh) {
×
2970
                zero_mass += 1.0;
×
2971
            }
2972
        }
2973
        zero_mass /= (double)total;
×
2974
    }
2975

2976
    float_buffer_free(&blurred);
×
2977
    float_buffer_free(&dx);
×
2978
    float_buffer_free(&dy);
×
2979
    float_buffer_free(&grad);
×
2980
    free(hist);
×
2981
    free(centers);
×
2982

2983
    if (sum_hist <= 0.0) {
×
2984
        return 0.0f;
2985
    }
2986
    return (float)(0.6 * (sum_residual / sum_hist) + 0.4 * zero_mass);
×
2987
}
2988
/*
2989
 * Clipping statistics
2990
 */
2991
static void clipping_rates(const float *pixels,
×
2992
                           int width,
2993
                           int height,
2994
                           float *clip_l,
2995
                           float *clip_r,
2996
                           float *clip_g,
2997
                           float *clip_b)
2998
{
2999
    sixel_assessment_float_buffer_t luma;
×
3000
    sixel_assessment_float_buffer_t rch;
×
3001
    sixel_assessment_float_buffer_t gch;
×
3002
    sixel_assessment_float_buffer_t bch;
×
3003
    size_t total;
×
3004
    size_t i;
×
3005
    double eps;
×
3006
    double lo;
×
3007
    double hi;
×
3008

3009
    luma = pixels_to_luma709(pixels, width, height);
×
3010
    rch = pixels_channel(pixels, width, height, 0);
×
3011
    gch = pixels_channel(pixels, width, height, 1);
×
3012
    bch = pixels_channel(pixels, width, height, 2);
×
3013
    total = (size_t)width * (size_t)height;
×
3014
    eps = 1e-6;
×
3015

3016
    lo = 0.0;
×
3017
    hi = 0.0;
×
3018
    for (i = 0; i < total; ++i) {
×
3019
        if (luma.values[i] <= eps) {
×
3020
            lo += 1.0;
×
3021
        }
3022
        if (luma.values[i] >= 1.0 - eps) {
×
3023
            hi += 1.0;
×
3024
        }
3025
    }
3026
    *clip_l = (float)((lo + hi) / (double)total);
×
3027

3028
    lo = hi = 0.0;
×
3029
    for (i = 0; i < total; ++i) {
×
3030
        if (rch.values[i] <= eps) {
×
3031
            lo += 1.0;
×
3032
        }
3033
        if (rch.values[i] >= 1.0 - eps) {
×
3034
            hi += 1.0;
×
3035
        }
3036
    }
3037
    *clip_r = (float)((lo + hi) / (double)total);
×
3038

3039
    lo = hi = 0.0;
×
3040
    for (i = 0; i < total; ++i) {
×
3041
        if (gch.values[i] <= eps) {
×
3042
            lo += 1.0;
×
3043
        }
3044
        if (gch.values[i] >= 1.0 - eps) {
×
3045
            hi += 1.0;
×
3046
        }
3047
    }
3048
    *clip_g = (float)((lo + hi) / (double)total);
×
3049

3050
    lo = hi = 0.0;
×
3051
    for (i = 0; i < total; ++i) {
×
3052
        if (bch.values[i] <= eps) {
×
3053
            lo += 1.0;
×
3054
        }
3055
        if (bch.values[i] >= 1.0 - eps) {
×
3056
            hi += 1.0;
×
3057
        }
3058
    }
3059
    *clip_b = (float)((lo + hi) / (double)total);
×
3060

3061
    float_buffer_free(&luma);
×
3062
    float_buffer_free(&rch);
×
3063
    float_buffer_free(&gch);
×
3064
    float_buffer_free(&bch);
×
3065
}
×
3066

3067
/*
3068
 * sRGB <-> CIELAB conversions
3069
 */
3070
static void
3071
srgb_to_linear(const float *src, float *dst, size_t count)
×
3072
{
3073
    size_t i;
×
3074
    float c;
×
3075
    float result;
×
3076
    for (i = 0; i < count; ++i) {
×
3077
        c = src[i];
×
3078
        if (c <= 0.04045f) {
×
3079
            result = c / 12.92f;
×
3080
        } else {
3081
            result = powf((c + 0.055f) / 1.055f, 2.4f);
×
3082
        }
3083
        dst[i] = result;
×
3084
    }
3085
}
×
3086

3087
static void
3088
linear_to_xyz(const float *rgb, float *xyz, size_t pixels)
×
3089
{
3090
    size_t i;
×
3091
    float r;
×
3092
    float g;
×
3093
    float b;
×
3094
    float X;
×
3095
    float Y;
×
3096
    float Z;
×
3097
    for (i = 0; i < pixels; ++i) {
×
3098
        r = rgb[i * 3 + 0];
×
3099
        g = rgb[i * 3 + 1];
×
3100
        b = rgb[i * 3 + 2];
×
3101
        X = 0.4124564f * r + 0.3575761f * g + 0.1804375f * b;
×
3102
        Y = 0.2126729f * r + 0.7151522f * g + 0.0721750f * b;
×
3103
        Z = 0.0193339f * r + 0.1191920f * g + 0.9503041f * b;
×
3104
        xyz[i * 3 + 0] = X;
×
3105
        xyz[i * 3 + 1] = Y;
×
3106
        xyz[i * 3 + 2] = Z;
×
3107
    }
3108
}
×
3109

3110
static float
3111
f_lab(float t)
×
3112
{
3113
    float delta;
×
3114
    delta = 6.0f / 29.0f;
×
3115
    if (t > delta * delta * delta) {
×
3116
        return cbrtf(t);
×
3117
    }
3118
    return t / (3.0f * delta * delta) + 4.0f / 29.0f;
×
3119
}
3120

3121
static void
3122
xyz_to_lab(const float *xyz, float *lab, size_t pixels)
×
3123
{
3124
    const float Xn = 0.95047f;
×
3125
    const float Yn = 1.00000f;
×
3126
    const float Zn = 1.08883f;
×
3127
    size_t i;
×
3128
    float X;
×
3129
    float Y;
×
3130
    float Z;
×
3131
    float fx;
×
3132
    float fy;
×
3133
    float fz;
×
3134
    float L;
×
3135
    float a;
×
3136
    float b;
×
3137
    for (i = 0; i < pixels; ++i) {
×
3138
        X = xyz[i * 3 + 0] / Xn;
×
3139
        Y = xyz[i * 3 + 1] / Yn;
×
3140
        Z = xyz[i * 3 + 2] / Zn;
×
3141
        fx = f_lab(X);
×
3142
        fy = f_lab(Y);
×
3143
        fz = f_lab(Z);
×
3144
        L = 116.0f * fy - 16.0f;
×
3145
        a = 500.0f * (fx - fy);
×
3146
        b = 200.0f * (fy - fz);
×
3147
        lab[i * 3 + 0] = L;
×
3148
        lab[i * 3 + 1] = a;
×
3149
        lab[i * 3 + 2] = b;
×
3150
    }
3151
}
×
3152

3153
static sixel_assessment_float_buffer_t
3154
rgb_to_lab(const float *pixels, int width, int height)
×
3155
{
3156
    sixel_assessment_float_buffer_t lab;
×
3157
    float *linear;
×
3158
    float *xyz;
×
3159
    size_t pixel_count;
×
3160

3161
    pixel_count = (size_t)width * (size_t)height;
×
3162
    lab = float_buffer_create(pixel_count * 3);
×
3163
    linear = (float *)xmalloc(pixel_count * 3 * sizeof(float));
×
3164
    xyz = (float *)xmalloc(pixel_count * 3 * sizeof(float));
×
3165
    srgb_to_linear(pixels, linear, pixel_count * 3);
×
3166
    linear_to_xyz(linear, xyz, pixel_count);
×
3167
    xyz_to_lab(xyz, lab.values, pixel_count);
×
3168
    free(linear);
×
3169
    free(xyz);
×
3170
    return lab;
×
3171
}
3172

3173
static sixel_assessment_float_buffer_t
3174
chroma_ab(const sixel_assessment_float_buffer_t *lab, size_t pixels)
×
3175
{
3176
    sixel_assessment_float_buffer_t chroma;
×
3177
    size_t i;
×
3178
    float a;
×
3179
    float b;
×
3180
    chroma = float_buffer_create(pixels);
×
3181
    for (i = 0; i < pixels; ++i) {
×
3182
        a = lab->values[i * 3 + 1];
×
3183
        b = lab->values[i * 3 + 2];
×
3184
        chroma.values[i] = sqrtf(a * a + b * b);
×
3185
    }
3186
    return chroma;
×
3187
}
3188

3189
static sixel_assessment_float_buffer_t
3190
deltaE00(const sixel_assessment_float_buffer_t *lab1,
×
3191
         const sixel_assessment_float_buffer_t *lab2, size_t pixels)
3192
{
3193
    sixel_assessment_float_buffer_t out;
×
3194
    size_t i;
×
3195
    double L1;
×
3196
    double a1;
×
3197
    double b1;
×
3198
    double L2;
×
3199
    double a2;
×
3200
    double b2;
×
3201
    double Lbar;
×
3202
    double C1;
×
3203
    double C2;
×
3204
    double Cbar;
×
3205
    double G;
×
3206
    double a1p;
×
3207
    double a2p;
×
3208
    double C1p;
×
3209
    double C2p;
×
3210
    double h1p;
×
3211
    double h2p;
×
3212
    double dLp;
×
3213
    double dCp;
×
3214
    double dhp;
×
3215
    double dHp;
×
3216
    double Lpm;
×
3217
    double Cpm;
×
3218
    double hp_sum;
×
3219
    double hp_diff;
×
3220
    double Hpm;
×
3221
    double T;
×
3222
    double Sl;
×
3223
    double Sc;
×
3224
    double Sh;
×
3225
    double dTheta;
×
3226
    double Rc;
×
3227
    double Rt;
×
3228
    double de;
×
3229
    out = float_buffer_create(pixels);
×
3230
    for (i = 0; i < pixels; ++i) {
×
3231
        L1 = lab1->values[i * 3 + 0];
×
3232
        a1 = lab1->values[i * 3 + 1];
×
3233
        b1 = lab1->values[i * 3 + 2];
×
3234
        L2 = lab2->values[i * 3 + 0];
×
3235
        a2 = lab2->values[i * 3 + 1];
×
3236
        b2 = lab2->values[i * 3 + 2];
×
3237
        Lbar = 0.5 * (L1 + L2);
×
3238
        C1 = sqrt(a1 * a1 + b1 * b1);
×
3239
        C2 = sqrt(a2 * a2 + b2 * b2);
×
3240
        Cbar = 0.5 * (C1 + C2);
×
3241
        G = 0.5 * (1.0 - sqrt(pow(Cbar, 7.0) /
×
3242
                               (pow(Cbar, 7.0) + pow(25.0, 7.0) + 1e-12)));
×
3243
        a1p = (1.0 + G) * a1;
×
3244
        a2p = (1.0 + G) * a2;
×
3245
        C1p = sqrt(a1p * a1p + b1 * b1);
×
3246
        C2p = sqrt(a2p * a2p + b2 * b2);
×
3247
        h1p = atan2(b1, a1p);
×
3248
        if (h1p < 0.0) {
×
3249
            h1p += 2.0 * M_PI;
×
3250
        }
3251
        h2p = atan2(b2, a2p);
×
3252
        if (h2p < 0.0) {
×
3253
            h2p += 2.0 * M_PI;
×
3254
        }
3255
        dLp = L2 - L1;
×
3256
        dCp = C2p - C1p;
×
3257
        dhp = h2p - h1p;
×
3258
        if (dhp > M_PI) {
×
3259
            dhp -= 2.0 * M_PI;
×
3260
        }
3261
        if (dhp < -M_PI) {
×
3262
            dhp += 2.0 * M_PI;
×
3263
        }
3264
        dHp = 2.0 * sqrt(C1p * C2p + 1e-12) * sin(dhp / 2.0);
×
3265
        Lpm = Lbar;
×
3266
        Cpm = 0.5 * (C1p + C2p);
×
3267
        hp_sum = h1p + h2p;
×
3268
        hp_diff = fabs(h1p - h2p);
×
3269
        if (C1p * C2p == 0.0) {
×
3270
            Hpm = hp_sum;
3271
        } else {
3272
            if (hp_diff <= M_PI) {
×
3273
                Hpm = 0.5 * hp_sum;
×
3274
            } else {
3275
                if (hp_sum < 2.0 * M_PI) {
×
3276
                    Hpm = 0.5 * (hp_sum + 2.0 * M_PI);
×
3277
                } else {
3278
                    Hpm = 0.5 * (hp_sum - 2.0 * M_PI);
×
3279
                }
3280
            }
3281
        }
3282
        T = 1.0 - 0.17 * cos(Hpm - M_PI / 6.0) +
×
3283
            0.24 * cos(2.0 * Hpm) +
×
3284
            0.32 * cos(3.0 * Hpm + M_PI / 30.0) -
×
3285
            0.20 * cos(4.0 * Hpm - 7.0 * M_PI / 20.0);
×
3286
        Sl = 1.0 + ((0.015 * pow(Lpm - 50.0, 2.0)) /
×
3287
                    sqrt(20.0 + pow(Lpm - 50.0, 2.0)));
×
3288
        Sc = 1.0 + 0.045 * Cpm;
×
3289
        Sh = 1.0 + 0.015 * Cpm * T;
×
3290
        dTheta = 30.0 * exp(-pow((Hpm - 275.0 * M_PI / 180.0) /
×
3291
                                 (25.0 * M_PI / 180.0), 2.0));
3292
        Rc = 2.0 * sqrt(pow(Cpm, 7.0) /
×
3293
                        (pow(Cpm, 7.0) + pow(25.0, 7.0) + 1e-12));
×
3294
        Rt = -Rc * sin(2.0 * dTheta * M_PI / 180.0);
×
3295
        de = sqrt(pow(dLp / (Sl), 2.0) + pow(dCp / (Sc), 2.0) +
×
3296
                  pow(dHp / (Sh), 2.0) +
×
3297
                  Rt * (dCp / Sc) * (dHp / Sh));
×
3298
        out.values[i] = (float)de;
×
3299
    }
3300
    return out;
×
3301
}
3302
/*
3303
 * GMSD and PSNR
3304
 */
3305
static float
3306
gmsd_metric(const sixel_assessment_float_buffer_t *ref,
×
3307
            const sixel_assessment_float_buffer_t *out,
3308
            int width,
3309
            int height)
3310
{
3311
    static const float kx[9] = {0.25f, 0.0f, -0.25f,
×
3312
                                0.5f, 0.0f, -0.5f,
3313
                                0.25f, 0.0f, -0.25f};
3314
    static const float ky[9] = {0.25f, 0.5f, 0.25f,
×
3315
                                0.0f, 0.0f, 0.0f,
3316
                                -0.25f, -0.5f, -0.25f};
3317
    sixel_assessment_float_buffer_t gx1;
×
3318
    sixel_assessment_float_buffer_t gy1;
×
3319
    sixel_assessment_float_buffer_t gx2;
×
3320
    sixel_assessment_float_buffer_t gy2;
×
3321
    sixel_assessment_float_buffer_t gm1;
×
3322
    sixel_assessment_float_buffer_t gm2;
×
3323
    double c;
×
3324
    double mean;
×
3325
    double sq_sum;
×
3326
    size_t total;
×
3327
    int y;
×
3328
    int x;
×
3329
    float accx1;
×
3330
    float accy1;
×
3331
    float accx2;
×
3332
    float accy2;
×
3333
    int dy;
×
3334
    int dx;
×
3335
    int yy;
×
3336
    int xx;
×
3337
    int kidx;
×
3338
    size_t idx;
×
3339
    float mag1;
×
3340
    float mag2;
×
3341
    double gms;
×
3342

3343
    gx1 = float_buffer_create((size_t)width * (size_t)height);
×
3344
    gy1 = float_buffer_create((size_t)width * (size_t)height);
×
3345
    gx2 = float_buffer_create((size_t)width * (size_t)height);
×
3346
    gy2 = float_buffer_create((size_t)width * (size_t)height);
×
3347

3348
    for (y = 0; y < height; ++y) {
×
3349
        for (x = 0; x < width; ++x) {
×
3350
            accx1 = 0.0f;
3351
            accy1 = 0.0f;
3352
            accx2 = 0.0f;
3353
            accy2 = 0.0f;
3354
            for (dy = -1; dy <= 1; ++dy) {
×
3355
                yy = y + dy;
×
3356
                if (yy < 0) {
×
3357
                    yy = -yy;
×
3358
                }
3359
                if (yy >= height) {
×
3360
                    yy = height - (yy - height) - 1;
×
3361
                    if (yy < 0) {
×
3362
                        yy = 0;
3363
                    }
3364
                }
3365
                for (dx = -1; dx <= 1; ++dx) {
×
3366
                    xx = x + dx;
×
3367
                    if (xx < 0) {
×
3368
                        xx = -xx;
×
3369
                    }
3370
                    if (xx >= width) {
×
3371
                        xx = width - (xx - width) - 1;
×
3372
                        if (xx < 0) {
×
3373
                            xx = 0;
3374
                        }
3375
                    }
3376
                    kidx = (dy + 1) * 3 + (dx + 1);
×
3377
                    accx1 += ref->values[yy * width + xx] * kx[kidx];
×
3378
                    accy1 += ref->values[yy * width + xx] * ky[kidx];
×
3379
                    accx2 += out->values[yy * width + xx] * kx[kidx];
×
3380
                    accy2 += out->values[yy * width + xx] * ky[kidx];
×
3381
                }
3382
            }
3383
            gx1.values[y * width + x] = accx1;
×
3384
            gy1.values[y * width + x] = accy1;
×
3385
            gx2.values[y * width + x] = accx2;
×
3386
            gy2.values[y * width + x] = accy2;
×
3387
        }
3388
    }
3389

3390
    gm1 = float_buffer_create((size_t)width * (size_t)height);
×
3391
    gm2 = float_buffer_create((size_t)width * (size_t)height);
×
3392
    total = (size_t)width * (size_t)height;
×
3393
    for (y = 0; y < height; ++y) {
×
3394
        for (x = 0; x < width; ++x) {
×
3395
            idx = (size_t)y * (size_t)width + (size_t)x;
×
3396
            mag1 = sqrtf(gx1.values[idx] * gx1.values[idx] +
×
3397
                         gy1.values[idx] * gy1.values[idx]) + 1e-12f;
×
3398
            mag2 = sqrtf(gx2.values[idx] * gx2.values[idx] +
×
3399
                         gy2.values[idx] * gy2.values[idx]) + 1e-12f;
×
3400
            gm1.values[idx] = mag1;
×
3401
            gm2.values[idx] = mag2;
×
3402
        }
3403
    }
3404

3405
    c = 0.0026;
3406
    mean = 0.0;
3407
    for (y = 0; y < height; ++y) {
×
3408
        for (x = 0; x < width; ++x) {
×
3409
            idx = (size_t)y * (size_t)width + (size_t)x;
×
3410
            gms = (2.0 * gm1.values[idx] * gm2.values[idx] + c) /
×
3411
                  (gm1.values[idx] * gm1.values[idx] +
×
3412
                   gm2.values[idx] * gm2.values[idx] + c);
×
3413
            mean += gms;
×
3414
        }
3415
    }
3416
    mean /= (double)total;
×
3417

3418
    sq_sum = 0.0;
×
3419
    for (y = 0; y < height; ++y) {
×
3420
        for (x = 0; x < width; ++x) {
×
3421
            idx = (size_t)y * (size_t)width + (size_t)x;
×
3422
            gms = (2.0 * gm1.values[idx] * gm2.values[idx] + c) /
×
3423
                  (gm1.values[idx] * gm1.values[idx] +
×
3424
                   gm2.values[idx] * gm2.values[idx] + c);
×
3425
            sq_sum += (gms - mean) * (gms - mean);
×
3426
        }
3427
    }
3428
    sq_sum /= (double)total;
×
3429

3430
    float_buffer_free(&gx1);
×
3431
    float_buffer_free(&gy1);
×
3432
    float_buffer_free(&gx2);
×
3433
    float_buffer_free(&gy2);
×
3434
    float_buffer_free(&gm1);
×
3435
    float_buffer_free(&gm2);
×
3436

3437
    return (float)sqrt(sq_sum);
×
3438
}
3439

3440
static float
3441
psnr_metric(const sixel_assessment_float_buffer_t *ref,
×
3442
            const sixel_assessment_float_buffer_t *out,
3443
            int width,
3444
            int height)
3445
{
3446
    double mse;
×
3447
    size_t total;
×
3448
    size_t i;
×
3449
    double diff;
×
3450

3451
    mse = 0.0;
×
3452
    total = (size_t)width * (size_t)height;
×
3453
    for (i = 0; i < total; ++i) {
×
3454
        diff = ref->values[i] - out->values[i];
×
3455
        mse += diff * diff;
×
3456
    }
3457
    mse /= (double)total;
×
3458
    if (mse <= 1e-12) {
×
3459
        return 99.0f;
3460
    }
3461
    return (float)(10.0 * log10(1.0 / mse));
×
3462
}
3463

3464
/*
3465
 * sixel_assessment_metrics_t aggregation
3466
 */
3467
static sixel_assessment_metrics_t
3468
evaluate_metrics(const float *ref_pixels,
×
3469
                 const float *out_pixels,
3470
                 int width,
3471
                 int height)
3472
{
3473
    sixel_assessment_metrics_t metrics;
×
3474
    sixel_assessment_float_buffer_t ref_luma;
×
3475
    sixel_assessment_float_buffer_t out_luma;
×
3476
    sixel_assessment_float_buffer_t ref_lab;
×
3477
    sixel_assessment_float_buffer_t out_lab;
×
3478
    sixel_assessment_float_buffer_t ref_chroma;
×
3479
    sixel_assessment_float_buffer_t out_chroma;
×
3480
    sixel_assessment_float_buffer_t de00;
×
3481
    size_t pixels;
×
3482
    size_t iter;
×
3483
    double sum_value;
×
3484

3485
    memset(&metrics, 0, sizeof(metrics));
×
3486
    metrics.lpips_alex = NAN;
×
3487

3488
    ref_luma = pixels_to_luma709(ref_pixels, width, height);
×
3489
    out_luma = pixels_to_luma709(out_pixels, width, height);
×
3490

3491
    metrics.ms_ssim = ms_ssim_luma(&ref_luma, &out_luma, width, height);
×
3492

3493
    metrics.high_freq_out = high_frequency_ratio(&out_luma, width, height,
×
3494
                                                 0.25f);
3495
    metrics.high_freq_ref = high_frequency_ratio(&ref_luma, width, height,
×
3496
                                                 0.25f);
3497
    metrics.high_freq_delta = metrics.high_freq_out - metrics.high_freq_ref;
×
3498

3499
    metrics.stripe_ref = stripe_score(&ref_luma, width, height, 180);
×
3500
    metrics.stripe_out = stripe_score(&out_luma, width, height, 180);
×
3501
    metrics.stripe_rel = metrics.stripe_out - metrics.stripe_ref;
×
3502

3503
    metrics.band_run_rel = banding_index_runlen(&out_luma, width, height, 32) -
×
3504
                           banding_index_runlen(&ref_luma, width, height, 32);
×
3505

3506
    metrics.band_grad_rel = banding_index_gradient(&out_luma, width, height) -
×
3507
                            banding_index_gradient(&ref_luma, width, height);
×
3508

3509
    clipping_rates(ref_pixels, width, height,
×
3510
                   &metrics.clip_l_ref,
3511
                   &metrics.clip_r_ref,
3512
                   &metrics.clip_g_ref,
3513
                   &metrics.clip_b_ref);
3514
    clipping_rates(out_pixels, width, height,
×
3515
                   &metrics.clip_l_out,
3516
                   &metrics.clip_r_out,
3517
                   &metrics.clip_g_out,
3518
                   &metrics.clip_b_out);
3519

3520
    metrics.clip_l_rel = metrics.clip_l_out - metrics.clip_l_ref;
×
3521
    metrics.clip_r_rel = metrics.clip_r_out - metrics.clip_r_ref;
×
3522
    metrics.clip_g_rel = metrics.clip_g_out - metrics.clip_g_ref;
×
3523
    metrics.clip_b_rel = metrics.clip_b_out - metrics.clip_b_ref;
×
3524

3525
    ref_lab = rgb_to_lab(ref_pixels, width, height);
×
3526
    out_lab = rgb_to_lab(out_pixels, width, height);
×
3527
    pixels = (size_t)width * (size_t)height;
×
3528
    ref_chroma = chroma_ab(&ref_lab, pixels);
×
3529
    out_chroma = chroma_ab(&out_lab, pixels);
×
3530

3531
    sum_value = 0.0;
×
3532
    for (iter = 0; iter < pixels; ++iter) {
×
3533
        sum_value += fabs(out_chroma.values[iter] -
×
3534
                          ref_chroma.values[iter]);
×
3535
    }
3536
    metrics.delta_chroma_mean = (float)(sum_value / (double)pixels);
×
3537

3538
    de00 = deltaE00(&ref_lab, &out_lab, pixels);
×
3539
    sum_value = 0.0;
×
3540
    for (iter = 0; iter < pixels; ++iter) {
×
3541
        sum_value += de00.values[iter];
×
3542
    }
3543
    metrics.delta_e00_mean = (float)(sum_value / (double)pixels);
×
3544

3545
    metrics.gmsd_value = gmsd_metric(&ref_luma, &out_luma, width, height);
×
3546
    metrics.psnr_y = psnr_metric(&ref_luma, &out_luma, width, height);
×
3547

3548
    float_buffer_free(&ref_luma);
×
3549
    float_buffer_free(&out_luma);
×
3550
    float_buffer_free(&ref_lab);
×
3551
    float_buffer_free(&out_lab);
×
3552
    float_buffer_free(&ref_chroma);
×
3553
    float_buffer_free(&out_chroma);
×
3554
    float_buffer_free(&de00);
×
3555

3556
    return metrics;
×
3557
}
3558

3559
/*
3560
 * LPIPS metric integration (ONNX Runtime)
3561
 */
3562
static float
3563
compute_lpips_alex(sixel_assessment_t *assessment,
3564
                   const float *ref_pixels,
3565
                   const float *out_pixels,
3566
                   int width,
3567
                   int height)
3568
{
3569
    float value;
3570

3571
    value = NAN;
×
3572
#if defined(HAVE_ONNXRUNTIME)
3573
    image_f32_t ref_tensor;
3574
    image_f32_t out_tensor;
3575
    float distance;
3576

3577
    ref_tensor.width = 0;
3578
    ref_tensor.height = 0;
3579
    ref_tensor.nchw = NULL;
3580
    out_tensor = ref_tensor;
3581
    distance = NAN;
3582

3583
    if (assessment->enable_lpips == 0) {
3584
        goto done;
3585
    }
3586
    if (convert_pixels_to_nchw(ref_pixels, width, height, &ref_tensor) != 0) {
3587
        fprintf(stderr,
3588
                "Warning: unable to convert reference image for LPIPS.\n");
3589
        goto done;
3590
    }
3591
    if (convert_pixels_to_nchw(out_pixels, width, height, &out_tensor) != 0) {
3592
        fprintf(stderr,
3593
                "Warning: unable to convert output image for LPIPS.\n");
3594
        goto done;
3595
    }
3596
    if (ensure_lpips_models(assessment) != 0) {
3597
        goto done;
3598
    }
3599
    if (run_lpips(assessment->diff_model_path,
3600
                  assessment->feat_model_path,
3601
                  &ref_tensor,
3602
                  &out_tensor,
3603
                  &distance) != 0) {
3604
        goto done;
3605
    }
3606
    value = distance;
3607

3608
done:
3609
    free_image_f32(&ref_tensor);
3610
    free_image_f32(&out_tensor);
3611
#else
3612
    (void)assessment;
3613
    (void)ref_pixels;
3614
    (void)out_pixels;
3615
    (void)width;
3616
    (void)height;
3617
#endif
3618
    return value;
3619
}
3620

3621
static void
3622
align_frame_pixels(float **ref_pixels,
×
3623
                   int *ref_width,
3624
                   int *ref_height,
3625
                   float **out_pixels,
3626
                   int *out_width,
3627
                   int *out_height)
3628
{
3629
    int width;
×
3630
    int height;
×
3631
    int channels;
×
3632
    float *ref_new;
×
3633
    float *out_new;
×
3634
    int y;
×
3635
    size_t row_bytes;
×
3636

3637
    if (ref_pixels == NULL || ref_width == NULL || ref_height == NULL ||
×
3638
            out_pixels == NULL || out_width == NULL || out_height == NULL) {
×
3639
        assessment_fail(SIXEL_BAD_ARGUMENT,
×
3640
                       "align_frame_pixels: invalid parameters");
3641
    }
3642

3643
    channels = SIXEL_ASSESSMENT_RGB_CHANNELS;
×
3644
    width = *ref_width < *out_width ? *ref_width : *out_width;
×
3645
    height = *ref_height < *out_height ? *ref_height : *out_height;
×
3646
    if (width <= 0 || height <= 0) {
×
3647
        assessment_fail(SIXEL_BAD_ARGUMENT,
×
3648
                       "align_frame_pixels: empty frame dimensions");
3649
    }
3650
    ref_new = (float *)xmalloc((size_t)width * (size_t)height *
×
3651
                               (size_t)channels * sizeof(float));
3652
    out_new = (float *)xmalloc((size_t)width * (size_t)height *
×
3653
                               (size_t)channels * sizeof(float));
3654
    row_bytes = (size_t)width * (size_t)channels * sizeof(float);
×
3655
    for (y = 0; y < height; ++y) {
×
3656
        memcpy(ref_new + (size_t)y * (size_t)width * (size_t)channels,
×
3657
               *ref_pixels + (size_t)y * (size_t)(*ref_width) *
×
3658
               (size_t)channels,
3659
               row_bytes);
3660
        memcpy(out_new + (size_t)y * (size_t)width * (size_t)channels,
×
3661
               *out_pixels + (size_t)y * (size_t)(*out_width) *
×
3662
               (size_t)channels,
3663
               row_bytes);
3664
    }
3665
    free(*ref_pixels);
×
3666
    free(*out_pixels);
×
3667
    *ref_pixels = ref_new;
×
3668
    *out_pixels = out_new;
×
3669
    *ref_width = width;
×
3670
    *ref_height = height;
×
3671
    *out_width = width;
×
3672
    *out_height = height;
×
3673
}
×
3674

3675
/*
3676
 * Assessment API bridge
3677
 */
3678
typedef struct MetricDescriptor {
3679
    int id;
3680
    const char *json_key;
3681
} MetricDescriptor;
3682

3683
static const MetricDescriptor g_metric_table[] = {
3684
    {SIXEL_ASSESSMENT_METRIC_MS_SSIM, "MS-SSIM"},
3685
    {SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_OUT, "HighFreqRatio_out"},
3686
    {SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_REF, "HighFreqRatio_ref"},
3687
    {SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_DELTA, "HighFreqRatio_delta"},
3688
    {SIXEL_ASSESSMENT_METRIC_STRIPE_REF, "StripeScore_ref"},
3689
    {SIXEL_ASSESSMENT_METRIC_STRIPE_OUT, "StripeScore_out"},
3690
    {SIXEL_ASSESSMENT_METRIC_STRIPE_REL, "StripeScore_rel"},
3691
    {SIXEL_ASSESSMENT_METRIC_BAND_RUN_REL, "BandingIndex_rel"},
3692
    {SIXEL_ASSESSMENT_METRIC_BAND_GRAD_REL, "BandingIndex_grad_rel"},
3693
    {SIXEL_ASSESSMENT_METRIC_CLIP_L_REF, "ClipRate_L_ref"},
3694
    {SIXEL_ASSESSMENT_METRIC_CLIP_R_REF, "ClipRate_R_ref"},
3695
    {SIXEL_ASSESSMENT_METRIC_CLIP_G_REF, "ClipRate_G_ref"},
3696
    {SIXEL_ASSESSMENT_METRIC_CLIP_B_REF, "ClipRate_B_ref"},
3697
    {SIXEL_ASSESSMENT_METRIC_CLIP_L_OUT, "ClipRate_L_out"},
3698
    {SIXEL_ASSESSMENT_METRIC_CLIP_R_OUT, "ClipRate_R_out"},
3699
    {SIXEL_ASSESSMENT_METRIC_CLIP_G_OUT, "ClipRate_G_out"},
3700
    {SIXEL_ASSESSMENT_METRIC_CLIP_B_OUT, "ClipRate_B_out"},
3701
    {SIXEL_ASSESSMENT_METRIC_CLIP_L_REL, "ClipRate_L_rel"},
3702
    {SIXEL_ASSESSMENT_METRIC_CLIP_R_REL, "ClipRate_R_rel"},
3703
    {SIXEL_ASSESSMENT_METRIC_CLIP_G_REL, "ClipRate_G_rel"},
3704
    {SIXEL_ASSESSMENT_METRIC_CLIP_B_REL, "ClipRate_B_rel"},
3705
    {SIXEL_ASSESSMENT_METRIC_DELTA_CHROMA, "Δ Chroma_mean"},
3706
    {SIXEL_ASSESSMENT_METRIC_DELTA_E00, "Δ E00_mean"},
3707
    {SIXEL_ASSESSMENT_METRIC_GMSD, "GMSD"},
3708
    {SIXEL_ASSESSMENT_METRIC_PSNR_Y, "PSNR_Y"},
3709
    {SIXEL_ASSESSMENT_METRIC_LPIPS_VGG, "LPIPS(alex)"},
3710
};
3711

3712
static void
3713
store_metrics(sixel_assessment_t *assessment,
×
3714
              const sixel_assessment_metrics_t *metrics)
3715
{
3716
    double *results;
×
3717

3718
    results = assessment->results;
×
3719
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_MS_SSIM)]
×
3720
        = metrics->ms_ssim;
×
3721
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_OUT)]
×
3722
        = metrics->high_freq_out;
×
3723
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_REF)]
×
3724
        = metrics->high_freq_ref;
×
3725
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_DELTA)]
×
3726
        = metrics->high_freq_delta;
×
3727
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_STRIPE_REF)]
×
3728
        = metrics->stripe_ref;
×
3729
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_STRIPE_OUT)]
×
3730
        = metrics->stripe_out;
×
3731
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_STRIPE_REL)]
×
3732
        = metrics->stripe_rel;
×
3733
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_BAND_RUN_REL)]
×
3734
        = metrics->band_run_rel;
×
3735
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_BAND_GRAD_REL)]
×
3736
        = metrics->band_grad_rel;
×
3737
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_L_REF)]
×
3738
        = metrics->clip_l_ref;
×
3739
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_R_REF)]
×
3740
        = metrics->clip_r_ref;
×
3741
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_G_REF)]
×
3742
        = metrics->clip_g_ref;
×
3743
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_B_REF)]
×
3744
        = metrics->clip_b_ref;
×
3745
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_L_OUT)]
×
3746
        = metrics->clip_l_out;
×
3747
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_R_OUT)]
×
3748
        = metrics->clip_r_out;
×
3749
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_G_OUT)]
×
3750
        = metrics->clip_g_out;
×
3751
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_B_OUT)]
×
3752
        = metrics->clip_b_out;
×
3753
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_L_REL)]
×
3754
        = metrics->clip_l_rel;
×
3755
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_R_REL)]
×
3756
        = metrics->clip_r_rel;
×
3757
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_G_REL)]
×
3758
        = metrics->clip_g_rel;
×
3759
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_B_REL)]
×
3760
        = metrics->clip_b_rel;
×
3761
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_DELTA_CHROMA)]
×
3762
        = metrics->delta_chroma_mean;
×
3763
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_DELTA_E00)]
×
3764
        = metrics->delta_e00_mean;
×
3765
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_GMSD)]
×
3766
        = metrics->gmsd_value;
×
3767
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_PSNR_Y)]
×
3768
        = metrics->psnr_y;
×
3769
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_LPIPS_VGG)]
×
3770
        = metrics->lpips_alex;
×
3771
}
×
3772

3773
static SIXELSTATUS
3774
sixel_assessment_capture_first_frame(sixel_frame_t *frame,
×
3775
                                     void *user_data)
3776
{
3777
    sixel_assessment_capture_t *capture;
×
3778

3779
    /*
3780
     * Loader pipeline sketch for encoded round trips:
3781
     *
3782
     *     +--------------+     +-----------------+
3783
     *     | decoder loop | --> | capture.frame   |
3784
     *     +--------------+     +-----------------+
3785
     */
3786

3787
    capture = (sixel_assessment_capture_t *)user_data;
×
3788
    if (capture == NULL) {
×
3789
        return SIXEL_BAD_ARGUMENT;
3790
    }
3791
    if (capture->frame == NULL) {
×
3792
        sixel_frame_ref(frame);
×
3793
        capture->frame = frame;
×
3794
    }
3795
    return SIXEL_OK;
3796
}
3797

3798
static int
3799
sixel_assessment_pixelformat_for_colorspace(int colorspace)
×
3800
{
3801
    switch (colorspace) {
×
3802
    case SIXEL_COLORSPACE_LINEAR:
3803
        return SIXEL_PIXELFORMAT_LINEARRGBFLOAT32;
3804
    case SIXEL_COLORSPACE_OKLAB:
3805
        return SIXEL_PIXELFORMAT_OKLABFLOAT32;
3806
    case SIXEL_COLORSPACE_CIELAB:
3807
        return SIXEL_PIXELFORMAT_CIELABFLOAT32;
3808
    case SIXEL_COLORSPACE_DIN99D:
3809
        return SIXEL_PIXELFORMAT_DIN99DFLOAT32;
3810
    default:
3811
        return SIXEL_PIXELFORMAT_RGB888;
3812
    }
3813
}
3814

3815
SIXELAPI SIXELSTATUS
3816
sixel_assessment_expand_quantized_frame(sixel_frame_t *source,
×
3817
                                        sixel_allocator_t *allocator,
3818
                                        sixel_frame_t **ppframe)
3819
{
3820
    SIXELSTATUS status;
×
3821
    sixel_frame_t *frame;
×
3822
    unsigned char *indices;
×
3823
    unsigned char const *palette;
×
3824
    unsigned char *rgb_pixels;
×
3825
    size_t pixel_count;
×
3826
    size_t rgb_bytes;
×
3827
    size_t palette_index;
×
3828
    size_t palette_offset;
×
3829
    size_t i;
×
3830
    int width;
×
3831
    int height;
×
3832
    int ncolors;
×
3833
    int colorspace;
×
3834
    int pixelformat;
×
3835
    unsigned char *dst;
×
3836

3837
    /*
3838
     * Convert the paletted capture into RGB triplets so that the
3839
     * assessment pipeline can compare against the original input in
3840
     * a like-for-like space.
3841
     *
3842
     *     +-----------+     +----------------+
3843
     *     | indices   | --> | RGB triplets   |
3844
     *     +-----------+     +----------------+
3845
     */
3846

3847
    status = SIXEL_FALSE;
×
3848
    frame = NULL;
×
3849
    indices = NULL;
×
3850
    palette = NULL;
×
3851
    rgb_pixels = NULL;
×
3852
    pixel_count = 0;
×
3853
    rgb_bytes = 0;
×
3854
    palette_index = 0;
×
3855
    palette_offset = 0;
×
3856
    i = 0;
×
3857
    width = 0;
×
3858
    height = 0;
×
3859
    ncolors = 0;
×
3860
    colorspace = SIXEL_COLORSPACE_GAMMA;
×
3861
    pixelformat = SIXEL_PIXELFORMAT_RGB888;
×
3862
    dst = NULL;
×
3863

3864
    if (source == NULL || allocator == NULL || ppframe == NULL) {
×
3865
        sixel_helper_set_additional_message(
×
3866
            "sixel_assessment_expand_quantized_frame: invalid argument.");
3867
        return SIXEL_BAD_ARGUMENT;
×
3868
    }
3869

3870
    *ppframe = NULL;
×
3871
    indices = (unsigned char *)sixel_frame_get_pixels(source);
×
3872
    palette = (unsigned char const *)sixel_frame_get_palette(source);
×
3873
    width = sixel_frame_get_width(source);
×
3874
    height = sixel_frame_get_height(source);
×
3875
    ncolors = sixel_frame_get_ncolors(source);
×
3876
    pixelformat = sixel_frame_get_pixelformat(source);
×
3877
    colorspace = sixel_frame_get_colorspace(source);
×
3878

3879
    if (pixelformat != SIXEL_PIXELFORMAT_PAL8) {
×
3880
        sixel_helper_set_additional_message(
×
3881
            "sixel_assessment_expand_quantized_frame: not paletted data.");
3882
        return SIXEL_RUNTIME_ERROR;
×
3883
    }
3884
    if (indices == NULL || palette == NULL || width <= 0 || height <= 0) {
×
3885
        sixel_helper_set_additional_message(
×
3886
            "sixel_assessment_expand_quantized_frame: capture incomplete.");
3887
        return SIXEL_RUNTIME_ERROR;
×
3888
    }
3889
    pixel_count = (size_t)width * (size_t)height;
×
3890
    if (height != 0 && pixel_count / (size_t)height != (size_t)width) {
×
3891
        sixel_helper_set_additional_message(
×
3892
            "sixel_assessment_expand_quantized_frame: size overflow.");
3893
        return SIXEL_RUNTIME_ERROR;
×
3894
    }
3895
    if (ncolors <= 0 || ncolors > 256) {
×
3896
        sixel_helper_set_additional_message(
×
3897
            "sixel_assessment_expand_quantized_frame: palette size invalid.");
3898
        return SIXEL_RUNTIME_ERROR;
×
3899
    }
3900

3901
    rgb_bytes = pixel_count * 3u;
×
3902
    if (pixel_count != 0 && rgb_bytes / 3u != pixel_count) {
×
3903
        sixel_helper_set_additional_message(
×
3904
            "sixel_assessment_expand_quantized_frame: RGB overflow.");
3905
        return SIXEL_RUNTIME_ERROR;
×
3906
    }
3907

3908
    status = sixel_frame_new(&frame, allocator);
×
3909
    if (SIXEL_FAILED(status)) {
×
3910
        return status;
3911
    }
3912

3913
    rgb_pixels = (unsigned char *)sixel_allocator_malloc(allocator,
×
3914
                                                         rgb_bytes);
3915
    if (rgb_pixels == NULL) {
×
3916
        sixel_helper_set_additional_message(
×
3917
            "sixel_assessment_expand_quantized_frame: malloc failed.");
3918
        status = SIXEL_BAD_ALLOCATION;
×
3919
        goto cleanup;
×
3920
    }
3921

3922
    dst = rgb_pixels;
3923
    for (i = 0; i < pixel_count; ++i) {
×
3924
        palette_index = (size_t)indices[i];
×
3925
        if (palette_index >= (size_t)ncolors) {
×
3926
            sixel_helper_set_additional_message(
×
3927
                "sixel_assessment_expand_quantized_frame: index overflow.");
3928
            status = SIXEL_RUNTIME_ERROR;
×
3929
            goto cleanup;
×
3930
        }
3931
        palette_offset = palette_index * 3u;
×
3932
        dst[0] = palette[palette_offset + 0];
×
3933
        dst[1] = palette[palette_offset + 1];
×
3934
        dst[2] = palette[palette_offset + 2];
×
3935
        dst += 3;
×
3936
    }
3937

3938
    status = sixel_frame_init(frame,
×
3939
                              rgb_pixels,
3940
                              width,
3941
                              height,
3942
                              SIXEL_PIXELFORMAT_RGB888,
3943
                              NULL,
3944
                              0);
3945
    if (SIXEL_FAILED(status)) {
×
3946
        goto cleanup;
×
3947
    }
3948

3949
    rgb_pixels = NULL;
×
3950
    status = sixel_frame_set_pixelformat(
×
3951
        frame,
3952
        sixel_assessment_pixelformat_for_colorspace(colorspace));
3953
    if (SIXEL_FAILED(status)) {
×
3954
        goto cleanup;
×
3955
    }
3956

3957
    *ppframe = frame;
×
3958
    return SIXEL_OK;
×
3959

3960
cleanup:
×
3961
    if (rgb_pixels != NULL) {
×
3962
        sixel_allocator_free(allocator, rgb_pixels);
×
3963
    }
3964
    if (frame != NULL) {
×
3965
        sixel_frame_unref(frame);
×
3966
    }
3967
    return status;
3968
}
3969

3970
SIXELAPI SIXELSTATUS
3971
sixel_assessment_load_single_frame(char const *path,
×
3972
                                   sixel_allocator_t *allocator,
3973
                                   sixel_frame_t **ppframe)
3974
{
3975
    SIXELSTATUS status;
×
3976
    sixel_loader_t *loader;
×
3977
    sixel_assessment_capture_t capture;
×
3978
    int fstatic;
×
3979
    int fuse_palette;
×
3980
    int reqcolors;
×
3981
    int loop_override;
×
3982
    int finsecure;
×
3983

3984
    /*
3985
     * Reload a single-frame image with the regular loader stack.  This helper
3986
     * is used when the encoded SIXEL output needs to be analyzed by the
3987
     * assessment pipeline.
3988
     */
3989

3990
    status = SIXEL_FALSE;
×
3991
    loader = NULL;
×
3992
    capture.frame = NULL;
×
3993
    fstatic = 1;
×
3994
    fuse_palette = 0;
×
3995
    reqcolors = SIXEL_PALETTE_MAX;
×
3996
    loop_override = SIXEL_LOOP_DISABLE;
×
3997
    finsecure = 0;
×
3998

3999
    if (path == NULL || allocator == NULL || ppframe == NULL) {
×
4000
        sixel_helper_set_additional_message(
×
4001
            "sixel_assessment_load_single_frame: invalid argument.");
4002
        return SIXEL_BAD_ARGUMENT;
×
4003
    }
4004

4005
    status = sixel_loader_new(&loader, allocator);
×
4006
    if (SIXEL_FAILED(status)) {
×
4007
        goto cleanup;
×
4008
    }
4009

4010
    status = sixel_loader_setopt(loader,
×
4011
                                 SIXEL_LOADER_OPTION_REQUIRE_STATIC,
4012
                                 &fstatic);
4013
    if (SIXEL_FAILED(status)) {
×
4014
        goto cleanup;
×
4015
    }
4016

4017
    status = sixel_loader_setopt(loader,
×
4018
                                 SIXEL_LOADER_OPTION_USE_PALETTE,
4019
                                 &fuse_palette);
4020
    if (SIXEL_FAILED(status)) {
×
4021
        goto cleanup;
×
4022
    }
4023

4024
    status = sixel_loader_setopt(loader,
×
4025
                                 SIXEL_LOADER_OPTION_REQCOLORS,
4026
                                 &reqcolors);
4027
    if (SIXEL_FAILED(status)) {
×
4028
        goto cleanup;
×
4029
    }
4030

4031
    status = sixel_loader_setopt(loader,
×
4032
                                 SIXEL_LOADER_OPTION_LOOP_CONTROL,
4033
                                 &loop_override);
4034
    if (SIXEL_FAILED(status)) {
×
4035
        goto cleanup;
×
4036
    }
4037

4038
    status = sixel_loader_setopt(loader,
×
4039
                                 SIXEL_LOADER_OPTION_INSECURE,
4040
                                 &finsecure);
4041
    if (SIXEL_FAILED(status)) {
×
4042
        goto cleanup;
×
4043
    }
4044

4045
    status = sixel_loader_setopt(loader,
×
4046
                                 SIXEL_LOADER_OPTION_CONTEXT,
4047
                                 &capture);
4048
    if (SIXEL_FAILED(status)) {
×
4049
        goto cleanup;
×
4050
    }
4051

4052
    status = sixel_loader_load_file(loader,
×
4053
                                    path,
4054
                                    sixel_assessment_capture_first_frame);
4055
    if (SIXEL_FAILED(status)) {
×
4056
        goto cleanup;
×
4057
    }
4058
    if (capture.frame == NULL) {
×
4059
        sixel_helper_set_additional_message(
×
4060
            "sixel_assessment_load_single_frame: no frames captured.");
4061
        status = SIXEL_RUNTIME_ERROR;
×
4062
        goto cleanup;
×
4063
    }
4064

4065
    sixel_frame_ref(capture.frame);
×
4066
    *ppframe = capture.frame;
×
4067
    capture.frame = NULL;
×
4068
    status = SIXEL_OK;
×
4069

4070
cleanup:
×
4071
    if (capture.frame != NULL) {
×
4072
        sixel_frame_unref(capture.frame);
×
4073
    }
4074
    if (loader != NULL) {
×
4075
        sixel_loader_unref(loader);
×
4076
    }
4077
    return status;
4078
}
4079

4080
SIXELSTATUS
4081
sixel_assessment_new(sixel_assessment_t **ppassessment,
3✔
4082
                    sixel_allocator_t *allocator)
4083
{
4084
    SIXELSTATUS status;
3✔
4085
    sixel_assessment_t *assessment;
3✔
4086

4087
    if (ppassessment == NULL) {
3!
4088
        return SIXEL_BAD_ARGUMENT;
4089
    }
4090
    if (allocator == NULL) {
3!
4091
        status = sixel_allocator_new(&allocator,
×
4092
                                     NULL,
4093
                                     NULL,
4094
                                     NULL,
4095
                                     NULL);
4096
        if (SIXEL_FAILED(status)) {
×
4097
            return status;
4098
        }
4099
    } else {
4100
        sixel_allocator_ref(allocator);
3✔
4101
    }
4102

4103
    assessment = (sixel_assessment_t *)sixel_allocator_malloc(
3✔
4104
        allocator,
4105
        sizeof(sixel_assessment_t));
4106
    if (assessment == NULL) {
3!
4107
        sixel_allocator_unref(allocator);
×
4108
        return SIXEL_BAD_ALLOCATION;
×
4109
    }
4110

4111
    assessment->refcount = 1;
3✔
4112
    assessment->allocator = allocator;
3✔
4113
    assessment->enable_lpips = 1;
3✔
4114
    assessment->results_ready = 0;
3✔
4115
    assessment->last_error = SIXEL_OK;
3✔
4116
    assessment->error_message[0] = '\0';
3✔
4117
    assessment->binary_dir[0] = '\0';
3✔
4118
    assessment->binary_dir_state = 0;
3✔
4119
    assessment->model_dir[0] = '\0';
3✔
4120
    assessment->model_dir_state = 0;
3✔
4121
    assessment->lpips_models_ready = 0;
3✔
4122
    assessment->diff_model_path[0] = '\0';
3✔
4123
    assessment->feat_model_path[0] = '\0';
3✔
4124
    memset(assessment->results, 0,
3✔
4125
           sizeof(assessment->results));
4126
    assessment_reset_stage_bookkeeping(assessment);
3✔
4127
    assessment->input_path[0] = '\0';
3✔
4128
    assessment->loader_name[0] = '\0';
3✔
4129
    assessment->format_name[0] = '\0';
3✔
4130
    assessment->input_pixelformat = (-1);
3✔
4131
    assessment->input_colorspace = (-1);
3✔
4132
    assessment->input_bytes = 0u;
3✔
4133
    assessment->source_pixels_bytes = 0u;
3✔
4134
    assessment->quantized_pixels_bytes = 0u;
3✔
4135
    assessment->palette_bytes = 0u;
3✔
4136
    assessment->palette_colors = 0;
3✔
4137
    assessment->output_bytes = 0u;
3✔
4138
    assessment->output_bytes_written = 0u;
3✔
4139
    assessment->sections_mask = SIXEL_ASSESSMENT_SECTION_NONE;
3✔
4140
    assessment->view_mask = SIXEL_ASSESSMENT_VIEW_ENCODED;
3✔
4141

4142
    *ppassessment = assessment;
3✔
4143
    return SIXEL_OK;
3✔
4144
}
4145

4146
void
4147
sixel_assessment_ref(sixel_assessment_t *assessment)
×
4148
{
4149
    if (assessment == NULL) {
×
4150
        return;
4151
    }
4152
    assessment->refcount += 1;
×
4153
}
4154

4155
void
4156
sixel_assessment_unref(sixel_assessment_t *assessment)
3✔
4157
{
4158
    if (assessment == NULL) {
3!
4159
        return;
4160
    }
4161
    assessment->refcount -= 1;
3✔
4162
    if (assessment->refcount == 0) {
3!
4163
        sixel_allocator_t *allocator;
3✔
4164

4165
        allocator = assessment->allocator;
3✔
4166
        sixel_allocator_free(allocator, assessment);
3✔
4167
        sixel_allocator_unref(allocator);
3✔
4168
    }
4169
}
4170

4171
static int
4172
parse_bool_option(char const *value, int *out)
×
4173
{
4174
    char lowered[8];
×
4175
    size_t len;
×
4176
    size_t i;
×
4177

4178
    if (value == NULL || out == NULL) {
×
4179
        return -1;
4180
    }
4181
    len = strlen(value);
×
4182
    if (len >= sizeof(lowered)) {
×
4183
        return -1;
4184
    }
4185
    for (i = 0; i < len; ++i) {
×
4186
        lowered[i] = (char)tolower((unsigned char)value[i]);
×
4187
    }
4188
    lowered[len] = '\0';
×
4189
    if (strcmp(lowered, "1") == 0 || strcmp(lowered, "true") == 0 ||
×
4190
        strcmp(lowered, "yes") == 0) {
×
4191
        *out = 1;
×
4192
        return 0;
×
4193
    }
4194
    if (strcmp(lowered, "0") == 0 || strcmp(lowered, "false") == 0 ||
×
4195
        strcmp(lowered, "no") == 0) {
×
4196
        *out = 0;
×
4197
        return 0;
×
4198
    }
4199
    return -1;
4200
}
4201

4202
SIXELSTATUS
4203
sixel_assessment_setopt(sixel_assessment_t *assessment,
×
4204
                       int option,
4205
                       char const *value)
4206
{
4207
    int bool_value;
×
4208
    int rc;
×
4209

4210
    if (assessment == NULL) {
×
4211
        return SIXEL_BAD_ARGUMENT;
4212
    }
4213
    switch (option) {
×
4214
    case SIXEL_ASSESSMENT_OPT_ENABLE_LPIPS:
×
4215
        if (parse_bool_option(value, &bool_value) != 0) {
×
4216
            return SIXEL_BAD_ARGUMENT;
4217
        }
4218
        assessment->enable_lpips = bool_value;
×
4219
        return SIXEL_OK;
×
4220
    case SIXEL_ASSESSMENT_OPT_MODEL_DIR:
×
4221
        if (value == NULL) {
×
4222
            return SIXEL_BAD_ARGUMENT;
4223
        }
4224
        if (strlen(value) >= sizeof(assessment->model_dir)) {
×
4225
            return SIXEL_BAD_ARGUMENT;
4226
        }
4227
        (void)sixel_compat_strcpy(assessment->model_dir,
×
4228
                                  sizeof(assessment->model_dir),
4229
                                  value);
4230
        assessment->model_dir_state = 1;
×
4231
        assessment->lpips_models_ready = 0;
×
4232
        return SIXEL_OK;
×
4233
    case SIXEL_ASSESSMENT_OPT_EXEC_PATH:
×
4234
        rc = assessment_resolve_executable_dir(value,
×
4235
                                              assessment->binary_dir,
×
4236
                                              sizeof(assessment->binary_dir));
4237
        if (rc != 0) {
×
4238
            assessment->binary_dir_state = -1;
×
4239
            assessment->binary_dir[0] = '\0';
×
4240
            return SIXEL_RUNTIME_ERROR;
×
4241
        }
4242
        assessment->binary_dir_state = 1;
×
4243
        assessment->lpips_models_ready = 0;
×
4244
        return SIXEL_OK;
×
4245
    default:
4246
        break;
4247
    }
4248
    return SIXEL_BAD_ARGUMENT;
4249
}
4250

4251
SIXELSTATUS
4252
sixel_assessment_analyze(sixel_assessment_t *assessment,
×
4253
                         sixel_frame_t *reference,
4254
                         sixel_frame_t *output)
4255
{
4256
    sixel_assessment_metrics_t metrics;
×
4257
    SIXELSTATUS status;
×
4258
    int bail;
×
4259
    float *ref_pixels;
×
4260
    float *out_pixels;
×
4261
    int ref_width;
×
4262
    int ref_height;
×
4263
    int out_width;
×
4264
    int out_height;
×
4265

4266
    if (assessment == NULL || reference == NULL || output == NULL) {
×
4267
        return SIXEL_BAD_ARGUMENT;
4268
    }
4269

4270
    ref_pixels = NULL;
×
4271
    out_pixels = NULL;
×
4272
    ref_width = 0;
×
4273
    ref_height = 0;
×
4274
    out_width = 0;
×
4275
    out_height = 0;
×
4276

4277
    assessment->last_error = SIXEL_OK;
×
4278
    assessment->error_message[0] = '\0';
×
4279
    assessment->results_ready = 0;
×
4280
    g_assessment_context = assessment;
×
4281
    bail = setjmp(assessment->bailout);
×
4282
    if (bail != 0) {
×
4283
        status = assessment->last_error;
×
4284
        goto cleanup;
×
4285
    }
4286

4287
    status = frame_to_rgb_float(reference,
×
4288
                                &ref_pixels,
4289
                                &ref_width,
4290
                                &ref_height);
4291
    if (SIXEL_FAILED(status)) {
×
4292
        goto cleanup;
×
4293
    }
4294
    status = frame_to_rgb_float(output,
×
4295
                                &out_pixels,
4296
                                &out_width,
4297
                                &out_height);
4298
    if (SIXEL_FAILED(status)) {
×
4299
        goto cleanup;
×
4300
    }
4301

4302
    align_frame_pixels(&ref_pixels,
×
4303
                       &ref_width,
4304
                       &ref_height,
4305
                       &out_pixels,
4306
                       &out_width,
4307
                       &out_height);
4308

4309
    metrics = evaluate_metrics(ref_pixels, out_pixels, ref_width, ref_height);
×
4310
    metrics.lpips_alex = compute_lpips_alex(assessment,
×
4311
                                            ref_pixels,
4312
                                            out_pixels,
4313
                                            ref_width,
4314
                                            ref_height);
4315
    store_metrics(assessment, &metrics);
×
4316
    assessment->results_ready = 1;
×
4317
    status = SIXEL_OK;
×
4318

4319
cleanup:
×
4320
    if (ref_pixels != NULL) {
×
4321
        free(ref_pixels);
×
4322
    }
4323
    if (out_pixels != NULL) {
×
4324
        free(out_pixels);
×
4325
    }
4326
    g_assessment_context = NULL;
×
4327
    return status;
×
4328
}
4329

4330
static SIXELSTATUS
4331
assessment_emit_quality_lines(sixel_assessment_t *assessment,
×
4332
                              sixel_assessment_json_callback_t callback,
4333
                              void *user_data,
4334
                              char const *indent)
4335
{
4336
    enum { line_buffer_size = PATH_MAX * 2 + 128 };
×
4337
    size_t i;
×
4338
    char line[line_buffer_size];
×
4339
    int written;
×
4340
    int last;
×
4341
    int index;
×
4342
    double value;
×
4343

4344
    if (indent == NULL) {
×
4345
        indent = "";
×
4346
    }
4347
    for (i = 0; i < sizeof(g_metric_table) / sizeof(g_metric_table[0]); ++i) {
×
4348
        last = (i + 1 == sizeof(g_metric_table) /
×
4349
                sizeof(g_metric_table[0]));
4350
        index = SIXEL_ASSESSMENT_INDEX(g_metric_table[i].id);
×
4351
        value = assessment->results[index];
×
4352
        if (isnan(value)) {
×
4353
            written = snprintf(line,
×
4354
                               sizeof(line),
4355
                               "%s\"%s\": null%s\n",
4356
                               indent,
4357
                               g_metric_table[i].json_key,
×
4358
                               last ? "" : ",");
4359
        } else {
4360
            written = snprintf(line,
×
4361
                               sizeof(line),
4362
                               "%s\"%s\": %.6f%s\n",
4363
                               indent,
4364
                               g_metric_table[i].json_key,
×
4365
                               value,
4366
                               last ? "" : ",");
4367
        }
4368
        if (written < 0 || (size_t)written >= sizeof(line)) {
×
4369
            return SIXEL_RUNTIME_ERROR;
4370
        }
4371
        callback(line, (size_t)written, user_data);
×
4372
    }
4373
    return SIXEL_OK;
4374
}
4375

4376
SIXELAPI SIXELSTATUS
4377
sixel_assessment_get_json(sixel_assessment_t *assessment,
3✔
4378
                          unsigned int sections,
4379
                          sixel_assessment_json_callback_t callback,
4380
                          void *user_data)
4381
{
4382
    enum { path_buffer_size = PATH_MAX * 2 + 32 };
3✔
4383
    enum { line_buffer_size = PATH_MAX * 2 + 128 };
3✔
4384
    unsigned int requested_sections;
3✔
4385
    unsigned int remaining_sections;
3✔
4386
    unsigned int stored_sections;
3✔
4387
    unsigned int requested_view;
3✔
4388
    unsigned int stored_view;
3✔
4389
    size_t stage_index;
3✔
4390
    sixel_assessment_stage_t stage;
3✔
4391
    char line[line_buffer_size];
3✔
4392
    char escaped_path[path_buffer_size];
3✔
4393
    char escaped_loader[128];
3✔
4394
    char escaped_format[64];
3✔
4395
    double duration_ms;
3✔
4396
    double throughput;
3✔
4397
    double total_duration;
3✔
4398
    double ratio_source;
3✔
4399
    double ratio_quantized;
3✔
4400
    double seconds;
3✔
4401
    size_t bytes;
3✔
4402
    int is_last_stage;
3✔
4403
    int written;
3✔
4404
    int has_more;
3✔
4405
    int next_index;
3✔
4406
    SIXELSTATUS status;
3✔
4407

4408
    if (assessment == NULL || callback == NULL) {
3!
4409
        return SIXEL_BAD_ARGUMENT;
4410
    }
4411
    requested_sections = sections & SIXEL_ASSESSMENT_SECTION_MASK;
3✔
4412
    if (requested_sections == 0u) {
3!
4413
        return SIXEL_BAD_ARGUMENT;
4414
    }
4415
    stored_sections = assessment->sections_mask;
3✔
4416
    if ((requested_sections & stored_sections) != requested_sections) {
3!
4417
        return SIXEL_RUNTIME_ERROR;
4418
    }
4419
    requested_view = sections & SIXEL_ASSESSMENT_VIEW_MASK;
3✔
4420
    stored_view = assessment->view_mask & SIXEL_ASSESSMENT_VIEW_MASK;
3✔
4421
    if (requested_view == 0u) {
3!
4422
        requested_view = stored_view;
3✔
4423
    }
4424
    if ((requested_sections & SIXEL_ASSESSMENT_SECTION_QUALITY) != 0u) {
3!
4425
        if (!assessment->results_ready) {
×
4426
            return SIXEL_RUNTIME_ERROR;
4427
        }
4428
        if (requested_view != stored_view) {
×
4429
            return SIXEL_BAD_ARGUMENT;
4430
        }
4431
    }
4432

4433
    callback("{\n", sizeof("{\n") - 1, user_data);
3✔
4434
    remaining_sections = requested_sections;
3✔
4435

4436
    if ((requested_sections & SIXEL_ASSESSMENT_SECTION_BASIC) != 0u) {
3!
4437
        assessment_guess_format(assessment);
3✔
4438
        if (assessment_escape_json(assessment->input_path,
3!
4439
                                   escaped_path,
4440
                                   sizeof(escaped_path)) != 0) {
4441
            return SIXEL_RUNTIME_ERROR;
4442
        }
4443
        if (assessment_escape_json(assessment->loader_name,
3!
4444
                                   escaped_loader,
4445
                                   sizeof(escaped_loader)) != 0) {
4446
            return SIXEL_RUNTIME_ERROR;
4447
        }
4448
        if (assessment_escape_json(assessment->format_name,
3!
4449
                                   escaped_format,
4450
                                   sizeof(escaped_format)) != 0) {
4451
            return SIXEL_RUNTIME_ERROR;
4452
        }
4453

4454
        written = snprintf(line,
3!
4455
                           sizeof(line),
4456
                           "  \"basic\": {\n");
4457
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4458
            return SIXEL_RUNTIME_ERROR;
4459
        }
4460
        callback(line, (size_t)written, user_data);
3✔
4461

4462
        written = snprintf(line,
3!
4463
                           sizeof(line),
4464
                           "    \"input_path\": \"%s\",\n",
4465
                           escaped_path);
4466
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4467
            return SIXEL_RUNTIME_ERROR;
4468
        }
4469
        callback(line, (size_t)written, user_data);
3✔
4470

4471
        written = snprintf(line,
3!
4472
                           sizeof(line),
4473
                           "    \"input_format\": \"%s\",\n",
4474
                           escaped_format);
4475
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4476
            return SIXEL_RUNTIME_ERROR;
4477
        }
4478
        callback(line, (size_t)written, user_data);
3✔
4479

4480
        written = snprintf(line,
3!
4481
                           sizeof(line),
4482
                           "    \"loader\": \"%s\",\n",
4483
                           escaped_loader);
4484
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4485
            return SIXEL_RUNTIME_ERROR;
4486
        }
4487
        callback(line, (size_t)written, user_data);
3✔
4488

4489
        written = snprintf(line,
3!
4490
                           sizeof(line),
4491
                           "    \"input_bytes\": %zu,\n",
4492
                           assessment->input_bytes);
4493
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4494
            return SIXEL_RUNTIME_ERROR;
4495
        }
4496
        callback(line, (size_t)written, user_data);
3✔
4497

4498
        written = snprintf(line,
3!
4499
                           sizeof(line),
4500
                           "    \"input_pixelformat\": \"%s\",\n",
4501
                           assessment_pixelformat_name(
4502
                               assessment->input_pixelformat));
4503
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4504
            return SIXEL_RUNTIME_ERROR;
4505
        }
4506
        callback(line, (size_t)written, user_data);
3✔
4507

4508
        written = snprintf(line,
3!
4509
                           sizeof(line),
4510
                           "    \"input_colorspace\": \"%s\",\n",
4511
                           assessment_colorspace_name(
4512
                               assessment->input_colorspace));
4513
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4514
            return SIXEL_RUNTIME_ERROR;
4515
        }
4516
        callback(line, (size_t)written, user_data);
3✔
4517

4518
        written = snprintf(line,
3!
4519
                           sizeof(line),
4520
                           "    \"source_pixels_bytes\": %zu,\n",
4521
                           assessment->source_pixels_bytes);
4522
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4523
            return SIXEL_RUNTIME_ERROR;
4524
        }
4525
        callback(line, (size_t)written, user_data);
3✔
4526

4527
        written = snprintf(line,
3!
4528
                           sizeof(line),
4529
                           "    \"quantized_pixels_bytes\": %zu,\n",
4530
                           assessment->quantized_pixels_bytes);
4531
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4532
            return SIXEL_RUNTIME_ERROR;
4533
        }
4534
        callback(line, (size_t)written, user_data);
3✔
4535

4536
        written = snprintf(line,
3!
4537
                           sizeof(line),
4538
                           "    \"palette_colors\": %d,\n",
4539
                           assessment->palette_colors);
4540
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4541
            return SIXEL_RUNTIME_ERROR;
4542
        }
4543
        callback(line, (size_t)written, user_data);
3✔
4544

4545
        written = snprintf(line,
3!
4546
                           sizeof(line),
4547
                           "    \"palette_bytes\": %zu\n",
4548
                           assessment->palette_bytes);
4549
        if (written < 0 || (size_t)written >= sizeof(line)) {
3!
4550
            return SIXEL_RUNTIME_ERROR;
4551
        }
4552
        callback(line, (size_t)written, user_data);
3✔
4553

4554
        remaining_sections &= ~SIXEL_ASSESSMENT_SECTION_BASIC;
3✔
4555
        has_more = (remaining_sections != 0u);
3✔
4556
        callback(has_more ? "  },\n" : "  }\n",
6!
4557
                 has_more ? sizeof("  },\n") - 1 : sizeof("  }\n") - 1,
4558
                 user_data);
4559
    }
4560

4561
    if ((requested_sections & SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
3!
4562
        written = snprintf(line,
×
4563
                           sizeof(line),
4564
                           "  \"performance\": {\n");
4565
        if (written < 0 || (size_t)written >= sizeof(line)) {
×
4566
            return SIXEL_RUNTIME_ERROR;
4567
        }
4568
        callback(line, (size_t)written, user_data);
×
4569

4570
        callback("    \"stages\": [\n",
×
4571
                 sizeof("    \"stages\": [\n") - 1,
4572
                 user_data);
4573

4574
        total_duration = 0.0;
×
4575
        stage_index = 0;
×
4576
        while (stage_index <
×
4577
                (int)(sizeof(g_stage_descriptors) /
4578
                    sizeof(g_stage_descriptors[0]))) {
4579
            if (!sixel_assessment_stage_should_emit(stage_index)) {
×
4580
                ++stage_index;
×
4581
                continue;
×
4582
            }
4583
            stage = g_stage_descriptors[stage_index].id;
×
4584
            seconds = assessment->stage_durations[stage];
×
4585
            bytes = (size_t)assessment->stage_bytes[stage];
×
4586
            if (g_stage_counts_toward_total[stage_index] != 0) {
×
4587
                total_duration += seconds;
×
4588
            }
4589
            duration_ms = seconds * 1000.0;
×
4590
            if (seconds > 0.0) {
×
4591
                throughput = (double)bytes / seconds;
×
4592
            } else {
4593
                throughput = 0.0;
4594
            }
4595

4596
            next_index = stage_index + 1;
×
4597
            while (next_index <
×
4598
                    (int)(sizeof(g_stage_descriptors) /
4599
                        sizeof(g_stage_descriptors[0])) &&
×
4600
                    !sixel_assessment_stage_should_emit(next_index)) {
×
4601
                ++next_index;
×
4602
            }
4603
            is_last_stage = (next_index >=
×
4604
                    (int)(sizeof(g_stage_descriptors) /
4605
                        sizeof(g_stage_descriptors[0])));
4606

4607
            written = snprintf(line,
×
4608
                               sizeof(line),
4609
                               "      {\n"
4610
                               "        \"name\": \"%s\",\n"
4611
                               "        \"duration_ms\": %.6f,\n",
4612
                               g_stage_descriptors[stage_index].label,
×
4613
                               duration_ms);
4614
            if (written < 0 || (size_t)written >= sizeof(line)) {
×
4615
                return SIXEL_RUNTIME_ERROR;
4616
            }
4617
            callback(line, (size_t)written, user_data);
×
4618

4619
            if (stage == SIXEL_ASSESSMENT_STAGE_ENCODE &&
×
4620
                    g_encode_parallel_threads > 1) {
×
4621
                written = snprintf(line,
×
4622
                                   sizeof(line),
4623
                                   "        \"parallel_threads\": %d,\n",
4624
                                   g_encode_parallel_threads);
4625
                if (written < 0 || (size_t)written >= sizeof(line)) {
×
4626
                    return SIXEL_RUNTIME_ERROR;
4627
                }
4628
                callback(line, (size_t)written, user_data);
×
4629
            }
4630

4631
            written = snprintf(
×
4632
                line,
4633
                sizeof(line),
4634
                "        \"bytes_processed\": %zu,\n"
4635
                "        \"throughput_bytes_per_second\": %.6f\n"
4636
                "      }%s\n",
4637
                bytes,
4638
                throughput,
4639
                is_last_stage ? "" : ",");
4640
            if (written < 0 || (size_t)written >= sizeof(line)) {
×
4641
                return SIXEL_RUNTIME_ERROR;
4642
            }
4643
            callback(line, (size_t)written, user_data);
×
4644

4645
            stage_index = next_index;
×
4646
        }
4647

4648
        callback("    ],\n", sizeof("    ],\n") - 1, user_data);
×
4649

4650
        written = snprintf(line,
×
4651
                           sizeof(line),
4652
                           "    \"total_duration_ms\": %.6f\n",
4653
                           total_duration * 1000.0);
4654
        if (written < 0 || (size_t)written >= sizeof(line)) {
×
4655
            return SIXEL_RUNTIME_ERROR;
4656
        }
4657
        callback(line, (size_t)written, user_data);
×
4658

4659
        remaining_sections &= ~SIXEL_ASSESSMENT_SECTION_PERFORMANCE;
×
4660
        has_more = (remaining_sections != 0u);
×
4661
        callback(has_more ? "  },\n" : "  }\n",
×
4662
                 has_more ? sizeof("  },\n") - 1 : sizeof("  }\n") - 1,
4663
                 user_data);
4664
    }
4665

4666
    if ((requested_sections & SIXEL_ASSESSMENT_SECTION_SIZE) != 0u) {
3!
4667
        ratio_source = 0.0;
×
4668
        if (assessment->source_pixels_bytes > 0) {
×
4669
            ratio_source = (double)assessment->output_bytes /
×
4670
                (double)assessment->source_pixels_bytes;
×
4671
        }
4672
        ratio_quantized = 0.0;
×
4673
        if (assessment->quantized_pixels_bytes > 0) {
×
4674
            ratio_quantized = (double)assessment->output_bytes /
×
4675
                (double)assessment->quantized_pixels_bytes;
×
4676
        }
4677

4678
        written = snprintf(line,
×
4679
                           sizeof(line),
4680
                           "  \"size\": {\n");
4681
        if (written < 0 || (size_t)written >= sizeof(line)) {
×
4682
            return SIXEL_RUNTIME_ERROR;
4683
        }
4684
        callback(line, (size_t)written, user_data);
×
4685

4686
        written = snprintf(line,
×
4687
                           sizeof(line),
4688
                           "    \"output_bytes\": %zu,\n",
4689
                           assessment->output_bytes);
4690
        if (written < 0 || (size_t)written >= sizeof(line)) {
×
4691
            return SIXEL_RUNTIME_ERROR;
4692
        }
4693
        callback(line, (size_t)written, user_data);
×
4694

4695
        written = snprintf(line,
×
4696
                           sizeof(line),
4697
                           "    \"output_vs_source_ratio\": %.6f,\n",
4698
                           ratio_source);
4699
        if (written < 0 || (size_t)written >= sizeof(line)) {
×
4700
            return SIXEL_RUNTIME_ERROR;
4701
        }
4702
        callback(line, (size_t)written, user_data);
×
4703

4704
        written = snprintf(line,
×
4705
                           sizeof(line),
4706
                           "    \"output_vs_quantized_ratio\": %.6f\n",
4707
                           ratio_quantized);
4708
        if (written < 0 || (size_t)written >= sizeof(line)) {
×
4709
            return SIXEL_RUNTIME_ERROR;
4710
        }
4711
        callback(line, (size_t)written, user_data);
×
4712

4713
        remaining_sections &= ~SIXEL_ASSESSMENT_SECTION_SIZE;
×
4714
        has_more = (remaining_sections != 0u);
×
4715
        callback(has_more ? "  },\n" : "  }\n",
×
4716
                 has_more ? sizeof("  },\n") - 1 : sizeof("  }\n") - 1,
4717
                 user_data);
4718
    }
4719

4720
    if ((requested_sections & SIXEL_ASSESSMENT_SECTION_QUALITY) != 0u) {
3!
4721
        written = snprintf(line,
×
4722
                           sizeof(line),
4723
                           "  \"quality\": {\n");
4724
        if (written < 0 || (size_t)written >= sizeof(line)) {
×
4725
            return SIXEL_RUNTIME_ERROR;
4726
        }
4727
        callback(line, (size_t)written, user_data);
×
4728

4729
        status = assessment_emit_quality_lines(assessment,
×
4730
                                               callback,
4731
                                               user_data,
4732
                                               "    ");
4733
        if (SIXEL_FAILED(status)) {
×
4734
            return status;
4735
        }
4736

4737
        remaining_sections &= ~SIXEL_ASSESSMENT_SECTION_QUALITY;
×
4738
        has_more = (remaining_sections != 0u);
×
4739
        callback(has_more ? "  },\n" : "  }\n",
×
4740
                 has_more ? sizeof("  },\n") - 1 : sizeof("  }\n") - 1,
4741
                 user_data);
4742
    }
4743

4744
    callback("}\n", sizeof("}\n") - 1, user_data);
3✔
4745
    return SIXEL_OK;
3✔
4746
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc