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

saitoha / libsixel / 19389365033

15 Nov 2025 11:44AM UTC coverage: 43.379% (-0.4%) from 43.821%
19389365033

push

github

saitoha
palette: refactor palette helpers into dedicated modules

8474 of 27744 branches covered (30.54%)

44 of 650 new or added lines in 4 files covered. (6.77%)

106 existing lines in 9 files now uncovered.

11581 of 26697 relevant lines covered (43.38%)

973309.3 hits per line

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

13.0
/src/assessment.c
1
/*
2
 * assessment.c - High-speed image quality evaluator ported from Python.
3
 *
4
 *  +-------------------------------------------------------------+
5
 *  |                        PIPELINE MAP                         |
6
 *  +---------------------+------------------+--------------------+
7
 *  | image loading (RGB) | metric kernels   | JSON emit          |
8
 *  |  (libsixel loader)  |  (MS-SSIM, etc.) | (stdout + files)   |
9
 *  +---------------------+------------------+--------------------+
10
 *
11
 *  ASCII flow for the metrics modules:
12
 *
13
 *        +-----------+      +---------------+      +-------------+
14
 *        |   Luma    | ---> |  Spectral +   | ---> |  Composite  |
15
 *        |  Stack    |      |  Spatial      |      |   Report    |
16
 *        +-----------+      +---------------+      +-------------+
17
 *              |                    |                      |
18
 *              |                    |                      +--> JSON writer
19
 *              |                    +--> FFT / histogram engines
20
 *              +--> MS-SSIM / PSNR / GMSD
21
 *
22
 *  Every function carries intentionally verbose comments so that future
23
 *  maintainers can follow the numerical steps without cross-referencing the
24
 *  removed Python original.
25
 */
26

27
#define _POSIX_C_SOURCE 200809L
28
#define _XOPEN_SOURCE 700
29

30
#include "config.h"
31

32
#include <ctype.h>
33
#include <errno.h>
34
#include <limits.h>
35
#include <math.h>
36
#include <setjmp.h>
37
#include <stdbool.h>
38
#include <stdarg.h>
39
#include <stdint.h>
40
#include <stdio.h>
41
#include <stdlib.h>
42
#include <string.h>
43

44
#if HAVE_TIME_H
45
#include <time.h>
46
#endif
47
#if HAVE_SYS_TIME_H
48
#include <sys/time.h>
49
#endif
50

51
#include <sixel.h>
52

53
#include "assessment.h"
54
#include "encoder.h"
55
#include "frame.h"
56
#include "compat_stub.h"
57

58
#if defined(_WIN32)
59
#include <io.h>
60
#include <windows.h>
61
#else
62
#include <unistd.h>
63
#endif
64

65
#if defined(__APPLE__)
66
#include <mach-o/dyld.h>
67
#endif
68

69
#if defined(__linux__)
70
#include <sys/stat.h>
71
#include <sys/types.h>
72
#endif
73

74
#if defined(HAVE_ONNXRUNTIME)
75
#include "onnxruntime_c_api.h"
76
#endif
77

78
#ifndef SIXEL_MODEL_DIR
79
#define SIXEL_MODEL_DIR ""
80
#endif
81

82
#if !defined(PATH_MAX)
83
#define PATH_MAX 4096
84
#endif
85

86
#if defined(_WIN32)
87
#define SIXEL_PATH_SEP '\\'
88
#define SIXEL_PATH_LIST_SEP ';'
89
#else
90
#define SIXEL_PATH_SEP '/'
91
#define SIXEL_PATH_LIST_SEP ':'
92
#endif
93

94
#define SIXEL_LOCAL_MODELS_SEG1 ".."
95
#define SIXEL_LOCAL_MODELS_SEG2 "models"
96
#define SIXEL_LOCAL_MODELS_SEG3 "lpips"
97

98
#ifndef M_PI
99
#define M_PI 3.14159265358979323846
100
#endif
101

102
enum { SIXEL_ASSESSMENT_RGB_CHANNELS = 3 };
103

104
typedef struct sixel_assessment_float_buffer {
105
    size_t length;
106
    float *values;
107
} sixel_assessment_float_buffer_t;
108

109
typedef struct sixel_assessment_complex {
110
    double re;
111
    double im;
112
} sixel_assessment_complex_t;
113

114
typedef struct sixel_assessment_metrics {
115
    float ms_ssim;
116
    float high_freq_out;
117
    float high_freq_ref;
118
    float high_freq_delta;
119
    float stripe_ref;
120
    float stripe_out;
121
    float stripe_rel;
122
    float band_run_rel;
123
    float band_grad_rel;
124
    float clip_l_ref;
125
    float clip_r_ref;
126
    float clip_g_ref;
127
    float clip_b_ref;
128
    float clip_l_out;
129
    float clip_r_out;
130
    float clip_g_out;
131
    float clip_b_out;
132
    float clip_l_rel;
133
    float clip_r_rel;
134
    float clip_g_rel;
135
    float clip_b_rel;
136
    float delta_chroma_mean;
137
    float delta_e00_mean;
138
    float gmsd_value;
139
    float psnr_y;
140
    float lpips_alex;
141
} sixel_assessment_metrics_t;
142

143
typedef struct sixel_assessment_capture {
144
    sixel_frame_t *frame;
145
} sixel_assessment_capture_t;
146

147
static sixel_assessment_t *g_assessment_context = NULL;
148
static sixel_assessment_t *g_active_encode_assessment = NULL;
149

150
typedef struct assessment_stage_descriptor {
151
    sixel_assessment_stage_t id;
152
    char const *label;
153
} assessment_stage_descriptor_t;
154

155
static assessment_stage_descriptor_t const g_stage_descriptors[] = {
156
    {SIXEL_ASSESSMENT_STAGE_IMAGE_CHUNK, "ImageRead"},
157
    {SIXEL_ASSESSMENT_STAGE_IMAGE_DECODE, "ImageDecode"},
158
    {SIXEL_ASSESSMENT_STAGE_SCALE, "Scale"},
159
    {SIXEL_ASSESSMENT_STAGE_CROP, "Crop"},
160
    {SIXEL_ASSESSMENT_STAGE_COLORSPACE, "ColorConvert"},
161
    {SIXEL_ASSESSMENT_STAGE_PALETTE_HISTOGRAM, "PaletteHistogram"},
162
    {SIXEL_ASSESSMENT_STAGE_PALETTE_SOLVE, "PaletteSolve"},
163
    {SIXEL_ASSESSMENT_STAGE_PALETTE_APPLY, "PaletteApply"},
164
    {SIXEL_ASSESSMENT_STAGE_ENCODE, "Encode"},
165
    {SIXEL_ASSESSMENT_STAGE_ENCODE_PREPARE, "EncodePrepare"},
166
    {SIXEL_ASSESSMENT_STAGE_ENCODE_CLASSIFY, "EncodeClassify"},
167
    {SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE, "EncodeCompose"},
168
    {SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE_SCAN, "EncodeComposeScan"},
169
    {SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE_QUEUE, "EncodeComposeQueue"},
170
    {SIXEL_ASSESSMENT_STAGE_ENCODE_EMIT, "EncodeEmit"},
171
    {SIXEL_ASSESSMENT_STAGE_OUTPUT, "Output"}
172
};
173

174
/*
175
 * Only top-level stages contribute to the total so nested probes do not
176
 * inflate the reported wall time by double counting their parents.
177
 */
178
static unsigned char const g_stage_counts_toward_total[] = {
179
    1, /* ImageRead */
180
    1, /* ImageDecode */
181
    1, /* Scale */
182
    1, /* Crop */
183
    1, /* ColorConvert */
184
    1, /* PaletteHistogram */
185
    1, /* PaletteSolve */
186
    1, /* PaletteApply */
187
    1, /* Encode */
188
    0, /* EncodePrepare */
189
    0, /* EncodeClassify */
190
    0, /* EncodeCompose */
191
    0, /* EncodeComposeScan */
192
    0, /* EncodeComposeQueue */
193
    0, /* EncodeEmit */
194
    1  /* Output */
195
};
196

197
static int g_encode_parallel_threads = 1;
198
static int g_encode_substages_visible = 1;
199

200
static int
201
sixel_assessment_stage_is_encode_detail(sixel_assessment_stage_t stage)
×
202
{
203
    switch (stage) {
×
UNCOV
204
    case SIXEL_ASSESSMENT_STAGE_ENCODE_PREPARE:
×
205
    case SIXEL_ASSESSMENT_STAGE_ENCODE_CLASSIFY:
206
    case SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE:
207
    case SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE_SCAN:
208
    case SIXEL_ASSESSMENT_STAGE_ENCODE_COMPOSE_QUEUE:
209
    case SIXEL_ASSESSMENT_STAGE_ENCODE_EMIT:
210
        return 1;
×
UNCOV
211
    default:
×
212
        break;
×
213
    }
214
    return 0;
×
215
}
216

217
static int
218
sixel_assessment_stage_should_emit(int stage_index)
×
219
{
220
    sixel_assessment_stage_t stage;
221

222
    stage = g_stage_descriptors[stage_index].id;
×
223
    if (!g_encode_substages_visible &&
×
224
            sixel_assessment_stage_is_encode_detail(stage)) {
×
225
        return 0;
×
226
    }
227
    return 1;
×
228
}
229

230
SIXELAPI double
231
sixel_assessment_timer_now(void)
96✔
232
{
233
#if defined(_WIN32)
234
    static LARGE_INTEGER frequency;
235
    LARGE_INTEGER counter;
236
    BOOL ok;
237

238
    if (frequency.QuadPart == 0) {
239
        ok = QueryPerformanceFrequency(&frequency);
240
        if (!ok || frequency.QuadPart == 0) {
241
            return (double)GetTickCount64() / 1000.0;
242
        }
243
    }
244
    QueryPerformanceCounter(&counter);
245
    return (double)counter.QuadPart / (double)frequency.QuadPart;
246
#elif defined(HAVE_CLOCK_GETTIME)
247
    struct timespec ts;
248

249
    if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
250
        return 0.0;
251
    }
252
    return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0;
253
#elif defined(HAVE_GETTIMEOFDAY)
254
    struct timeval tv;
255

256
    if (gettimeofday(&tv, NULL) != 0) {
257
        return 0.0;
258
    }
259
    return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0;
260
#else
261
    return (double)clock() / (double)CLOCKS_PER_SEC;
96✔
262
#endif
263
}
264

265
static void
266
assessment_reset_stage_bookkeeping(sixel_assessment_t *assessment)
2✔
267
{
268
    if (assessment == NULL) {
2!
269
        return;
×
270
    }
271
    assessment->active_stage = SIXEL_ASSESSMENT_STAGE_NONE;
2✔
272
    assessment->stage_started_at = 0.0;
2✔
273
    assessment->stage_active = 0;
2✔
274
    memset(assessment->stage_durations, 0,
2✔
275
           sizeof(assessment->stage_durations));
276
    memset(assessment->stage_bytes, 0, sizeof(assessment->stage_bytes));
2✔
277
    assessment->output_bytes_written = 0u;
2✔
278
    assessment->encode_output_time_pending = 0.0;
2✔
279
}
280

281
static void
282
assessment_guess_format(sixel_assessment_t *assessment)
4✔
283
{
284
    char const *loader;
285
    char const *extension;
286
    size_t len;
287
    size_t index;
288

289
    if (assessment == NULL) {
4!
290
        return;
×
291
    }
292
    if (assessment->format_name[0] != '\0') {
4✔
293
        return;
2✔
294
    }
295
    loader = assessment->loader_name;
2✔
296
    if (loader[0] != '\0') {
2!
297
        if (strcmp(loader, "libpng") == 0) {
2!
298
            (void)sixel_compat_strcpy(assessment->format_name,
×
299
                                      sizeof(assessment->format_name),
300
                                      "png");
301
            return;
×
302
        } else if (strcmp(loader, "libjpeg") == 0) {
2!
303
            (void)sixel_compat_strcpy(assessment->format_name,
×
304
                                      sizeof(assessment->format_name),
305
                                      "jpeg");
306
            return;
×
307
        } else if (strcmp(loader, "wic") == 0) {
2!
308
            (void)sixel_compat_strcpy(assessment->format_name,
×
309
                                      sizeof(assessment->format_name),
310
                                      "wic");
311
            return;
×
312
        } else if (strcmp(loader, "builtin") == 0) {
2!
313
            (void)sixel_compat_strcpy(assessment->format_name,
2✔
314
                                      sizeof(assessment->format_name),
315
                                      "builtin");
316
            return;
2✔
317
        }
318
    }
319
    extension = strrchr(assessment->input_path, '.');
×
320
    if (extension != NULL && extension[1] != '\0') {
×
321
        len = strlen(extension + 1);
×
322
        if (len >= sizeof(assessment->format_name)) {
×
323
            len = sizeof(assessment->format_name) - 1;
×
324
        }
325
        for (index = 0; index < len; ++index) {
×
326
            assessment->format_name[index] =
×
327
                (char)tolower((unsigned char)extension[1 + index]);
×
328
        }
329
        assessment->format_name[len] = '\0';
×
330
    } else {
331
        (void)sixel_compat_strcpy(assessment->format_name,
×
332
                                  sizeof(assessment->format_name),
333
                                  "unknown");
334
    }
335
}
336

337
static int
338
assessment_escape_json(char const *input,
6✔
339
                       char *output,
340
                       size_t output_size)
341
{
342
    size_t index;
343
    size_t written;
344
    unsigned char ch;
345
    int n;
346

347
    if (output == NULL || output_size == 0) {
6!
348
        return -1;
×
349
    }
350
    written = 0;
6✔
351
    if (input == NULL) {
6!
352
        output[0] = '\0';
×
353
        return 0;
×
354
    }
355
    for (index = 0; input[index] != '\0'; ++index) {
138✔
356
        ch = (unsigned char)input[index];
132✔
357
        if (ch == '"' || ch == '\\') {
132!
358
            if (written + 2 >= output_size) {
×
359
                return -1;
×
360
            }
361
            output[written++] = '\\';
×
362
            output[written++] = (char)ch;
×
363
        } else if (ch <= 0x1F) {
132!
364
            if (written + 6 >= output_size) {
×
365
                return -1;
×
366
            }
367
            n = snprintf(output + written, output_size - written,
×
368
                         "\\u%04x", ch);
369
            if (n < 0 || (size_t)n >= output_size - written) {
×
370
                return -1;
×
371
            }
372
            written += (size_t)n;
×
373
        } else {
374
            if (written + 1 >= output_size) {
132!
375
                return -1;
×
376
            }
377
            output[written++] = (char)ch;
132✔
378
        }
379
    }
380
    if (written >= output_size) {
6!
381
        return -1;
×
382
    }
383
    output[written] = '\0';
6✔
384
    return 0;
6✔
385
}
386

387
static char const *
388
assessment_pixelformat_name(int pixelformat)
2✔
389
{
390
    switch (pixelformat) {
2!
UNCOV
391
    case SIXEL_PIXELFORMAT_PAL1:
×
392
        return "PAL1";
×
UNCOV
393
    case SIXEL_PIXELFORMAT_PAL2:
×
394
        return "PAL2";
×
UNCOV
395
    case SIXEL_PIXELFORMAT_PAL4:
×
396
        return "PAL4";
×
UNCOV
397
    case SIXEL_PIXELFORMAT_PAL8:
×
398
        return "PAL8";
×
UNCOV
399
    case SIXEL_PIXELFORMAT_RGB555:
×
400
        return "RGB555";
×
UNCOV
401
    case SIXEL_PIXELFORMAT_RGB565:
×
402
        return "RGB565";
×
403
    case SIXEL_PIXELFORMAT_RGB888:
2✔
404
        return "RGB888";
2✔
UNCOV
405
    case SIXEL_PIXELFORMAT_BGR555:
×
406
        return "BGR555";
×
UNCOV
407
    case SIXEL_PIXELFORMAT_BGR565:
×
408
        return "BGR565";
×
UNCOV
409
    case SIXEL_PIXELFORMAT_BGR888:
×
410
        return "BGR888";
×
UNCOV
411
    case SIXEL_PIXELFORMAT_G1:
×
412
        return "G1";
×
UNCOV
413
    case SIXEL_PIXELFORMAT_G2:
×
414
        return "G2";
×
UNCOV
415
    case SIXEL_PIXELFORMAT_G4:
×
416
        return "G4";
×
UNCOV
417
    case SIXEL_PIXELFORMAT_G8:
×
418
        return "G8";
×
UNCOV
419
    default:
×
420
        break;
×
421
    }
422
    return "unknown";
×
423
}
424

425
static char const *
426
assessment_colorspace_name(int colorspace)
2✔
427
{
428
    switch (colorspace) {
2!
429
    case SIXEL_COLORSPACE_GAMMA:
2✔
430
        return "gamma";
2✔
UNCOV
431
    case SIXEL_COLORSPACE_LINEAR:
×
432
        return "linear";
×
UNCOV
433
    case SIXEL_COLORSPACE_SMPTEC:
×
434
        return "smpte-c";
×
UNCOV
435
    default:
×
436
        break;
×
437
    }
438
    return "unknown";
×
439
}
440

441
SIXELAPI void
442
sixel_assessment_stage_transition(sixel_assessment_t *assessment,
18✔
443
                                  sixel_assessment_stage_t stage)
