2626 </ head >
2727 < body >
2828 < div style ="padding: 30px; ">
29- < h3 > Pure JS Histogram Matching - client-side</ h3 >
29+ < h3 > Pure JS, client-side, Histogram Matching </ h3 >
3030 < p >
3131Inspired by < a href ="https://github.com/mapbox/rio-hist/issues/3 "> mapbox/rio-hist</ a > (python), this demo showcases pure-JS Histogram Matching. < br /> Implementation based on scikit-image (< a href ="https://scikit-image.org/docs/stable/auto_examples/color_exposure/plot_histogram_matching.html "> doc</ a > and < a href ="https://github.com/scikit-image/scikit-image/blob/v0.25.2/skimage/exposure/histogram_matching.py#L33-L93 "> code</ a > ) which simply relies on numpy < code > bincount, cumsum, interp</ code > . < br /> scikit-image and numpy use BSD3 License.
3232 </ p >
@@ -48,10 +48,6 @@ <h3>Pure JS Histogram Matching - client-side</h3>
4848// // See [doc](https://scikit-image.org/docs/stable/auto_examples/color_exposure/plot_histogram_matching.html) and [histogram_matching.py](https://github.com/scikit-image/scikit-image/blob/v0.25.2/skimage/exposure/histogram_matching.py#L33-L93
4949// Relies on numpy bincount, cumsum, interp, reimplemented below with some assumptions
5050
51- const img_objs = [ ] ;
52- let source_filepath = './source1.jpg' ;
53- let target_filepath = './reference1.jpg' ;
54-
5551
5652// Compute histogram bins for an array, with optional min/max for faster compute, and optional normalization parameter
5753export function bincount ( arr , binCount , min , max , normalize = false ) {
@@ -169,122 +165,100 @@ <h3>Pure JS Histogram Matching - client-side</h3>
169165
170166
171167
172- function newImageWithOnload ( filepath ) {
173- const img = new Image ( ) ;
168+ // --- image handling ---
169+ const img_objs = { } ;
170+ // slot is source or target, indexing img_objs, mapped to input and img slot+"-img-div
171+ function loadImage ( fileOrUrl , slot ) {
172+ const img = new Image ( ) ;
174173 img . onload = function ( ) {
175174 const canvas = document . createElement ( "canvas" ) ;
176- canvas . width = img . width ;
177- canvas . height = img . height ;
175+ canvas . width = img . naturalWidth ; // naturalWidth instead of width
176+ canvas . height = img . naturalHeight ; // naturalHeight instead of height
178177 const ctx = canvas . getContext ( "2d" ) ;
179178 ctx . drawImage ( img , 0 , 0 ) ;
180- img_objs . push ( { filepath, ctx, img, canvas } ) ;
181-
182- if ( img_objs . length === 2 ) {
183- const source = img_objs . find ( i => i . filepath === source_filepath ) ;
184- const target = img_objs . find ( i => i . filepath === target_filepath ) ;
185- console . log ( 'source' , source , target )
186-
187- const source_arr = source . ctx . getImageData ( 0 , 0 , source . canvas . width , source . canvas . height ) ;
188- const target_arr = target . ctx . getImageData ( 0 , 0 , target . canvas . width , target . canvas . height ) ;
189- console . log ( 'source_arr' , source_arr , target_arr )
190-
179+ img_objs [ slot ] = { ctx, img, canvas} ;
180+ document . getElementById ( slot + "-img-div" ) . replaceChildren ( img ) ;
181+
182+ if ( img_objs . source && img_objs . target ) {
183+ // Get source and target arrays from dom imgs
184+ const source_canvas = img_objs . source . canvas
185+ const target_canvas = img_objs . target . canvas
186+ const sourceArr = img_objs . source . ctx . getImageData ( 0 , 0 , source_canvas . width , source_canvas . height ) . data ;
187+ const targetArr = img_objs . target . ctx . getImageData ( 0 , 0 , target_canvas . width , target_canvas . height ) . data ;
188+
189+ // Match Histograms Computation
190+ // Check getImageData always return 4 channels RGBA
191191 const start = Date . now ( ) ;
192- const returnType = 'MATCHED_DATA'
193- const channels = 3 // should be computed from image, or getImageData is always same type?
194- const matched = matchHistograms ( source_arr . data , target_arr . data , channels , returnType ) ;
195- console . log ( 'matched' , matched , 'mappings slice' )
196- console . log ( matched . mappings [ 0 ] . slice ( 0 , 30 ) )
192+ const matched = matchHistograms ( sourceArr , targetArr , 4 , 'MATCHED_DATA' ) ;
197193 const elapsed_ms = Date . now ( ) - start
198194
199- // Render matched image
200- const canvas_out = document . createElement ( "canvas" ) ;
201- canvas_out . width = source . img . width ;
202- canvas_out . height = source . img . height ;
203- const ctx_out = canvas_out . getContext ( "2d" ) ;
204- const imageData = ctx_out . createImageData ( canvas_out . width , canvas_out . height ) ;
205- imageData . data . set ( matched . matched ) ;
206- ctx_out . putImageData ( imageData , 0 , 0 ) ;
207-
208- container = document . getElementById ( 'matched-img-div' ) ;
209-
210- const stats_div = document . createElement ( "div" ) ;
195+ // Write matched image to outCanvas
196+ const outCanvas = document . createElement ( "canvas" ) ;
197+ outCanvas . width = source_canvas . width ;
198+ outCanvas . height = source_canvas . height ;
199+ const ctxOut = outCanvas . getContext ( "2d" ) ;
200+ const outData = ctxOut . createImageData ( outCanvas . width , outCanvas . height ) ;
201+ outData . data . set ( matched . matched ) ;
202+ ctxOut . putImageData ( outData , 0 , 0 ) ;
203+
204+ // Display to user and log stats
205+ const container = document . getElementById ( 'matched-img-div' ) ;
206+ console . log ( 'matched' , matched , 'mappings slice' )
207+ console . log ( matched . mappings [ 0 ] . slice ( 0 , 30 ) )
208+ const stats_div = document . createElement ( "pre" ) ;
211209 const statsObj = {
212210 source : {
213- width : source . img . width ,
214- height : source . img . height ,
215- Mpix : Math . floor ( source . img . width * source . img . height / 1000000 ) ,
211+ width : img_objs . source . canvas . width ,
212+ height : img_objs . source . canvas . height ,
213+ Mpix : 0.01 * Math . floor ( img_objs . source . canvas . width * img_objs . source . canvas . height / 1000000 * 100 ) ,
216214 } ,
217215 target : {
218- width : target . img . width ,
219- height : target . img . height ,
220- Mpix : Math . floor ( target . img . width * target . img . height / 1000000 ) ,
216+ width : img_objs . target . canvas . width ,
217+ height : img_objs . target . canvas . height ,
218+ Mpix : 0.01 * Math . floor ( img_objs . target . canvas . width * img_objs . target . canvas . height / 1000000 * 100 ) ,
221219 } ,
222220 timings : {
223221 ms : elapsed_ms
224222 }
225223 }
226- stats_div . innerHTML = JSON . stringify ( statsObj , null , " " ) ;
224+ const statsStr = JSON . stringify ( statsObj , null , " " )
225+ console . log ( statsStr )
226+ stats_div . textContent = `Histogram matching took ${ statsObj . timings . ms } ms\n` + statsStr ;
227+ stats_div . style = 'text-align: left; padding: 20px;'
227228
228- // container.replaceChildren(canvas_out );
229- canvas_out . toBlob ( function ( blob ) {
229+ // container.replaceChildren(outCanvas );
230+ outCanvas . toBlob ( function ( blob ) {
230231 const url = URL . createObjectURL ( blob ) ;
231232 const img = document . createElement ( "img" ) ;
232233 img . src = url ;
233234 container . replaceChildren ( img ) ;
234235
235- container . addChildren ( stats_div )
236+ container . appendChild ( stats_div )
236237 } , "image/png" ) ;
238+
237239 }
238- }
239- img . src = filepath ;
240- let container
241- if ( filepath === source_filepath )
242- container = document . getElementById ( 'source-img-div' ) ;
243- else
244- container = document . getElementById ( 'target-img-div' ) ;
245- container . replaceChildren ( img ) ;
240+ } ;
241+ img . src = fileOrUrl ;
246242}
247243
248- let img_source = newImageWithOnload ( source_filepath )
249- let img_target = newImageWithOnload ( target_filepath )
250- console . log ( 'done loading image, will start processing callbacks to compute Histogram Matching' )
251-
252-
253- let obj
254- function onChangeInput ( event ) {
255- console . log ( event )
256- const target = event . target
257- const srcId = target . id
258- console . log ( srcId )
259- if ( srcId == 'source-input' ) {
260- obj = img_objs . find ( i => i . filepath === source_filepath ) ;
261- source_filepath = target . value
262- console . log ( 'source-input' )
263- } else if ( srcId == 'target-input' ) {
264- obj = img_objs . find ( i => i . filepath === target_filepath ) ;
265- target_filepath = target . value
266- console . log ( 'target-input' )
267- }
268- console . log ( obj , source_filepath , target_filepath )
269- var file = target . files [ 0 ] ;
270- console . log ( file )
271- var reader = new FileReader ( ) ;
272- reader . onloadend = function ( ) {
273- console . log ( 'loadend' )
274- obj . img . src = reader . result ;
275- console . log ( 'end of loadend' )
276- }
277- if ( file ) {
278- console . log ( 'readAsDataURL' , file ) ;
279- reader . readAsDataURL ( file ) ;
280- console . log ( 'readAsDataURL done' ) ;
281- } else {
282- preview . src = "" ;
283- }
284- } ;
285- window . onChangeInput = onChangeInput
286- document . getElementById ( 'source-input' ) . onchange = onChangeInput
287- document . getElementById ( 'target-input' ) . onchange = onChangeInput
244+ function handleInputChange ( e ) {
245+ const file = e . target . files [ 0 ] ;
246+ if ( ! file ) return ;
247+ const reader = new FileReader ( ) ;
248+ // reader.onload = ev => loadImage(ev.target.result, e.target.id.split("-")[0]);
249+ reader . onload = ev => loadImage ( ev . target . result , e . target . id . includes ( 'source' ) ? 'source' : 'target' ) ;
250+ reader . readAsDataURL ( file ) ;
251+ }
252+
253+ document . getElementById ( "source-input" ) . addEventListener ( "change" , handleInputChange ) ;
254+ document . getElementById ( "target-input" ) . addEventListener ( "change" , handleInputChange ) ;
255+
256+ // preload defaults
257+ let source_filepath = './source1.jpg' ;
258+ let target_filepath = './reference1.jpg' ;
259+
260+ loadImage ( source_filepath , "source" ) ;
261+ loadImage ( target_filepath , "target" ) ;
288262
289263</ script >
290264
0 commit comments