JPEG XL

Info

rules 57
github 35276
reddit 647

JPEG XL

tools 4225
website 1655
adoption 20712
image-compression-forum 0

General chat

welcome 3810
introduce-yourself 291
color 1414
photography 3435
other-codecs 23765
on-topic 24923
off-topic 22701

Voice Channels

General 2147

Archived

bot-spam 4380

Specification issues

_wb_
2025-07-06 05:50:16
I am just back in Belgium after a pretty long trip (Daejeon conference center - Daejeon station - Seoul - Incheon terminal 2 - Amsterdam - Brussels - now on the way home), so it is very much possible I don't.
Traneptora
2025-07-07 02:32:20
> the array window[] is initialized to all zeroes when the first symbol is decoded from the stream
2025-07-07 02:32:23
Section C.3.3
2025-07-07 02:33:02
since it just does `window[copy_pos++ & 0xFFFFF]` that would mean that it hits a zero if it's not written
2025-07-07 10:03:46
however, it also has the following: ``` distance = min(distance, num_decoded, 1 << 20); copy_pos = num_decoded − distance; ``` this means that if `num_decoded` is less than distance, for example, if it's zero, then `distance` is set to `num_decoded` and `copy_pos` is set to 0
2025-07-07 10:04:20
This means that if `num_decoded` is nonzero, then the backreferences can only go to decoded symbols
2025-07-07 10:05:03
if `num_decoded`is zero, then it looks like it sets `copy_pos` to zero as well, which will end up being the calloc'd zeroes
Lucas Chollet
2025-10-16 11:53:44
Is it specified how frames are supposed to compose the final image? I might be missing something, but the only relevant parts that I've found are: 1. > If can_reference, then the samples of the decoded frame are recorded as Reference[save_as_reference] and may be referenced by subsequent frames. 2. > All blending operations consider as “previous sample” the sample at the corresponding coordinates in the source frame, which is the frame that was previously stored in Reference[source] – if no frame was previously stored, the source frame is assumed to have all sample values set to zeroes. 3. > If duration is zero and !is_last, the decoder does not present the current frame, but the frame may be composed together with the next frames, for example through blending. As an example, let's try to apply that with `cmyk_layers` which has 4 frames: "Background" depends on None "Layer 1" depends on "Background" "Test Name" depends on "Background" "Black" depends on "Background" This is not an animated image, so the duration is always zero, and only "Black" is `is_last`. So according to 3., all except "Black" should not be presented. So we take "Black" we see that it refers "Background" (no reference so blending on zeroes according to 2.), then we render "Background", all dependency are resolved so we blend "Black" on top of it. And that's it? (according to the spec, I know current decoders do more)
_wb_
2025-10-16 08:54:18
no, first "Background" is decoded and stored in ref[0] (or whatever reference id that file uses), then "Layer 1" is decoded, blended with background (then in ref[0]) and the result of that is stored in ref[0], then "Test Name" is decoded and blended with Background+Layer 1 which is what is then in ref[0] and the result of that is stored in ref[0], then "Black" is decoded and blended with ref[0] which at this point contains the previous three layers merged.
2025-10-16 08:54:50
in other words you have to apply things sequentially in the order the frames appear in the bitstream
Lucas Chollet
2025-10-17 07:12:25
Thanks for clarifying this, that's what I had in mind but it's good to have a confirmation. I was also asking the question because this seems to be underspecified (or is it me being unable to read the spec?), and I thought that you might want to know about this kind of thing.
_wb_
2025-10-17 07:31:28
Yes, I agree the frame blending process could use some clarification in the spec
Lucas Chollet
2025-10-21 07:52:30
From `F.3.1 General`: > Otherwise, there is one entry for each of the following sections, in the order they are listed: > - one section for LfGlobal; > - num_lf_groups sections, one per LfGroup (in raster order); > - one section for HfGlobal; > - num_groups * frame_header.passes.num_passes sections, one per PassGroup. The first num_groups PassGroup sections are the groups (in raster order) of the first pass, followed by all groups (in raster order) of the second pass (if present), and so on. There is even this note: > NOTE 1 Some of these sections may be empty in case encoding == kModular, i.e. their size in bytes is zero. However, in `Table F.1 — Frame bundle`, there is the `encoding == kVarDCT` condition on the `HFGlobal` field. Which seems wrong to me, the `HFGlobal` field is always here (and thus has its section in the TOC) but its content is dependent on the condition, as it is already explicitly written in `Table G.4 — HfGlobal bundle`.
_wb_
2025-10-21 08:14:14
right, it will be empty in the modular case but it would be clearer to say it is still part of the frame bundle, since it will be in the TOC
max.erne
2025-10-21 12:10:10
(Apologies if this is the wrong thread.) Hiya! It seems the implementation differs from the spec in how WP handles the rightmost column of pixels. The spec states (H.5.2; Second edition): > The weights `weight[i]` for each of the 4 sub-predictions are computed as specified by the following code, based on the `err` values already computed for sub-predictors of earlier samples. > ``` > [..] > err_sum[i] = (err[i]_N + err[i]_W + err[i]_NW + err[i]_WW + err[i]_NE) Umod (1 << 32); > [..] > ``` `UpdateErrors()` in `context_predict.h` is a little tricky so that `Predict()` can take a shortcut:: > ```cpp > JXL_INLINE pixel_type_w Predict(size_t x, size_t y, size_t xsize, > [..] > for (size_t i = 0; i < kNumPredictors; i++) { > // pred_errors[pos_N] also contains the error of pixel W. > // pred_errors[pos_NW] also contains the error of pixel WW. > weights[i] = pred_errors[i][pos_N] + pred_errors[i][pos_NE] + > pred_errors[i][pos_NW]; > weights[i] = ErrorWeight(weights[i], header.w[i]); > } > > [..] > > JXL_INLINE void UpdateErrors(pixel_type_w val, size_t x, size_t y, > [..] > for (size_t i = 0; i < kNumPredictors; i++) { > [..] > // For predicting in the next row. > pred_errors[i][cur_row + x] = err; > // Add the error on this pixel to the error on the NE pixel. This has the > // effect of adding the error on this pixel to the E and EE pixels. > pred_errors[i][prev_row + x + 1] += err; > } > } > ``` But as `pos_NE` turns into `pos_N` if it doesn't exist (also H.5.2), for each rightmost pixel this becomes: ```cpp // pred_errors[pos_N] also contains the error of pixel W. // pred_errors[pos_NW] also contains the error of pixel WW. weights[i] = pred_errors[i][pos_N] /* = N + W */ + pred_errors[i][pos_NE] /* = "pred_errors[i][pos_N]" = N + W */ + pred_errors[i][pos_NW]; /* = NW + WW */ ``` This gives an extra `W` compared to the `N + W + NW + WW + N` the first quote suggests.
max.erne (Apologies if this is the wrong thread.) Hiya! It seems the implementation differs from the spec in how WP handles the rightmost column of pixels. The spec states (H.5.2; Second edition): > The weights `weight[i]` for each of the 4 sub-predictions are computed as specified by the following code, based on the `err` values already computed for sub-predictors of earlier samples. > ``` > [..] > err_sum[i] = (err[i]_N + err[i]_W + err[i]_NW + err[i]_WW + err[i]_NE) Umod (1 << 32); > [..] > ``` `UpdateErrors()` in `context_predict.h` is a little tricky so that `Predict()` can take a shortcut:: > ```cpp > JXL_INLINE pixel_type_w Predict(size_t x, size_t y, size_t xsize, > [..] > for (size_t i = 0; i < kNumPredictors; i++) { > // pred_errors[pos_N] also contains the error of pixel W. > // pred_errors[pos_NW] also contains the error of pixel WW. > weights[i] = pred_errors[i][pos_N] + pred_errors[i][pos_NE] + > pred_errors[i][pos_NW]; > weights[i] = ErrorWeight(weights[i], header.w[i]); > } > > [..] > > JXL_INLINE void UpdateErrors(pixel_type_w val, size_t x, size_t y, > [..] > for (size_t i = 0; i < kNumPredictors; i++) { > [..] > // For predicting in the next row. > pred_errors[i][cur_row + x] = err; > // Add the error on this pixel to the error on the NE pixel. This has the > // effect of adding the error on this pixel to the E and EE pixels. > pred_errors[i][prev_row + x + 1] += err; > } > } > ``` But as `pos_NE` turns into `pos_N` if it doesn't exist (also H.5.2), for each rightmost pixel this becomes: ```cpp // pred_errors[pos_N] also contains the error of pixel W. // pred_errors[pos_NW] also contains the error of pixel WW. weights[i] = pred_errors[i][pos_N] /* = N + W */ + pred_errors[i][pos_NE] /* = "pred_errors[i][pos_N]" = N + W */ + pred_errors[i][pos_NW]; /* = NW + WW */ ``` This gives an extra `W` compared to the `N + W + NW + WW + N` the first quote suggests.
2025-10-21 12:12:12
When I update `UpdateErrors` and `Predict` to look more like the spec: ```diff --- a/lib/jxl/modular/encoding/context_predict.h +++ b/lib/jxl/modular/encoding/context_predict.h @@ -141,12 +141,13 @@ struct State { + size_t pos_W = x > 0 ? cur_row + x - 1 : cur_row + x; + size_t pos_WW = x > 1 ? cur_row + x - 2 : pos_W; std::array<uint32_t, kNumPredictors> weights; for (size_t i = 0; i < kNumPredictors; i++) { - // pred_errors[pos_N] also contains the error of pixel W. - // pred_errors[pos_NW] also contains the error of pixel WW. - weights[i] = pred_errors[i][pos_N] + pred_errors[i][pos_NE] + - pred_errors[i][pos_NW]; + weights[i] = pred_errors[i][pos_N] + pred_errors[i][pos_W] + + pred_errors[i][pos_NE] + pred_errors[i][pos_NW] + + pred_errors[i][pos_WW]; weights[i] = ErrorWeight(weights[i], header.w[i]); } @@ -195,17 +196,12 @@ struct State { JXL_INLINE void UpdateErrors(pixel_type_w val, size_t x, size_t y, size_t xsize) { size_t cur_row = y & 1 ? 0 : (xsize + 2); - size_t prev_row = y & 1 ? (xsize + 2) : 0; val = AddBits(val); error[cur_row + x] = pred - val; for (size_t i = 0; i < kNumPredictors; i++) { pixel_type_w err = (std::abs(prediction[i] - val) + kPredictionRound) >> kPredExtraBits; - // For predicting in the next row. pred_errors[i][cur_row + x] = err; - // Add the error on this pixel to the error on the NE pixel. This has the - // effect of adding the error on this pixel to the E and EE pixels. - pred_errors[i][prev_row + x + 1] += err; } } ``` the predictions differ occasionally. (Rounding helps, though.) Some tests (`RoundTripAlphaResampling`, `JxlTest.RoundTripLossless[8|8Alpha|8Falcon|8Gray]`) now fail with different compressed image size, but `AlphaResampling` also fails from the image actually changing. Should I make an issue (or PR) in the repo?
_wb_
2025-10-21 01:50:57
Generally at this point, if libjxl and the spec disagree, we will define libjxl to be right and the spec to be wrong. Otherwise we will break existing bitstreams.
2025-10-21 01:58:04
That part of the libjxl code is such a convoluted mess, with that tricky shortcut — that dates back all the way to Alexander Rhatushnyak's original code for the weighted predictor back when it was PIK's lossless mode.
2025-10-21 02:08:36
Good catch though, I think you're right that err_W ends up getting double counted on the rightmost pixel. I propose we fix this by just making it spec that err_W is added twice for the last pixel, with a NOTE explaining that this allows some optimization. WDYT <@179701849576833024> ?
veluca
2025-10-21 02:23:47
I guess so, also ugh
max.erne
2025-10-22 07:38:09
Alright, that makes sense! While you're at it, there's also a small wrong comment (but nothing more than that) in the pseudocode a little further down H.5.2: ``` // if true_err_N, true_err_W and true_err_NW don't have the same sign if (((true_err_N ^ true_err_W) | (true_err_N ^ true_err_NW)) <= 0) { prediction = clamp(prediction, min(W3, N3, NE3), max(W3, N3, NE3)); } ``` Due to the `<=` instead of `<` this also triggers when `true_err_N`, `true_err_W` and `true_err_NW` are all equal, so it might be helpful to update it to `// if [..] don't have the same sign, or are all equal`. (The codebase at least matches the spec here, so this is much less of an issue `:p`)
Lucas Chollet
2025-10-30 09:20:10
Very small nit in `F.1 General`: > If lf_level > 0 (which is also a field in frame_header), then width = ceil(width / (1 << (3 * lf_level))). and height = ceil(height / (1 << (3 * lf_level))). There is one too many dot in that sentence (just before the "and").
Zamarusdz
2025-11-01 11:59:02
Hi, i'm making tests for lossless compressing/decompressing png with latest libjxl and i found colors difference (they're pixel duplicates); due to metadata of the decompressed png not corresponding to the original?! They're actually visually different. Is there a way i can make sure to keep the information somewhere (with the help of exiftool maybe)?
2025-11-02 12:44:26
I understand the difference between my original png and the jxl can be due to the rendering software but i think the decompressed png should look the same as the original, else it's.. Not lossless. I'm probably missing something
RaveSteel
2025-11-02 12:49:47
what file are you using and how are you encoding and decoding the image?
2025-11-02 12:50:12
I have not yet encountered a roundtrip which did not result in an exact replica of the source file
Zamarusdz
2025-11-02 12:52:48
I'm using cjxl/djxl on windows
2025-11-02 12:53:35
Here is an example image
RaveSteel
2025-11-02 12:54:15
what are the parameters you are using with cjxl and djxl?
Zamarusdz
2025-11-02 12:54:53
I'm not sure it's going to work if i drop it on discord so here are the two pngs from my pov
2025-11-02 12:56:39
Sorry it was a lossy conversion i'm doing it again
2025-11-02 01:00:31
2025-11-02 01:01:16
Gif quality is bad but there is a color difference
RaveSteel
2025-11-02 01:01:23
please share the commands with parameters you used and I will try to replicate
Zamarusdz
2025-11-02 01:01:50
simply cjxl test1.png test1.jxl -d 0
2025-11-02 01:02:16
Then djxl test1.jxl uncompressed.png
2025-11-02 01:02:31
With cjxl & djxl exes in the directory from latest release
2025-11-02 01:03:28
RaveSteel
2025-11-02 01:03:54
round trip results in bit identical files for me, latest release
2025-11-02 01:04:01
maybe the windows version is borked?
2025-11-02 01:04:09
I am on linux
Zamarusdz
2025-11-02 01:04:43
No idea, if i set the metadatas back to original the pngs are back to being the same.
RaveSteel
2025-11-02 01:07:59
what kind of metadata?
2025-11-02 01:08:19
just encoding and decoding with the windows build still results in identical files for me
Zamarusdz
2025-11-02 01:13:32
If i copy the new icc profile from the decompressed png to the original with exiftool -tagsfromfile decompressed.png -icc_profile test1.png
2025-11-02 01:19:41
It's actually v0.11.1 of libjxl. Here's in a bad windows photo viewer. In my other software there is still a light difference though
2025-11-02 01:20:04
(the two pngs, the jxl has the same color as the decompressed)
2025-11-02 01:21:08
Here are the metadatas if it can help i don't know
RaveSteel
2025-11-02 01:21:19
Ah, I assume that's the (known) problem with image viewers rendering colors etc. differently. Use ssimulacra2 on both images and they should be identical
2025-11-02 01:21:42
Or run ImageMagicks `identify -format "%M %#'\n'"` on both
Zamarusdz
2025-11-02 01:22:08
I'm not sure we have identify on windows, they're pixel duplicates though
RaveSteel
2025-11-02 01:22:39
ssimulacra should be included with the the libjxl binaries for windows though, so try that
Zamarusdz
2025-11-02 01:26:03
I'll try that. Now that i'm here i also had trouble converting apng files to jxl with the Getting pixel data failed. Seems like a known problem though.
RaveSteel
2025-11-02 01:26:42
That should be fixed after 0.10, so with latest 0.11.1
Zamarusdz
2025-11-02 01:30:51
It says 100 between the three files. I'm a little confused since there is a color change though in my softwares and i suppose there is no way to retrieve the original metadata if needed...
RaveSteel
2025-11-02 01:31:12
It's just a visual difference caused by image viewer
2025-11-02 01:31:17
The files are identical
Quackdoc
RaveSteel The files are identical
2025-11-02 01:32:11
dunno why but this just triggered memories of that losslessly denoise it dude
RaveSteel
2025-11-02 01:32:12
Should be this issue
2025-11-02 01:32:18
https://github.com/libjxl/libjxl/issues/4307
Quackdoc dunno why but this just triggered memories of that losslessly denoise it dude
2025-11-02 01:33:18
afraid I am not aware of what occurence you are talking about
Zamarusdz
2025-11-02 01:33:29
The software i use uses pillow for jxl support i believe
Quackdoc
RaveSteel afraid I am not aware of what occurence you are talking about
2025-11-02 01:33:39
whaaaa, lemme try and find it, it was a wild twitter thread
Zamarusdz
2025-11-02 01:36:56
Yes the problem is they're both pngs in that case so it's a problem with ICC profiles support then?
RaveSteel
2025-11-02 01:37:18
I guess so
Zamarusdz
2025-11-02 01:38:01
Thanks for the help, i'll store the metadata somewhere just in case and convert to jxl then
Quackdoc
RaveSteel afraid I am not aware of what occurence you are talking about
2025-11-02 01:40:03
ahaha I found it https://x.com/lookoutitsbbear/status/1794962035714785570
username
2025-11-02 01:40:40
I think what's happening is the PNG does not have any ICC profile or colorspace defined meanwhile JXL itself does not support not being color managed and assumes that if an image does not have anything defined that it's sRGB and then when you convert it back to PNG it adds a sRGB profile since the JXL it ends up as is registered as being sRGB. so something like: PNG (nothing) > JXL (sRGB) > PNG (sRGB)
RaveSteel
Quackdoc ahaha I found it https://x.com/lookoutitsbbear/status/1794962035714785570
2025-11-02 01:42:20
uhhhhhhhh lmao
2025-11-02 01:43:13
wow, it gets wilder the more you read
Quackdoc
2025-11-02 01:43:44
right?
2025-11-02 01:43:56
RaveSteel
2025-11-02 01:44:17
he doubles and triples down, amazing
Zamarusdz
2025-11-02 01:45:06
<:KekDog:805390049033191445> Yep it's kinda what jxl does in that case Edit: In the end i'm still not too happy with jxl changing the metadata as it sees fit even if for technical reasons it's better with an sRGB profile or whatnot. I'll have to backup the metadatas while converting now for peace of mind. Though simply deleting the profile with exiftool works.
_wb_
2025-11-02 07:43:57
Do the two png files look different when viewed in Chrome?
username
2025-11-02 10:47:31
is this a valid JXL file? https://github.com/libjxl/jxl-rs/issues/431#issuecomment-3473137851
Tirr
2025-11-02 10:49:17
I guess the resulting sample values are way out of range, so it's technically valid but unspecified?
Zamarusdz
_wb_ Do the two png files look different when viewed in Chrome?
2025-11-02 01:26:28
Yes they do in chrome/chromium and there is the same small difference in color. ||on latest chrome. windows 10 Pro N 22H2 19045.5737 If i take two random pixels on the same location: Original RGB(70,6,71) -> (68,1,70) decompressed Original RGB(103,12,94) -> (103,4,94) decompressed||
2025-11-02 01:51:51
https://imgur.com/a/lv0pvw4#l09golK It's indeed due to the ICC profile, if i paste it on the original image i get the uncompressed image's colors.
2025-11-02 03:08:05
The icc profile because why not <:Hypers:808826266060193874>
_wb_
2025-11-02 07:56:12
can you open a github issue for it with the example png in it, so we don't forget about this? looks like something that needs investigation
Zamarusdz
2025-11-02 08:12:16
Yes i can't reproduce it with the imgur one so it must modify it. Do i just drop it in github?
2025-11-02 08:19:22
Would be nice to have a way to actually share the exact same .png <:KekDog:805390049033191445>
2025-11-02 08:23:32
Here if someone can try to replicate on windows it would be nice https://drive.google.com/file/d/1wEmris70JkIyLoWZrKElOINs23pLxrTL/view?usp=drive_link
jonnyawsom3
2025-11-02 09:44:50
Here's the issue they opened https://github.com/libjxl/libjxl/issues/4503
_wb_
2025-11-03 09:09:35
Also let's move this discussion to <#804324493420920833> since this is very likely not a specification issue but an implementation issue.
Zamarusdz
2025-11-03 04:26:57
Sure well anyone can ping me there if they find something related or for more infos. <:PepeOK:805388754545934396>
Lucas Chollet
2025-11-06 04:26:46
Again small nit, in `I.5.3 HF dequantization`: > Every quantized HF coefficient quant is first bias-adjusted as specified by the following code, depending on its channel (0 for X, 1 for Y or 2 for B). I think "quant" should be inside quoted to look like `quant`
2025-11-07 10:56:19
It seems that the composition of VarDCT frames is not specified. For modular groups (both LF and pass) it is written that the data should be patched into the GlobalModular's channels, but nothing is said for VarDCT's data. IMO there should be a section that says something around the lines of: With the data from Lf and Pass groups, one should: - Dequantize coefficients - Apply IDCT - Copy the data to three channels of the size of the destination image
_wb_
2025-11-07 11:06:09
I suppose more broadly it would be useful to have a description of the frame rendering, something like this: (of the top of my head, maybe I got some order wrong but that's exactly why it would be useful to make this more explicit instead of having to read it between the lines all across the spec) - main color channels (XYB/YCbCr/RGB) from VarDCT data or from modular if it's a modular mode frame - extra channels from modular - Gaborish and EPF are done - patches applied on top of that - splines applied on top of that - noise applied on top of that - frame saved to reference slot if save_before_ct - XYB or YCbCr converted to RGB - frame saved to reference slot if !save_before_ct
Lucas Chollet
2025-11-07 11:08:12
This already a bit covered by `A.2 Decoding process`
lonjil
2025-11-07 11:45:50
Might we get an updated version of the spec posted here? Been more than a year since the last one.
_wb_
Lucas Chollet This already a bit covered by `A.2 Decoding process`
2025-11-07 11:49:41
Yes in principle the info is already there, but it could be made a bit more clear, in particular upsampling frame blending and reference slot saving is now not very clear since these are not mentioned explicitly in A.2 so you have to understand it from the prose in F.2 which is pretty dense
lonjil Might we get an updated version of the spec posted here? Been more than a year since the last one.
2025-11-07 11:52:41
the most recent version I have is 2024-07-19, nothing has been updated yet since the second edition got published. This channel is collecting stuff that can be improved, but so far I haven't yet edited any of it into a draft for some future third edition...
lonjil
2025-11-07 11:52:59
ah, alright
Lucas Chollet
2025-11-07 03:15:02
More nits: in `I.5.3 HF dequantization`: > Every quantized HF coefficient quant is first bias-adjusted as specified by the following code, depending on its channel (0 for X, 1 for Y or 2 for B). I think "channel" should be quoted `channel` as well. > For the X and B channels, it is then multiplied by by pow(0.8, frame_header.x_qm_scale − 2) and pow(0.8, frame_header.b_qm_scale - 2), respectively. There are two "by" before the first pow.
Traneptora
2025-11-12 03:35:06
found another spec issue when debugging jxlatte
2025-11-12 03:35:24
Annex J.4 - EdgePreservingFilter
2025-11-12 03:36:16
It specifies that you must iterate between 0 and 3, but it makes no statement on what to do if the input is grayscale modular (i.e. 1 color channel)
2025-11-12 03:36:42
grayscale-public-university.jxl is an example of such a file (in conformance)
2025-11-12 03:37:53
the answer here is that you pretend the image is 3-color with three identical channels, but only when calculating Distance0 and Distance1
2025-11-12 03:38:28
For example, it has the following:
Lucas Chollet
Traneptora the answer here is that you pretend the image is 3-color with three identical channels, but only when calculating Distance0 and Distance1
2025-11-12 03:38:40
I was typing the same thing
Traneptora
2025-11-12 03:39:17
``` DistanceStep2(x, y, cx, cy) { dist = 0; for (c = 0; c < 3; c++) { dist += abs(sample(x, y, c) − sample(x + cx, y + cy, c)) * rf.epf_channel_scale[c]; } return dist; }``` it should instead have ``` DistanceStep2(x, y, cx, cy) { dist = 0; for (c = 0; c < 3; c++) { i = num_channels == 1 ? 0 : c; dist += abs(sample(x, y, i) − sample(x + cx, y + cy, i)) * rf.epf_channel_scale[c]; } return dist; } ```
2025-11-12 03:39:37
and similar for DistanceStep0and1
Lucas Chollet
2025-11-12 03:41:24
What if there are different values in epf_channel_scale? That could affect the end result
Traneptora
2025-11-12 03:42:47
there are highly likely to be different values in epf_channel_scale
2025-11-12 03:43:12
if you don't use them then you'll get a result that doesn't match libjxl
Lucas Chollet
2025-11-12 03:45:19
Sorry I misread your code, thought you were taking epf_channel_scale[i]. Forget it.
lonjil
2025-11-16 11:06:01
I'm not sure if this was already reported, but there appears to be a missing parenthesis in J.1