444
{
445
    double now;
446
    double elapsed;
447
    sixel_assessment_stage_t previous_stage;
448
    double total_pending;
449

450
    if (assessment == NULL) {
18!
451
        return;
×
452
    }
453
    if ((assessment->sections_mask &
18!
454
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
455
        return;
18✔
456
    }
457
    if (stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
458
            stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
459
        return;
×
460
    }
461
    now = sixel_assessment_timer_now();
×
462
    previous_stage = assessment->active_stage;
×
463
    if (assessment->stage_active &&
×
464
            previous_stage > SIXEL_ASSESSMENT_STAGE_NONE &&
×
465
            previous_stage < SIXEL_ASSESSMENT_STAGE_COUNT) {
466
        elapsed = now - assessment->stage_started_at;
×
467
        if (previous_stage != SIXEL_ASSESSMENT_STAGE_OUTPUT) {
×
468
            /* Output spans rely on explicit fn_write() timing. */
469
            assessment->stage_durations[previous_stage] += elapsed;
×
470
        }
471
        if (previous_stage == SIXEL_ASSESSMENT_STAGE_ENCODE &&
×
472
                (assessment->encode_output_time_pending > 0.0 ||
×
473
                 assessment->encode_palette_time_pending > 0.0)) {
×
474
            /*
475
             * Rebalance the encode slot so that we only keep the pure
476
             * encoder core time.  The write spans move to the output
477
             * bucket, and the palette spans return to the palette
478
             * bucket.  The ASCII map shows how the same wall-clock
479
             * region is split across the stages.
480
             *
481
             *     encode wall time
482
             *     +----------+-----------+------------------+
483
             *     | palette  | encode    | write callback ->|
484
             *     | (return) | core stay | (output bucket) |
485
             *     +----------+-----------+------------------+
486
             */
487
            total_pending = assessment->encode_output_time_pending +
×
488
                assessment->encode_palette_time_pending;
×
489
            assessment->stage_durations[previous_stage] -= total_pending;
×
490
            if (assessment->stage_durations[previous_stage] < 0.0) {
×
491
                assessment->stage_durations[previous_stage] = 0.0;
×
492
            }
493
            assessment->encode_output_time_pending = 0.0;
×
494
            assessment->encode_palette_time_pending = 0.0;
×
495
            if (g_active_encode_assessment == assessment) {
×
496
                g_active_encode_assessment = NULL;
×
497
            }
498
        }
499
    }
500
    assessment->active_stage = stage;
×
501
    assessment->stage_started_at = now;
×
502
    assessment->stage_active = 1;
×
503
    if (stage == SIXEL_ASSESSMENT_STAGE_ENCODE) {
×
504
        assessment->encode_output_time_pending = 0.0;
×
505
        assessment->encode_palette_time_pending = 0.0;
×
506
        g_active_encode_assessment = assessment;
×
507
    } else if (g_active_encode_assessment == assessment) {
×
508
        g_active_encode_assessment = NULL;
×
509
    }
510
}
511

512
SIXELAPI void
513
sixel_assessment_stage_finish(sixel_assessment_t *assessment)
4✔
514
{
515
    double now;
516
    double elapsed;
517
    sixel_assessment_stage_t finished_stage;
518
    double total_pending;
519

520
    if (assessment == NULL) {
4!
521
        return;
×
522
    }
523
    if ((assessment->sections_mask &
4!
524
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
525
        assessment->stage_active = 0;
4✔
526
        assessment->active_stage = SIXEL_ASSESSMENT_STAGE_NONE;
4✔
527
        assessment->stage_started_at = 0.0;
4✔
528
        assessment->encode_output_time_pending = 0.0;
4✔
529
        if (g_active_encode_assessment == assessment) {
4!
530
            g_active_encode_assessment = NULL;
×
531
        }
532
        return;
4✔
533
    }
534
    if (!assessment->stage_active ||
×
535
            assessment->active_stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
536
            assessment->active_stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
×
537
        assessment->stage_active = 0;
×
538
        assessment->active_stage = SIXEL_ASSESSMENT_STAGE_NONE;
×
539
        assessment->stage_started_at = 0.0;
×
540
        assessment->encode_output_time_pending = 0.0;
×
541
        return;
×
542
    }
543
    now = sixel_assessment_timer_now();
×
544
    elapsed = now - assessment->stage_started_at;
×
545
    finished_stage = assessment->active_stage;
×
546
    if (finished_stage != SIXEL_ASSESSMENT_STAGE_OUTPUT) {
×
547
        /* Output spans rely on explicit fn_write() timing. */
548
        assessment->stage_durations[finished_stage] += elapsed;
×
549
    }
550
    if (finished_stage == SIXEL_ASSESSMENT_STAGE_ENCODE &&
×
551
            (assessment->encode_output_time_pending > 0.0 ||
×
552
             assessment->encode_palette_time_pending > 0.0)) {
×
553
        /* Mirror the transition logic for the final leg. */
554
        total_pending = assessment->encode_output_time_pending +
×
555
            assessment->encode_palette_time_pending;
×
556
        assessment->stage_durations[finished_stage] -= total_pending;
×
557
        if (assessment->stage_durations[finished_stage] < 0.0) {
×
558
            assessment->stage_durations[finished_stage] = 0.0;
×
559
        }
560
        assessment->encode_output_time_pending = 0.0;
×
561
        assessment->encode_palette_time_pending = 0.0;
×
562
        if (g_active_encode_assessment == assessment) {
×
563
            g_active_encode_assessment = NULL;
×
564
        }
565
    }
566
    assessment->stage_active = 0;
×
567
    assessment->active_stage = SIXEL_ASSESSMENT_STAGE_NONE;
×
568
    assessment->stage_started_at = 0.0;
×
569
}
570

571
SIXELAPI void
572
sixel_assessment_record_stage_duration(sixel_assessment_t *assessment,
×
573
                                       sixel_assessment_stage_t stage,
574
                                       double duration)
575
{
576
    if (assessment == NULL) {
×
577
        return;
×
578
    }
579
    if ((assessment->sections_mask &
×
580
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
581
        return;
×
582
    }
583
    if (stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
584
            stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
585
        return;
×
586
    }
587
    if (duration < 0.0) {
×
588
        duration = 0.0;
×
589
    }
590
    assessment->stage_durations[stage] += duration;
×
591
}
592

593
SIXELAPI void
594
sixel_assessment_record_loader(sixel_assessment_t *assessment,
2✔
595
                               char const *path,
596
                               char const *loader_name,
597
                               size_t input_bytes)
598
{
599
    unsigned int mask;
600

601
    if (assessment == NULL) {
2!
602
        return;
×
603
    }
604
    mask = assessment->sections_mask;
2✔
605
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
2!
606
                 SIXEL_ASSESSMENT_SECTION_PERFORMANCE)) == 0u) {
607
        return;
×
608
    }
609
    if ((mask & SIXEL_ASSESSMENT_SECTION_BASIC) != 0u) {
2!
610
        if (path != NULL) {
2!
611
            (void)snprintf(assessment->input_path,
2✔
612
                           sizeof(assessment->input_path),
613
                           "%s",
614
                           path);
615
        } else {
616
            assessment->input_path[0] = '\0';
×
617
        }
618
        if (loader_name != NULL) {
2!
619
            (void)snprintf(assessment->loader_name,
2✔
620
                           sizeof(assessment->loader_name),
621
                           "%s",
622
                           loader_name);
623
        } else {
624
            assessment->loader_name[0] = '\0';
×
625
        }
626
        assessment->input_bytes = input_bytes;
2✔
627
        assessment_guess_format(assessment);
2✔
628
    }
629
    if ((mask & SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
2!
630
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_IMAGE_CHUNK] +=
×
631
            input_bytes;
632
    }
633
}
634

635
SIXELAPI void
636
sixel_assessment_record_source_frame(sixel_assessment_t *assessment,
2✔
637
                                     sixel_frame_t *frame)
638
{
639
    int width;
640
    int height;
641
    int pixelformat;
642
    int colorspace;
643
    int depth;
644
    size_t bytes;
645
    unsigned int mask;
646

647
    if (assessment == NULL || frame == NULL) {
2!
648
        return;
×
649
    }
650
    mask = assessment->sections_mask;
2✔
651
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
2!
652
                 SIXEL_ASSESSMENT_SECTION_SIZE |
653
                 SIXEL_ASSESSMENT_SECTION_PERFORMANCE)) == 0u) {
654
        return;
×
655
    }
656
    width = sixel_frame_get_width(frame);
2✔
657
    height = sixel_frame_get_height(frame);
2✔
658
    pixelformat = sixel_frame_get_pixelformat(frame);
2✔
659
    colorspace = sixel_frame_get_colorspace(frame);
2✔
660
    depth = sixel_helper_compute_depth(pixelformat);
2✔
661
    if (depth <= 0 || width <= 0 || height <= 0) {
2!
662
        bytes = 0u;
×
663
    } else {
664
        bytes = (size_t)width;
2✔
665
        bytes *= (size_t)height;
2✔
666
        bytes *= (size_t)depth;
2✔
667
    }
668
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
2!
669
                 SIXEL_ASSESSMENT_SECTION_SIZE)) != 0u) {
670
        assessment->input_pixelformat = pixelformat;
2✔
671
        assessment->input_colorspace = colorspace;
2✔
672
        assessment->source_pixels_bytes = bytes;
2✔
673
    }
674
    if ((mask & SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
2!
675
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_IMAGE_DECODE] +=
×
676
            bytes;
677
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_SCALE] += bytes;
×
678
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_CROP] += bytes;
×
679
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_COLORSPACE] += bytes;
×
680
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_PALETTE_HISTOGRAM] +=
×
681
            bytes;
682
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_PALETTE_SOLVE] +=
×
683
            bytes;
684
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_PALETTE_APPLY] +=
×
685
            bytes;
686
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_ENCODE] += bytes;
×
687
    }
688
}
689

690
SIXELAPI void
691
sixel_assessment_record_quantized_capture(
2✔
692
    sixel_assessment_t *assessment,
693
    sixel_encoder_t *encoder)
694
{
695
    unsigned int mask;
696

697
    if (assessment == NULL || encoder == NULL) {
2!
698
        return;
×
699
    }
700
    mask = assessment->sections_mask;
2✔
701
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
2!
702
                 SIXEL_ASSESSMENT_SECTION_SIZE |
703
                 SIXEL_ASSESSMENT_SECTION_QUALITY)) == 0u) {
704
        return;
×
705
    }
706
    if ((mask & (SIXEL_ASSESSMENT_SECTION_BASIC |
2!
UNCOV
707
                 SIXEL_ASSESSMENT_SECTION_SIZE)) == 0u &&
×
708
            (assessment->view_mask & SIXEL_ASSESSMENT_VIEW_QUANTIZED) == 0u) {
×
709
        return;
×
710
    }
711
    assessment->quantized_pixels_bytes = encoder->capture_pixel_bytes;
2✔
712
    assessment->palette_bytes = encoder->capture_palette_size;
2✔
713
    assessment->palette_colors = encoder->capture_ncolors;
2✔
714
    if (assessment->quantized_pixels_bytes == 0 &&
2!
715
            assessment->palette_colors == 0) {
×
716
        assessment->palette_bytes = 0;
×
717
    }
718
}
719

720
SIXELAPI void
721
sixel_assessment_record_output_size(sixel_assessment_t *assessment,
2✔
722
                                    size_t output_bytes)
