QuoDB

After subzin.com release, the last year a friend of mine and I were working hard in this project called QuoDB. It’s basically a JSON API rest for quotes extracted from subtitles. Although the api is not public yet I have created a client to test it. Its basically a movie quotes search engine implemented as a responsive single app using QuoDB API like backend, where you can find any quote in thousands of movies and series.

quodb-background-home

All quotes are time-based contextualized and linked with the imdb movie database. Register users can edit the current lines or submit their owns increasing the database.

In the next months we will made public the API and release more sample applications that have been built with it.

If you want try it, it’s here: www.quodb.com

I hope you enjoy it:)

Best,

Luis

Google maps navigation using WebRTC + HTML5 Canvas

WebRTC + Canvas + JavaScript = Air finger web browsing. You can see this demo with almost any modern browser, by default a video demo is showed but to test this project with your own camera you need to have a WebRTC enabled Chrome browser  with enable media stream in chrome://flags.

For video tracking I have developed a tool called videotracker.js. Its running is based in color detection and blend difference, every frame is captured from computer camera using WebRTC and copied to a Canvas object to be processed using javascript. In this experiment you can control google maps direction using an object with the selected color (skin color as default). The working is very simple: If your have enabled webrtc your camera begins to record and you can see yourself in the small screen. Then move your finger across the screen to start to scroll the map.

The speed of movement is related to the distance of the finger to the center of the small screen. To detect your finger a skin color range has been definited, but if the tracker is not working very well with your finger you can choose another color to track, for example I have created a small cover for my finger using a red color gum wrapper  that works quite well.

The color detection algorimth is using hsv colour to extract  color selected areas in the picture combined with a blend mode to capture only the objects with movement, it is a good way to have real-time results.

Although this method is not very stable, it is an efficient way to detect objects using a javascript browser engine, the contextual environment had significant influence on the accuracy of the results but configuring carefully the color range and the environment it can be a good way for tracking objects.

As we do with google maps, we can use videotracker.js. to control any DOM component like textareas, sliders, video players, etc.

Making an hybrid mobile app from any web site in a few minutes

This is a simple way to create a prototype mobile application from any website mobile version.

These kind of apps are called “hybrids” because they are made using a native browser container that load web mobile pages inside.

The main content is built using web technologies, and then wrapped in a platform-specific container that allows it to be installed like a native application. Thus it can be downloaded from the device’s app store/market.

I think this method could be useful to make a rapid alpha mobile app to test users reactions before making a final mobile app.

home from mobile subzin app

 

Native page transitions

To improve the user experience, instead of using the jquery mobile’s navigation model, a native progress dialog is showed between pages transitions.

home loading from mobile subzin appMovies found page from mobile subzin app

This is the piece of Java code that show the native dialog:

private class SubzinClient extends WebViewClient {

    public void onPageFinished(WebView view, String url){

        mProgressDialog.cancel();

    }

    public void onPageStarted(WebView view, String url, Bitmap favicon){

        mProgressDialog.show();

    }

}

What you need to make the app

  1. The url of your site mobile version, in my case I have used the mobile version from  www.subzin.com (made with jquerymobile framework).
  2. Android sdk (I’ve used Eclipse as IDE)
  3. An android device to test the application (And android simulator is valid too)

The main advantage of these kind of applications (besides the developing time saved)  is that any web improvement developed will be in the app without install any update.

Besides it’s possible to insert html files into the application project to get an initial loading faster.

On the other hand, the main disavantage is that the look and feel is not as good as native applications, althought the render performance of the web browsers is improving in every new release.

We can use the same procedure to make a similar app for Iphone/Ipad using Xcode and IOS sdk instead of Eclipse and Android sdk.

There are specifical frameworks like PhoneGap that allow you to take a web app and turn it into a native app for iOS, Android, BlackBerry , etc. These frameworks typically have APIs as well to access the device’s hardware and features that are locked out from the browser.

Here is the complete eclipse project that you can modify to make your own application: subzin_mobile_project.zip

F1 Live timing map

This is a live timing map application for f1 championship races made using javascript and google maps markers. The live timing data is supplied by formula1.com.

