@@ -112,17 +112,24 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
112112
113113// helper for uint8 histograms
114114function getNormalizedCdfUint8 ( arr ) {
115- return cumsum ( bincount ( arr , 256 , 0 , 255 , true ) ) ;
115+ const bincounts = bincount ( arr , 256 , 0 , 255 , true )
116+ const cdf = cumsum ( bincounts )
117+ return { bincounts, cdf } ;
116118}
117119
118120// compute mapping between source and target
119121export function matchCumulativeCdf ( source , target ) {
120122 const srcCdf = getNormalizedCdfUint8 ( source ) ;
121123 const tgtCdf = getNormalizedCdfUint8 ( target ) ;
122124 const values = Array . from ( { length : 256 } , ( _ , i ) => i ) ;
125+ const mapping = interp ( srcCdf . cdf , tgtCdf . cdf , values )
123126
124127 // For each source intensity, find the CDF value, then find which target intensity has the closest CDF value.
125- return interp ( srcCdf , tgtCdf , values ) ;
128+ return {
129+ mapping,
130+ srcHist : srcCdf . bincounts ,
131+ tgtHist : tgtCdf . bincounts
132+ } ;
126133}
127134
128135// ------------------- high-level API -------------------
@@ -134,20 +141,29 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
134141 *
135142 * @param {Uint8Array } source - Source image array (flat 1D, 1/3/4 channels interleaved).
136143 * @param {Uint8Array } target - Target image array (same shape/channels as source).
137- * @param Number channels - count of channels to do correct de-interleaving of RGB channels values.
144+ * @param { Number } channels - count of channels to do correct de-interleaving of RGB channels values.
138145 * @param {'MAPPING'|'MATCHED_DATA' } returnType - Whether to return mappings or matched data.
146+ * @param {Number } maxMpx - Whether to subsample/apply downscaling to source/target image rasters If pixelCount > maxMpx. 1 = 100% res, 2 = 2x downsampling/subsampling
139147 * @returns {{mappings: Float64Array[], matched?: Uint8Array} }
140148 */
141- export function matchHistograms ( source , target , channels = 3 , returnType = 'MATCHED_DATA' ) {
142- // const channels = source.length % 4 === 0 ? 4 :
143- // source.length % 3 === 0 ? 3 : 1;
149+ export function matchHistograms ( source , target , channels = 3 , returnType = 'MATCHED_DATA' , maxMpx = - 1 ) {
150+ // Downsample source and target arrays in case they exceed user-defined maxMpx param
151+ let srcDownsample = 1 , tgtDownsample = 1 ;
152+ if ( maxMpx > 0 ) {
153+ // srcDownsample = Math.max(1, Math.ceil((source.length / channels / 1e6) / maxMpx))
154+ // tgtDownsample = Math.max(1, Math.ceil((source.length / channels / 1e6) / maxMpx))
155+ srcDownsample = Math . ceil ( ( source . length / channels / 1e6 ) / maxMpx )
156+ tgtDownsample = Math . ceil ( ( source . length / channels / 1e6 ) / maxMpx )
157+ console . log ( 'Downsampling factors' , srcDownsample , tgtDownsample )
158+ }
159+
144160 // RGB / RGBA
145161 const mappings = [ ] ;
146162 for ( let c = 0 ; c < Math . min ( 3 , channels ) ; c ++ ) {
147163 const srcChannel = [ ] ;
148164 const tgtChannel = [ ] ;
149- for ( let i = c ; i < source . length ; i += channels ) srcChannel . push ( source [ i ] ) ;
150- for ( let i = c ; i < target . length ; i += channels ) tgtChannel . push ( target [ i ] ) ;
165+ for ( let i = c ; i < source . length ; i += channels * srcDownsample ) srcChannel . push ( source [ i ] ) ;
166+ for ( let i = c ; i < target . length ; i += channels * tgtDownsample ) tgtChannel . push ( target [ i ] ) ;
151167 mappings [ c ] = matchCumulativeCdf ( srcChannel , tgtChannel ) ;
152168 }
153169 console . log ( 'mappings' , mappings )
@@ -156,26 +172,27 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
156172 const out = new Uint8Array ( source . length ) ;
157173 for ( let i = 0 ; i < source . length ; i += channels ) {
158174 for ( let c = 0 ; c < 3 ; c ++ ) {
159- out [ i + c ] = mappings [ 0 + c ] [ source [ i + c ] ] ;
175+ out [ i + c ] = mappings [ 0 + c ] . mapping [ source [ i + c ] ] ;
160176 }
161177 if ( channels === 4 ) out [ i + 3 ] = source [ i + 3 ] ; // keep alpha
162178 }
163179 return { mappings, matched : out } ;
164180}
165181
166-
167-
168182// --- image handling ---
169183const img_objs = { } ;
170184// slot is source or target, indexing img_objs, mapped to input and img slot+"-img-div
171- function loadImage ( fileOrUrl , slot ) {
185+ function loadImage ( fileOrUrl , slot , maxMpx = - 1 ) { // TODO 2 is for testing default downscale = 1
172186 const img = new Image ( ) ;
173187 img . onload = function ( ) {
174188 const canvas = document . createElement ( "canvas" ) ;
189+ // naturalWidth / downscale to make canvas smaller and downsample naturally, downscale = 2 is nicer, but then would need to use source input and re-apply mapping, so passed source resolution instead
175190 canvas . width = img . naturalWidth ; // naturalWidth instead of width
176191 canvas . height = img . naturalHeight ; // naturalHeight instead of height
177192 const ctx = canvas . getContext ( "2d" ) ;
178- ctx . drawImage ( img , 0 , 0 ) ;
193+ ctx . drawImage ( img , 0 , 0 ) ; // rescale image
194+ // ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // rescale image
195+
179196 img_objs [ slot ] = { ctx, img, canvas} ;
180197 document . getElementById ( slot + "-img-div" ) . replaceChildren ( img ) ;
181198
@@ -189,7 +206,7 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
189206 // Match Histograms Computation
190207 // Check getImageData always return 4 channels RGBA
191208 const start = Date . now ( ) ;
192- const matched = matchHistograms ( sourceArr , targetArr , 4 , 'MATCHED_DATA' ) ;
209+ const matched = matchHistograms ( sourceArr , targetArr , 4 , 'MATCHED_DATA' , maxMpx ) ;
193210 const elapsed_ms = Date . now ( ) - start
194211
195212 // Write matched image to outCanvas
@@ -204,7 +221,8 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
204221 // Display to user and log stats
205222 const container = document . getElementById ( 'matched-img-div' ) ;
206223 console . log ( 'matched' , matched , 'mappings slice' )
207- console . log ( matched . mappings [ 0 ] . slice ( 0 , 30 ) )
224+ console . log ( matched . mappings [ 0 ] . mapping . slice ( 0 , 30 ) )
225+
208226 const stats_div = document . createElement ( "pre" ) ;
209227 const statsObj = {
210228 source : {
@@ -228,12 +246,21 @@ <h3>Pure JS, client-side, Histogram Matching</h3>
228246
229247 // container.replaceChildren(outCanvas);
230248 outCanvas . toBlob ( function ( blob ) {
249+ // Add image resulting from histogram matching to output div
231250 const url = URL . createObjectURL ( blob ) ;
232251 const img = document . createElement ( "img" ) ;
233252 img . src = url ;
234253 container . replaceChildren ( img ) ;
235254
236- container . appendChild ( stats_div )
255+ // Also show some stats
256+ const stats_details = document . createElement ( 'details' ) ;
257+ stats_details . style = "text-align: left;"
258+ const summary = document . createElement ( "summary" ) ;
259+ summary . textContent = "Show stats" ;
260+ stats_details . appendChild ( summary ) ;
261+ stats_details . appendChild ( stats_div ) ;
262+ stats_details . open = true ;
263+ container . appendChild ( stats_details )
237264 } , "image/png" ) ;
238265
239266 }
0 commit comments