723
{
724
    if (assessment == NULL) {
2!
725
        return;
×
726
    }
727
    if (output_bytes == 0u && assessment->output_bytes_written > 0u) {
2!
728
        output_bytes = assessment->output_bytes_written;
2✔
729
    }
730
    assessment->output_bytes = output_bytes;
2✔
731
    if ((assessment->sections_mask &
2!
732
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
733
        assessment->stage_bytes[SIXEL_ASSESSMENT_STAGE_OUTPUT] +=
×
734
            output_bytes;
735
    }
736
}
737

738
SIXELAPI void
739
sixel_assessment_record_output_write(sixel_assessment_t *assessment,
48✔
740
                                     size_t bytes,
741
                                     double duration)
742
{
743
    if (assessment == NULL) {
48!
744
        return;
×
745
    }
746
    if (bytes > 0u) {
48!
747
        assessment->output_bytes_written += bytes;
48✔
748
    }
749
    if (duration > 0.0 &&
48!
750
            (assessment->sections_mask &
48!
751
             SIXEL_ASSESSMENT_SECTION_PERFORMANCE) != 0u) {
752
        /* The output bucket collects every fn_write() span verbatim. */
753
        assessment->stage_durations[SIXEL_ASSESSMENT_STAGE_OUTPUT] += duration;
×
754
        if (assessment->active_stage == SIXEL_ASSESSMENT_STAGE_ENCODE) {
×
755
            assessment->encode_output_time_pending += duration;
×
756
        }
757
    }
758
}
759

760
SIXELAPI int
761
sixel_assessment_palette_probe_enabled(void)
170✔
762
{
763
    return g_active_encode_assessment != NULL;
170✔
764
}
765

766
SIXELAPI void
767
sixel_assessment_record_palette_apply_span(double duration)
×
768
{
769
    sixel_assessment_t *assessment;
770

771
    if (duration <= 0.0) {
×
772
        return;
×
773
    }
774
    assessment = g_active_encode_assessment;
×
775
    if (assessment == NULL) {
×
776
        return;
×
777
    }
778
    if ((assessment->sections_mask &
×
779
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
780
        return;
×
781
    }
782
    /*
783
     * Palette work performed inside sixel_encode() belongs to the
784
     * palette bucket even though the call happens while the encode
785
     * stage is active.  The time goes straight to the palette stage
786
     * while we stash the same span for the later encode rebalance.
787
     */
788
    assessment->stage_durations[SIXEL_ASSESSMENT_STAGE_PALETTE_APPLY] +=
×
789
        duration;
790
    assessment->encode_palette_time_pending += duration;
×
791
}
792

793
SIXELAPI int
794
sixel_assessment_encode_probe_enabled(void)
1,068✔
795
{
796
    sixel_assessment_t *assessment;
797

798
    assessment = g_active_encode_assessment;
1,068✔
799
    if (assessment == NULL) {
1,068!
800
        return 0;
1,068✔
801
    }
802
    if ((assessment->sections_mask &
×
803
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
804
        return 0;
×
805
    }
806
    return 1;
×
807
}
808

809
SIXELAPI void
810
sixel_assessment_set_encode_parallelism(int threads)
1,068✔
811
{
812
    if (threads < 1) {
1,068!
813
        threads = 1;
×
814
    }
815
    g_encode_parallel_threads = threads;
1,068✔
816
    if (threads > 1) {
1,068!
817
        g_encode_substages_visible = 0;
×
818
    } else {
819
        g_encode_substages_visible = 1;
1,068✔
820
    }
821
}
1,068✔
822

823
SIXELAPI void
824
sixel_assessment_record_encode_span(sixel_assessment_stage_t stage,
×
825
                                    double duration)
826
{
827
    sixel_assessment_t *assessment;
828

829
    if (duration <= 0.0) {
×
830
        return;
×
831
    }
832
    if (stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
833
            stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
834
        return;
×
835
    }
836
    assessment = g_active_encode_assessment;
×
837
    if (assessment == NULL) {
×
838
        return;
×
839
    }
840
    if ((assessment->sections_mask &
×
841
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
842
        return;
×
843
    }
844
    assessment->stage_durations[stage] += duration;
×
845
}
846

847
SIXELAPI void
848
sixel_assessment_record_encode_work(sixel_assessment_stage_t stage,
×
849
                                    double amount)
850
{
851
    sixel_assessment_t *assessment;
852

853
    if (amount <= 0.0) {
×
854
        return;
×
855
    }
856
    if (stage <= SIXEL_ASSESSMENT_STAGE_NONE ||
×
857
            stage >= SIXEL_ASSESSMENT_STAGE_COUNT) {
858
        return;
×
859
    }
860
    assessment = g_active_encode_assessment;
×
861
    if (assessment == NULL) {
×
862
        return;
×
863
    }
864
    if ((assessment->sections_mask &
×
865
            SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
866
        return;
×
867
    }
868
    assessment->stage_bytes[stage] += amount;
×
869
}
870

871
SIXELAPI void
872
sixel_assessment_select_sections(sixel_assessment_t *assessment,
2✔
873
                                 unsigned int sections)
874
{
875
    unsigned int section_mask;
876
    unsigned int view_mask;
877

878
    if (assessment == NULL) {
2!
879
        return;
×
880
    }
881
    section_mask = sections & SIXEL_ASSESSMENT_SECTION_MASK;
2✔
882
    if ((section_mask & SIXEL_ASSESSMENT_SECTION_QUALITY) == 0u) {
2!
883
        view_mask = SIXEL_ASSESSMENT_VIEW_ENCODED;
2✔
UNCOV
884
    } else if ((sections & SIXEL_ASSESSMENT_VIEW_MASK) != 0u) {
×
885
        view_mask = SIXEL_ASSESSMENT_VIEW_QUANTIZED;
×
886
    } else {
887
        view_mask = SIXEL_ASSESSMENT_VIEW_ENCODED;
×
888
    }
889
    assessment->sections_mask = section_mask;
2✔
890
    assessment->view_mask = view_mask;
2✔
891
    if ((section_mask & SIXEL_ASSESSMENT_SECTION_PERFORMANCE) == 0u) {
2!
892
        assessment->stage_active = 0;
2✔
893
        if (g_active_encode_assessment == assessment) {
2!
894
            g_active_encode_assessment = NULL;
×
895
        }
896
    }
897
}
898

899
SIXELAPI void
900
sixel_assessment_attach_encoder(sixel_assessment_t *assessment,
2✔
901
                                sixel_encoder_t *encoder)
902
{
903
    if (encoder == NULL) {
2!
904
        return;
×
905
    }
906
    encoder->assessment_observer = assessment;
2✔
907
}
908

909
static int assessment_resolve_executable_dir(char const *argv0,
910
                                            char *buffer,
911
                                            size_t size);
912
static void align_frame_pixels(float **ref_pixels,
913
                               int *ref_width,
914
                               int *ref_height,
915
                               float **out_pixels,
916
                               int *out_width,
917
                               int *out_height);
918

919
static void
920
assessment_fail(SIXELSTATUS status, char const *message)
×
921
{
922
    sixel_assessment_t *ctx;
923
    size_t length;
924

925
    ctx = g_assessment_context;
×
926
    if (ctx != NULL) {
×
927
        ctx->last_error = status;
×
928
        if (message != NULL) {
×
929
            length = strlen(message);
×
930
            if (length >= sizeof(ctx->error_message)) {
×
931
                length = sizeof(ctx->error_message) - 1u;
×
932
            }
933
            memcpy(ctx->error_message, message, length);
×
934
            ctx->error_message[length] = '\0';
×
935
        } else {
936
            ctx->error_message[0] = '\0';
×
937
        }
938
        longjmp(ctx->bailout, 1);
×
939
    }
940
    if (message != NULL) {
×
941
        fprintf(stderr, "%s\n", message);
×
942
    } else {
943
        fprintf(stderr, "assessment failure\n");
×
944
    }
945
    abort();
×
946
}
947

948
/*
949
 * Memory helpers
950
 */
951
static void *xmalloc(size_t size)
×
952
{
953
    void *ptr;
954
    ptr = malloc(size);
×
955
    if (ptr == NULL) {
×
956
        assessment_fail(SIXEL_BAD_ALLOCATION,
×
957
                       "malloc failed while building assessment state");
958
    }
959
    return ptr;
×
960
}
961

962
static void *xcalloc(size_t nmemb, size_t size)
×
963
{
964
    void *ptr;
965
    ptr = calloc(nmemb, size);
×
966
    if (ptr == NULL) {
×
967
        assessment_fail(SIXEL_BAD_ALLOCATION,
×
968
                       "calloc failed while building assessment state");
969
    }
970
    return ptr;
×
971
}
972

973
/*
974
 * Loader bridge (libsixel -> float RGB)
975
 */
976
static SIXELSTATUS copy_frame_to_rgb(sixel_frame_t *frame,
×
977
                                     unsigned char **pixels,
978
                                     int *width,
979
                                     int *height)
980
{
981
    SIXELSTATUS status;
982
    int frame_width;
983
    int frame_height;
984
    int pixelformat;
985
    size_t size;
986
    unsigned char *buffer;
987
    int normalized_format;
988

989
    frame_width = sixel_frame_get_width(frame);
×
990
    frame_height = sixel_frame_get_height(frame);
×
991
    status = sixel_frame_strip_alpha(frame, NULL);
×
992
    if (SIXEL_FAILED(status)) {
×
993
        return status;
×
994
    }
995
    pixelformat = sixel_frame_get_pixelformat(frame);
×
996
    size = (size_t)frame_width * (size_t)frame_height * 3u;
×
997
    /*
998
     * Use malloc here because the loader's allocator enforces a strict
999
     * allocation ceiling that can reject large frames even on hosts with
1000
     * sufficient RAM available.
1001
     */
1002
    buffer = (unsigned char *)malloc(size);
×
1003
    if (buffer == NULL) {
×
1004
        return SIXEL_BAD_ALLOCATION;
×
1005
    }
1006
    if (pixelformat == SIXEL_PIXELFORMAT_RGB888) {
×
1007
        memcpy(buffer, sixel_frame_get_pixels(frame), size);
×
1008
    } else {
1009
        normalized_format = pixelformat;
×
1010
        status = sixel_helper_normalize_pixelformat(
×
1011
            buffer,
1012
            &normalized_format,
1013
            sixel_frame_get_pixels(frame),
×
1014
            pixelformat,
1015
            frame_width,
1016
            frame_height);
1017
        if (SIXEL_FAILED(status)) {
×
1018
            free(buffer);
×
1019
            return status;
×
1020
        }
1021
    }
1022
    *pixels = buffer;
×
1023
    *width = frame_width;
×
1024
    *height = frame_height;
×
1025
    return SIXEL_OK;
×
1026
}
1027

1028
static SIXELSTATUS
1029
frame_to_rgb_float(sixel_frame_t *frame,
×
1030
                   float **pixels_out,
1031
                   int *width_out,
1032
                   int *height_out)
1033
{
1034
    SIXELSTATUS status;
1035
    unsigned char *pixels;
1036
    int width;
1037
    int height;
1038
    float *converted;
1039
    size_t count;
1040
    size_t index;
1041

1042
    status = SIXEL_FALSE;
×
1043
    pixels = NULL;
×
1044
    width = 0;
×
1045
    height = 0;
×
1046
    converted = NULL;
×
1047
    count = 0;
×
1048
    index = 0;
×
1049

1050
    if (frame == NULL || pixels_out == NULL || width_out == NULL ||
×
1051
            height_out == NULL) {
1052
        return SIXEL_BAD_ARGUMENT;
×
1053
    }
1054

1055
    *pixels_out = NULL;
×
1056
    *width_out = 0;
×
1057
    *height_out = 0;
×
1058

1059
    status = copy_frame_to_rgb(frame, &pixels, &width, &height);
×
1060
    if (SIXEL_FAILED(status)) {
×
1061
        goto cleanup;
×
1062
    }
1063
    count = (size_t)width * (size_t)height *
×
1064
            (size_t)SIXEL_ASSESSMENT_RGB_CHANNELS;
1065
    converted = (float *)xmalloc(count * sizeof(float));
×
1066
    for (index = 0; index < count; ++index) {
×
1067
        converted[index] = pixels[index] / 255.0f;
×
1068
    }
1069
    *pixels_out = converted;
×
1070
    *width_out = width;
×
1071
    *height_out = height;
×
1072
    converted = NULL;
×
1073
    status = SIXEL_OK;
×
1074

UNCOV
1075
cleanup:
×
1076
    if (pixels != NULL) {
×
1077
        free(pixels);
×
1078
    }
1079
    if (converted != NULL) {
×
1080
        free(converted);
×
1081
    }
1082
    return status;
×
1083
}
1084

1085
/*
1086
 * Path discovery helpers (shared by CLI + LPIPS bridge)
1087
 */
1088
#if defined(HAVE_ONNXRUNTIME) || \
1089
    (!defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__))
1090
static int path_accessible(char const *path)
1091
{
1092
#if defined(_WIN32)
1093
    int rc;
1094

1095
    rc = _access(path, 4);
1096
    return rc == 0;
1097
#else
1098
    return access(path, R_OK) == 0;
1099
#endif
1100
}
1101

1102
static int
1103
join_path(char const *dir,
1104
          char const *leaf,
1105
          char *buffer,
1106
          size_t size)
1107
{
1108
    size_t dir_len;
1109
    size_t leaf_len;
1110
    int need_sep;
1111
    size_t total;
1112

1113
    dir_len = strlen(dir);
1114
    leaf_len = strlen(leaf);
1115
    need_sep = 0;
1116
    if (leaf_len > 0 && (leaf[0] == '/' || leaf[0] == '\\')) {
1117
        dir_len = 0;
1118
    } else if (dir_len > 0 && dir[dir_len - 1] != SIXEL_PATH_SEP) {
1119
        need_sep = 1;
1120
    }
1121
    total = dir_len + need_sep + leaf_len + 1u;
1122
    if (total > size) {
1123
        return -1;
1124
    }
1125
    if (dir_len > 0) {
1126
        memcpy(buffer, dir, dir_len);
1127
    }
1128
    if (need_sep) {
1129
        buffer[dir_len] = SIXEL_PATH_SEP;
1130
        ++dir_len;
1131
    }
1132
    if (leaf_len > 0) {
1133
        memcpy(buffer + dir_len, leaf, leaf_len);
1134
        dir_len += leaf_len;
1135
    }
1136
    buffer[dir_len] = '\0';
1137
    return 0;
1138
}
1139
#endif
1140

1141
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__)
1142
static int
1143
resolve_from_path_env(char const *name,
1144
                      char *buffer,
1145
                      size_t size)
1146
{
1147
    char const *env;
1148
    char const *cursor;
1149
    char const *separator;
1150
    size_t chunk_len;
1151

1152
    env = getenv("PATH");
1153
    if (env == NULL || *env == '\0') {
1154
        return -1;
1155
    }
1156
    cursor = env;
1157
    while (*cursor != '\0') {
1158
        separator = strchr(cursor, SIXEL_PATH_LIST_SEP);
1159
        if (separator == NULL) {
1160
            chunk_len = strlen(cursor);
1161
        } else {
1162
            chunk_len = (size_t)(separator - cursor);
1163
        }
1164
        if (chunk_len >= size) {
1165
            return -1;
1166
        }
1167
        memcpy(buffer, cursor, chunk_len);
1168
        buffer[chunk_len] = '\0';
1169
        if (join_path(buffer, name, buffer, size) != 0) {
1170
            return -1;
1171
        }
1172
        if (path_accessible(buffer)) {
1173
            return 0;
1174
        }
1175
        if (separator == NULL) {
1176
            break;
1177
        }
1178
        cursor = separator + 1;
1179
    }
1180
    return -1;
1181
}
1182
#endif
1183

1184
static int
1185
assessment_resolve_executable_dir(char const *argv0,
×
1186
                                  char *buffer,
1187
                                  size_t size)
1188
{
1189
    char candidate[PATH_MAX];
1190
    size_t length;
1191
    char *slash;
1192
#if defined(_WIN32)
1193
    DWORD written;
1194
#elif defined(__APPLE__)
1195
    uint32_t bufsize;
1196
#elif defined(__linux__)
1197
    ssize_t count;
1198
#endif
1199

1200
    candidate[0] = '\0';
×
1201
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
1202
    (void)argv0;
1203
#endif
1204
#if defined(_WIN32)
1205
    written = GetModuleFileNameA(NULL, candidate, (DWORD)sizeof(candidate));
1206
    if (written == 0 || written >= sizeof(candidate)) {
1207
        return -1;
1208
    }
1209
#elif defined(__APPLE__)
1210
    bufsize = (uint32_t)sizeof(candidate);
1211
    if (_NSGetExecutablePath(candidate, &bufsize) != 0) {
×
1212
        return -1;
1213
    }
1214
#elif defined(__linux__)
UNCOV
1215
    count = readlink("/proc/self/exe", candidate, sizeof(candidate) - 1u);
×
UNCOV
1216
    if (count < 0 || count >= (ssize_t)sizeof(candidate)) {
×
UNCOV
1217
        return -1;
×
1218
    }
UNCOV
1219
    candidate[count] = '\0';
×
1220
#else
1221
    if (argv0 == NULL) {
1222
        return -1;
1223
    }
1224
    if (strchr(argv0, '/') != NULL || strchr(argv0, '\\') != NULL) {
1225
        if (strlen(argv0) >= sizeof(candidate)) {
1226
            return -1;
1227
        }
1228
        (void)sixel_compat_strcpy(candidate,
1229
                                  sizeof(candidate),
1230
                                  argv0);
1231
    } else if (resolve_from_path_env(argv0, candidate,
1232
                                     sizeof(candidate)) != 0) {
1233
        return -1;
1234
    }
1235
    {
1236
        char *resolved;
1237

1238
        resolved = realpath(candidate, NULL);
1239
        if (resolved == NULL) {
1240
            return -1;
1241
        }
1242
        if (strlen(resolved) >= sizeof(candidate)) {
1243
            free(resolved);
1244
            return -1;
1245
        }
1246
        (void)sixel_compat_strcpy(candidate,
1247
                                  sizeof(candidate),
1248
                                  resolved);
1249
        free(resolved);
1250
    }
1251
#endif
1252
#if defined(_WIN32)
1253
    {
1254
        char *resolved;
1255

1256
        resolved = _fullpath(NULL, candidate, 0u);
1257
        if (resolved != NULL) {
1258
            if (strlen(resolved) < sizeof(candidate)) {
1259
                (void)sixel_compat_strcpy(candidate,
1260
                                          sizeof(candidate),
1261
                                          resolved);
1262
            }
1263
            free(resolved);
1264
        }
1265
    }
1266
#endif
1267
    length = strlen(candidate);
×
1268
    if (length == 0) {
×
1269
        return -1;
×
1270
    }
1271
    slash = strrchr(candidate, '/');
×
1272
#if defined(_WIN32)
1273
    if (slash == NULL) {
1274
        slash = strrchr(candidate, '\\');
1275
    }
1276
#endif
1277
    if (slash == NULL) {
×
1278
        return -1;
×
1279
    }
1280
    *slash = '\0';
×
1281
    if (strlen(candidate) + 1u > size) {
×
1282
        return -1;
×
1283
    }
1284
    (void)sixel_compat_strcpy(buffer, size, candidate);
×
1285
    return 0;
×
1286
}
1287

1288
#if defined(HAVE_ONNXRUNTIME)
1289
/*
1290
 * LPIPS helper plumbing (model discovery + tensor formatting)
1291
 */
1292
typedef struct image_f32 {
1293
    int width;
1294
    int height;
1295
    float *nchw;
1296
} image_f32_t;
1297

1298
static const OrtApi *g_lpips_api = NULL;
1299

1300
static int build_local_model_path(char const *binary_dir,
1301
                                  char const *name,
1302
                                  char *buffer,
1303
                                  size_t size)
1304
{
1305
    char stage1[PATH_MAX];
1306
    char stage2[PATH_MAX];
1307
    char stage3[PATH_MAX];
1308

1309
    if (binary_dir == NULL || binary_dir[0] == '\0') {
1310
        return (-1);
1311
    }
1312

1313
    if (join_path(binary_dir, SIXEL_LOCAL_MODELS_SEG1,
1314
                  stage1, sizeof(stage1)) != 0) {
1315
        return (-1);
1316
    }
1317
    if (join_path(stage1, SIXEL_LOCAL_MODELS_SEG2,
1318
                  stage2, sizeof(stage2)) != 0) {
1319
        return (-1);
1320
    }
1321
    if (join_path(stage2, SIXEL_LOCAL_MODELS_SEG3,
1322
                  stage3, sizeof(stage3)) != 0) {
1323
        return (-1);
1324
    }
1325
    if (join_path(stage3, name, buffer, size) != 0) {
1326
        return (-1);
1327
    }
1328
    return (0);
1329
}
1330

1331
static int find_model(char const *binary_dir,
1332
                      char const *override_dir,
1333
                      char const *name,
1334
                      char *buffer,
1335
                      size_t size)
1336
{
1337
    char env_root[PATH_MAX];
1338
    char install_root[PATH_MAX];
1339
    char binary_parent_path[PATH_MAX];
1340
    char const *env_dir;
1341

1342
    env_dir = getenv("LIBSIXEL_MODEL_DIR");
1343
    if (env_dir != NULL && env_dir[0] != '\0') {
1344
        if (join_path(env_dir, SIXEL_LOCAL_MODELS_SEG3,
1345
                      env_root, sizeof(env_root)) == 0) {
1346
            if (join_path(env_root, name, buffer, size) == 0) {
1347
                if (path_accessible(buffer)) {
1348
                    return (0);
1349
                }
1350
            }
1351
        }
1352
    }
1353
    if (override_dir != NULL && override_dir[0] != '\0') {
1354
        if (join_path(override_dir, name, buffer, size) == 0) {
1355
            if (path_accessible(buffer)) {
1356
                return (0);
1357
            }
1358
        }
1359
    }
1360
    if (SIXEL_MODEL_DIR[0] != '\0') {
1361
        /* checking ${packagedatadir}/models */
1362
        if (join_path(SIXEL_MODEL_DIR, SIXEL_LOCAL_MODELS_SEG3,
1363
                      install_root, sizeof(install_root)) == 0) {
1364
            if (join_path(install_root, name, buffer, size) == 0) {
1365
                if (path_accessible(buffer)) {
1366
                    return (0);
1367
                }
1368
            }
1369
        }
1370
    }
1371

1372
    /*
1373
     * Try ../models/lpips and ../../models/lpips so staged binaries in
1374
     * converters/ or tools/ still find the ONNX assets.
1375
     */
1376
    if (binary_dir != NULL && binary_dir[0] != '\0') {
1377
        /* checking ../models/lpips */
1378
        if (build_local_model_path(binary_dir, name, buffer, size) == 0) {
1379
            if (path_accessible(buffer)) {
1380
                return (0);
1381
            }
1382
        }
1383
    }
1384
    if (binary_dir != NULL && binary_dir[0] != '\0') {
1385
        /* checking ../../models/lpips */
1386
        if (join_path(binary_dir, SIXEL_LOCAL_MODELS_SEG1,
1387
                      binary_parent_path, sizeof(binary_parent_path)) == 0) {
1388
            if (build_local_model_path(binary_parent_path, name, buffer, size) == 0) {
1389
                if (path_accessible(buffer)) {
1390
                    return (0);
1391
                }
1392
            }
1393
        }
1394
    }
1395
    return (-1);
1396
}
1397

1398
static int
1399
ensure_lpips_models(sixel_assessment_t *assessment)
1400
{
1401
    if (assessment->lpips_models_ready) {
1402
        return 0;
1403
    }
1404
    if (find_model(assessment->binary_dir,
1405
                   assessment->model_dir_state > 0
1406
                       ? assessment->model_dir
1407
                       : NULL,
1408
                   "lpips_diff.onnx",
1409
                   assessment->diff_model_path,
1410
                   sizeof(assessment->diff_model_path)) != 0) {
1411
        fprintf(stderr,
1412
                "Warning: lpips_diff.onnx not found.\n");
1413
        return -1;
1414
    }
1415
    if (find_model(assessment->binary_dir,
1416
                   assessment->model_dir_state > 0
1417
                       ? assessment->model_dir
1418
                       : NULL,
1419
                   "lpips_feature.onnx",
1420
                   assessment->feat_model_path,
1421
                   sizeof(assessment->feat_model_path)) != 0) {
1422
        fprintf(stderr,
1423
                "Warning: lpips_feature.onnx not found.\n");
1424
        return -1;
1425
    }
1426
    assessment->lpips_models_ready = 1;
1427
    return 0;
1428
}
1429

1430
static void
1431
free_image_f32(image_f32_t *image)
1432
{
1433
    if (image->nchw != NULL) {
1434
        free(image->nchw);
1435
        image->nchw = NULL;
1436
    }
1437
}
1438

1439
static int
1440
convert_pixels_to_nchw(float const *src_pixels,
1441
                       int width,
1442
                       int height,
1443
                       image_f32_t *dst)
1444
{
1445
    size_t plane_size;
1446
    size_t index;
1447
    float *buffer;
1448

1449
    if (src_pixels == NULL || dst == NULL) {
1450
        return -1;
1451
    }
1452
    if (width <= 0 || height <= 0) {
1453
        return -1;
1454
    }
1455
    plane_size = (size_t)width * (size_t)height;
1456
    buffer = (float *)malloc(plane_size *
1457
                             (size_t)SIXEL_ASSESSMENT_RGB_CHANNELS *
1458
                             sizeof(float));
1459
    if (buffer == NULL) {
1460
        return -1;
1461
    }
1462
    for (index = 0; index < plane_size; ++index) {
1463
        buffer[plane_size * 0u + index] =
1464
            src_pixels[index * 3u + 0u] * 2.0f - 1.0f;
1465
        buffer[plane_size * 1u + index] =
1466
            src_pixels[index * 3u + 1u] * 2.0f - 1.0f;
1467
        buffer[plane_size * 2u + index] =
1468
            src_pixels[index * 3u + 2u] * 2.0f - 1.0f;
1469
    }
1470
    dst->width = width;
1471
    dst->height = height;
1472
    dst->nchw = buffer;
1473
    return 0;
1474
}
1475

1476
static float *
1477
bilinear_resize_nchw3(float const *src,
1478
                      int src_width,
1479
                      int src_height,
1480
                      int dst_width,
1481
                      int dst_height)
1482
{
1483
    float *dst;
1484
    int channel;
1485
    int y;
1486
    int x;
1487
    float scale_y;
1488
    float scale_x;
1489
    float fy;
1490
    float fx;
1491
    int y0;
1492
    int x0;
1493
    float wy;
1494
    float wx;
1495
    size_t src_stride;
1496
    size_t dst_index;
1497

1498
    dst = (float *)malloc((size_t)3 * (size_t)dst_height *
1499
                          (size_t)dst_width * sizeof(float));
1500
    if (dst == NULL) {
1501
        return NULL;
1502
    }
1503
    src_stride = (size_t)src_width * (size_t)src_height;
1504
    for (channel = 0; channel < 3; ++channel) {
1505
        for (y = 0; y < dst_height; ++y) {
1506
            scale_y = (float)src_height / (float)dst_height;
1507
            fy = (float)y * scale_y;
1508
            y0 = (int)fy;
1509
            if (y0 >= src_height - 1) {
1510
                y0 = src_height - 2;
1511
            }
1512
            wy = fy - (float)y0;
1513
            for (x = 0; x < dst_width; ++x) {
1514
                scale_x = (float)src_width / (float)dst_width;
1515
                fx = (float)x * scale_x;
1516
                x0 = (int)fx;
1517
                if (x0 >= src_width - 1) {
1518
                    x0 = src_width - 2;
1519
                }
1520
                wx = fx - (float)x0;
1521
                dst_index = (size_t)channel * (size_t)dst_width *
1522
                            (size_t)dst_height +
1523
                            (size_t)y * (size_t)dst_width + (size_t)x;
1524
                dst[dst_index] =
1525
                    (1.0f - wx) * (1.0f - wy) *
1526
                        src[(size_t)channel * src_stride +
1527
                            (size_t)y0 * (size_t)src_width + (size_t)x0] +
1528
                    wx * (1.0f - wy) *
1529
                        src[(size_t)channel * src_stride +
1530
                            (size_t)y0 * (size_t)src_width +
1531
                            (size_t)(x0 + 1)] +
1532
                    (1.0f - wx) * wy *
1533
                        src[(size_t)channel * src_stride +
1534
                            (size_t)(y0 + 1) * (size_t)src_width +
1535
                            (size_t)x0] +
1536
                    wx * wy *
1537
                        src[(size_t)channel * src_stride +
1538
                            (size_t)(y0 + 1) * (size_t)src_width +
1539
                            (size_t)(x0 + 1)];
1540
            }
1541
        }
1542
    }
1543
    return dst;
1544
}
1545

1546
static int
1547
ort_status_to_error(OrtStatus *status)
1548
{
1549
    char const *message;
1550

1551
    if (status == NULL) {
1552
        return 0;
1553
    }
1554
    message = g_lpips_api->GetErrorMessage(status);
1555
    fprintf(stderr,
1556
            "ONNX Runtime error: %s\n",
1557
            message != NULL ? message : "(null)");
1558
    g_lpips_api->ReleaseStatus(status);
1559
    return -1;
1560
}
1561

1562
static void
1563
get_first_input_shape(OrtSession *session,
1564
                      int64_t *dims,
1565
                      size_t *rank)
1566
{
1567
    OrtTypeInfo *type_info;
1568
    OrtTensorTypeAndShapeInfo const *shape_info;
1569

1570
    type_info = NULL;
1571
    shape_info = NULL;
1572
    if (ort_status_to_error(g_lpips_api->SessionGetInputTypeInfo(
1573
            session, 0, &type_info)) != 0) {
1574
        return;
1575
    }
1576
    if (ort_status_to_error(g_lpips_api->CastTypeInfoToTensorInfo(
1577
            type_info, &shape_info)) != 0) {
1578
        g_lpips_api->ReleaseTypeInfo(type_info);
1579
        return;
1580
    }
1581
    if (ort_status_to_error(g_lpips_api->GetDimensionsCount(
1582
            shape_info, rank)) != 0) {
1583
        g_lpips_api->ReleaseTypeInfo(type_info);
1584
        return;
1585
    }
1586
    (void)ort_status_to_error(g_lpips_api->GetDimensions(
1587
        shape_info, dims, *rank));
1588
    g_lpips_api->ReleaseTypeInfo(type_info);
1589
}
1590

1591
static int
1592
tail_index(char const *name)
1593
{
1594
    int length;
1595
    int index;
1596

1597
    length = (int)strlen(name);
1598
    index = length - 1;
1599
    while (index >= 0 && isdigit((unsigned char)name[index])) {
1600
        --index;
1601
    }
1602
    if (index == length - 1) {
1603
        return -1;
1604
    }
1605
    return atoi(name + index + 1);
1606
}
1607

1608
static int
1609
run_lpips(char const *diff_model,
1610
          char const *feat_model,
1611
          image_f32_t *image_a,
1612
          image_f32_t *image_b,
1613
          float *result_out)
1614
{
1615
    OrtEnv *env;
1616
    OrtAllocator *allocator;
1617
    OrtSessionOptions *options;
1618
    OrtSession *diff_session;
1619
    OrtSession *feat_session;
1620
    OrtMemoryInfo *memory_info;
1621
    OrtValue *tensor_a;
1622
    OrtValue *tensor_b;
1623
    OrtValue **features_a;
1624
    OrtValue **features_b;
1625
    OrtValue const **diff_values;
1626
    OrtValue *diff_outputs[1];
1627
    char *feat_input_name;
1628
    char **feat_output_names;
1629
    char **diff_input_names;
1630
    char *diff_output_name;
1631
    int64_t feat_dims[8];
1632
    size_t feat_rank;
1633
    size_t feat_outputs;
1634
    size_t diff_inputs;
1635
    int target_width;
1636
    int target_height;
1637
    float *resized_a;
1638
    float *resized_b;
1639
    float const *tensor_data_a;
1640
    float const *tensor_data_b;
1641
    size_t plane_size;
1642
    size_t i;
1643
    int64_t tensor_shape[4];
1644
    OrtStatus *status;
1645
    int rc;
1646

1647
    env = NULL;
1648
    allocator = NULL;
1649
    options = NULL;
1650
    diff_session = NULL;
1651
    feat_session = NULL;
1652
    memory_info = NULL;
1653
    tensor_a = NULL;
1654
    tensor_b = NULL;
1655
    features_a = NULL;
1656
    features_b = NULL;
1657
    diff_values = NULL;
1658
    diff_outputs[0] = NULL;
1659
    feat_input_name = NULL;
1660
    feat_output_names = NULL;
1661
    diff_input_names = NULL;
1662
    diff_output_name = NULL;
1663
    target_width = image_a->width;
1664
    target_height = image_a->height;
1665
    resized_a = NULL;
1666
    resized_b = NULL;
1667
    tensor_data_a = image_a->nchw;
1668
    tensor_data_b = image_b->nchw;
1669
    feat_rank = 0;
1670
    feat_outputs = 0;
1671
    diff_inputs = 0;
1672
    status = NULL;
1673
    rc = -1;
1674
    *result_out = NAN;
1675

1676
    g_lpips_api = OrtGetApiBase()->GetApi(ORT_API_VERSION);
1677
    if (g_lpips_api == NULL) {
1678
        fprintf(stderr, "ONNX Runtime API unavailable.\n");
1679
        goto cleanup;
1680
    }
1681

1682
    status = g_lpips_api->CreateEnv(ORT_LOGGING_LEVEL_WARNING,
1683
                                    "lpips",
1684
                                    &env);
1685
    if (ort_status_to_error(status) != 0) {
1686
        goto cleanup;
1687
    }
1688
    status = g_lpips_api->GetAllocatorWithDefaultOptions(&allocator);
1689
    if (ort_status_to_error(status) != 0) {
1690
        goto cleanup;
1691
    }
1692
    status = g_lpips_api->CreateSessionOptions(&options);
1693
    if (ort_status_to_error(status) != 0) {
1694
        goto cleanup;
1695
    }
1696
    status = g_lpips_api->CreateSession(env, diff_model, options,
1697
                                        &diff_session);
1698
    if (ort_status_to_error(status) != 0) {
1699
        goto cleanup;
1700
    }
1701
    status = g_lpips_api->CreateSession(env, feat_model, options,
1702
                                        &feat_session);
1703
    if (ort_status_to_error(status) != 0) {
1704
        goto cleanup;
1705
    }
1706

1707
    get_first_input_shape(feat_session, feat_dims, &feat_rank);
1708
    if (feat_rank >= 4 && feat_dims[3] > 0) {
1709
        target_width = (int)feat_dims[3];
1710
    }
1711
    if (feat_rank >= 4 && feat_dims[2] > 0) {
1712
        target_height = (int)feat_dims[2];
1713
    }
1714

1715
    if (image_a->width != target_width ||
1716
        image_a->height != target_height) {
1717
        resized_a = bilinear_resize_nchw3(image_a->nchw,
1718
                                          image_a->width,
1719
                                          image_a->height,
1720
                                          target_width,
1721
                                          target_height);
1722
        if (resized_a == NULL) {
1723
            fprintf(stderr,
1724
                    "Warning: unable to resize LPIPS reference tensor.\n");
1725
            goto cleanup;
1726
        }
1727
        tensor_data_a = resized_a;
1728
    }
1729
    if (image_b->width != target_width ||
1730
        image_b->height != target_height) {
1731
        resized_b = bilinear_resize_nchw3(image_b->nchw,
1732
                                          image_b->width,
1733
                                          image_b->height,
1734
                                          target_width,
1735
                                          target_height);
1736
        if (resized_b == NULL) {
1737
            fprintf(stderr,
1738
                    "Warning: unable to resize LPIPS output tensor.\n");
1739
            goto cleanup;
1740
        }
1741
        tensor_data_b = resized_b;
1742
    }
1743

1744
    plane_size = (size_t)target_width * (size_t)target_height;
1745
    tensor_shape[0] = 1;
1746
    tensor_shape[1] = 3;
1747
    tensor_shape[2] = target_height;
1748
    tensor_shape[3] = target_width;
1749

1750
    status = g_lpips_api->CreateCpuMemoryInfo(OrtArenaAllocator,
1751
                                              OrtMemTypeDefault,
1752
                                              &memory_info);
1753
    if (ort_status_to_error(status) != 0) {
1754
        goto cleanup;
1755
    }
1756
    status = g_lpips_api->CreateTensorWithDataAsOrtValue(
1757
        memory_info,
1758
        (void *)tensor_data_a,
1759
        plane_size * 3u * sizeof(float),
1760
        tensor_shape,
1761
        4,
1762
        ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,
1763
        &tensor_a);
1764
    if (ort_status_to_error(status) != 0) {
1765
        goto cleanup;
1766
    }
1767
    status = g_lpips_api->CreateTensorWithDataAsOrtValue(
1768
        memory_info,
1769
        (void *)tensor_data_b,
1770
        plane_size * 3u * sizeof(float),
1771
        tensor_shape,
1772
        4,
1773
        ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,
1774
        &tensor_b);
1775
    if (ort_status_to_error(status) != 0) {
1776
        goto cleanup;
1777
    }
1778

1779
    status = g_lpips_api->SessionGetInputName(feat_session,
1780
                                              0,
1781
                                              allocator,
1782
                                              &feat_input_name);
1783
    if (ort_status_to_error(status) != 0) {
1784
        goto cleanup;
1785
    }
1786
    status = g_lpips_api->SessionGetOutputCount(feat_session,
1787
                                                &feat_outputs);
1788
    if (ort_status_to_error(status) != 0) {
1789
        goto cleanup;
1790
    }
1791
    feat_output_names = (char **)calloc(feat_outputs, sizeof(char *));
1792
    features_a = (OrtValue **)calloc(feat_outputs, sizeof(OrtValue *));
1793
    features_b = (OrtValue **)calloc(feat_outputs, sizeof(OrtValue *));
1794
    if (feat_output_names == NULL ||
1795
        features_a == NULL ||
1796
        features_b == NULL) {
1797
        fprintf(stderr,
1798
                "Warning: out of memory while preparing LPIPS features.\n");
1799
        goto cleanup;
1800
    }
1801
    for (i = 0; i < feat_outputs; ++i) {
1802
        status = g_lpips_api->SessionGetOutputName(feat_session,
1803
                                                   i,
1804
                                                   allocator,
1805
                                                   &feat_output_names[i]);
1806
        if (ort_status_to_error(status) != 0) {
1807
            goto cleanup;
1808
        }
1809
    }
1810
    status = g_lpips_api->Run(feat_session,
1811
                              NULL,
1812
                              (char const *const *)&feat_input_name,
1813
                              (OrtValue const *const *)&tensor_a,
1814
                              1,
1815
                              (char const *const *)feat_output_names,
1816
                              feat_outputs,
1817
                              features_a);
1818
    if (ort_status_to_error(status) != 0) {
1819
        goto cleanup;
1820
    }
1821
    status = g_lpips_api->Run(feat_session,
1822
                              NULL,
1823
                              (char const *const *)&feat_input_name,
1824
                              (OrtValue const *const *)&tensor_b,
1825
                              1,
1826
                              (char const *const *)feat_output_names,
1827
                              feat_outputs,
1828
                              features_b);
1829
    if (ort_status_to_error(status) != 0) {
1830
        goto cleanup;
1831
    }
1832

1833
    status = g_lpips_api->SessionGetInputCount(diff_session,
1834
                                               &diff_inputs);
1835
    if (ort_status_to_error(status) != 0) {
1836
        goto cleanup;
1837
    }
1838
    diff_input_names = (char **)calloc(diff_inputs, sizeof(char *));
1839
    diff_values = (OrtValue const **)calloc(diff_inputs,
1840
                                            sizeof(OrtValue *));
1841
    if (diff_input_names == NULL || diff_values == NULL) {
1842
        fprintf(stderr,
1843
                "Warning: out of memory while preparing LPIPS diff inputs.\n");
1844
        goto cleanup;
1845
    }
1846
    for (i = 0; i < diff_inputs; ++i) {
1847
        status = g_lpips_api->SessionGetInputName(diff_session,
1848
                                                  i,
1849
                                                  allocator,
1850
                                                  &diff_input_names[i]);
1851
        if (ort_status_to_error(status) != 0) {
1852
            goto cleanup;
1853
        }
1854
        if (diff_input_names[i] == NULL) {
1855
            continue;
1856
        }
1857
        if (strncmp(diff_input_names[i], "feat_x_", 7) == 0) {
1858
            int index;
1859

1860
            index = tail_index(diff_input_names[i]);
1861
            if (index >= 0 && (size_t)index < feat_outputs) {
1862
                diff_values[i] = features_a[index];
1863
            }
1864
        } else if (strncmp(diff_input_names[i], "feat_y_", 7) == 0) {
1865
            int index;
1866

1867
            index = tail_index(diff_input_names[i]);
1868
            if (index >= 0 && (size_t)index < feat_outputs) {
1869
                diff_values[i] = features_b[index];
1870
            }
1871
        }
1872
    }
1873

1874
    status = g_lpips_api->SessionGetOutputName(diff_session,
1875
                                               0,
1876
                                               allocator,
1877
                                               &diff_output_name);
1878
    if (ort_status_to_error(status) != 0) {
1879
        goto cleanup;
1880
    }
1881
    status = g_lpips_api->Run(diff_session,
1882
                              NULL,
1883
                              (char const *const *)diff_input_names,
1884
                              diff_values,
1885
                              diff_inputs,
1886
                              (char const *const *)&diff_output_name,
1887
                              1,
1888
                              diff_outputs);
1889
    if (ort_status_to_error(status) != 0) {
1890
        goto cleanup;
1891
    }
1892

1893
    if (diff_outputs[0] != NULL) {
1894
        float *result_data;
1895

1896
        result_data = NULL;
1897
        status = g_lpips_api->GetTensorMutableData(diff_outputs[0],
1898
                                                   (void **)&result_data);
1899
        if (ort_status_to_error(status) != 0) {
1900
            goto cleanup;
1901
        }
1902
        if (result_data != NULL) {
1903
            *result_out = result_data[0];
1904
            rc = 0;
1905
        }
1906
    }
1907

1908
cleanup:
1909
    if (diff_outputs[0] != NULL) {
1910
        g_lpips_api->ReleaseValue(diff_outputs[0]);
1911
    }
1912
    /*
1913
     * Clean up ORT-managed string buffers with explicit status release.
1914
     *
1915
     * We always release the temporary OrtStatus objects to prevent
1916
     * resource leaks when ONNX Runtime reports cleanup diagnostics.
1917
     */
1918
    if (diff_output_name != NULL) {
1919
        status = g_lpips_api->AllocatorFree(allocator, diff_output_name);
1920
        if (status != NULL) {
1921
            g_lpips_api->ReleaseStatus(status);
1922
        }
1923
    }
1924
    if (diff_input_names != NULL) {
1925
        for (i = 0; i < diff_inputs; ++i) {
1926
            if (diff_input_names[i] != NULL) {
1927
                status = g_lpips_api->AllocatorFree(allocator,
1928
                                                    diff_input_names[i]);
1929
                if (status != NULL) {
1930
                    g_lpips_api->ReleaseStatus(status);
1931
                }
1932
            }
1933
        }
1934
        free(diff_input_names);
1935
    }
1936
    if (diff_values != NULL) {
1937
        free(diff_values);
1938
    }
1939
    if (feat_output_names != NULL) {
1940
        for (i = 0; i < feat_outputs; ++i) {
1941
            if (feat_output_names[i] != NULL) {
1942
                status = g_lpips_api->AllocatorFree(allocator,
1943
                                                    feat_output_names[i]);
1944
                if (status != NULL) {
1945
                    g_lpips_api->ReleaseStatus(status);
1946
                }
1947
            }
1948
        }
1949
        free(feat_output_names);
1950
    }
1951
    if (features_a != NULL) {
1952
        for (i = 0; i < feat_outputs; ++i) {
1953
            if (features_a[i] != NULL) {
1954
                g_lpips_api->ReleaseValue(features_a[i]);
1955
            }
1956
        }
1957
        free(features_a);
1958
    }
1959
    if (features_b != NULL) {
1960
        for (i = 0; i < feat_outputs; ++i) {
1961
            if (features_b[i] != NULL) {
1962
                g_lpips_api->ReleaseValue(features_b[i]);
1963
            }
1964
        }
1965
        free(features_b);
1966
    }
1967
    if (feat_input_name != NULL) {
1968
        status = g_lpips_api->AllocatorFree(allocator, feat_input_name);
1969
        if (status != NULL) {
1970
            g_lpips_api->ReleaseStatus(status);
1971
        }
1972
    }
1973
    if (tensor_a != NULL) {
1974
        g_lpips_api->ReleaseValue(tensor_a);
1975
    }
1976
    if (tensor_b != NULL) {
1977
        g_lpips_api->ReleaseValue(tensor_b);
1978
    }
1979
    if (memory_info != NULL) {
1980
        g_lpips_api->ReleaseMemoryInfo(memory_info);
1981
    }
1982
    if (feat_session != NULL) {
1983
        g_lpips_api->ReleaseSession(feat_session);
1984
    }
1985
    if (diff_session != NULL) {
1986
        g_lpips_api->ReleaseSession(diff_session);
1987
    }
1988
    if (options != NULL) {
1989
        g_lpips_api->ReleaseSessionOptions(options);
1990
    }
1991
    if (env != NULL) {
1992
        g_lpips_api->ReleaseEnv(env);
1993
    }
1994
    if (resized_a != NULL) {
1995
        free(resized_a);
1996
    }
1997
    if (resized_b != NULL) {
1998
        free(resized_b);
1999
    }
2000
    return rc;
2001
}
2002
#endif /* HAVE_ONNXRUNTIME */
2003

2004
/*
2005
 * Array math helpers
2006
 */
2007
static sixel_assessment_float_buffer_t
2008
float_buffer_create(size_t length)
×
2009
{
2010
    sixel_assessment_float_buffer_t buf;
2011
    buf.length = length;
×
2012
    buf.values = (float *)xcalloc(length, sizeof(float));
×
2013
    return buf;
×
2014
}
2015

2016
static void
2017
float_buffer_free(sixel_assessment_float_buffer_t *buf)
×
2018
{
2019
    if (buf->values != NULL) {
×
2020
        free(buf->values);
×
2021
        buf->values = NULL;
×
2022
        buf->length = 0;
×
2023
    }
2024
}
×
2025

2026
static float
2027
clamp_float(float v, float min_v, float max_v)
×
2028
{
2029
    float result;
2030
    result = v;
×
2031
    if (result < min_v) {
×
2032
        result = min_v;
×
2033
    }
2034
    if (result > max_v) {
×
2035
        result = max_v;
×
2036
    }
2037
    return result;
×
2038
}
2039

2040
/*
2041
 * Luma conversion and resizing utilities
2042
 */
2043
static sixel_assessment_float_buffer_t
2044
pixels_to_luma709(const float *pixels, int width, int height)
×
2045
{
2046
    sixel_assessment_float_buffer_t buf;
2047
    size_t total;
2048
    size_t i;
2049
    float r;
2050
    float g;
2051
    float b;
2052

2053
    total = (size_t)width * (size_t)height;
×
2054
    buf = float_buffer_create(total);
×
2055
    for (i = 0; i < total; ++i) {
×
2056
        r = pixels[i * 3 + 0];
×
2057
        g = pixels[i * 3 + 1];
×
2058
        b = pixels[i * 3 + 2];
×
2059
        buf.values[i] = 0.2126f * r + 0.7152f * g + 0.0722f * b;
×
2060
    }
2061
    return buf;
×
2062
}
2063

2064
static sixel_assessment_float_buffer_t
2065
pixels_channel(const float *pixels, int width, int height, int channel)
×
2066
{
2067
    sixel_assessment_float_buffer_t buf;
2068
    size_t total;
2069
    size_t i;
2070
    float value;
2071

2072
    total = (size_t)width * (size_t)height;
×
2073
    buf = float_buffer_create(total);
×
2074
    for (i = 0; i < total; ++i) {
×
2075
        value = pixels[i * 3 + channel];
×
2076
        buf.values[i] = value;
×
2077
    }
2078
    return buf;
×
2079
}
2080
/*
2081
 * Gaussian kernel and separable convolution
2082
 */
2083
static sixel_assessment_float_buffer_t gaussian_kernel1d(int size, float sigma)
×
2084
{
2085
    sixel_assessment_float_buffer_t kernel;
2086
    int i;
2087
    float mean;
2088
    float sum;
2089
    float x;
2090
    float value;
2091

2092
    kernel = float_buffer_create((size_t)size);
×
2093
    mean = ((float)size - 1.0f) * 0.5f;
×
2094
    sum = 0.0f;
×
2095
    for (i = 0; i < size; ++i) {
×
2096
        x = (float)i - mean;
×
2097
        value = expf(-0.5f * (x / sigma) * (x / sigma));
×
2098
        kernel.values[i] = value;
×
2099
        sum += value;
×
2100
    }
2101
    if (sum > 0.0f) {
×
2102
        for (i = 0; i < size; ++i) {
×
2103
            kernel.values[i] /= sum;
×
2104
        }
2105
    }
2106
    return kernel;
×
2107
}
2108

2109
static sixel_assessment_float_buffer_t
2110
separable_conv2d(const sixel_assessment_float_buffer_t *img, int width,
×
2111
                 int height, const sixel_assessment_float_buffer_t *kernel)
2112
{
2113
    sixel_assessment_float_buffer_t tmp;
2114
    sixel_assessment_float_buffer_t out;
2115
    int pad;
2116
    int x;
2117
    int y;
2118
    int k;
2119
    int kernel_size;
2120
    float acc;
2121
    int px;
2122
    int py;
2123
    int idx;
2124
    float sample;
2125

2126
    pad = (int)kernel->length / 2;
×
2127
    kernel_size = (int)kernel->length;
×
2128
    tmp = float_buffer_create((size_t)width * (size_t)height);
×
2129
    out = float_buffer_create((size_t)width * (size_t)height);
×
2130

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

2153
    for (y = 0; y < height; ++y) {
×
2154
        for (x = 0; x < width; ++x) {
×
2155
            acc = 0.0f;
×
2156
            for (k = 0; k < kernel_size; ++k) {
×
2157
                py = y + k - pad;
×
2158
                if (py < 0) {
×
2159
                    py = -py;
×
2160
                }
2161
                if (py >= height) {
×
2162
                    py = height - (py - height) - 1;
×
2163
                    if (py < 0) {
×
2164
                        py = 0;
×
2165
                    }
2166
                }
2167
                idx = py * width + x;
×
2168
                sample = tmp.values[idx];
×
2169
                acc += kernel->values[k] * sample;
×
2170
            }
2171
            out.values[y * width + x] = acc;
×
2172
        }
2173
    }
2174

2175
    float_buffer_free(&tmp);
×
2176
    return out;
×
2177
}
2178

2179
/*
2180
 * SSIM and MS-SSIM computation
2181
 */
2182
static float ssim_luma(
×
2183
    const sixel_assessment_float_buffer_t *x,
2184
    const sixel_assessment_float_buffer_t *y,
2185
    int width,
2186
    int height,
2187
    float k1,
2188
    float k2,
2189
    int win_size,
2190
    float sigma)
2191
{
2192
    sixel_assessment_float_buffer_t kernel;
2193
    sixel_assessment_float_buffer_t mu_x;
2194
    sixel_assessment_float_buffer_t mu_y;
2195
    sixel_assessment_float_buffer_t mu_x2;
2196
    sixel_assessment_float_buffer_t mu_y2;
2197
    sixel_assessment_float_buffer_t mu_xy;
2198
    sixel_assessment_float_buffer_t sigma_x2;
2199
    sixel_assessment_float_buffer_t sigma_y2;
2200
    sixel_assessment_float_buffer_t sigma_xy;
2201
    float C1;
2202
    float C2;
2203
    size_t total;
2204
    size_t i;
2205
    float mean;
2206
    float numerator;
2207
    float denominator;
2208
    float value;
2209
    sixel_assessment_float_buffer_t x_sq;
2210
    sixel_assessment_float_buffer_t y_sq;
2211
    sixel_assessment_float_buffer_t xy_buf;
2212

2213
    kernel = gaussian_kernel1d(win_size, sigma);
×
2214
    mu_x = separable_conv2d(x, width, height, &kernel);
×
2215
    mu_y = separable_conv2d(y, width, height, &kernel);
×
2216

2217
    total = (size_t)width * (size_t)height;
×
2218
    mu_x2 = float_buffer_create(total);
×
2219
    mu_y2 = float_buffer_create(total);
×
2220
    mu_xy = float_buffer_create(total);
×
2221
    sigma_x2 = float_buffer_create(total);
×
2222
    sigma_y2 = float_buffer_create(total);
×
2223
    sigma_xy = float_buffer_create(total);
×
2224
    x_sq = float_buffer_create(total);
×
2225
    y_sq = float_buffer_create(total);
×
2226
    xy_buf = float_buffer_create(total);
×
2227

2228
    for (i = 0; i < total; ++i) {
×
2229
        mu_x2.values[i] = mu_x.values[i] * mu_x.values[i];
×
2230
        mu_y2.values[i] = mu_y.values[i] * mu_y.values[i];
×
2231
        mu_xy.values[i] = mu_x.values[i] * mu_y.values[i];
×
2232
        x_sq.values[i] = x->values[i] * x->values[i];
×
2233
        y_sq.values[i] = y->values[i] * y->values[i];
×
2234
        xy_buf.values[i] = x->values[i] * y->values[i];
×
2235
    }
2236

2237
    float_buffer_free(&sigma_x2);
×
2238
    float_buffer_free(&sigma_y2);
×
2239
    float_buffer_free(&sigma_xy);
×
2240
    sigma_x2 = separable_conv2d(&x_sq, width, height, &kernel);
×
2241
    sigma_y2 = separable_conv2d(&y_sq, width, height, &kernel);
×
2242
    sigma_xy = separable_conv2d(&xy_buf, width, height, &kernel);
×
2243

2244
    for (i = 0; i < total; ++i) {
×
2245
        sigma_x2.values[i] -= mu_x2.values[i];
×
2246
        sigma_y2.values[i] -= mu_y2.values[i];
×
2247
        sigma_xy.values[i] -= mu_xy.values[i];
×
2248
    }
2249

2250
    C1 = (k1 * 1.0f) * (k1 * 1.0f);
×
2251
    C2 = (k2 * 1.0f) * (k2 * 1.0f);
×
2252

2253
    mean = 0.0f;
×
2254
    for (i = 0; i < total; ++i) {
×
2255
        numerator = (2.0f * mu_xy.values[i] + C1) *
×
2256
                    (2.0f * sigma_xy.values[i] + C2);
×
2257
        denominator = (mu_x2.values[i] + mu_y2.values[i] + C1) *
×
2258
                      (sigma_x2.values[i] + sigma_y2.values[i] + C2);
×
2259
        if (denominator != 0.0f) {
×
2260
            value = numerator / (denominator + 1e-12f);
×
2261
        } else {
2262
            value = 0.0f;
×
2263
        }
2264
        mean += value;
×
2265
    }
2266
    mean /= (float)total;
×
2267
    mean = clamp_float(mean, 0.0f, 1.0f);
×
2268

2269
    float_buffer_free(&kernel);
×
2270
    float_buffer_free(&mu_x);
×
2271
    float_buffer_free(&mu_y);
×
2272
    float_buffer_free(&mu_x2);
×
2273
    float_buffer_free(&mu_y2);
×
2274
    float_buffer_free(&mu_xy);
×
2275
    float_buffer_free(&sigma_x2);
×
2276
    float_buffer_free(&sigma_y2);
×
2277
    float_buffer_free(&sigma_xy);
×
2278
    float_buffer_free(&x_sq);
×
2279
    float_buffer_free(&y_sq);
×
2280
    float_buffer_free(&xy_buf);
×
2281

2282
    return mean;
×
2283
}
2284

2285
static sixel_assessment_float_buffer_t
2286
downsample2(const sixel_assessment_float_buffer_t *img, int width, int height,
×
2287
            int *new_width, int *new_height)
2288
{
2289
    int h2;
2290
    int w2;
2291
    int y;
2292
    int x;
2293
    sixel_assessment_float_buffer_t out;
2294
    float sum;
2295
    int idx0;
2296
    int idx1;
2297
    int idx2;
2298
    int idx3;
2299

2300
    h2 = height / 2;
×
2301
    w2 = width / 2;
×
2302
    out = float_buffer_create((size_t)h2 * (size_t)w2);
×
2303
    for (y = 0; y < h2; ++y) {
×
2304
        for (x = 0; x < w2; ++x) {
×
2305
            sum = 0.0f;
×
2306
            idx0 = (2 * y) * width + (2 * x);
×
2307
            idx1 = (2 * y + 1) * width + (2 * x);
×
2308
            idx2 = (2 * y) * width + (2 * x + 1);
×
2309
            idx3 = (2 * y + 1) * width + (2 * x + 1);
×
2310
            sum += img->values[idx0];
×
2311
            sum += img->values[idx1];
×
2312
            sum += img->values[idx2];
×
2313
            sum += img->values[idx3];
×
2314
            out.values[y * w2 + x] = sum * 0.25f;
×
2315
        }
2316
    }
2317
    *new_width = w2;
×
2318
    *new_height = h2;
×
2319
    return out;
×
2320
}
2321

2322
static float
2323
ms_ssim_luma(const sixel_assessment_float_buffer_t *ref,
×
2324
             const sixel_assessment_float_buffer_t *out,
2325
             int width,
2326
             int height)
2327
{
2328
    static const float weights[5] = {
2329
        0.0448f, 0.2856f, 0.3001f, 0.2363f, 0.1333f
2330
    };
2331
    sixel_assessment_float_buffer_t cur_ref;
2332
    sixel_assessment_float_buffer_t cur_out;
2333
    sixel_assessment_float_buffer_t next_ref;
2334
    sixel_assessment_float_buffer_t next_out;
2335
    int cur_width;
2336
    int cur_height;
2337
    float weighted_sum;
2338
    float weight_total;
2339
    int level;
2340
    float ssim_value;
2341
    int next_width;
2342
    int next_height;
2343

2344
    cur_ref = float_buffer_create((size_t)width * (size_t)height);
×
2345
    cur_out = float_buffer_create((size_t)width * (size_t)height);
×
2346
    memcpy(cur_ref.values, ref->values,
×
UNCOV
2347
           sizeof(float) * (size_t)width * (size_t)height);
×
2348
    memcpy(cur_out.values, out->values,
×
UNCOV
2349
           sizeof(float) * (size_t)width * (size_t)height);
×
2350
    cur_width = width;
×
2351
    cur_height = height;
×
2352
    weighted_sum = 0.0f;
×
2353
    weight_total = 0.0f;
×
2354

2355
    for (level = 0; level < 5; ++level) {
×
2356
        ssim_value = ssim_luma(&cur_ref, &cur_out, cur_width, cur_height,
×
2357
                               0.01f, 0.03f, 11, 1.5f);
2358
        weighted_sum += ssim_value * weights[level];
×
2359
        weight_total += weights[level];
×
2360
        if (level < 4) {
×
2361
            next_ref = downsample2(&cur_ref, cur_width, cur_height,
×
2362
                                   &next_width, &next_height);
2363
            next_out = downsample2(&cur_out, cur_width, cur_height,
×
2364
                                   &next_width, &next_height);
2365
            float_buffer_free(&cur_ref);
×
2366
            float_buffer_free(&cur_out);
×
2367
            cur_ref = next_ref;
×
2368
            cur_out = next_out;
×
2369
            cur_width = next_width;
×
2370
            cur_height = next_height;
×
2371
        }
2372
    }
2373

2374
    float_buffer_free(&cur_ref);
×
2375
    float_buffer_free(&cur_out);
×
2376

2377
    if (weight_total > 0.0f) {
×
2378
        return weighted_sum / weight_total;
×
2379
    }
2380
    return 0.0f;
×
2381
}
2382
/*
2383
 * FFT helpers for spectral metrics
2384
 */
2385
static int
2386
next_power_of_two(int value)
×
2387
{
2388
    int n;
2389
    n = 1;
×
2390
    while (n < value) {
×
2391
        n <<= 1;
×
2392
    }
2393
    return n;
×
2394
}
2395

2396
static void
2397
fft_bit_reverse(sixel_assessment_complex_t *data, int n)
×
2398
{
2399
    int i;
2400
    int j;
2401
    int bit;
2402
    sixel_assessment_complex_t tmp;
2403

2404
    j = 0;
×
2405
    for (i = 0; i < n; ++i) {
×
2406
        if (i < j) {
×
2407
            tmp = data[i];
×
2408
            data[i] = data[j];
×
2409
            data[j] = tmp;
×
2410
        }
2411
        bit = n >> 1;
×
2412
        while ((j & bit) != 0) {
×
2413
            j &= ~bit;
×
2414
            bit >>= 1;
×
2415
        }
2416
        j |= bit;
×
2417
    }
2418
}
×
2419

2420
static void
2421
fft_cooley_tukey(sixel_assessment_complex_t *data, int n, int inverse)
×
2422
{
2423
    int len;
2424
    double angle;
2425
    sixel_assessment_complex_t wlen;
2426
    int half;
2427
    int i;
2428
    sixel_assessment_complex_t w;
2429
    int j;
2430
    sixel_assessment_complex_t u;
2431
    sixel_assessment_complex_t v;
2432
    double tmp_re;
2433
    double tmp_im;
2434

2435
    fft_bit_reverse(data, n);
×
2436
    for (len = 2; len <= n; len <<= 1) {
×
2437

2438
        angle = 2.0 * M_PI / (double)len;
×
2439
        if (inverse) {
×
2440
            angle = -angle;
×
2441
        }
2442
        wlen.re = cos(angle);
×
2443
        wlen.im = sin(angle);
×
2444
        half = len >> 1;
×
2445
        for (i = 0; i < n; i += len) {
×
2446
            w.re = 1.0;
×
2447
            w.im = 0.0;
×
2448
            for (j = 0; j < half; ++j) {
×
2449
                u = data[i + j];
×
2450
                v.re = data[i + j + half].re * w.re -
×
2451
                       data[i + j + half].im * w.im;
×
2452
                v.im = data[i + j + half].re * w.im +
×
2453
                       data[i + j + half].im * w.re;
×
2454
                data[i + j].re = u.re + v.re;
×
2455
                data[i + j].im = u.im + v.im;
×
2456
                data[i + j + half].re = u.re - v.re;
×
2457
                data[i + j + half].im = u.im - v.im;
×
2458
                tmp_re = w.re * wlen.re - w.im * wlen.im;
×
2459
                tmp_im = w.re * wlen.im + w.im * wlen.re;
×
2460
                w.re = tmp_re;
×
2461
                w.im = tmp_im;
×
2462
            }
2463
        }
2464
    }
2465
    if (inverse) {
×
2466
        for (i = 0; i < n; ++i) {
×
2467
            data[i].re /= n;
×
2468
            data[i].im /= n;
×
2469
        }
2470
    }
2471
}
×
2472

2473
static void
2474
fft2d(sixel_assessment_float_buffer_t *input, int width, int height,
×
2475
      sixel_assessment_complex_t *output, int out_width, int out_height)
2476
{
2477
    int padded_width;
2478
    int padded_height;
2479
    int y;
2480
    int x;
2481
    sixel_assessment_complex_t *row;
2482
    sixel_assessment_complex_t *col;
2483
    sixel_assessment_complex_t value;
2484

2485
    padded_width = out_width;
×
2486
    padded_height = out_height;
×
2487
    row = (sixel_assessment_complex_t *)xmalloc(
×
2488
        sizeof(sixel_assessment_complex_t) * (size_t)padded_width);
×
2489
    col = (sixel_assessment_complex_t *)xmalloc(
×
2490
        sizeof(sixel_assessment_complex_t) * (size_t)padded_height);
×
2491

2492
    for (y = 0; y < padded_height; ++y) {
×
2493
        for (x = 0; x < padded_width; ++x) {
×
2494
            if (y < height && x < width) {
×
2495
                value.re = input->values[y * width + x];
×
2496
                value.im = 0.0;
×
2497
            } else {
2498
                value.re = 0.0;
×
2499
                value.im = 0.0;
×
2500
            }
2501
            output[y * padded_width + x] = value;
×
2502
        }
2503
    }
2504

2505
    for (y = 0; y < padded_height; ++y) {
×
2506
        for (x = 0; x < padded_width; ++x) {
×
2507
            row[x] = output[y * padded_width + x];
×
2508
        }
2509
        fft_cooley_tukey(row, padded_width, 0);
×
2510
        for (x = 0; x < padded_width; ++x) {
×
2511
            output[y * padded_width + x] = row[x];
×
2512
        }
2513
    }
2514

2515
    for (x = 0; x < padded_width; ++x) {
×
2516
        for (y = 0; y < padded_height; ++y) {
×
2517
            col[y] = output[y * padded_width + x];
×
2518
        }
2519
        fft_cooley_tukey(col, padded_height, 0);
×
2520
        for (y = 0; y < padded_height; ++y) {
×
2521
            output[y * padded_width + x] = col[y];
×
2522
        }
2523
    }
2524

2525
    free(row);
×
2526
    free(col);
×
2527
}
×
2528

2529
static void
2530
fft_shift(sixel_assessment_complex_t *data, int width, int height)
×
2531
{
2532
    int half_w;
2533
    int half_h;
2534
    int y;
2535
    int x;
2536
    int nx;
2537
    int ny;
2538
    sixel_assessment_complex_t tmp;
2539

2540
    half_w = width / 2;
×
2541
    half_h = height / 2;
×
2542
    for (y = 0; y < height; ++y) {
×
2543
        for (x = 0; x < width; ++x) {
×
2544
            nx = (x + half_w) % width;
×
2545
            ny = (y + half_h) % height;
×
2546
            if (ny > y || (ny == y && nx > x)) {
×
2547
                continue;
×
2548
            }
2549
            tmp = data[y * width + x];
×
2550
            data[y * width + x] = data[ny * width + nx];
×
2551
            data[ny * width + nx] = tmp;
×
2552
        }
2553
    }
2554
}
×
2555
static float
2556
high_frequency_ratio(const sixel_assessment_float_buffer_t *img,
×
2557
                     int width, int height, float cutoff)
2558
{
2559
    int padded_width;
2560
    int padded_height;
2561
    sixel_assessment_complex_t *freq;
2562
    size_t total;
2563
    sixel_assessment_float_buffer_t centered;
2564
    double hi_sum;
2565
    double total_sum;
2566
    int y;
2567
    int x;
2568
    double cy;
2569
    double cx;
2570
    double mean;
2571
    size_t i;
2572
    double dy;
2573
    double dx;
2574
    double r;
2575
    double norm;
2576
    double power;
2577

2578
    padded_width = next_power_of_two(width);
×
2579
    padded_height = next_power_of_two(height);
×
2580
    total = (size_t)padded_width * (size_t)padded_height;
×
2581
    freq = (sixel_assessment_complex_t *)xmalloc(
×
2582
        total * sizeof(sixel_assessment_complex_t));
2583
    centered = float_buffer_create((size_t)width * (size_t)height);
×
2584

2585
    for (y = 0; y < height; ++y) {
×
2586
        for (x = 0; x < width; ++x) {
×
2587
            centered.values[y * width + x] = img->values[y * width + x];
×
2588
        }
2589
    }
2590
    mean = 0.0;
×
2591
    for (i = 0; i < (size_t)width * (size_t)height; ++i) {
×
2592
        mean += centered.values[i];
×
2593
    }
2594
    mean /= (double)((size_t)width * (size_t)height);
×
2595
    for (i = 0; i < (size_t)width * (size_t)height; ++i) {
×
2596
        centered.values[i] -= (float)mean;
×
2597
    }
2598

2599
    fft2d(&centered, width, height, freq, padded_width, padded_height);
×
2600
    fft_shift(freq, padded_width, padded_height);
×
2601

2602
    hi_sum = 0.0;
×
2603
    total_sum = 0.0;
×
2604
    cy = padded_height / 2.0;
×
2605
    cx = padded_width / 2.0;
×
2606

2607
    for (y = 0; y < padded_height; ++y) {
×
2608
        for (x = 0; x < padded_width; ++x) {
×
2609
            dy = (double)y - cy;
×
2610
            dx = (double)x - cx;
×
2611
            r = sqrt(dy * dy + dx * dx);
×
2612
            norm = r / (0.5 * sqrt((double)padded_height *
×
2613
                                   (double)padded_height +
×
2614
                                   (double)padded_width *
×
2615
                                   (double)padded_width));
×
2616
            power = freq[y * padded_width + x].re *
×
2617
                    freq[y * padded_width + x].re +
×
2618
                    freq[y * padded_width + x].im *
×
2619
                    freq[y * padded_width + x].im;
×
2620
            total_sum += power;
×
2621
            if (norm >= cutoff) {
×
2622
                hi_sum += power;
×
2623
            }
2624
        }
2625
    }
2626

2627
    free(freq);
×
2628
    float_buffer_free(&centered);
×
2629

2630
    if (total_sum <= 0.0) {
×
2631
        return 0.0f;
×
2632
    }
2633
    return (float)(hi_sum / total_sum);
×
2634
}
2635

2636
static float
2637
stripe_score(const sixel_assessment_float_buffer_t *img, int width, int height,
×
2638
             int bins)
2639
{
2640
    int padded_width;
2641
    int padded_height;
2642
    sixel_assessment_complex_t *freq;
2643
    sixel_assessment_float_buffer_t centered;
2644
    double cy;
2645
    double cx;
2646
    double rmin;
2647
    double *hist;
2648
    int y;
2649
    int x;
2650
    double mean_val;
2651
    double max_val;
2652
    double mean;
2653
    size_t i;
2654
    double dy;
2655
    double dx;
2656
    double r;
2657
    double ang;
2658
    int index;
2659
    double power;
2660

2661
    padded_width = next_power_of_two(width);
×
2662
    padded_height = next_power_of_two(height);
×
2663
    freq = (sixel_assessment_complex_t *)xmalloc(
×
2664
        sizeof(sixel_assessment_complex_t) *
2665
        (size_t)padded_width * (size_t)padded_height);
×
2666
    centered = float_buffer_create((size_t)width * (size_t)height);
×
2667
    for (y = 0; y < height; ++y) {
×
2668
        for (x = 0; x < width; ++x) {
×
2669
            centered.values[y * width + x] = img->values[y * width + x];
×
2670
        }
2671
    }
2672
    mean = 0.0;
×
2673
    for (i = 0; i < (size_t)width * (size_t)height; ++i) {
×
2674
        mean += centered.values[i];
×
2675
    }
2676
    mean /= (double)((size_t)width * (size_t)height);
×
2677
    for (i = 0; i < (size_t)width * (size_t)height; ++i) {
×
2678
        centered.values[i] -= (float)mean;
×
2679
    }
2680

2681
    fft2d(&centered, width, height, freq, padded_width, padded_height);
×
2682
    fft_shift(freq, padded_width, padded_height);
×
2683

2684
    hist = (double *)xcalloc((size_t)bins, sizeof(double));
×
2685
    cy = padded_height / 2.0;
×
2686
    cx = padded_width / 2.0;
×
2687
    rmin = 0.05 * (double)(width > height ? width : height);
×
2688

2689
    for (y = 0; y < padded_height; ++y) {
×
2690
        for (x = 0; x < padded_width; ++x) {
×
2691
            dy = (double)y - cy;
×
2692
            dx = (double)x - cx;
×
2693
            r = sqrt(dy * dy + dx * dx);
×
2694
            if (r < rmin) {
×
2695
                continue;
×
2696
            }
2697
            ang = atan2(dy, dx);
×
2698
            if (ang < 0.0) {
×
2699
                ang += M_PI;
×
2700
            }
2701
            index = (int)(ang / M_PI * bins);
×
2702
            if (index < 0) {
×
2703
                index = 0;
×
2704
            }
2705
            if (index >= bins) {
×
2706
                index = bins - 1;
×
2707
            }
2708
            power = freq[y * padded_width + x].re *
×
2709
                    freq[y * padded_width + x].re +
×
2710
                    freq[y * padded_width + x].im *
×
2711
                    freq[y * padded_width + x].im;
×
2712
            hist[index] += power;
×
2713
        }
2714
    }
2715

2716
    mean_val = 0.0;
×
2717
    for (x = 0; x < bins; ++x) {
×
2718
        mean_val += hist[x];
×
2719
    }
2720
    mean_val = mean_val / (double)bins + 1e-12;
×
2721

2722
    max_val = hist[0];
×
2723
    for (x = 1; x < bins; ++x) {
×
2724
        if (hist[x] > max_val) {
×
2725
            max_val = hist[x];
×
2726
        }
2727
    }
2728

2729
    free(hist);
×
2730
    free(freq);
×
2731
    float_buffer_free(&centered);
×
2732

2733
    return (float)(max_val / mean_val);
×
2734
}
2735
/*
2736
 * Banding metrics (run-length and gradient-based)
2737
 */
2738
static float
2739
banding_index_runlen(const sixel_assessment_float_buffer_t *img,
×
2740
                     int width, int height, int levels)
2741
{
2742
    int y;
2743
    int x;
2744
    double total_runs;
2745
    double total_segments;
2746
    int prev;
2747
    int run_len;
2748
    int segs;
2749
    int runs_sum;
2750
    int value;
2751

2752
    total_runs = 0.0;
×
2753
    total_segments = 0.0;
×
2754
    for (y = 0; y < height; ++y) {
×
2755
        prev = (int)clamp_float(img->values[y * width] * (levels - 1) + 0.5f,
×
2756
                                0.0f, (float)(levels - 1));
×
2757
        run_len = 1;
×
2758
        segs = 0;
×
2759
        runs_sum = 0;
×
2760
        for (x = 1; x < width; ++x) {
×
2761
            value = (int)clamp_float(
×
2762
                img->values[y * width + x] * (levels - 1) + 0.5f,
×
2763
                0.0f, (float)(levels - 1));
×
2764
            if (value == prev) {
×
2765
                run_len += 1;
×
2766
            } else {
2767
                runs_sum += run_len;
×
2768
                segs += 1;
×
2769
                run_len = 1;
×
2770
                prev = value;
×
2771
            }
2772
        }
2773
        runs_sum += run_len;
×
2774
        segs += 1;
×
2775
        total_runs += (double)runs_sum / (double)segs;
×
2776
        total_segments += 1.0;
×
2777
    }
2778
    if (total_segments <= 0.0) {
×
2779
        return 0.0f;
×
2780
    }
2781
    return (float)((total_runs / total_segments) / (double)width);
×
2782
}
2783

2784
static sixel_assessment_float_buffer_t
2785
gaussian_blur(const sixel_assessment_float_buffer_t *img, int width,
×
2786
              int height, float sigma, int ksize)
2787
{
2788
    sixel_assessment_float_buffer_t kernel;
2789
    sixel_assessment_float_buffer_t blurred;
2790

2791
    kernel = gaussian_kernel1d(ksize, sigma);
×
2792
    blurred = separable_conv2d(img, width, height, &kernel);
×
2793
    float_buffer_free(&kernel);
×
2794
    return blurred;
×
2795
}
2796

2797
static void
2798
finite_diff(const sixel_assessment_float_buffer_t *img,
×
2799
            int width,
2800
            int height,
2801
            sixel_assessment_float_buffer_t *dx,
2802
            sixel_assessment_float_buffer_t *dy)
2803
{
2804
    int x;
2805
    int y;
2806
    int xm1;
2807
    int xp1;
2808
    int ym1;
2809
    int yp1;
2810
    float vxm1;
2811
    float vxp1;
2812
    float vym1;
2813
    float vyp1;
2814

2815
    *dx = float_buffer_create((size_t)width * (size_t)height);
×
2816
    *dy = float_buffer_create((size_t)width * (size_t)height);
×
2817

2818
    for (y = 0; y < height; ++y) {
×
2819
        for (x = 0; x < width; ++x) {
×
2820
            xm1 = x - 1;
×
2821
            xp1 = x + 1;
×
2822
            ym1 = y - 1;
×
2823
            yp1 = y + 1;
×
2824
            if (xm1 < 0) {
×
2825
                xm1 = 0;
×
2826
            }
2827
            if (xp1 >= width) {
×
2828
                xp1 = width - 1;
×
2829
            }
2830
            if (ym1 < 0) {
×
2831
                ym1 = 0;
×
2832
            }
2833
            if (yp1 >= height) {
×
2834
                yp1 = height - 1;
×
2835
            }
2836
            vxm1 = img->values[y * width + xm1];
×
2837
            vxp1 = img->values[y * width + xp1];
×
2838
            vym1 = img->values[ym1 * width + x];
×
2839
            vyp1 = img->values[yp1 * width + x];
×
2840
            dx->values[y * width + x] = (vxp1 - vxm1) * 0.5f;
×
2841
            dy->values[y * width + x] = (vyp1 - vym1) * 0.5f;
×
2842
        }
2843
    }
2844
}
×
2845

2846
static int
2847
compare_floats(const void *a, const void *b)
×
2848
{
2849
    float fa;
2850
    float fb;
2851
    fa = *(const float *)a;
×
2852
    fb = *(const float *)b;
×
2853
    if (fa < fb) {
×
2854
        return -1;
×
2855
    }
2856
    if (fa > fb) {
×
2857
        return 1;
×
2858
    }
2859
    return 0;
×
2860
}
2861

2862
static float
2863
banding_index_gradient(const sixel_assessment_float_buffer_t *img,
×
2864
                       int width, int height)
2865
{
2866
    sixel_assessment_float_buffer_t blurred;
2867
    sixel_assessment_float_buffer_t dx;
2868
    sixel_assessment_float_buffer_t dy;
2869
    sixel_assessment_float_buffer_t grad;
2870
    size_t total;
2871
    float *sorted;
2872
    double g99;
2873
    int bins;
2874
    double *hist;
2875
    double *centers;
2876
    int half;
2877
    int i;
2878
    double sum_hist;
2879
    double sum_residual;
2880
    double zero_thresh;
2881
    double zero_mass;
2882
    double sum_x;
2883
    double sum_y;
2884
    double sum_xx;
2885
    double sum_xy;
2886
    int count;
2887
    double slope;
2888
    double intercept;
2889
    double gx;
2890
    double gy;
2891
    size_t idx_pos;
2892
    double value;
2893
    int bin_index;
2894
    double xval;
2895
    double yval;
2896
    double denom;
2897
    double env;
2898
    double resid;
2899

2900
    blurred = gaussian_blur(img, width, height, 1.0f, 7);
×
2901
    finite_diff(&blurred, width, height, &dx, &dy);
×
2902
    grad = float_buffer_create((size_t)width * (size_t)height);
×
2903

2904
    total = (size_t)width * (size_t)height;
×
2905
    for (i = 0; (size_t)i < total; ++i) {
×
2906
        gx = dx.values[i];
×
2907
        gy = dy.values[i];
×
2908
        grad.values[i] = (float)sqrt(gx * gx + gy * gy);
×
2909
    }
2910

2911
    sorted = (float *)xmalloc(total * sizeof(float));
×
2912
    memcpy(sorted, grad.values, total * sizeof(float));
×
2913
    qsort(sorted, total, sizeof(float), compare_floats);
×
2914
    if (total == 0) {
×
2915
        g99 = 0.0;
×
2916
    } else {
2917
        idx_pos = (size_t)((double)(total - 1) * 0.995);
×
2918
        g99 = sorted[idx_pos];
×
2919
    }
2920
    g99 += 1e-9;
×
2921
    free(sorted);
×
2922

2923
    bins = 128;
×
2924
    hist = (double *)xcalloc((size_t)bins, sizeof(double));
×
2925
    centers = (double *)xmalloc((size_t)bins * sizeof(double));
×
2926
    for (i = 0; i < bins; ++i) {
×
2927
        centers[i] = ((double)i + 0.5) * (g99 / (double)bins);
×
2928
    }
2929

2930
    for (i = 0; (size_t)i < total; ++i) {
×
2931
        if (grad.values[i] > (float)g99) {
×
2932
            grad.values[i] = (float)g99;
×
2933
        }
2934
        value = grad.values[i];
×
2935
        bin_index = (int)(value / g99 * bins);
×
2936
        if (bin_index < 0) {
×
2937
            bin_index = 0;
×
2938
        }
2939
        if (bin_index >= bins) {
×
2940
            bin_index = bins - 1;
×
2941
        }
2942
        hist[bin_index] += 1.0;
×
2943
    }
2944

2945
    for (i = 0; i < bins; ++i) {
×
2946
        hist[i] += 1e-12;
×
2947
    }
2948

2949
    half = bins / 2;
×
2950
    sum_x = 0.0;
×
2951
    sum_y = 0.0;
×
2952
    sum_xx = 0.0;
×
2953
    sum_xy = 0.0;
×
2954
    count = bins - half;
×
2955
    for (i = half; i < bins; ++i) {
×
2956
        xval = centers[i];
×
2957
        yval = log(hist[i]);
×
2958
        sum_x += xval;
×
2959
        sum_y += yval;
×
2960
        sum_xx += xval * xval;
×
2961
        sum_xy += xval * yval;
×
2962
    }
2963
    slope = 0.0;
×
2964
    intercept = 0.0;
×
2965
    if (count > 1) {
×
2966
        denom = (double)count * sum_xx - sum_x * sum_x;
×
2967
        if (fabs(denom) > 1e-12) {
×
2968
            slope = ((double)count * sum_xy - sum_x * sum_y) / denom;
×
2969
            intercept = (sum_y - slope * sum_x) / (double)count;
×
2970
        }
2971
    }
2972

2973
    sum_hist = 0.0;
×
2974
    sum_residual = 0.0;
×
2975
    for (i = 0; i < bins; ++i) {
×
2976
        env = exp(intercept + slope * centers[i]);
×
2977
        resid = hist[i] - env;
×
2978
        if (resid < 0.0) {
×
2979
            resid = 0.0;
×
2980
        }
2981
        sum_hist += hist[i];
×
2982
        sum_residual += resid;
×
2983
    }
2984

2985
    zero_thresh = 0.01 * g99;
×
2986
    zero_mass = 0.0;
×
2987
    if (total > 0) {
×
2988
        for (i = 0; (size_t)i < total; ++i) {
×
2989
            if (grad.values[i] <= (float)zero_thresh) {
×
2990
                zero_mass += 1.0;
×
2991
            }
2992
        }
2993
        zero_mass /= (double)total;
×
2994
    }
2995

2996
    float_buffer_free(&blurred);
×
2997
    float_buffer_free(&dx);
×
2998
    float_buffer_free(&dy);
×
2999
    float_buffer_free(&grad);
×
3000
    free(hist);
×
3001
    free(centers);
×
3002

3003
    if (sum_hist <= 0.0) {
×
3004
        return 0.0f;
×
3005
    }
3006
    return (float)(0.6 * (sum_residual / sum_hist) + 0.4 * zero_mass);
×
3007
}
3008
/*
3009
 * Clipping statistics
3010
 */
3011
static void clipping_rates(const float *pixels,
×
3012
                           int width,
3013
                           int height,
3014
                           float *clip_l,
3015
                           float *clip_r,
3016
                           float *clip_g,
3017
                           float *clip_b)
3018
{
3019
    sixel_assessment_float_buffer_t luma;
3020
    sixel_assessment_float_buffer_t rch;
3021
    sixel_assessment_float_buffer_t gch;
3022
    sixel_assessment_float_buffer_t bch;
3023
    size_t total;
3024
    size_t i;
3025
    double eps;
3026
    double lo;
3027
    double hi;
3028

3029
    luma = pixels_to_luma709(pixels, width, height);
×
3030
    rch = pixels_channel(pixels, width, height, 0);
×
3031
    gch = pixels_channel(pixels, width, height, 1);
×
3032
    bch = pixels_channel(pixels, width, height, 2);
×
3033
    total = (size_t)width * (size_t)height;
×
3034
    eps = 1e-6;
×
3035

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

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

3059
    lo = hi = 0.0;
×
3060
    for (i = 0; i < total; ++i) {
×
3061
        if (gch.values[i] <= eps) {
×
3062
            lo += 1.0;
×
3063
        }
3064
        if (gch.values[i] >= 1.0 - eps) {
×
3065
            hi += 1.0;
×
3066
        }
3067
    }
3068
    *clip_g = (float)((lo + hi) / (double)total);
×
3069

3070
    lo = hi = 0.0;
×
3071
    for (i = 0; i < total; ++i) {
×
3072
        if (bch.values[i] <= eps) {
×
3073
            lo += 1.0;
×
3074
        }
3075
        if (bch.values[i] >= 1.0 - eps) {
×
3076
            hi += 1.0;
×
3077
        }
3078
    }
3079
    *clip_b = (float)((lo + hi) / (double)total);
×
3080

3081
    float_buffer_free(&luma);
×
3082
    float_buffer_free(&rch);
×
3083
    float_buffer_free(&gch);
×
3084
    float_buffer_free(&bch);
×
3085
}
×
3086

3087
/*
3088
 * sRGB <-> CIELAB conversions
3089
 */
3090
static void
3091
srgb_to_linear(const float *src, float *dst, size_t count)
×
3092
{
3093
    size_t i;
3094
    float c;
3095
    float result;
3096
    for (i = 0; i < count; ++i) {
×
3097
        c = src[i];
×
3098
        if (c <= 0.04045f) {
×
3099
            result = c / 12.92f;
×
3100
        } else {
3101
            result = powf((c + 0.055f) / 1.055f, 2.4f);
×
3102
        }
3103
        dst[i] = result;
×
3104
    }
3105
}
×
3106

3107
static void
3108
linear_to_xyz(const float *rgb, float *xyz, size_t pixels)
×
3109
{
3110
    size_t i;
3111
    float r;
3112
    float g;
3113
    float b;
3114
    float X;
3115
    float Y;
3116
    float Z;
3117
    for (i = 0; i < pixels; ++i) {
×
3118
        r = rgb[i * 3 + 0];
×
3119
        g = rgb[i * 3 + 1];
×
3120
        b = rgb[i * 3 + 2];
×
3121
        X = 0.4124564f * r + 0.3575761f * g + 0.1804375f * b;
×
3122
        Y = 0.2126729f * r + 0.7151522f * g + 0.0721750f * b;
×
3123
        Z = 0.0193339f * r + 0.1191920f * g + 0.9503041f * b;
×
3124
        xyz[i * 3 + 0] = X;
×
3125
        xyz[i * 3 + 1] = Y;
×
3126
        xyz[i * 3 + 2] = Z;
×
3127
    }
3128
}
×
3129

3130
static float
3131
f_lab(float t)
×
3132
{
3133
    float delta;
3134
    delta = 6.0f / 29.0f;
×
3135
    if (t > delta * delta * delta) {
×
3136
        return cbrtf(t);
×
3137
    }
3138
    return t / (3.0f * delta * delta) + 4.0f / 29.0f;
×
3139
}
3140

3141
static void
3142
xyz_to_lab(const float *xyz, float *lab, size_t pixels)
×
3143
{
3144
    const float Xn = 0.95047f;
×
3145
    const float Yn = 1.00000f;
×
3146
    const float Zn = 1.08883f;
×
3147
    size_t i;
3148
    float X;
3149
    float Y;
3150
    float Z;
3151
    float fx;
3152
    float fy;
3153
    float fz;
3154
    float L;
3155
    float a;
3156
    float b;
3157
    for (i = 0; i < pixels; ++i) {
×
3158
        X = xyz[i * 3 + 0] / Xn;
×
3159
        Y = xyz[i * 3 + 1] / Yn;
×
3160
        Z = xyz[i * 3 + 2] / Zn;
×
3161
        fx = f_lab(X);
×
3162
        fy = f_lab(Y);
×
3163
        fz = f_lab(Z);
×
3164
        L = 116.0f * fy - 16.0f;
×
3165
        a = 500.0f * (fx - fy);
×
3166
        b = 200.0f * (fy - fz);
×
3167
        lab[i * 3 + 0] = L;
×
3168
        lab[i * 3 + 1] = a;
×
3169
        lab[i * 3 + 2] = b;
×
3170
    }
3171
}
×
3172

3173
static sixel_assessment_float_buffer_t
3174
rgb_to_lab(const float *pixels, int width, int height)
×
3175
{
3176
    sixel_assessment_float_buffer_t lab;
3177
    float *linear;
3178
    float *xyz;
3179
    size_t pixel_count;
3180

3181
    pixel_count = (size_t)width * (size_t)height;
×
3182
    lab = float_buffer_create(pixel_count * 3);
×
3183
    linear = (float *)xmalloc(pixel_count * 3 * sizeof(float));
×
3184
    xyz = (float *)xmalloc(pixel_count * 3 * sizeof(float));
×
3185
    srgb_to_linear(pixels, linear, pixel_count * 3);
×
3186
    linear_to_xyz(linear, xyz, pixel_count);
×
3187
    xyz_to_lab(xyz, lab.values, pixel_count);
×
3188
    free(linear);
×
3189
    free(xyz);
×
3190
    return lab;
×
3191
}
3192

3193
static sixel_assessment_float_buffer_t
3194
chroma_ab(const sixel_assessment_float_buffer_t *lab, size_t pixels)
×
3195
{
3196
    sixel_assessment_float_buffer_t chroma;
3197
    size_t i;
3198
    float a;
3199
    float b;
3200
    chroma = float_buffer_create(pixels);
×
3201
    for (i = 0; i < pixels; ++i) {
×
3202
        a = lab->values[i * 3 + 1];
×
3203
        b = lab->values[i * 3 + 2];
×
3204
        chroma.values[i] = sqrtf(a * a + b * b);
×
3205
    }
3206
    return chroma;
×
3207
}
3208

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

3363
    gx1 = float_buffer_create((size_t)width * (size_t)height);
×
3364
    gy1 = float_buffer_create((size_t)width * (size_t)height);
×
3365
    gx2 = float_buffer_create((size_t)width * (size_t)height);
×
3366
    gy2 = float_buffer_create((size_t)width * (size_t)height);
×
3367

3368
    for (y = 0; y < height; ++y) {
×
3369
        for (x = 0; x < width; ++x) {
×
3370
            accx1 = 0.0f;
×
3371
            accy1 = 0.0f;
×
3372
            accx2 = 0.0f;
×
3373
            accy2 = 0.0f;
×
3374
            for (dy = -1; dy <= 1; ++dy) {
×
3375
                yy = y + dy;
×
3376
                if (yy < 0) {
×
3377
                    yy = -yy;
×
3378
                }
3379
                if (yy >= height) {
×
3380
                    yy = height - (yy - height) - 1;
×
3381
                    if (yy < 0) {
×
3382
                        yy = 0;
×
3383
                    }
3384
                }
3385
                for (dx = -1; dx <= 1; ++dx) {
×
3386
                    xx = x + dx;
×
3387
                    if (xx < 0) {
×
3388
                        xx = -xx;
×
3389
                    }
3390
                    if (xx >= width) {
×
3391
                        xx = width - (xx - width) - 1;
×
3392
                        if (xx < 0) {
×
3393
                            xx = 0;
×
3394
                        }
3395
                    }
3396
                    kidx = (dy + 1) * 3 + (dx + 1);
×
3397
                    accx1 += ref->values[yy * width + xx] * kx[kidx];
×
3398
                    accy1 += ref->values[yy * width + xx] * ky[kidx];
×
3399
                    accx2 += out->values[yy * width + xx] * kx[kidx];
×
3400
                    accy2 += out->values[yy * width + xx] * ky[kidx];
×
3401
                }
3402
            }
3403
            gx1.values[y * width + x] = accx1;
×
3404
            gy1.values[y * width + x] = accy1;
×
3405
            gx2.values[y * width + x] = accx2;
×
3406
            gy2.values[y * width + x] = accy2;
×
3407
        }
3408
    }
3409

3410
    gm1 = float_buffer_create((size_t)width * (size_t)height);
×
3411
    gm2 = float_buffer_create((size_t)width * (size_t)height);
×
3412
    total = (size_t)width * (size_t)height;
×
3413
    for (y = 0; y < height; ++y) {
×
3414
        for (x = 0; x < width; ++x) {
×
3415
            idx = (size_t)y * (size_t)width + (size_t)x;
×
3416
            mag1 = sqrtf(gx1.values[idx] * gx1.values[idx] +
×
3417
                         gy1.values[idx] * gy1.values[idx]) + 1e-12f;
×
3418
            mag2 = sqrtf(gx2.values[idx] * gx2.values[idx] +
×
3419
                         gy2.values[idx] * gy2.values[idx]) + 1e-12f;
×
3420
            gm1.values[idx] = mag1;
×
3421
            gm2.values[idx] = mag2;
×
3422
        }
3423
    }
3424

3425
    c = 0.0026;
×
3426
    mean = 0.0;
×
3427
    for (y = 0; y < height; ++y) {
×
3428
        for (x = 0; x < width; ++x) {
×
3429
            idx = (size_t)y * (size_t)width + (size_t)x;
×
3430
            gms = (2.0 * gm1.values[idx] * gm2.values[idx] + c) /
×
3431
                  (gm1.values[idx] * gm1.values[idx] +
×
3432
                   gm2.values[idx] * gm2.values[idx] + c);
×
3433
            mean += gms;
×
3434
        }
3435
    }
3436
    mean /= (double)total;
×
3437

3438
    sq_sum = 0.0;
×
3439
    for (y = 0; y < height; ++y) {
×
3440
        for (x = 0; x < width; ++x) {
×
3441
            idx = (size_t)y * (size_t)width + (size_t)x;
×
3442
            gms = (2.0 * gm1.values[idx] * gm2.values[idx] + c) /
×
3443
                  (gm1.values[idx] * gm1.values[idx] +
×
3444
                   gm2.values[idx] * gm2.values[idx] + c);
×
3445
            sq_sum += (gms - mean) * (gms - mean);
×
3446
        }
3447
    }
3448
    sq_sum /= (double)total;
×
3449

3450
    float_buffer_free(&gx1);
×
3451
    float_buffer_free(&gy1);
×
3452
    float_buffer_free(&gx2);
×
3453
    float_buffer_free(&gy2);
×
3454
    float_buffer_free(&gm1);
×
3455
    float_buffer_free(&gm2);
×
3456

3457
    return (float)sqrt(sq_sum);
×
3458
}
3459

3460
static float
3461
psnr_metric(const sixel_assessment_float_buffer_t *ref,
×
3462
            const sixel_assessment_float_buffer_t *out,
3463
            int width,
3464
            int height)
3465
{
3466
    double mse;
3467
    size_t total;
3468
    size_t i;
3469
    double diff;
3470

3471
    mse = 0.0;
×
3472
    total = (size_t)width * (size_t)height;
×
3473
    for (i = 0; i < total; ++i) {
×
3474
        diff = ref->values[i] - out->values[i];
×
3475
        mse += diff * diff;
×
3476
    }
3477
    mse /= (double)total;
×
3478
    if (mse <= 1e-12) {
×
3479
        return 99.0f;
×
3480
    }
3481
    return (float)(10.0 * log10(1.0 / mse));
×
3482
}
3483

3484
/*
3485
 * sixel_assessment_metrics_t aggregation
3486
 */
3487
static sixel_assessment_metrics_t
3488
evaluate_metrics(const float *ref_pixels,
×
3489
                 const float *out_pixels,
3490
                 int width,
3491
                 int height)
3492
{
3493
    sixel_assessment_metrics_t metrics;
3494
    sixel_assessment_float_buffer_t ref_luma;
3495
    sixel_assessment_float_buffer_t out_luma;
3496
    sixel_assessment_float_buffer_t ref_lab;
3497
    sixel_assessment_float_buffer_t out_lab;
3498
    sixel_assessment_float_buffer_t ref_chroma;
3499
    sixel_assessment_float_buffer_t out_chroma;
3500
    sixel_assessment_float_buffer_t de00;
3501
    size_t pixels;
3502
    size_t iter;
3503
    double sum_value;
3504

3505
    memset(&metrics, 0, sizeof(metrics));
×
3506
    metrics.lpips_alex = NAN;
×
3507

3508
    ref_luma = pixels_to_luma709(ref_pixels, width, height);
×
3509
    out_luma = pixels_to_luma709(out_pixels, width, height);
×
3510

3511
    metrics.ms_ssim = ms_ssim_luma(&ref_luma, &out_luma, width, height);
×
3512

3513
    metrics.high_freq_out = high_frequency_ratio(&out_luma, width, height,
×
3514
                                                 0.25f);
3515
    metrics.high_freq_ref = high_frequency_ratio(&ref_luma, width, height,
×
3516
                                                 0.25f);
3517
    metrics.high_freq_delta = metrics.high_freq_out - metrics.high_freq_ref;
×
3518

3519
    metrics.stripe_ref = stripe_score(&ref_luma, width, height, 180);
×
3520
    metrics.stripe_out = stripe_score(&out_luma, width, height, 180);
×
3521
    metrics.stripe_rel = metrics.stripe_out - metrics.stripe_ref;
×
3522

3523
    metrics.band_run_rel = banding_index_runlen(&out_luma, width, height, 32) -
×
3524
                           banding_index_runlen(&ref_luma, width, height, 32);
×
3525

3526
    metrics.band_grad_rel = banding_index_gradient(&out_luma, width, height) -
×
3527
                            banding_index_gradient(&ref_luma, width, height);
×
3528

3529
    clipping_rates(ref_pixels, width, height,
×
3530
                   &metrics.clip_l_ref,
3531
                   &metrics.clip_r_ref,
3532
                   &metrics.clip_g_ref,
3533
                   &metrics.clip_b_ref);
3534
    clipping_rates(out_pixels, width, height,
×
3535
                   &metrics.clip_l_out,
3536
                   &metrics.clip_r_out,
3537
                   &metrics.clip_g_out,
3538
                   &metrics.clip_b_out);
3539

3540
    metrics.clip_l_rel = metrics.clip_l_out - metrics.clip_l_ref;
×
3541
    metrics.clip_r_rel = metrics.clip_r_out - metrics.clip_r_ref;
×
3542
    metrics.clip_g_rel = metrics.clip_g_out - metrics.clip_g_ref;
×
3543
    metrics.clip_b_rel = metrics.clip_b_out - metrics.clip_b_ref;
×
3544

3545
    ref_lab = rgb_to_lab(ref_pixels, width, height);
×
3546
    out_lab = rgb_to_lab(out_pixels, width, height);
×
3547
    pixels = (size_t)width * (size_t)height;
×
3548
    ref_chroma = chroma_ab(&ref_lab, pixels);
×
3549
    out_chroma = chroma_ab(&out_lab, pixels);
×
3550

3551
    sum_value = 0.0;
×
3552
    for (iter = 0; iter < pixels; ++iter) {
×
3553
        sum_value += fabs(out_chroma.values[iter] -
×
3554
                          ref_chroma.values[iter]);
×
3555
    }
3556
    metrics.delta_chroma_mean = (float)(sum_value / (double)pixels);
×
3557

3558
    de00 = deltaE00(&ref_lab, &out_lab, pixels);
×
3559
    sum_value = 0.0;
×
3560
    for (iter = 0; iter < pixels; ++iter) {
×
3561
        sum_value += de00.values[iter];
×
3562
    }
3563
    metrics.delta_e00_mean = (float)(sum_value / (double)pixels);
×
3564

3565
    metrics.gmsd_value = gmsd_metric(&ref_luma, &out_luma, width, height);
×
3566
    metrics.psnr_y = psnr_metric(&ref_luma, &out_luma, width, height);
×
3567

3568
    float_buffer_free(&ref_luma);
×
3569
    float_buffer_free(&out_luma);
×
3570
    float_buffer_free(&ref_lab);
×
3571
    float_buffer_free(&out_lab);
×
3572
    float_buffer_free(&ref_chroma);
×
3573
    float_buffer_free(&out_chroma);
×
3574
    float_buffer_free(&de00);
×
3575

3576
    return metrics;
×
3577
}
3578

3579
/*
3580
 * LPIPS metric integration (ONNX Runtime)
3581
 */
3582
static float
3583
compute_lpips_alex(sixel_assessment_t *assessment,
×
3584
                   const float *ref_pixels,
3585
                   const float *out_pixels,
3586
                   int width,
3587
                   int height)
3588
{
3589
    float value;
3590

3591
    value = NAN;
×
3592
#if defined(HAVE_ONNXRUNTIME)
3593
    image_f32_t ref_tensor;
3594
    image_f32_t out_tensor;
3595
    float distance;
3596

3597
    ref_tensor.width = 0;
3598
    ref_tensor.height = 0;
3599
    ref_tensor.nchw = NULL;
3600
    out_tensor = ref_tensor;
3601
    distance = NAN;
3602

3603
    if (assessment->enable_lpips == 0) {
3604
        goto done;
3605
    }
3606
    if (convert_pixels_to_nchw(ref_pixels, width, height, &ref_tensor) != 0) {
3607
        fprintf(stderr,
3608
                "Warning: unable to convert reference image for LPIPS.\n");
3609
        goto done;
3610
    }
3611
    if (convert_pixels_to_nchw(out_pixels, width, height, &out_tensor) != 0) {
3612
        fprintf(stderr,
3613
                "Warning: unable to convert output image for LPIPS.\n");
3614
        goto done;
3615
    }
3616
    if (ensure_lpips_models(assessment) != 0) {
3617
        goto done;
3618
    }
3619
    if (run_lpips(assessment->diff_model_path,
3620
                  assessment->feat_model_path,
3621
                  &ref_tensor,
3622
                  &out_tensor,
3623
                  &distance) != 0) {
3624
        goto done;
3625
    }
3626
    value = distance;
3627

3628
done:
3629
    free_image_f32(&ref_tensor);
3630
    free_image_f32(&out_tensor);
3631
#else
3632
    (void)assessment;
3633
    (void)ref_pixels;
3634
    (void)out_pixels;
3635
    (void)width;
3636
    (void)height;
3637
#endif
3638
    return value;
×
3639
}
3640

3641
static void
3642
align_frame_pixels(float **ref_pixels,
×
3643
                   int *ref_width,
3644
                   int *ref_height,
3645
                   float **out_pixels,
3646
                   int *out_width,
3647
                   int *out_height)
3648
{
3649
    int width;
3650
    int height;
3651
    int channels;
3652
    float *ref_new;
3653
    float *out_new;
3654
    int y;
3655
    size_t row_bytes;
3656

3657
    if (ref_pixels == NULL || ref_width == NULL || ref_height == NULL ||
×
3658
            out_pixels == NULL || out_width == NULL || out_height == NULL) {
×
3659
        assessment_fail(SIXEL_BAD_ARGUMENT,
×
3660
                       "align_frame_pixels: invalid parameters");
3661
    }
3662

3663
    channels = SIXEL_ASSESSMENT_RGB_CHANNELS;
×
3664
    width = *ref_width < *out_width ? *ref_width : *out_width;
×
3665
    height = *ref_height < *out_height ? *ref_height : *out_height;
×
3666
    if (width <= 0 || height <= 0) {
×
3667
        assessment_fail(SIXEL_BAD_ARGUMENT,
×
3668
                       "align_frame_pixels: empty frame dimensions");
3669
    }
3670
    ref_new = (float *)xmalloc((size_t)width * (size_t)height *
×
3671
                               (size_t)channels * sizeof(float));
×
3672
    out_new = (float *)xmalloc((size_t)width * (size_t)height *
×
3673
                               (size_t)channels * sizeof(float));
×
3674
    row_bytes = (size_t)width * (size_t)channels * sizeof(float);
×
3675
    for (y = 0; y < height; ++y) {
×
3676
        memcpy(ref_new + (size_t)y * (size_t)width * (size_t)channels,
×
UNCOV
3677
               *ref_pixels + (size_t)y * (size_t)(*ref_width) *
×
UNCOV
3678
               (size_t)channels,
×
3679
               row_bytes);
3680
        memcpy(out_new + (size_t)y * (size_t)width * (size_t)channels,
×
UNCOV
3681
               *out_pixels + (size_t)y * (size_t)(*out_width) *
×
UNCOV
3682
               (size_t)channels,
×
3683
               row_bytes);
3684
    }
3685
    free(*ref_pixels);
×
3686
    free(*out_pixels);
×
3687
    *ref_pixels = ref_new;
×
3688
    *out_pixels = out_new;
×
3689
    *ref_width = width;
×
3690
    *ref_height = height;
×
3691
    *out_width = width;
×
3692
    *out_height = height;
×
3693
}
×
3694

3695
/*
3696
 * Assessment API bridge
3697
 */
3698
typedef struct MetricDescriptor {
3699
    int id;
3700
    const char *json_key;
3701
} MetricDescriptor;
3702

3703
static const MetricDescriptor g_metric_table[] = {
3704
    {SIXEL_ASSESSMENT_METRIC_MS_SSIM, "MS-SSIM"},
3705
    {SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_OUT, "HighFreqRatio_out"},
3706
    {SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_REF, "HighFreqRatio_ref"},
3707
    {SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_DELTA, "HighFreqRatio_delta"},
3708
    {SIXEL_ASSESSMENT_METRIC_STRIPE_REF, "StripeScore_ref"},
3709
    {SIXEL_ASSESSMENT_METRIC_STRIPE_OUT, "StripeScore_out"},
3710
    {SIXEL_ASSESSMENT_METRIC_STRIPE_REL, "StripeScore_rel"},
3711
    {SIXEL_ASSESSMENT_METRIC_BAND_RUN_REL, "BandingIndex_rel"},
3712
    {SIXEL_ASSESSMENT_METRIC_BAND_GRAD_REL, "BandingIndex_grad_rel"},
3713
    {SIXEL_ASSESSMENT_METRIC_CLIP_L_REF, "ClipRate_L_ref"},
3714
    {SIXEL_ASSESSMENT_METRIC_CLIP_R_REF, "ClipRate_R_ref"},
3715
    {SIXEL_ASSESSMENT_METRIC_CLIP_G_REF, "ClipRate_G_ref"},
3716
    {SIXEL_ASSESSMENT_METRIC_CLIP_B_REF, "ClipRate_B_ref"},
3717
    {SIXEL_ASSESSMENT_METRIC_CLIP_L_OUT, "ClipRate_L_out"},
3718
    {SIXEL_ASSESSMENT_METRIC_CLIP_R_OUT, "ClipRate_R_out"},
3719
    {SIXEL_ASSESSMENT_METRIC_CLIP_G_OUT, "ClipRate_G_out"},
3720
    {SIXEL_ASSESSMENT_METRIC_CLIP_B_OUT, "ClipRate_B_out"},
3721
    {SIXEL_ASSESSMENT_METRIC_CLIP_L_REL, "ClipRate_L_rel"},
3722
    {SIXEL_ASSESSMENT_METRIC_CLIP_R_REL, "ClipRate_R_rel"},
3723
    {SIXEL_ASSESSMENT_METRIC_CLIP_G_REL, "ClipRate_G_rel"},
3724
    {SIXEL_ASSESSMENT_METRIC_CLIP_B_REL, "ClipRate_B_rel"},
3725
    {SIXEL_ASSESSMENT_METRIC_DELTA_CHROMA, "Δ Chroma_mean"},
3726
    {SIXEL_ASSESSMENT_METRIC_DELTA_E00, "Δ E00_mean"},
3727
    {SIXEL_ASSESSMENT_METRIC_GMSD, "GMSD"},
3728
    {SIXEL_ASSESSMENT_METRIC_PSNR_Y, "PSNR_Y"},
3729
    {SIXEL_ASSESSMENT_METRIC_LPIPS_VGG, "LPIPS(alex)"},
3730
};
3731

3732
static void
3733
store_metrics(sixel_assessment_t *assessment,
×
3734
              const sixel_assessment_metrics_t *metrics)
3735
{
3736
    double *results;
3737

3738
    results = assessment->results;
×
3739
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_MS_SSIM)]
3740
        = metrics->ms_ssim;
×
3741
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_OUT)]
×
3742
        = metrics->high_freq_out;