It’s interactive, you can press over a driver to track him or press into an empty map zone to untrack and have a general view.

It has also been made with a responsive design to adapt it to mobile browsers using jquerymobile framework.

F1 live timing map

How it works:

  • The client side:

    Until the race start date a countdown and a demo race is showed.
    When the countdown finishes it will connect to server (using websockets) to get the live timing data from server (every second) and the interface will be updated using this data.

  • The server side:

    It uses a Django app for the web page and the static race data (circuit, laps, drivers) is put into the html using the django template system.
    For the dynamic data (live timing) the websocket server is a node.js serving a json with the race data. I have modified the source of a C program for the linux terminal called live-f1 to generate a json with the data that the node serves instead of printing it on terminal screen.

Enjoy the race!

Tracking a football match with HTML5 and Javascript

This is an experiment for a real-time tracking of a football match using only web technologies. In this case I’ve chosen to track a football match from Futbol Club Barcelona (fcb).

All the work is made by the browser and it’s interactive (there are four checkboxes to activate/deactivate each tracker). It is based on tracking the location of colored objects in the frames using the HSV colour space.

As a practical use for the tracker, I’ve added a smart volume feature that activates the sound only when there are goal chances (every time the ball is near the area). So you can browse other pages and change to the video one only when chances are happening.

It works based on the quantity of green color in the sides of the video.

The path tracker shows ‘possible’ passes between fcb players

If you want to watch it in action here is the demo

So far it only works in chrome and firefox and because of the low resolution of the video (and my limited skills), the tracking is not perfect, but it could be improved for tracking videos from external sources like Youtube, Veetle, etc.

Here is the code:

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
function rgbToHsv(r, g, b){
    r = r/255, g = g/255, b = b/255;
    var max = Math.max(r, g, b), min = Math.min(r, g, b);
    var h, s, v = max;

    var d = max - min;
    s = max == 0 ? 0 : d / max;

    if(max == min){
        h = 0; // achromatic
    }else{
        switch(max){
            case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                    case b: h = (r - g) / d + 4; break;
                        }
        h /= 6;
    }

    return [h, s, v];
}

