44 * PerBlockModulations, Blur1x1Masking), on the whole image instead of tiles. Copyright (c) the JPEG XL Project Authors, BSD-3-Clause
55 * License (see THIRD_PARTY_NOTICES.md)
66 */
7+ import { runChunks , type ChunkTask , type Thread } from '../../util/threads.ts' ;
78
89const SG_MUL = 226.77216153508914 , SG_MUL2 = 1 / 73.377132366608819 , LOG2 = 0.693147181 ;
910const SG_RET_MUL = SG_MUL2 * 18.6580932135 * LOG2 , SG_V_OFFSET = 7.7825991679894591 ;
@@ -35,36 +36,65 @@ export interface QuantField {
3536 mask1x1 : Float32Array ;
3637}
3738
39+ /** The masking of bands of 64 rows (see initialQuantField) */
40+ export interface MaskingTask extends ChunkTask {
41+ y : Float32Array ;
42+ w : number ;
43+ h : number ;
44+ // the results: the masking of the pixels (and blurred), the pre-erosion
45+ mask1x1 : Float32Array ;
46+ blurred : Float32Array ;
47+ preErosion : Float32Array ;
48+ }
49+
3850/**
39- * libjxl's initial quantization field of XYB planes (w x h, whole blocks) for a Butteraugli distance (scale: the
40- * field's scale)
51+ * The masking of a band of 64 rows (the chunk): of each pixel the inverse of the gamma corrected Laplacian (of Y);
52+ * the pre-erosion: its square (limited), through maskingSqrt, averaged over 4x4 pixels
4153 */
42- export function initialQuantField ( distance : number , planes : Float32Array [ ] , w : number , h : number , scale : number ) : QuantField {
43- const [ px , py , pb ] = planes , bw = w / 8 , bh = h / 8 ;
44- const MATCH_GAMMA_OFFSET = 0.019 , LIMIT = 0.2 ;
45- // the masking of each pixel: the inverse of the gamma corrected Laplacian; the pre-erosion: its square (limited),
46- // through maskingSqrt, averaged over 4x4 pixels
47- const mask1x1 = new Float32Array ( w * h ) , preErosion = new Float32Array ( ( w / 4 ) * ( h / 4 ) ) , rowSum = new Float32Array ( w ) ;
48- for ( let y = 0 ; y < h ; y ++ ) {
49- const r = y * w , r1 = ( y > 0 ? y - 1 : y ) * w , r2 = ( y + 1 < h ? y + 1 : y ) * w ;
50- for ( let x = 0 ; x < w ; x ++ ) {
51- const x1 = x > 0 ? x - 1 : x , x2 = x + 1 < w ? x + 1 : x ;
52- const base = 0.25 * ( py [ r2 + x ] + py [ r1 + x ] + py [ r + x1 ] + py [ r + x2 ] ) ;
53- const gammac = gammaRatio ( py [ r + x ] + MATCH_GAMMA_OFFSET ) ;
54- const d = gammac * ( py [ r + x ] - base ) ;
55- mask1x1 [ r + x ] = 1 / ( Math . log1p ( Math . abs ( d ) ) + 0.01 ) ;
56- const v = maskingSqrt ( Math . min ( d * d , LIMIT ) ) ;
57- rowSum [ x ] = y % 4 ? rowSum [ x ] + v : v ;
58- }
59- if ( y % 4 === 3 ) {
60- const out = ( y >> 2 ) * ( w / 4 ) ;
61- for ( let x = 0 ; x < w / 4 ; x ++ ) preErosion [ out + x ] = ( rowSum [ 4 * x ] + rowSum [ 4 * x + 1 ] + rowSum [ 4 * x + 2 ] + rowSum [ 4 * x + 3 ] ) * 0.25 ;
54+ export function maskingChunks ( { y : py , w, h, mask1x1, preErosion } : MaskingTask ) : ( chunk : number ) => void {
55+ const MATCH_GAMMA_OFFSET = 0.019 , LIMIT = 0.2 , rowSum = new Float32Array ( w ) ;
56+ return band => {
57+ for ( let y = band * 64 ; y < Math . min ( h , band * 64 + 64 ) ; y ++ ) {
58+ const r = y * w , r1 = ( y > 0 ? y - 1 : y ) * w , r2 = ( y + 1 < h ? y + 1 : y ) * w ;
59+ for ( let x = 0 ; x < w ; x ++ ) {
60+ const x1 = x > 0 ? x - 1 : x , x2 = x + 1 < w ? x + 1 : x ;
61+ const base = 0.25 * ( py [ r2 + x ] + py [ r1 + x ] + py [ r + x1 ] + py [ r + x2 ] ) ;
62+ const gammac = gammaRatio ( py [ r + x ] + MATCH_GAMMA_OFFSET ) ;
63+ const d = gammac * ( py [ r + x ] - base ) ;
64+ mask1x1 [ r + x ] = 1 / ( Math . log1p ( Math . abs ( d ) ) + 0.01 ) ;
65+ const v = maskingSqrt ( Math . min ( d * d , LIMIT ) ) ;
66+ rowSum [ x ] = y % 4 ? rowSum [ x ] + v : v ;
67+ }
68+ if ( y % 4 === 3 ) {
69+ const out = ( y >> 2 ) * ( w / 4 ) ;
70+ for ( let x = 0 ; x < w / 4 ; x ++ ) preErosion [ out + x ] = ( rowSum [ 4 * x ] + rowSum [ 4 * x + 1 ] + rowSum [ 4 * x + 2 ] + rowSum [ 4 * x + 3 ] ) * 0.25 ;
71+ }
6272 }
63- }
64- const aq = fuzzyErosion ( distance , preErosion , w / 4 , h / 4 ) ;
73+ } ;
74+ }
75+
76+ /** The 5x5 blur of the pixels' masking of a band of 64 rows (the chunk) */
77+ export function maskBlurChunks ( { w, h, mask1x1, blurred } : MaskingTask ) : ( chunk : number ) => void {
78+ const k = [ 0.25647067633737225 , 0.2050056912354399 , 0.1540820486684973 , 0.08149576591362004 , 0.05127501048123085 ] ;
79+ const n = 1 / Math . max ( 1 + 4 * ( k [ 0 ] + k [ 1 ] + k [ 2 ] + k [ 4 ] + 2 * k [ 3 ] ) , 1e-5 ) ;
80+ const weights = [ n , n * k [ 0 ] , n * k [ 2 ] , n * k [ 1 ] , n * k [ 4 ] , n * k [ 3 ] ] ;
81+ return band => symmetric5 ( mask1x1 , w , h , weights , blurred , band * 64 , Math . min ( h , band * 64 + 64 ) ) ;
82+ }
83+
84+ /**
85+ * libjxl's initial quantization field of XYB planes (w x h, whole blocks) for a Butteraugli distance (scale: the
86+ * field's scale); the pixels' masking in threads, if given
87+ */
88+ export async function initialQuantField ( distance : number , planes : Float32Array [ ] , w : number , h : number , scale : number , threads : Thread [ ] | null ) : Promise < QuantField > {
89+ const [ px , py , pb ] = planes , bw = w / 8 , bh = h / 8 , bands = Math . ceil ( h / 64 ) ;
90+ const array = ( n : number ) : Float32Array => new Float32Array ( threads ? new SharedArrayBuffer ( 4 * n ) : new ArrayBuffer ( 4 * n ) ) ;
91+ const task : MaskingTask = { task : 'jxl-masking' , y : py , w, h, mask1x1 : array ( w * h ) , blurred : array ( w * h ) , preErosion : array ( ( w / 4 ) * ( h / 4 ) ) } ;
92+ await runChunks ( threads , task , bands , maskingChunks ( task ) ) ;
93+ const aq = fuzzyErosion ( distance , task . preErosion , w / 4 , h / 4 ) ;
6594 const mask = Float32Array . from ( aq , v => 1 / ( v + 0.001 ) ) ;
6695 perBlockModulations ( distance , px , py , pb , w , bw , bh , scale , aq ) ;
67- return { quant : aq , mask, mask1x1 : blur1x1 ( mask1x1 , w , h ) } ;
96+ await runChunks ( threads , { ...task , task : 'jxl-mask-blur' } , bands , maskBlurChunks ( task ) ) ;
97+ return { quant : aq , mask, mask1x1 : task . blurred } ;
6898}
6999
70100// the weighted 4 smallest values of each 3x3 neighbourhood, summed over 2x2 (the output is half the size)
@@ -148,16 +178,15 @@ function perBlockModulations(distance: number, px: Float32Array, py: Float32Arra
148178 }
149179}
150180
151- // a symmetric 5x5 convolution with mirrored borders (libjxl's Symmetric5): the weights of the center, the edges 1 and 2
152- // away, the diagonals 1 and 2 away and the knight moves
153- function symmetric5 ( m : Float32Array , w : number , h : number , [ c , r , R , d , D , L ] : number [ ] ) : Float32Array {
181+ // rows y0 to y1 of a symmetric 5x5 convolution with mirrored borders (libjxl's Symmetric5): the weights of the center,
182+ // the edges 1 and 2 away, the diagonals 1 and 2 away and the knight moves
183+ function symmetric5 ( m : Float32Array , w : number , h : number , [ c , r , R , d , D , L ] : number [ ] , out : Float32Array , y0 : number , y1 : number ) : void {
154184 const weights = [ D , L , R , L , D , L , d , r , d , L , R , r , c , r , R , L , d , r , d , L , D , L , R , L , D ] ;
155185 const mirror = ( v : number , n : number ) : number => {
156186 while ( v < 0 || v >= n ) v = v < 0 ? - v - 1 : 2 * n - 1 - v ;
157187 return v ;
158188 } ;
159- const out = new Float32Array ( w * h ) ;
160- for ( let y = 0 ; y < h ; y ++ ) {
189+ for ( let y = y0 ; y < y1 ; y ++ ) {
161190 const rows = [ - 2 , - 1 , 0 , 1 , 2 ] . map ( dy => mirror ( y + dy , h ) * w ) ;
162191 for ( let x = 0 ; x < w ; x ++ ) {
163192 let sum = 0 ;
@@ -168,12 +197,4 @@ function symmetric5(m: Float32Array, w: number, h: number, [c, r, R, d, D, L]: n
168197 out [ y * w + x ] = sum ;
169198 }
170199 }
171- return out ;
172- }
173-
174- // the 5x5 blur of the pixels' masking
175- function blur1x1 ( m : Float32Array , w : number , h : number ) : Float32Array {
176- const k = [ 0.25647067633737225 , 0.2050056912354399 , 0.1540820486684973 , 0.08149576591362004 , 0.05127501048123085 ] ;
177- const n = 1 / Math . max ( 1 + 4 * ( k [ 0 ] + k [ 1 ] + k [ 2 ] + k [ 4 ] + 2 * k [ 3 ] ) , 1e-5 ) ;
178- return symmetric5 ( m , w , h , [ n , n * k [ 0 ] , n * k [ 2 ] , n * k [ 1 ] , n * k [ 4 ] , n * k [ 3 ] ] ) ;
179200}
0 commit comments