Parcourir la source

Update of quest expe with load screen

Jérôme BUISINE il y a 4 ans
Parent
commit
6a0a019eb3

+ 1 - 2
.gitignore

@@ -115,6 +115,5 @@ GitHub.sublime-settings
 !.vscode/extensions.json 
 .history
 
-expe_results
-expe_models
+# exclude media folder
 media

+ 9 - 3
expe/config.py

@@ -13,9 +13,15 @@ output_tmp_folder           = "tmp"
 expe_name_list              = ["quest_one_image"]
 
 # configure experiences labels
-expe_questions              = {
+expes_configuration         = {
     'quest_one_image':{
-        'question': "Do you see one image or a composition of more than one?",
-        'indication': "press left if you see one image, right if not"
+        'text':{
+            'question': "Do you see one image or a composition of more than one?",
+            'indication': "press left if you see one image, right if not",
+            'end_text': "Experience is finished. Thanks for your participation",
+        },
+        'params':{
+            'iterations': 5
+        }
     }
 }

expe/quest/__init__.py → expe/expes/__init__.py


+ 25 - 0
expe/quest/quest_plus.py

@@ -7,6 +7,11 @@ import pandas as pd
 # TODO : Currently `weibull` is not used as default function
 # from psychometric import weibull
 
+
+# PARAMETERS of the psychometric function
+chance_level = 0 #e.g. chance_level should be 0.5 for 2AFC (Two-alternative forced choice) procedure
+threshold_prob = 1.-(1.-chance_level)/2.0 #the probability level at the threshold
+
 def reformat_params(params):
     '''Unroll multiple lists into array of their products.'''
     if isinstance(params, list):
@@ -18,6 +23,26 @@ def reformat_params(params):
     return params
 
 
+# quest_plus.py comes also with psychometric.py wich includes the definition of the weibull and weibull_db function
+# here I define the logistic function using the same template that works with the quest_plus implementation
+def logistic(x, params, corr_at_thresh=threshold_prob, chance_level=chance_level):
+        # unpack params
+        if len(params) == 3:
+            THRESHOLD, SLOPE, lapse = params
+        else:
+            THRESHOLD, SLOPE = params
+            lapse = 0.
+
+        b = 4 * SLOPE
+        a = -b * THRESHOLD
+
+        return chance_level + (1 - lapse - chance_level) / (1 + np.exp(-(a + b*x)))
+    
+
+# that's a wrapper function to specify wich  psychometric function one we want to use for the QUEST procedure
+def psychometric_fun( x , params ):
+    return logistic(x , params ,  corr_at_thresh=threshold_prob, chance_level=chance_level)
+
 # TODO:
 # - [ ] highlight lowest point in entropy in plot
 class QuestPlus(object):

+ 106 - 0
expe/expes/run.py

@@ -0,0 +1,106 @@
+# main imports
+import os
+import time
+import numpy as np
+import pickle
+
+# module imports
+from ..utils import api
+
+from ..utils.processing import crop_images
+from .. import config as cfg
+
+# expe imports
+from .quest_plus import QuestPlus
+from .quest_plus import psychometric_fun
+
+
+def run_quest_one_image(request, model_filepath, output_file):
+
+    # get parameters
+    qualities = request.session.get('qualities')
+    scene_name = request.session.get('scene')
+    expe_name = request.session.get('expe')
+
+    # by default
+    iterations = 0
+
+    # first time only init `quest`
+    # if experience is started we can save data
+    if request.session.get('expe_started'):
+        answer = int(request.GET.get('answer'))
+        iterations = int(request.GET.get('iteration'))
+
+        answer_time = time.time() - request.session['answer_time']
+        print("Answer time is ", answer_time)
+        previous_percentage = request.session.get('expe_percentage')
+        previous_orientation = request.session.get('expe_orientation')
+        previous_position = request.session.get('expe_position')
+
+    # default params
+    thresholds = np.arange(50, 10000, 50)
+    stim_space=np.asarray(qualities)
+    slopes = np.arange(0.0001, 0.001, 0.00003)
+
+    # check if necessary to construct `quest` object
+    if not os.path.exists(model_filepath):
+        qp = QuestPlus(stim_space, [thresholds, slopes], function=psychometric_fun)
+    else:
+        print('Load `qp` model')
+        filehandler = open(model_filepath, 'rb') 
+        qp = pickle.load(filehandler)
+    
+    # construct image and update `quest` only if necessary
+    if iterations < cfg.expes_configuration[expe_name]['params']['iterations']:
+        # process `quest`
+        next_stim = qp.next_contrast()
+        print("Next quality ", next_stim)
+
+        # construct new image
+        noisy_image = api.get_image(scene_name, next_stim)
+
+        # reconstruct reference image from list stored into session
+        ref_image = api.get_image(scene_name, 'max')
+        img_merge, percentage, orientation, position = crop_images(noisy_image, ref_image)
+    else:
+        request.session['expe_finished'] = True
+        return None
+    
+    # if experience is already begin
+    if request.session.get('expe_started'):
+
+        # TODO : check `i` variable 
+        # update of `quest`
+        # qp.update(qualities[i], answer)
+        qp.update(qualities[iterations], answer) 
+        entropy = qp.get_entropy()
+
+        line = str(next_stim) 
+        line += ";" + scene_name 
+        line += ";" + str(previous_percentage)
+        line += ";" + str(previous_orientation) 
+        line += ";" + str(previous_position) 
+        line += ";" + str(answer) 
+        line += ";" + str(answer_time) 
+        line += ";" + str(entropy) 
+        line += '\n'
+
+        print(line)
+        # TODO : add answer time from javascript
+        output_file.write(line)
+        output_file.flush()
+
+    # save `quest` model
+    file_pi = open(model_filepath, 'wb') 
+    pickle.dump(qp, file_pi)
+
+    # set current step data
+    request.session['expe_percentage'] = percentage
+    request.session['expe_orientation'] = orientation
+    request.session['expe_position'] = position
+    request.session['answer_time'] = time.time()
+    
+    # expe is now started
+    request.session['expe_started'] = True
+
+    return img_merge

+ 0 - 39
expe/quest/expe.py

@@ -1,39 +0,0 @@
-# main imports 
-import numpy as np
-import os
-import time
-from datetime import datetime
-import re
-
-# image processing imports
-from .processing import crop_images
-
-# expe imports
-from .quest_plus import QuestPlus
-
-# load `config` variables
-from .. import config as cfg
-
-# PARAMETERS of the psychometric function
-chance_level = 0 #e.g. chance_level should be 0.5 for 2AFC (Two-alternative forced choice) procedure
-threshold_prob = 1.-(1.-chance_level)/2.0 #the probability level at the threshold
-
-# quest_plus.py comes also with psychometric.py wich includes the definition of the weibull and weibull_db function
-# here I define the logistic function using the same template that works with the quest_plus implementation
-def logistic(x, params, corr_at_thresh=threshold_prob, chance_level=chance_level):
-        # unpack params
-        if len(params) == 3:
-            THRESHOLD, SLOPE, lapse = params
-        else:
-            THRESHOLD, SLOPE = params
-            lapse = 0.
-
-        b = 4 * SLOPE
-        a = -b * THRESHOLD
-
-        return chance_level + (1 - lapse - chance_level) / (1 + np.exp(-(a + b*x)))
-    
-
-# that's a wrapper function to specify wich  psychometric function one we want to use for the QUEST procedure
-def psychometric_fun( x , params ):
-    return logistic(x , params ,  corr_at_thresh=threshold_prob, chance_level=chance_level)

+ 15 - 7
expe/templates/expe/expe.html

@@ -8,22 +8,30 @@
 
 {% block content %}
     
-    {% if request.session.begin %}
-        <h3>{{ question }}</h3>
-        <p>{{ indication }}</p>
+    {% if request.session.expe_finished %}
+        <h3>{{end_text}}</h3>
 
         <br />
-        <p>Press enter to begin experience</p>
+        <p>You will be redirected in <span id="refreshTime">5</span> sec to home page</p>
     {% endif %}
-    
+
     <!-- TODO : Load    img from bitmap with javascript `loadImg.js` -->
-    {% if not request.session.begin %}
+    {% if not request.session.expe_finished %}
         <img id="expeImg" src="/{{img_merged_path}}" data-img="{{request.session.img_merged}}"/>
     {% endif %}
 
     {% block javascripts %}
         <script type="text/javascript"> 
-            var BEGIN_EXPE = "{{request.session.begin}}"
+            // Utils informations
+            var host     = window.location.host
+            var pathname = window.location.pathname
+
+            var baseUrl      = location.protocol + "//" + host
+            var baseExpeUrl  = location.protocol + "//" + host + pathname
+
+            // get access to django variables
+            var BEGIN_EXPE = "{{request.session.expe_started}}"
+            var END_EXPE   = "{{request.session.expe_finished}}"
         </script>
         <script src="{% static "js/loadImg.js" %}"></script>
         <script src="{% static "js/keyEvents.js" %}"></script>

+ 20 - 0
expe/templates/expe/expe_indications.html

@@ -0,0 +1,20 @@
+{% extends 'base.html' %}
+
+{% load staticfiles %}
+
+{% block title %}
+    Expe {{ expe_name }}
+{% endblock %}
+
+{% block content %}
+    
+    <h3>{{question}}</h3>
+    <p>{{indication}}</p>
+
+    <br />
+    <p>Press enter to begin experience</p>
+    
+    {% block javascripts %}
+        <script src="{% static "js/indications.js" %}"></script>
+    {% endblock %}
+{% endblock %}

+ 1 - 3
expe/templates/expe/expe_list.html

@@ -11,7 +11,7 @@
 
     <div class="row">
         <div class="col-md-4 col-md-offset-4">
-            <form action="/expe" id="expeChoice">
+            <form action="/indications" id="expeChoice">
 
                 <div class="form-group">
                     <label for="scene">Select scene:</label>
@@ -31,8 +31,6 @@
                     </select>
                 </div>
 
-                <!--<input type="hidden" value="0" name="iteration"/>-->
-
                 <button type="submit" class="btn btn-primary">Submit</button>
             </form>
         </div>

+ 1 - 0
expe/urls.py

@@ -8,6 +8,7 @@ from . import views
 urlpatterns = [
     path('', views.expe_list, name='expe_list'),
     path('expe', views.expe, name='expe'),
+    path('indications', views.indications, name='indications'),
 ]
 
 if settings.DEBUG is True:

+ 1 - 0
expe/utils/functions.py

@@ -1,3 +1,4 @@
+# main imports
 import random
 
 def uniqueID():

+ 4 - 2
expe/quest/processing.py

@@ -1,9 +1,11 @@
-
-from PIL import Image
+# main imports
 import os
 import numpy as np
 import random
 
+# processing imports
+from PIL import Image
+
 def crop_images(img1, img2, per=None, orien=None, swap_img=None):
     '''
     crop and gather reference image and a noisy one randomly

+ 39 - 115
expe/views.py

@@ -14,18 +14,20 @@ import pickle
 import time
 
 # expe imports
-from .quest.quest_plus import QuestPlus
-from .quest.expe import psychometric_fun
+from .expes.quest_plus import QuestPlus
+from .expes.quest_plus import psychometric_fun
+
+from .expes.run import run_quest_one_image
 
 # image processing imports
 import io
 from PIL import Image
 
-# api imports
+# module imports
 from .utils import api
 from .utils import functions
 
-from .quest.processing import crop_images
+from .utils.processing import crop_images
 from . import config as cfg
 
 
@@ -39,6 +41,20 @@ def expe_list(request):
 
     return render(request, 'expe/expe_list.html', {'scenes': scenes, 'expes': expes})
 
+def indications(request):
+
+    # get param 
+    expe_name = request.GET.get('expe')
+
+    # expe parameters
+    data = {
+        'expe_name': expe_name,
+        'question': cfg.expes_configuration[expe_name]['text']['question'],
+        'indication': cfg.expes_configuration[expe_name]['text']['indication']
+    }
+
+    return render(request, 'expe/expe_indications.html', data)
+
 
 # Create your views here.
 def expe(request):
@@ -46,6 +62,9 @@ def expe(request):
     # get param 
     expe_name = request.GET.get('expe')
     scene_name = request.GET.get('scene')
+    
+    # default filepath name
+    filepath_img = ''
 
     # unique user ID during session (user can launch multiple exeperiences)
     if 'id' not in request.session:
@@ -53,33 +72,15 @@ def expe(request):
 
     # first time expe is launched add expe information
     if 'expe' not in request.session or expe_name != request.session.get('expe'):
-        request.session['expe'] = expe_name
-        request.session['scene'] = scene_name
-
-        request.session['begin'] = True
-        request.session['qualities'] = api.get_scene_qualities(scene_name)
-        # update unique timestamp each time new experience is launched
-        request.session['timestamp'] = datetime.strftime(datetime.utcnow(), "%Y-%m-%d_%Hh%Mm%Ss")
-
-    else:
-        request.session['begin'] = False
+        refresh_data(request, expe_name, scene_name)
 
     # refresh if scene_name changed
     if 'scene' not in request.session or scene_name != request.session.get('scene'):
-        request.session['expe'] = expe_name
-        request.session['scene'] = scene_name
-
-        request.session['begin'] = True
-        request.session['qualities'] = api.get_scene_qualities(scene_name)
-        # update unique timestamp each time new experience is launched
-        request.session['timestamp'] = datetime.strftime(datetime.utcnow(), "%Y-%m-%d_%Hh%Mm%Ss")
-
-    else:
-        request.session['begin'] = False
+        refresh_data(request, expe_name, scene_name)
 
     # create output folder for expe_result
     current_day = datetime.strftime(datetime.utcnow(), "%Y-%m-%d")
-    results_folder = os.path.join(cfg.output_expe_folder.format(current_day))
+    results_folder = os.path.join(settings.MEDIA_ROOT, cfg.output_expe_folder.format(current_day))
 
     if not os.path.exists(results_folder):
         os.makedirs(results_folder)
@@ -94,7 +95,7 @@ def expe(request):
         output_file = open(results_filepath, 'a')
 
     # create `quest` object if not exists    
-    models_folder = os.path.join(cfg.model_expe_folder.format(current_day))
+    models_folder = os.path.join(settings.MEDIA_ROOT, cfg.model_expe_folder.format(current_day))
 
     if not os.path.exists(models_folder):
         os.makedirs(models_folder)
@@ -105,7 +106,7 @@ def expe(request):
     # run `quest` expe
     img_merge = run_quest_one_image(request, model_filepath, output_file)
 
-    if img_merge is not None:
+    if not request.session.get('expe_finished'):
         # create output folder for tmp files if necessary
         tmp_folder = os.path.join(settings.MEDIA_ROOT, cfg.output_tmp_folder)
 
@@ -116,13 +117,18 @@ def expe(request):
         # TODO : add crontab task to erase generated img
         filepath_img = os.path.join(tmp_folder, request.session.get('id') + '_' + scene_name + '' + expe_name + '.png')
         img_merge.save(filepath_img)
+    else:
+        # reinit session as default value
+        del request.session['expe']
+        del request.session['scene']
+        del request.session['qualities']
+        del request.session['timestamp']
 
     # expe parameters
     data = {
         'expe_name': expe_name,
         'img_merged_path': filepath_img,
-        'question': cfg.expe_questions[expe_name]['question'],
-        'indication': cfg.expe_questions[expe_name]['indication']
+        'end_text': cfg.expes_configuration[expe_name]['text']['end_text']
     }
 
     return render(request, 'expe/expe.html', data)
@@ -133,7 +139,9 @@ def refresh_data(request, expe_name, scene_name):
     request.session['expe'] = expe_name
     request.session['scene'] = scene_name
 
-    request.session['begin'] = True
+    request.session['expe_started'] = False
+    request.session['expe_finished'] = False
+
     request.session['qualities'] = api.get_scene_qualities(scene_name)
     # update unique timestamp each time new experience is launched
     request.session['timestamp'] = datetime.strftime(datetime.utcnow(), "%Y-%m-%d_%Hh%Mm%Ss")
@@ -142,88 +150,4 @@ def refresh_data(request, expe_name, scene_name):
     # get reference image
     #ref_image = api.get_image(scene_name, 'max')
     # save ref image as list (can't save python object)
-    #request.session['ref_img'] = np.array(ref_image).tolist()
-
-
-def run_quest_one_image(request, model_filepath, output_file):
-
-    # get parameters
-    qualities = request.session.get('qualities')
-    scene_name = request.session.get('scene')
-    # by default
-    iteration = 0
-
-    # first time only init `quest`
-    # if experience is started we can save data
-    if request.session.get('begin'):
-        answer = int(request.GET.get('answer'))
-        iteration = int(request.GET.get('iteration'))
-
-        answer_time = time.time() - request.session['answer_time']
-        previous_percentage = request.session.get('expe_percentage')
-        previous_orientation = request.session.get('expe_orientation')
-        previous_position = request.session.get('expe_percentage')
-        previous_entropy = request.session.get('expe_entropy')
-
-    # default params
-    max_iteration = 10
-    thresholds = np.arange(50, 10000, 50)
-    stim_space=np.asarray(qualities)
-    slopes = np.arange(0.0001, 0.001, 0.00003)
-
-    # check if necessary to construct `quest` object
-    if not os.path.exists(model_filepath):
-        qp = QuestPlus(stim_space, [thresholds, slopes], function=psychometric_fun)
-    else:
-        filehandler = open(model_filepath, 'rb') 
-        qp = pickle.load(filehandler)
-    
-    # construct image and update `quest` only if necessary
-    if iteration < max_iteration:
-        # process `quest`
-        next_stim = qp.next_contrast()
-        print(next_stim)
-
-        # construct new image
-        noisy_image = api.get_image(scene_name, next_stim)
-
-        # reconstruct reference image from list stored into session
-        ref_image = api.get_image(scene_name, 'max')
-        img_merge, percentage, orientation, position = crop_images(noisy_image, ref_image)
-    else:
-        return None
-    
-    # if experience is started we can save data
-    if request.session.get('begin'):
-
-        # TODO : check `i` variable 
-        # update of `quest`
-        # qp.update(qualities[i], answer)
-        qp.update(str(qualities[iteration]), answer) 
-        entropy = qp.get_entropy()
-
-        line = str(next_stim) 
-        line += ";" + scene_name 
-        line += ";" + str(previous_percentage)
-        line += ";" + str(previous_orientation) 
-        line += ";" + str(previous_orientation) 
-        line += ";" + str(answer) 
-        line += ";" + str(answer_time) 
-        line += ";" + str(entropy) 
-        line += '\n'
-        # TODO : add answer time from javascript
-        output_file.write(line)
-        output_file.flush()
-
-
-    # save `quest` model
-    file_pi = open(model_filepath, 'wb') 
-    pickle.dump(qp, file_pi)
-
-    # set current step data
-    request.session['expe_percentage'] = percentage
-    request.session['expe_orientation'] = orientation
-    request.session['expe_position'] = position
-    request.session['answer_time'] = time.time()
-
-    return img_merge
+    #request.session['ref_img'] = np.array(ref_image).tolist()

+ 32 - 0
static/js/indications.js

@@ -0,0 +1,32 @@
+// implement `key` events
+document.onkeydown = checkKey;
+
+var host     = window.location.host
+var expe_url = '/expe'     
+var baseUrl  = location.protocol + "//" + host
+
+// Utils informations
+var KEYCODE_Q           = '81'
+var KEYCODE_ENTER       = '13'
+
+urlParams = new URLSearchParams(window.location.search);
+
+var scene = urlParams.get('scene')
+var expe  = urlParams.get('expe')
+
+function checkKey(e) {
+
+   e = e || window.event;
+
+   if (e.keyCode == KEYCODE_Q) {
+        // `q` for quit expe
+        console.log('`q` key is pressed')
+        window.location = ''
+   }
+   else if (e.keyCode == KEYCODE_ENTER) {
+
+        // right arrow
+        var params = "?scene=" + scene + "&expe=" + expe + "&iteration=0"
+        window.location = baseUrl + expe_url + params
+   }
+}

+ 6 - 14
static/js/keyEvents.js

@@ -4,11 +4,6 @@ document.onkeydown = checkKey;
 urlParams = new URLSearchParams(window.location.search);
 
 // Utils informations
-var host     = window.location.host
-var pathname = window.location.pathname
-var baseUrl  = location.protocol + "//" + host + pathname
-
-
 var KEYCODE_Q           = '81'
 var KEYCODE_ENTER       = '13'
 var KEYCODE_LEFT_ARROW  = '37'
@@ -21,9 +16,6 @@ if (urlParams.has('scene')){
    var expe  = urlParams.get('expe')
 }
 
-console.log(scene)
-console.log(expe)
-
 function checkKey(e) {
 
    e = e || window.event;
@@ -31,13 +23,15 @@ function checkKey(e) {
    if (e.keyCode == '81') {
       // `q` for quit expe
       console.log('`q` key is pressed')
-      window.location = ''
+      window.location = baseUrl
    }
    else if (e.keyCode == '13') {
 
+      console.log("Here")
       // check if experience is begin
       if (!BEGIN_EXPE){
 
+         console.log("And Here")
          // right arrow
          window.location = window.location.href + "&begin=true"
       } 
@@ -69,12 +63,10 @@ function checkKey(e) {
             // increment step
             iteration++;
          }
-
-         var params = "?scene=" + scene + "&expe=" + expe + "&begin=true&iteration=" + iteration + "&answer=" + answer
          
-         console.log(baseUrl)
-
-         window.location = baseUrl + params
+         // construct url with params for experience
+         var params = "?scene=" + scene + "&expe=" + expe + "&iteration=" + iteration + "&answer=" + answer
+         window.location = baseExpeUrl + params
       }
    }
 }

+ 24 - 6
static/js/loadImg.js

@@ -1,9 +1,27 @@
-// https://stackoverflow.com/questions/20756042/javascript-how-to-display-image-from-byte-array-using-javascript-or-servlet
-
 window.onload = function () {
-    console.log('Load img here...');
 
-    setTimeout(function(){ 
-        document.getElementById("expeImg").style.display = "inline";
-    }, 500);
+    console.log("End expe " + END_EXPE)
+
+    // only if not end of expe
+    if (END_EXPE == "False"){
+        setTimeout(function(){ 
+            document.getElementById("expeImg").style.display = "inline";
+        }, 500);
+    }
+
+    // redirect if end of expe after 5 sec
+    if (END_EXPE == "True"){
+        
+        for(var i=0; i<6; i++){
+            ((x)=>{
+                setTimeout(()=> 
+                    document.getElementById("refreshTime").textContent = 5 - x
+                ,1000 * i)
+            })(i);
+        } 
+
+        setTimeout(function(){ 
+            window.location = baseUrl
+        }, 5000);
+    }
 }

+ 0 - 214
static/js/pnglib.js

@@ -1,214 +0,0 @@
-/**
-* A handy class to calculate color values.
-*
-* @version 1.0
-* @author Robert Eisele <robert@xarg.org>
-* @copyright Copyright (c) 2010, Robert Eisele
-* @link http://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/
-* @license http://www.opensource.org/licenses/bsd-license.php BSD License
-*
-*/
-
-(function() {
-
-	// helper functions for that ctx
-	function write(buffer, offs) {
-		for (var i = 2; i < arguments.length; i++) {
-			for (var j = 0; j < arguments[i].length; j++) {
-				buffer[offs++] = arguments[i].charAt(j);
-			}
-		}
-	}
-
-	function byte2(w) {
-		return String.fromCharCode((w >> 8) & 255, w & 255);
-	}
-
-	function byte4(w) {
-		return String.fromCharCode((w >> 24) & 255, (w >> 16) & 255, (w >> 8) & 255, w & 255);
-	}
-
-	function byte2lsb(w) {
-		return String.fromCharCode(w & 255, (w >> 8) & 255);
-	}
-
-	window.PNGlib = function(width,height,depth) {
-
-		this.width   = width;
-		this.height  = height;
-		this.depth   = depth;
-
-		// pixel data and row filter identifier size
-		this.pix_size = height * (width + 1);
-
-		// deflate header, pix_size, block headers, adler32 checksum
-		this.data_size = 2 + this.pix_size + 5 * Math.floor((0xfffe + this.pix_size) / 0xffff) + 4;
-
-		// offsets and sizes of Png chunks
-		this.ihdr_offs = 0;									// IHDR offset and size
-		this.ihdr_size = 4 + 4 + 13 + 4;
-		this.plte_offs = this.ihdr_offs + this.ihdr_size;	// PLTE offset and size
-		this.plte_size = 4 + 4 + 3 * depth + 4;
-		this.trns_offs = this.plte_offs + this.plte_size;	// tRNS offset and size
-		this.trns_size = 4 + 4 + depth + 4;
-		this.idat_offs = this.trns_offs + this.trns_size;	// IDAT offset and size
-		this.idat_size = 4 + 4 + this.data_size + 4;
-		this.iend_offs = this.idat_offs + this.idat_size;	// IEND offset and size
-		this.iend_size = 4 + 4 + 4;
-		this.buffer_size  = this.iend_offs + this.iend_size;	// total PNG size
-
-		this.buffer  = new Array();
-		this.palette = new Object();
-		this.pindex  = 0;
-
-		var _crc32 = new Array();
-
-		// initialize buffer with zero bytes
-		for (var i = 0; i < this.buffer_size; i++) {
-			this.buffer[i] = "\x00";
-		}
-
-		// initialize non-zero elements
-		write(this.buffer, this.ihdr_offs, byte4(this.ihdr_size - 12), 'IHDR', byte4(width), byte4(height), "\x08\x03");
-		write(this.buffer, this.plte_offs, byte4(this.plte_size - 12), 'PLTE');
-		write(this.buffer, this.trns_offs, byte4(this.trns_size - 12), 'tRNS');
-		write(this.buffer, this.idat_offs, byte4(this.idat_size - 12), 'IDAT');
-		write(this.buffer, this.iend_offs, byte4(this.iend_size - 12), 'IEND');
-
-		// initialize deflate header
-		var header = ((8 + (7 << 4)) << 8) | (3 << 6);
-		header+= 31 - (header % 31);
-
-		write(this.buffer, this.idat_offs + 8, byte2(header));
-
-		// initialize deflate block headers
-		for (var i = 0; (i << 16) - 1 < this.pix_size; i++) {
-			var size, bits;
-			if (i + 0xffff < this.pix_size) {
-				size = 0xffff;
-				bits = "\x00";
-			} else {
-				size = this.pix_size - (i << 16) - i;
-				bits = "\x01";
-			}
-			write(this.buffer, this.idat_offs + 8 + 2 + (i << 16) + (i << 2), bits, byte2lsb(size), byte2lsb(~size));
-		}
-
-		/* Create crc32 lookup table */
-		for (var i = 0; i < 256; i++) {
-			var c = i;
-			for (var j = 0; j < 8; j++) {
-				if (c & 1) {
-					c = -306674912 ^ ((c >> 1) & 0x7fffffff);
-				} else {
-					c = (c >> 1) & 0x7fffffff;
-				}
-			}
-			_crc32[i] = c;
-		}
-
-		// compute the index into a png for a given pixel
-		this.index = function(x,y) {
-			var i = y * (this.width + 1) + x + 1;
-			var j = this.idat_offs + 8 + 2 + 5 * Math.floor((i / 0xffff) + 1) + i;
-			return j;
-		}
-
-		// convert a color and build up the palette
-		this.color = function(red, green, blue, alpha) {
-
-			alpha = alpha >= 0 ? alpha : 255;
-			var color = (((((alpha << 8) | red) << 8) | green) << 8) | blue;
-
-			if (typeof this.palette[color] == "undefined") {
-				if (this.pindex == this.depth) return "\x00";
-
-				var ndx = this.plte_offs + 8 + 3 * this.pindex;
-
-				this.buffer[ndx + 0] = String.fromCharCode(red);
-				this.buffer[ndx + 1] = String.fromCharCode(green);
-				this.buffer[ndx + 2] = String.fromCharCode(blue);
-				this.buffer[this.trns_offs+8+this.pindex] = String.fromCharCode(alpha);
-
-				this.palette[color] = String.fromCharCode(this.pindex++);
-			}
-			return this.palette[color];
-		}
-
-		// output a PNG string, Base64 encoded
-		this.getBase64 = function() {
-
-			var s = this.getDump();
-
-			// If the current browser supports the Base64 encoding
-			// function, then offload the that to the browser as it
-			// will be done in native code.
-			if ((typeof window.btoa !== 'undefined') && (window.btoa !== null)) {
-				return window.btoa(s);
-			}
-
-			var ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
-			var c1, c2, c3, e1, e2, e3, e4;
-			var l = s.length;
-			var i = 0;
-			var r = "";
-
-			do {
-				c1 = s.charCodeAt(i);
-				e1 = c1 >> 2;
-				c2 = s.charCodeAt(i+1);
-				e2 = ((c1 & 3) << 4) | (c2 >> 4);
-				c3 = s.charCodeAt(i+2);
-				if (l < i+2) { e3 = 64; } else { e3 = ((c2 & 0xf) << 2) | (c3 >> 6); }
-				if (l < i+3) { e4 = 64; } else { e4 = c3 & 0x3f; }
-				r+= ch.charAt(e1) + ch.charAt(e2) + ch.charAt(e3) + ch.charAt(e4);
-			} while ((i+= 3) < l);
-			return r;
-		}
-
-		// output a PNG string
-		this.getDump = function() {
-
-			// compute adler32 of output pixels + row filter bytes
-			var BASE = 65521; /* largest prime smaller than 65536 */
-			var NMAX = 5552;  /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */
-			var s1 = 1;
-			var s2 = 0;
-			var n = NMAX;
-
-			for (var y = 0; y < this.height; y++) {
-				for (var x = -1; x < this.width; x++) {
-					s1+= this.buffer[this.index(x, y)].charCodeAt(0);
-					s2+= s1;
-					if ((n-= 1) == 0) {
-						s1%= BASE;
-						s2%= BASE;
-						n = NMAX;
-					}
-				}
-			}
-			s1%= BASE;
-			s2%= BASE;
-			write(this.buffer, this.idat_offs + this.idat_size - 8, byte4((s2 << 16) | s1));
-
-			// compute crc32 of the PNG chunks
-			function crc32(png, offs, size) {
-				var crc = -1;
-				for (var i = 4; i < size-4; i += 1) {
-					crc = _crc32[(crc ^ png[offs+i].charCodeAt(0)) & 0xff] ^ ((crc >> 8) & 0x00ffffff);
-				}
-				write(png, offs+size-4, byte4(crc ^ -1));
-			}
-
-			crc32(this.buffer, this.ihdr_offs, this.ihdr_size);
-			crc32(this.buffer, this.plte_offs, this.plte_size);
-			crc32(this.buffer, this.trns_offs, this.trns_size);
-			crc32(this.buffer, this.idat_offs, this.idat_size);
-			crc32(this.buffer, this.iend_offs, this.iend_size);
-
-			// convert PNG to string
-			return "\211PNG\r\n\032\n"+this.buffer.join('');
-		}
-	}
-
-})();