//Main object
var processor = {
    isFirefox35: function() {
        // Let a chance to the navigator to deal with it:
        return true; var ua = navigator.userAgent;
        // Gecko ?
        if (ua.indexOf("Gecko") == -1)
            return false;
        // Geck >= 1.9.1 ?
        return !(ua.indexOf("rv:1.9.1") == -1 &amp;&amp; ua.indexOf("rv:1.9.2") == -1);
    },
    // Init
    doLoad: function() {
        if (!this.isFirefox35()) {
            document.getElementById("nofirefoxbeta").style.display = "block"; }
        // Some init
        this.displayBackground = true;
        this.video = document.getElementById("video");
        this.mirrorVideo = document.getElementById("mirrorVideo");
        this.mirrorVideoCtx = this.mirrorVideo.getContext("2d");
        var self = this;
        // If the videos end, play again
        this.video.addEventListener("ended", function(){
            try {
                clearTimeout(self.timeout);
            } catch(e){}
            self.video.play();
            // Work around: https://bugzilla.mozilla.org/show_bug.cgi?id=488287
            self.videoIsPlaying();
        }, true);
        // Set the events listeners for the main video (update button)
        this.video.addEventListener("pause", function() { self.updateButtons(false); }, false);
        this.pageLoaded = true;
        this.startPlayer();
    },
    videoIsPlaying: function() {
        this.updateButtons(true);
        this.timerCallback();
    },
    videoIsReady: function() {
        this.videoLoaded = true;
        this.startPlayer();
    },
    startPlayer: function() {
        if (!this.videoLoaded || !this.pageLoaded) return;
            document.getElementById("wait").style.display = "none";
        document.getElementById("player").style.display = "block";
        this.width = this.video.videoWidth;
        this.height = this.video.videoHeight;
        this.mirrorVideo.width = this.width;
        this.mirrorVideo.height = this.height;
    },
    playVideo:function(){
        this.video.play();
        this.videoIsPlaying();
    },
    stopVideo: function(){
        this.video.pause();
        clearTimeout(this.timeout);
    },
    volumeDown:function(){
        if(this.video.volume>0)
            this.video.volume=Math.round((this.video.volume - 0.1)*10)/10;
    },
    volumeUp:function(){
        if(this.video.volume<1)
            this.video.volume=Math.round((this.video.volume + 0.1)*10)/10;
    },
    // Main loop
    timerCallback: function() {
        if (this.video.paused || this.video.ended) { return; }
        this.computeFrame();
        var self = this;
        this.timeout = setTimeout(function () {
            self.timerCallback(); }, 50);
    },
    // Update the SVG button
    updateButtons: function(play) {
        document.getElementById("playButton").setAttribute("play", play);
        document.getElementById("stopButton").setAttribute("play", play);
    }, // Handling some patterns (text, drawing)
    has_green_around: function(frameData, pos) {
        pos_left = pos+ 24;
        pos_right = pos - 24;
        r_left = frameData[pos_left+0];
        g_left = frameData[pos_left+1];
        b_left = frameData[pos_left+2];
        r_right = frameData[pos_right+0];
        g_right = frameData[pos_right+1];
        b_right = frameData[pos_right+2];
        return ((r_left < 125 &amp;&amp; g_left > 125 &amp;&amp; b_left < 80) &amp;&amp;
                (r_right < 125 &amp;&amp; g_right > 125 &amp;&amp; b_right < 80));

    },
    is_team_color: function(frameData, pos){
        for (i=pos-4; i<=pos+4; i+=4){
            r = frameData[i+0];
            g = frameData[i+1];
            b = frameData[i+2];
            hsv = rgbToHsv(r,g,b);
            //if(hsv[0] < 0.60|| hsv[1] < 0.35)
            if((hsv[0] < 0.40 || hsv[1] < 0.25 || hsv[2] < 0) ||
               (hsv[0] > 0.70 || hsv[1] > 1 || hsv[2] > 1))
                return false;
        }
        return true;
    },
    is_ball_color: function(frameData, pos){
        for (i=pos-4; i<=pos+4; i+=4){
            r = frameData[i+0];
            g = frameData[i+1];
            b = frameData[i+2];
            hsv = rgbToHsv(r,g,b);
            if((hsv[0] < 0.23 || hsv[1] < 0.40 || hsv[2] < 0.90) ||
               (hsv[0] > 0.27 || hsv[1] > 0.50 || hsv[2] > 1))
                return false;
        }
        return true;
    },
    dist: function(x1, y1, x2, y2) {
        return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); },
    computeFrame: function() {
        try {
            this.mirrorVideoCtx.clearRect(0, 0, this.width, this.height);
            this.mirrorVideoCtx.drawImage(this.video, 0, 0, this.width, this.height);
        }
        catch(e) { return; }
        var frame = this.mirrorVideoCtx.getImageData(0, 0, 640, 360);
        this.mirrorVideoCtx.fillStyle = 'rgba(200,200,200,0.5)';
        this.mirrorVideoCtx.strokeStyle = 'rgba(200,200,200,0.5)';
        this.mirrorVideoCtx.lineWidth = 1;
        var x, y;
        var weight = 0;
        var team_shape = null;
        var ball_shape = null;
        var ball_found = false;
        var ball_tracker = document.getElementById("balltracker").checked;
        var team_tracker = document.getElementById("teamtracker").checked;
        var path_tracker = document.getElementById("pathtracker").checked;
        var smart_volume = document.getElementById("smartvolume").checked;
        var shapes = [];
        var x, y;
        var green_bar_left = 0;
        var green_bar_rigth =0;
        var green_bar_down = 0;
        var frame_length = frame.data.length/4;
        // We dont' need to compute each pixels
        var step = 4;
        for (var i = 0; i < frame_length; i += step) {
            pos = i*4;
            x = i % this.width;
            y = Math.round(i / this.width);
            if (x == 4)
                green_bar_left += g;
            else
                if (x == this.width-4) {
                    green_bar_rigth += g;
                    if (y == 4)
                        green_bar_down += g;
                }
            ball_found = ball_tracker &amp;&amp; this.is_ball_color(frame.data, pos);
            team_found = team_tracker &amp;&amp; this.is_team_color(frame.data, pos);
            if (this.has_green_around(frame.data, pos) &amp;&amp; (ball_found || team_found)){
                if (ball_found) {
                    ball_shape = {};
                    ball_shape.x = x;
                    ball_shape.y = y;
                }
                if(team_found)
                {
                    if (!team_shape) {
                        // no shape yet, create the first one
                        team_shape = {};
                        team_shape.x = x;
                        team_shape.y = y;
                        team_shape.weight = 1;
                        shapes.push(team_shape);
                    } else {
                        var d = this.dist(x, y, team_shape.x, team_shape.y);
                        if (d>25){
                            team_shape = {};
                            team_shape.x = x;
                            team_shape.y = y;
                            team_shape.weight = 1;
                            shapes.push(team_shape);
                        }

                    }
                }
            }
        }
        if (shapes.length>0)
        {
            if (path_tracker){
                this.mirrorVideoCtx.beginPath();
                this.mirrorVideoCtx.moveTo(shapes[0].x, shapes[0].y);
            }
            for( var s=0; s<shapes.length; s+=1){
                tracker_size = shapes[s].weight+10;
                this.mirrorVideoCtx.strokeRect(shapes[s].x-tracker_size/2,
                shapes[s].y-tracker_size/2, tracker_size+2, tracker_size+2);
                if (path_tracker)
                    this.mirrorVideoCtx.lineTo(shapes[s].x, shapes[s].y);
            }
            if (path_tracker){
                this.mirrorVideoCtx.closePath();
                this.mirrorVideoCtx.stroke();
            }
        }
        if (ball_shape) {
            this.mirrorVideoCtx.strokeStyle = "red";
            this.mirrorVideoCtx.strokeRect(ball_shape.x-5, ball_shape.y-5, 10, 10);
        }

        if(smart_volume)
        {
            green_right_average = Math.round(green_bar_rigth / this.height);
            green_left_average = Math.round(green_bar_left / this.height);

            if ( shapes.length>0 &amp;&amp;
                Math.abs(green_left_average-green_right_average)>15){
                //this.volumeUp();
                this.video.volume=1;
                document.getElementById("soundIcon").setAttribute("mute", false);
            }else{
                this.volumeDown();
                //this.video.volume=0;
                document.getElementById("soundIcon").setAttribute("mute", true);
            }
        }
        return;
            }
}