×
3743
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_REF)]
×
3744
        = metrics->high_freq_ref;
×
3745
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_HIGH_FREQ_DELTA)]
×
3746
        = metrics->high_freq_delta;
×
3747
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_STRIPE_REF)]
×
3748
        = metrics->stripe_ref;
×
3749
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_STRIPE_OUT)]
×
3750
        = metrics->stripe_out;
×
3751
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_STRIPE_REL)]
×
3752
        = metrics->stripe_rel;
×
3753
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_BAND_RUN_REL)]
×
3754
        = metrics->band_run_rel;
×
3755
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_BAND_GRAD_REL)]
×
3756
        = metrics->band_grad_rel;
×
3757
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_L_REF)]
×
3758
        = metrics->clip_l_ref;
×
3759
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_R_REF)]
×
3760
        = metrics->clip_r_ref;
×
3761
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_G_REF)]
×
3762
        = metrics->clip_g_ref;
×
3763
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_B_REF)]
×
3764
        = metrics->clip_b_ref;
×
3765
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_L_OUT)]
×
3766
        = metrics->clip_l_out;
×
3767
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_R_OUT)]
×
3768
        = metrics->clip_r_out;
×
3769
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_G_OUT)]
×
3770
        = metrics->clip_g_out;
×
3771
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_B_OUT)]
×
3772
        = metrics->clip_b_out;