Django-IDE

The Django-IDE is a web based IDE made with html5 and javascript
Currently, the following features have been written and are working:

  • Local storage: When you open a file once, this is copied to browser local storage,
    so the next times you open this file, if it has not changed, it will open intantaneusly
    from browser storage instead of getting it from server.
  • Offline mode:
    When you lose your connection, as it uses the local storage to save data on your browser,
    you can continue working in the open files and the changes generated offline will be sent
    to the server the next time the ide detects a connection.
  • Sync changes:
    If you are editing a file and, at the same time, other person edits it, the ide notify you
    of these changes and proposes you to get external changes or override with yours
  • Resources filter
  • Source code color syntax
  • Source code formatting

Installation

  • From pypi:
    $pip install django-ide

  • From source:
    $python setup.py install

Configuration

The django-ide has two settings that can be set in settings.py:

  1. Add djide to your INSTALLED_APPS in your settings.py project:
    INSTALLED_APPS = (
    ‘djide’,

  2. Include djide urls in your urls.py project:
    urlpatterns = patterns(
    (r’^djide/’, include(‘djide.urls’)),

RUN

$python manage.py runserver
That’s it, the last command should start a local server on port 8000, now you can
open your browser and go to 127.0.0.1:8000/djide to edit your projects apps.

TODOs and BUGS

See: https://github.com/lusob/django-ide/issues

Welcome to lusob.com

A weblog by Luis Sobrecueva, a programmer working in Barcelona City, about  tech projects and experiments.