×
3773
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_L_REL)]
×
3774
        = metrics->clip_l_rel;
×
3775
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_R_REL)]
×
3776
        = metrics->clip_r_rel;
×
3777
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_G_REL)]
×
3778
        = metrics->clip_g_rel;
×
3779
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_CLIP_B_REL)]
×
3780
        = metrics->clip_b_rel;
×
3781
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_DELTA_CHROMA)]
×
3782
        = metrics->delta_chroma_mean;
×
3783
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_DELTA_E00)]
×
3784
        = metrics->delta_e00_mean;
×
3785
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_GMSD)]
×
3786
        = metrics->gmsd_value;
×
3787
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_PSNR_Y)]
×
3788
        = metrics->psnr_y;
×
3789
    results[SIXEL_ASSESSMENT_INDEX(SIXEL_ASSESSMENT_METRIC_LPIPS_VGG)]
×
3790
        = metrics->lpips_alex;
×
3791
}
×
3792

3793
static SIXELSTATUS
3794
sixel_assessment_capture_first_frame(sixel_frame_t *frame,
×
3795
                                     void *user_data)
3796
{
3797
    sixel_assessment_capture_t *capture;
3798

3799
    /*
3800
     * Loader pipeline sketch for encoded round trips:
3801
     *
3802
     *     +--------------+     +-----------------+
3803
     *     | decoder loop | --> | capture.frame   |
3804
     *     +--------------+     +-----------------+
3805
     */
3806

3807
    capture = (sixel_assessment_capture_t *)user_data;
×
3808
    if (capture == NULL) {
×
3809
        return SIXEL_BAD_ARGUMENT;
×
3810
    }
3811
    if (capture->frame == NULL) {
×
3812
        sixel_frame_ref(frame);
×
3813
        capture->frame = frame;
×
3814
    }
3815
    return SIXEL_OK;
×
3816
}
3817

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

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

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

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

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

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

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

3911
    status = sixel_frame_new(&frame, allocator);
×
3912
    if (SIXEL_FAILED(status)) {
×
3913
        return status;
×
3914
    }
3915

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

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

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

3952
    rgb_pixels = NULL;
×
3953
    status = sixel_frame_ensure_colorspace(frame, colorspace);
×
3954
    if (SIXEL_FAILED(status)) {
×
3955
        goto cleanup;
×
3956
    }
3957

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4143
    *ppassessment = assessment;
2✔
4144
    return SIXEL_OK;
2✔
4145
}
4146

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4646
            stage_index = next_index;
×
4647
        }
4648

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

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

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

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

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

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

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

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

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

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

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

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

4745
    callback("}\n", sizeof("}\n") - 1, user_data);
2✔
4746
    return SIXEL_OK;
2✔
4747
}
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