Monday, December 6, 2010

mini guide how to install OpenCV 2.2 for Visual Studio 2010

mini guide how to install OpenCV 2.10 for Visual Studio 2008

เห็นตัวอย่างนี้คุ้นๆ ก็ไม่ต้องแปลกใจเพราะว่าแก้มาจากบทความ คราวที่แล้ว mini guide how to install OpenCV 2.10 for Visual Studio 2008

- หาที่ download ก่อน http://sourceforge.net/projects/opencvlibrary/files/

- โหลด OpenCV-2.2.0-win32-vs2010.exe มาใช้ได้เลย เพราะว่าคอมไพล์เป้นไลบรารี มาให้เรียบร้อยแล้วมีข้อจำกัดนิดนึงที่ว่าถ้าใช้ สำหรับคนที่ต้องการคอมไพล์เองก็โหลดอีกตัวมาใช้ แล้ว ใช้ CMAKE สร้าง

- เปิดโปรเจกต์ C ธรรมดาขึ้นมาแล้วเซตค่า include path กับ library ก็จบ
 include path ใส่ที่นี่
  เลือก Project property
    Configuration properties
     C/C++
      General
       Additional Include Directories ใส่ C:\OpenCV2.2\include
 library ใส่ที่นี่
  เลือก Project property
    Configuration properties
     Linker
      Additional Dependencies

 ใส่ค่าประมาณนี้เข้าไปสำหรับ Debug configuration
  C:\OpenCV2.2\lib\opencv_core220d.lib
  C:\OpenCV2.2\lib\opencv_highgui220d.lib

 ในส่วน release ใส่อันนี้แทน
  C:\OpenCV2.2\lib\opencv_core220.lib
  C:\OpenCV2.2\lib\opencv_highgui220.lib

ส่วนสำคัญสำหรับคนที่อัพมาจาก 2.1 จะเห็นว่า library เปลี่ยนชื่อไป และมีการจัดกลุ่ม header file ใหม่ ดังนั้นเวลาเขียนโปรแกรมแบบใหม่ header file จะเป็นแยกตามการจัดกลุ่มใหม่ เช่น
#include "opencv2\core\core_c.h"
#include "opencv2\imgproc\imgproc_c.h"

หรือจะ include ไฟล์เดียวคือ
#include "opencv2\opencv.hpp"

ก็อยู่

สำหรับคนที่ต้องการจะใช้ code เก่า โดยไม่ต้องการแก้ เช่น
#include <cv.h>

ให้เติม
Additional Include Directories ใส่ C:\OpenCV2.1\include\opencv เพิ่มด้วย
ไม่อย่างนั้นต้องแก้ของเก่าทั้งหมดให้อยู่ในรูปแบบ
#include "opencv\cv.h"

Sunday, September 26, 2010

Learning Django: Form

การสร้าง form เป็นเรื่องจำเป็นสำหรับ web app ทั่วไป ในหัวข้อนี้จะกล่าวเกี่ยวกับเรื่อง Form ใน Django

Getting Data from the Request Object

เราสามารถรับค่าจาก request object (browser) ได้ ใน view แต่ละ view จะมี HttpRequest เป็นพารามิเตอร์แรก ลองดูตัวอย่าง hello view ของเรา


from django.http import HttpResponse
def hello(request):
    return HttpResponse("Hello world")

ในตัว request มี attributes และ methods ดังนี้
- request.path
- request.get_host()
- request.get_full_path()
- request.is_secure()
- request.META เป็น python dictionary ปกติจะมี ข้อมูลดังนี้
    - HTTP_REFERER
    - HTTP_USER_AGENT
    - REMOTE_ADDR
- request.GET เป็น python dictionary
- request.POST เป็น python dictionary


Form example

เราลองมาสร้าง form จาก application ของเรากันดู



1. สร้าง template search_form.html โดยมี input ประมาณนี้

<form action="/search/" method="get">
<input type="text" name="q">
<input type="submit" value="Search">
</form>


2. สร้าง template สำหรับ search_results.html

<p>You searched for: <strong>{{ query }}</strong></p>
{% if books %}
<p>Found {{ books|length }} book{{ books|pluralize }}.</p>
<ul>
{% for book in books %}
<li>{{ book.title }}</li>
{% endfor %}
</ul>
{% else %}
<p>No books matched your search criteria.</p>
{% endif %}

3. สร้าง view สำหรับ input form และ search ใน books/views.py

from django.http import HttpResponse


from django.shortcuts import render_to_response



from mysite.books.models import Book


def search_form(request):
    return render_to_response('search_form.html')



def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render_to_response('search_results.html',
        {'books': books, 'query': q})
    else:
        return HttpResponse('Please submit a search term.')

3. map input form และ search ใน urls.py




urlpatterns = patterns('',
    # ...
    (r'^search-form/$', views.search_form),
    (r'^search/$', views.search),
    # ...
)


ลองมาดูตัวอย่างที่ซับซ้อนขึ้น เราจะสร้าง contact form โดยมีลักษณะดังนี้


1. สร้าง contact_form.html ที่เป็น template สำหรับกรอกข้อมูล
2. contact_form.html ส่งข้อมูลไปยัง contact
3. contact ตรวจสอบข้อมูล
    -ถ้าไม่มีข้อผิดพลาดส่งเมลล์ แล้ว ไปยัง thank
    -ถ้าเกิดข้อผิดพลาดไปยังหน้า contact_form.html ใหม่


ตัวอย่างของ views.py กับ contact_form.html จะมีลักษณะดังนี้


views.py



def contact(request):
    errors = []
    if request.method == 'POST':
        if not request.POST.get('subject', ''):
            errors.append('Enter a subject.')
        if not request.POST.get('message', ''):
            errors.append('Enter a message.')
        if request.POST.get('e-mail') and '@' not in request.POST['e-mail']:
            errors.append('Enter a valid e-mail address.')
        if not errors:
            send_mail(
                request.POST['subject'],




            



                request.POST['message'],

                request.POST.get('e-mail', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    return render_to_response('contact_form.html', {
        'errors': errors,
        'subject': request.POST.get('subject', ''),
        'message': request.POST.get('message', ''),
        'e-mail': request.POST.get('e-mail', ''),
    })

contact_form.html

<html>
<head>
<title>Contact us</title>
</head>
<body>
<h1>Contact us</h1>
{% if errors %}
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<form action="/contact/" method="post">
<p>Subject: <input type="text" name="subject" value="{{ subject }}"></p>
<p>Your e-mail (optional):
<input type="text" name="e-mail" value="{{ e-mail }}">

</p>
<p>Message:
<textarea name="message" rows="10" cols="50">
**{{ message }}**
</textarea>
</p>
<input type="submit" value="Submit">
</form>
</body>
</html>






พบว่าในกรณีที่มีหลายๆ field แล้วมีการ validate ด้วยการสร้าง form และ view จะยุ่งยากมาก Django มี library ที่ชื่อ django.forms ที่ช่วยเราจัดการเรื่อง form ได้


Django Form

การสร้าง form ของ Django (จะยกตัวอย่างจาก contact form ข้างต้น)
1. กำหนด form ขึ้นมา ใน form.py (จริงๆ เขียนใส่ใน views.py ไปเลยก็ได้)

from django import forms


class ContactForm(forms.Form):
    subject = forms.CharField()
    e-mail = forms.EmailField(required=False)
    message = forms.CharField()


2.เปลี่ยน contact view ใหม่โดยใช้ Django form แทนที่ requst.POST


from django.shortcuts import render_to_response
from mysite.contact.forms import ContactForm


def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
    if form.is_valid():
        cd = form.cleaned_data
            send_mail(
            cd['subject'],
            cd['message'],
            cd.get('e-mail', 'noreply@example.com'),
            ['siteowner@example.com'],
        )
        return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm()
    return render_to_response('contact_form.html', {'form': form})


3. สร้าง contact_form.html


<html>
<head>
<title>Contact us</title>
</head>
<body>
<h1>Contact us</h1>

{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<form action="" method="post">
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>
</body>
</html>




นอกจากนี้ยังมีวิธีการปรับแต่ง form อื่นๆ อีกเช่น

1. กำหนดวิธีการแสดงผลของ field
2. ตั้งค่า maximum
3. กำหนด label
4. การเพิ่ม custom validation 



from django import forms


class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    e-mail = forms.EmailField(required=False, label='Your e-mail address')
    message = forms.CharField(widget=forms.Textarea)


    def clean_message(self):
        message = self.cleaned_data['message']
        num_words = len(message.split())
        if num_words &lt; 4:
            raise forms.ValidationError("Not enough words!")
        return message



5. การตั้งค่า initial ให้กับ form

form = ContactForm(
    initial={'subject': 'I love your site!'}
)
return render_to_response('contact_form.html', {'form': form})





Learning Django: Django Admin Site

นอกจากการสร้าง web page แล้ว ใน web site ส่วนใหญ่จำเป็นต้องสร้าง interface สำหรับ admin ด้วย ซึ่งด้วยทั่วไปแล้ว interface มักมีหน้าตาคล้ายๆ กัน ดังนั้น Django ได้มี module ส่วนนี้มาให้ อยู่ใน django.contrib package

Activating the Admin Interface

เริ่มจากการแก้ไข settings.py ดังต่อไปนี้
1. เพิ่ม django.contrib.admin ไปในตัวแปร INSTALLED_APPS
2. เช็คดูว่า INSTALLED_APPS มี app เหล่านี้อยู่ (ปกติมีมาให้แล้ว)
- django.contrib.auth
- django.contrib.contenttypes
- django.contrib.sessions
3. เช็คดูว่า MIDDLEWARE_CLASSES มี class เหล่านี้อยู่

    'django.middleware.common.CommonMiddleware'
    'django.contrib.sessions.middleware.SessionMiddleware'
    'django.contrib.auth.middleware.AuthenticationMiddleware'

หลังจากนั้น sync database (pythong manage.py syncdb) ในขั้นตอนนี้ ระบบจะถามถึง super user name และ password สำหรับ web site ให้ตั้งค่าไป


ขั้นต่อไปให้ทำการแก้ไข map ใน urls.py โดยการ uncomment บรรทัดเหล่านี้ออก


# from django.contrib import admin
# admin.autodiscover()

# (r'^admin/', include(admin.site.urls)),


เท่านี้เราก็จะได้ admin site ที่สามารถจัดการ user และข้อมูลต่างๆ (ลองเปิดดูด้วย http://127.0.0.1:8000/admin/)

ทีนี้เราจะเพิ่ม models ของเราเข้าไปใน admin site เพื่อให้สามารถ เพิ่ม, ลบ, แก้ไข database ได้ โดยทำตามขั้นตอนดังนี้
1. ใน folder app (ภายใน project folder ในที่นี้คือ mysite/books) สร้าง ไฟล์ชื่อ admin.py เนื้อหาดังต่อไปนี้

from django.contrib import admin
from mysite.books.models import Publisher, Author, Book
admin.site.register(Publisher)
admin.site.register(Author)
admin.site.register(Book)


About Model in Admin Site

การจัดการ models ใน Admin Site จากตัวอย่าง app books เราสามารถเพิ่ม, ลบ ข้อมูลได้ แต่อาจจะมีปัญหาจุกจิก เล็กน้อยเช่น การเพิ่ม record จะต้องเติมข้อมูลทุกช่องลงไป (เนื่องจากตอนสร้าง ไม่ได้กำหนดว่าสามารถ null ได้ เป็นต้น) ในส่วนนี้จะทำการสรุป วิธีการปรับแต่งข้อมูล เล็กๆ น้อยๆ เพื่อที่จะสามารถ edit database ได้ใน Admin Site

1. Make Fields Optional


class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField(blank=True)



class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField(blank=True, null=True)



2. Customizing Fields Labels กำหนดชื่อ field ให้สื่อความหมายมากขึ้น


class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField(blank=True, verbose_name='e-mail')


Model Admin Class

การแก้ไขการแสดงผล database ในส่วนที่กล่าวมานั้นเป็นการ แก้ไขที่ model level ถ้าจะแก้ไขที่ admin level เราสามารถใช้ ModelAdmin class มาช่วยกำหนดการแสดงผลได้

วิธีการใช้ ModelAdmin class ใน admin.py สามารถสรุปได้ดังนี้
1. สร้าง class โดย inherit มากจาก admin.ModelAdmin
2. กำหนด customization ที่ต้องการ
3. register model โดยมี class ใหม่นี้เป็น พารามิเตอร์


ลองดูตัวอย่างสิ่งที่เรา customize ใน admin.py

1.ปรับlist display สำหรับ author, book, publisher
2.เพิ่ม search bar สำหรับ author
3.เพิ่ม date filter ให้ book
4.เรียงลำดับ date สำหรับ book
5.กำหนดและเรียงลำดับของ field

from django.contrib import admin
from mysite.books.models import Publisher, Author, Book


class AuthorAdmin(admin.ModelAdmin):
    list_display = ('first_name', 'last_name', 'email')
   search_fields = ('first_name', 'last_name')



class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'publisher', 'publication_date')
    list_filter = ('publication_date',)
    ordering = ('-publication_date',)

    fields = ('title', 'authors', 'publisher')


admin.site.register(Publisher)
admin.site.register(Author, AuthorAdmin)
admin.site.register(Book, BookAdmin)

Learning Django: Data access

หลังจากที่เรา sync database แล้วเราก็สามารถจัดการ database ได้ ในตอนนี้เราจะยกตัวอย่างวิธีการจัดการ database ผ่านทาง shell python ของ Django

เริ่มต้นโดยใช้คำสั่ง

python manage.py shell

เป็นการเรียก python shell โดยใช้ environment ของ project นี้ (mysite)

ลองดูคำสั่งเหล่านี้


>>> from books.models import Publisher
>>> p1 = Publisher(name='Apress', address='2855 Telegraph Avenue',
...    city='Berkeley', state_province='CA', country='U.S.A.',
...    website='http://www.apress.com/')
>>> p1.save()
>>> p2 = Publisher(name="O'Reilly", address='10 Fawcett St.',
...    city='Cambridge', state_province='MA', country='U.S.A.',
...    website='http://www.oreilly.com/')
>>> p2.save()
>>> publisher_list = Publisher.objects.all()
>>> publisher_list
[, ]

เป็นการสร้าง object แบบ Publisher สองตัว (p1,p2) โดยแล้วใส่ลงใน database โดย method ที่ชื่อ save
ถ้าเราใส่ข้อมูลลงใน database เลยเราสามารถใช้คำสั่ง ต่อไปนี้แทนได้


>>> p1 = Publisher.objects.create(name='Apress',
... address='2855 Telegraph Avenue',
... city='Berkeley', state_province='CA', country='U.S.A.',
... website='http://www.apress.com/')
>>> p2 = Publisher.objects.create(name="O'Reilly",
... address='10 Fawcett St.', city='Cambridge',
... state_province='MA', country='U.S.A.',
... website='http://www.oreilly.com/')

สังเกตว่า publisher_list แสดงค่าออกมาเป็น [, ] ซึ่งอ่านเข้าใจลำบาก เราจะแก้ไข method __unicode__(self) ของ model ต่างๆ เพื่อให้อ่านเข้าใจง่ายขึ้น (คล้ายกับการ override function toString())

ผลจากการแก้ไข models.py


from django.db import models


class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()
    
    def __unicode__(self):
        return self.name


class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField()
    
    def __unicode__(self):
        return u'%s %s' % (self.first_name, self.last_name)


class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()


    def __unicode__(self):
        return self.title

หลังจากแก้ไขแล้วเราลองมาดูความเปลี่ยนแปลง(ออกจาก shell แล้วเข้าใหม่)


>>> from books.models import Publisher
>>> publisher_list = Publisher.objects.all()
>>> publisher_list
[, ]


ลองมาดู database operation เบื้องต้น ใน Django
1. การ insert และ update data
- สร้าง record

>>> p = Publisher(name='Apress',
... address='2855 Telegraph Ave.',
... city='Berkeley',
... state_province='CA',
... country='U.S.A.',
... website='http://www.apress.com/')
- insert record
>>> p.save()
- change record
>>> p.name = 'Apress Publishing'
- update record
>>> p.save()

2. select record
- select all

>>> Publisher.objects.all()
[, ]

- select with where clause

>>> Publisher.objects.filter(name='Apress')
[]


>>> Publisher.objects.filter(country="U.S.A.", state_province="CA")
[]


>>> Publisher.objects.filter(name__contains="press")
[]

3. select single record

>>> Publisher.objects.get(name="Apress")


4. select with order

>>> Publisher.objects.order_by("name")
[, ]


>>> Publisher.objects.order_by("state_province", "address")
[, ]


>>> Publisher.objects.order_by("-name")
[, ]

5. select with where and order

>>> Publisher.objects.filter(country="U.S.A.").order_by("-name")
[, ]

6. select with limit

>>> Publisher.objects.order_by('name')[0]


>>> Publisher.objects.order_by('name')[0:2]

7. update record
>>> Publisher.objects.filter(id=52).update(name='Apress Publishing')

>>> Publisher.objects.all().update(country='USA')

8. remove record

>>> p = Publisher.objects.get(name="O'Reilly")
>>> p.delete()
>>> Publisher.objects.all()
[]

Learning Django: Create Model and Django App

ในขั้นแรก เราจะสร้าง app ใหม่ใน project ขึ้นมาก่อน เนื่องจากใน Django ถ้าจะสร้าง models (database layers ของ Django) จะต้องสร้าง Django app ขึ้นมา เพราะ models จะต้องอยู่ใน app

Create Django App

เราสามารถสร้าง Django App โดยใช้คึำสั่ง

python manage.py startapp app_name


ในตัวอย่างเราจะสร้าง app ที่ชื่อ books => python manage.py startapp books

Django จะสร้าง folder app_name พร้อมไฟล์อีก 4 ไฟล์
1. __init__.py
2. models.py
3. tests.py
4. views.py

Create Model

การสร้าง model นั้นเราสามารถสร้างโดยการแก้ไข models.py ใน app folder ลองดูตัวอย่างของ model


from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField()

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()


เป็นการสร้าง model ขึ้นมา 3 ตัว (เหมือนกับการกำหนด table ใน database ต่างกันตรงที่ใช้ไวยกรณ์ของ python)


Installing Model

หลังจากที่เราสร้าง Model (3 ตัว) เราจะทำการติดตั้ง model นี้ เนื่องจาก model ต้องอยุ่ใน app (book) เราจะทำการติดตั้ง books ไปใน project เรา โดยการแก้ไขตัวแปร INSTALLED_APPS ในไฟล์ settings.py


INSTALLED_APPS = (
    # 'django.contrib.auth',
    # 'django.contrib.contenttypes',
    # 'django.contrib.sessions',
    # 'django.contrib.sites',
     'mysite.books',
)

หลังจากนั้นให้สร้าง table ตาม model นี้ใน database ตัว Django เอง มี utility ช่วยโดยใช้คำสั่ง syncdb

python manage.py syncdb

คำสั่งนี้จะช่วย sync ตัว model ที่เรามีอยู่ใหัตรงกับ database

ต่อไปเราจะมารู้จักกับวิธี access data กัน

Learning Django: Configuring Database


Configuring the Database


สิ่งที่เป็นจุดเด่นของ Django คือการ สร้าง Database driven application ได้ง่าย โดยอาศัย framework แบบ MTV (คล้ายๆ MVC) ทั้งนี้เนื่องจากพื้นฐานของ Django เป็น python ดังนั้นเราสามารถใช้คำสั่ง sql ในการติดต่อกับ database ได้ โดยอาศัย template, กับ view ดูตัวอย่าง code


from django.shortcuts import render_to_response
import MySQLdb

def book_list(request):
    db = MySQLdb.connect(user='me', db='mydb', passwd='secret', host='localhost')
    cursor = db.cursor()
    cursor.execute('SELECT name FROM books ORDER BY name')
    names = [row[0] for row in cursor.fetchall()]
    db.close()
    return render_to_response('book_list.html', {'names': names})

แน่นอนว่า Django วิธีที่จะจัดการส่วนที่ซ้ำซ้อนพวกนี้ (เช่นการสร้าง query, การเปิดปิด connection)
เรามาเริ่มต้นที่การ config database



เพื่อความง่ายในบทความนี้จะใช้ database engine SQLite3 ที่มากับตัว python 2.5+ ดังนั้น หลังจากนั้นแก้ค่าตัวแปร  DATABASE ที่อยู่ใน settings.py ดังนี้

- ENGINE = 'django.db.backends.sqlite3'
- NAME = ตั้งค่าเป็น full path file ที่เราจะใช้เก็บฐานข้อมูล

นอกนั้นไม่ต้องตั้งค่า(สำหรับ SQLite3)

Learning Django: Template

จากตอนที่แล้วจะเห็นว่าการสร้าง view ขึ้นมายังจะต้องยุ่งยากในการ print html tag ขึ้นมาอีกซึ่งไม่ต่างกับ PHP เท่าไร ใน Django เราแก้ปัญหานี้โดยใช้ template

Template

ใน Django เราสามารถสร้าง template สำหรับแสดงผล (presentation) ให้แยกกับส่วน logic ลองมาดู concept ของ template กันอย่างกระชับ

เริ่มจากการเปิด python prompt แล้ว ลอง code ต่อไปนี้


>>> from django import template
>>> t = template.Template('My name is {{ name }}.')
>>> c = template.Context({'name': 'Adrian'})
>>> print t.render(c)
My name is Adrian.
>>> c = template.Context({'name': 'Fred'})
>>> print t.render(c)
My name is Fred.

ลองมาดู code กัน
1. import template เข้ามา
2. สร้าง template ชื่อ t
3. สร้าง context  c ขึ้นมา โดยใน context นี้มีค่า name=Andrian
4. ให้ template t แสดงผลโดยอ้างอิง context c จะได้ผลลัพธ์ออกมา
5. เปลี่ยนค่า context c โดยให้ name=Fred
6. ให้ template t แสดงผลโดยอ้างอิง context c จะได้ผลลัพธ์ออกมา เนื่องจาก context c เปลี่ยนค่าไป ผลจึงออกมาไม่เหมือนเดิม

เรานำ template มาประยุกต์ใช้กับ view ได้หลายวิธี แต่วิธีแนะนำเพื่อที่จะแยก ระหว่าง presentation กับ logic ขั้นตอนมีดังนี้

1. กำหนด template directory ใน settings.py โดย TEMPLATE_DIRS ในที่นี้จะสร้าง folder ที่ชื่อ templates ใน folder mysite


import os.path
....................
TEMPLATE_DIRS = (
    os.path.join(os.path.dirname(__file__), 'templates').replace('\\','/'),
)

2. เพิ่ม code ส่วนนี้ไปใน views.py



from django.template.loader import get_template
from django.template import Context
from django.http import HttpResponse
import datetime


def current_datetime(request):
    now = datetime.datetime.now()
    t = get_template('current_datetime.html')
    html = t.render(Context({'current_date': now}))
    return HttpResponse(html)

หรือเราจะใช้ feature ที่ชื่อ shortcut ใน Django เพื่อประหยัดการเขียน code

from django.shortcuts import render_to_response


import datetime


def current_datetime(request):
    now = datetime.datetime.now()
    return render_to_response('current_datetime.html', {'current_date': now})


โดย current_datetime.html จะเป็น html ไฟล์ที่มี Tag (คล้ายๆ PHP) โดยส่งค่า context ไปให้(ในกรณีนี้ current_date เป็น object ประเภท datetime)


Template Tag

ในส่วนนี้เราจะสรุป Template Tag คร่าวๆ

1. Tag ใน template จะอยู่ในเครื่องหมายปีกกา {} (คล้ายๆ PHP อยู่ระหว่า )
2. การอ้างอิงถึง variable ทำได้โดย ใช้ {{ variable_name }}
3. มี control ต่างๆ ดังนี้
    - {% if %}, {% else %}, {% endif %}
    - {% for %}, {% endfor %}
    - {% ifequal %},{% ifnotequal %}, {% endifequal %}, {% endifnotequal %}
4. comment 
    - บรรทัดเดียวใช้ {# #}
    - หลายบรรทัดใช้ {% comment %}, {% endcomment %}
5. สามารถใช้ filter ได้
    - {{ name|lower }} เป็นการเปลี่ยนค่า name ให้เป็น lower case
    - filter สามารถ pipe ได้ {{ my_list|first|upper }} เป็นการดึงค่าแรกใน my_list ออกมาแล้วเปลี่ยนเป็น upper case


Learning Django: Create Views

Creaet Views

Django ใช้การพัฒนาในแบบ MTV คือ Model, Template, และ View ในส่วนนี้เราจะมาดูเรื่อง View กัน

เริ่มต้นจากการสร้างไฟล์ที่ชื่อ

views.py


from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello world")


เป็นไฟล์ python ทีมี module เดียวชื่อ hello ซึ่งจะถือเป็น view อันหนึ่ง ลองรัน web server (python manage.py runserver) ดูก็จะไม่เห็นการเปลี่ยนแปลงใดๆ เนื่องจากยังไม่ได้มีการ map view ที่ชื่อ hello นี้เข้ากับ pattern ใดๆ ซึ่งคงเดาใด้ว่า การ map นี้เรากำหนดไว้ในไฟล์ urls.py

ลองดูเนื้อหาในไฟล์ urls.py ที่ Django สร้างขึ้นมาให้ (ตัวอย่างนี้ลบ comment ออกแล้ว)


from django.conf.urls.defaults import *


urlpatterns = patterns('',
)

ในส่วนนี้จะเป็นการกำหนดการ map ระหว่าง pattern กับ view จะเห็นว่าเรายังไม่มีการ map ใดๆ (พารามิเตอร์ตัวแรกไม่ได้อ้างถึงการ map) ดังนั้น ไม่ว่าเราจะ browse ไปที่ใดใน http://127.0.0.1:8000/ ก็ยังจะขึ้นหน้าเหมือนเดิม (ไม่มี error 404)

ทีนี้เราจะ map view hello เข้ากับ url http://127.0.0.1:8000/hello/ โดยการแก้ไข urls.py ตามขั้นตอนดังนี้
1. import module hello โดยใช้คำสั่ง from mysite.views import hello
2. เพิ่ม map ระหว่าง view hello กับ pattern /hello/ โดยเพิ่ม  ('^hello/$', hello) เข้าไปใน urlpatterns
(พารามิเตอร์แรกเป็น pattern เป็น regular exprssion โดย '^' หมายถึงจุดเริ่่ม pattern '$' หมายถึงจุดสิ้นสุด ถ้าเรากำหนดเป็น 'hello' ธรรมดา มันจะไปตรงกับ path อื่นเช่น http://127.0.0.1:8000/hello/test/ เป็นต้น ส่วนพารามิเตอร์ที่สองเป็นชื่อ view ที่กำหนดไว้ใน module)

ไฟล์ที่แก้ไขแล้วจะมีลักษณะดังนี้


from django.conf.urls.defaults import *
from mysite.views import hello


urlpatterns = patterns('',
    ('^hello/$', hello),
)
*หลัง  ('^hello/$', hello) ต้องมี  "," ด้วย

เมื่อลองทดสอบ run server ดู จะสามารถ browse view ที่ชื่อ hello โดยผ่าน url http://127.0.0.1:8000/hello/
แล้วถ้าไปยัง path อื่นคราวนี้จะเกิด error 404 ขึ้น

เราสามารถสร้าง view แบบไดนามิกได้ ลองดูตัวอย่าง code ที่เพิ่มขึ้นใน views.py

from django.http import HttpResponse
import datetime

def hello(request):
    return HttpResponse("Hello world")

def current_datetime(request):
    now = datetime.datetime.now()
    html = "It is now %s." % now
    return HttpResponse(html)

แลัวแก้ไข urlpatterns เป็น

urlpatterns = patterns('',
    ('^hello/$', hello),
    ('^time/$', current_datetime),
)


เป็นการเพิ่ม view ขื่อ current_datetime ขึ้นมาอีก โดย view นี้ไม่ใช่ view แบบ static มีการเปลี่ยนแปลงเนื้อหาทุกครั้ง


Dynamic URL

ทีนี้เราจะมาลอง map dynamic URL เริ่มต้นที่ การแก้ไข urlpatterns

urlpatterns = patterns('',
    (r'^hello/$', hello),
    (r'^time/$', current_datetime),
    (r'^time/plus/(\d{1,2})/$', hours_ahead),
)

เราได้สร้าง map สำหรับ pattern time/plus/(\d{1,2}) (ถ้าไม่เข้าใจลองหาอ่านเรื่อง regular expression) ซึ่งทำให้สามารถส่งผ่านค่าในวงเล็บไปให้กับ view ได้เสมือนเป็น parameter ใน view 

สังเกตว่าเราใช้ r นำหน้า pattern เพื่อไม่ให้ python ทำการประมวลผล escape sequence

โคดสำหรั้บ hours_ahead view

from django.http import Http404, HttpResponse
import datetime

def hours_ahead(request, offset):
    try:
        offset = int(offset)
    except ValueError:
        raise Http404()
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    html = "In %s hour(s), it will be %s." % (offset, dt)
    return HttpResponse(html)











Learning Django: Create Website

Create Website

ขั้นแรกก่อนจะสร้าง web page ก็ต้องสร้าง website กันก่อน ลองมาสร้าง website โดยใช้ชื่อ project ว่า mysite
การสร้างมีขั้นตอนดังนี้
1. ไปยัง folder ที่จะสร้าง project
2. รัน
django-admin.py startproject mysite 
(ใน windows อาจจะมีปัญหาเรื่อง path ในการเรียก file django-admin.py จะอยู่ใต้ folder scripts ใน python directory)
3. เราจะได้ folder ที่ชื่อ mysite ภายในประกอบด้วย file ประเภท .py ทั้งหมด 4 files
    1. __init__.py ไฟล์สำหรับจัดการ package ของ python
    2. manage.py ไฟล์ utilites สำหรับจัดการ Django project นั้นๆ
    3. settings.py ไฟล์ตั้งค่าสำหรับ Django project
    4. urls.py เอาไว้ map ระหว่าง url และ module ต่างๆ

Running Application

หลังการสร้าง website แล้วก็เตรียมพร้อมจะสร้าง page แต่ก่อนอื่น มาดูวิธีการ run project ก่อน

ในตัว Django เองจะมี web server มาให้แล้วสามารถใช้ทดสอบในระหว่างการพัฒนาก่อนที่จะ deploy ลง server ของจริง เราสามารถ startup web server โดยใช้คำสั่ง

python manage.py runserver

(manage.py ที่อยู่ใน folder mysite)

หลังจากนั้นจะสามารถ view page ได้โดยผ่านทาง http://127.0.0.1:8000/

เราสามารถเปลี่ยน port และ ip ของ web server ได้

Learning Django: Install

ความต้องการพื้นฐานและสิ่งที่ต้อง download

การจะใช้งาน Django จะต้องติดตั้ง component ต่างๆ ดังต่อไปนี้
1. python เนื่องจาก Django ใช้ภาษา python ดังนั้นจะต้องมี python ในระบบ หาโหลดจาก http://www.python.org/download/ เนื่องจากขณะนี้ Django ไม่ support python 3.x ให้ติดตั้งเวอร์ชัน 2.x แทน ซึ่งจริงๆ ควรจะเป็น 2.5 ขึ้นไป
2. Django แน่นอนต้องมี โหลดได้จาก http://www.djangoproject.com/download/
3. Database engine ไม่จำเป็นต้องมี (แต่ถ้าไม่ใช้ แนะนำให้พัฒนาโดยใช้ php จะง่ายกว่า 555) ในตัว Django เองรองรับ อยู่ 4 ตัวคือ

- PostgreSQL
- SQLite3
- MySQL
- Oracle
ถ้าจะลองใช้ดูก่อนสามารถใช้ SQLite3 ซึ่งมีมาให้แล้วใน python 2.5 เลยไม่ต้องติดตั้้งเพิ่ม


วิธีการติดตั้ง(สำหรับ windows)

1. ติดตั้ง python (เป็น .msi)
2. แตกไฟล์ Django ใส่ใน folder ที่ต้องการ
3. เปิด command prompt ใน mode admin
4. รันคำสั่ง setup.py install ที่ folder จากข้อสอง ซึ่งในขึ้นตอนนี้ ถ้าไม่ได้ associate file ประเภท py ไว้ก็ให้
ระบุเอง
5. ลองตรวจสอบ การติดตั้ง Django โดยเข้าไปใน python prompt ใส่คำสั่ง import django แล้วตามด้วย django.VERSION ตามลำดับ ถ้าไม่เกิด error แสดงว่าการติดตั้งเรียบร้อย
6. ติดตั้ง database engine เพิ่ม (ในกรณีนี้เราใช้ SQLite3 จึงไม่ต้องติดตั้งเพิ่ม)
7. ติดตั้ง IDE เพิ่มถ้าต้องการ (แนะนำ aptana studio3 แต่ตอนนี้ยังเป็น beta อยู่) ใน blog นี้จะใช้ notepad

เป็นอันเสร็จสิ้นการติดตั้ง Django พร้อมใช้งาน

Create website using Django

มีงานที่ต้องเขียน web app อีกแล้ว เลยลองนั่ง search หา web application framework มาดู เนื่องจาก ASP.NET กับ Flex ที่ใช้อยู่ไม่น่าจะเวิร์ค เขียน PHP ก็ยุ่งยากอีก  มาสะดุด Django ตรงที่มันเขียนว่า "The Web framework for perfectionists with deadlines" โดนใจมากๆ เลยลองหาหนังสืออ่านแล้วสรุป ตามความเข้าใจแบบคร่าวๆ สุดขีด ใครสนใจเนื้อหาเต็มๆ ของที่แปลมาลองดูได้จากหนังสือ The Definitive Guide
to Django Web Development Done Right, Second Edition ของ Apress

Saturday, April 17, 2010

Learning OpenCV: Delaunay Triangulation, Voronoi Tesselation

Delaunay triangulation

เป็นวิธีการเชื่อมจุดใน space ให้เป็นสามเหลี่ยม (เรียกวา่ Delaunay) โดยคุณสมบัติคือ มุมที่น้อยที่สุดในสามเหลี่ยมทั้งหมดจะมีค่ามากสุด(ก็คงพยายามทำให้เป็ฯสามเหลี่ยมด้านเท่า เท่าที่เป็นไปได้) และ circum-circle( หมายถึงวงกลมที่ล้อมรอบสามเหลี่ยม) ทุกอันจะต้องไม่มี จุด อยู่ภาพในวงกลม(ไม่งั้นก็หมายความว่า รูปนี้สามารถสร้างสามเหลี่ยมเล็กลงไปได้อีก)

จริงๆ สองส่วนนี้สัมพันธ์กันในลักษณะที่ว่า ถ้ามีสามเหลี่ยมมี่ผอมๆ (มีมุมหนึ่งน้อยๆ imply ว่าต้องมีอย่างน้อยอีกมุมมากๆ) จะทำให้ จุดศูนย์กลางของ circum-circle อยู่นอกสามเหลี่ยมนั้น ความเป็นจริงต้องไม่มีมุมใดมีขนาดถึง 90 องศา ลองดูภาพ circumcenter (จุดศูนย์กลางภาพ) ในกรณีต่างๆ




ซ้ายเป็นภาพที่ไม่มีมุมไหนถึง 90 องศา กลางภาพสามเหลี่ยมมุมฉาก ขวาสามเหลี่ยมมุมป้าน


วิธีคำนวณหาสามเหลี่ยม Delaunay วิธีหนึ่งคือ

  1. เริ่มจากการกำหนดจุดนอก space นี้แล้วสร้างสามเหลี่ยมโดยร่วมกับอีกสองจุดใน space
  2. เพิ่มจุดใน space ไปในกลุ่มสามเหลี่ยมที่เราสร้างแล้ว (ในตอนแรกจะมีสามเหลี่ยมเดียวที่มีจุดจากนอก space มาด้วย) คำนวณหา circum-circle ทุกอันว่ามีอันไหนที่ล้อมรอบจุดใหม่นี้ ถ้ามี สามเหลี่ยมนั้นไม่ใช่ (ตามที่ได้อธิบาย) ลบสามเหลี่ยมนั้นทิ้ง
  3. จากขั้นที่สองสร้างสามเหลี่ยมใหม่ จากจุดที่เพิ่มเข้าไปในขั้นสอง และจุดที่โดนลบสามเหลี่ยมทิ้งไป
  4. ไปยังข้อสองจนกว่าทุกจุดใน space จะถูกเพิ่มเข้าไปใน สามเหลี่ยม Delaunay 

ในพื้นที่ระหว่างจุดใน space เราสามารถหาได้ว่ามันอยู่ในอิทธิพลของจุดใด (ในหนังสือเรียก owned by) เราจึงสามารถแบ่ง partition ของ space ที่ owned โดยจุดได้ partition นั้นเรียกว่า Voronoi tessellation



ภาพซ้ายคือ Delaunay triangle ที่มี circum-circle และ circumcenter
ภาพขวาคือ Voronoi tessellation

จากรูปจะเห็นได้ว่า Voronoi tessellation เกิดจากการเชื่อมต่อ circumcenter เข้าด้วยกัน ซึ่งก็สมเหตุผล เพราะ เราอาจจะมอง ว่า circumcenter เป็นส่วนในวงกลม ที่มีจุดศูนย์กลางที่จุดต่างๆ ก็ได้ ดังนั้นเส้นที่ลากระหว่าง circumcenter ก็อาจจะมองได้ว่าเป็นเส้นที่ลากเชื่อมระหว่างจุดตัดของวงกลมของวง ซึ่งเส้นนี้จะอยู่ห่างจากจุดศูนย์กลางของวงกลมเ่ท่ากันทั้งสองวง  จึงสามารถนำเส้นเชื่อมระหว่าง circumcenter มาใช้ในการแบ่งเขตของ จุดใน space ได้


Creating a Delaunay or Voronoi Subdivision

มาดูวิธีการสร้าง Delaunay Subdivision กัน


CvRect rect = { 0, 0, 600, 600 };                //Our outer bounding box
CvMemStorage* storage;                        //Storage for the Delaunay subdivsion
storage = cvCreateMemStorage(0);        //Initialize the storage
CvSubdiv2D* subdiv;                              //The subdivision itself
subdiv = init_delaunay( storage, rect); //See this function below


ขั้นแรกคือการกำหนดพื้นที่สี่เหลี่ยมกัน (อย่าลืมว่า OpenCV จะสร้างสามเหลี่ยมโดยเริ่มจาก external vertex)
สร้าง memory storage และใช้คำสั่ง init_delaunay ในการสร้าง CvSubdiv2D ขึ้นมา


หลังจากนั้นก็เพิ่มจุดเข้าไป


CvPoint2D32f fp;             //This is our point holder
for( i = 0; i < as_many_points_as_you_want; i++ ) {
     // However you want to set points
    //
    fp = your_32f_point_list[i];
    cvSubdivDelaunay2DInsert( subdiv, fp );
}

เราสามารถสร้างข้อมูล Voronoi Tesselation ใน cvSubdiv2D ได้โดยคำสั่ง cvCalcSubdivVoronoi2D และลบข้อมูลเหล่านั้นโดยคำสั่ง cvClearSubdivVoronoi2D


Navigating Delaunay Subdivisions

หลังจากได้ Delaunay Subdivisions แล้วเราคงต้องการจะทำอะไรสักอย่างกับทุก edge (โดยการ navigate)

structure cvQuadEdge2D จะเก็บ edge ของ Delaunay และ Voronoi (ต้องสร้างก่อนด้วยคำสั่ง cvCalcSubdivVoronoi2D ) ลองดูรูป


e คือ edge ของ Delaunay ที่จะเก็บ เส้นประคือ voronoi edge



ส่วน structure CvSubdiv2DPoint จะเก็บ  Delaunay edge กับ associate point

e คือ Delaunay edge ที่เริ่มที่จุดที่มีสี่เหลี่ยม

Walking on edges

จากรูปที่สามเมื่อเรามีข้อมูล input edge เราสามารถหา edge ที่เหลือทั้งสี่ได้โดยใช้คำสั่ง


CvSubdiv2DEdge cvSubdiv2DRotateEdge(
    CvSubdiv2DEdge edge,
    int type
);

edge คือ input edge
type

  • 0 หมายถึง input edge (e)
  • 1 หมายถึง rotated edge (eRot)
  • 2 หมายถึง reversed edge (reversed e)
  • 3 หมายถึง reversed rotated edge (reversed eRot)
หรือจะใช้้คำสั่ง cvSubdiv2DGetEdge  เพื่อหา edge ที่ associate กับ input edge (ดูรูปที่ 4 ประกอบ)

CvSubdiv2DEdge cvSubdiv2DGetEdge(
    CvSubdiv2DEdge edge,
    CvNextEdgeType type
); 

edge คือ input edge
type 
  • CV_NEXT_AROUND_ORG edge ถัดจากจุดกำเนิด (eOnext)
  • CV_NEXT_AROUND_DST edge ถัดจากจุดปลาย (eDnext)
  • CV_PREV_AROUND_ORG edge ก่อนจุดกำเนิด (reversed eRnext)
  • CV_PREV_AROUND_DST edge ก่อนจุดปลาย (reversed eLnext)
  • CV_NEXT_AROUND_LEFT edge ถัดไปใน left facet (eLnext)
  • CV_NEXT_AROUND_RIGHT edge ถัดไปใน right facet (eRnext)
  • CV_PREV_AROUND_LEFT edge ก่อนหน้าใน left facet (reversed eOnext)
  • CV_PREV_AROUND_RIGHT  edge ก่อนหน้าใน left facet (reversed eDnext)

เราสามารถหาจุดใดๆ จาก edge โดยคำสั่ง
CvSubdiv2DPoint* cvSubdiv2DEdgeOrg( CvSubdiv2DEdge edge );
CvSubdiv2DPoint* cvSubdiv2DEdgeDst( CvSubdiv2DEdge edge );

(เราอาจจะต้องแปลง จุดเหล่านี้ให้อยู่ในรูปที่เข้าใจง่าย  cvPointForm32f(output_Org->pt)





ก่อนอื่นที่จะ travel ไปใน edge หรือจุดต่างๆ บน subdivision ได้ ก่อนอื่น ต้องหาจุด หรือ edge แรกก่อน
วิธีแรกใช้ฟังก์ชัน

CvSubdiv2DPointLocation cvSubdiv2DLocate(
    CvSubdiv2D* subdiv,
    CvPoint2D32f pt,
    CvSubdiv2DEdge* edge,
    CvSubdiv2DPoint** vertex = NULL
);

subdiv คือ subdivision ที่จะหา edge
pt คือจุดใดๆ

ผลลัพธ์ที่ได้มีดังนี้


  • CV_PTLOC_INSIDE pt อยู่ใน จะคืนค่า edge หนึ่งใน facet กลับมาใน พารามิเตอร์ edge.
  • CV_PTLOC_ON_EDGE pt อยู่ใน edge จะคืนค่า edge กลับมาใน พารามิเตอร์ edge
  • CV_PTLOC_VERTEX pt เป็น vertex  จะคือนค่า vertex กลับมาในพารามิเตอร์ vertex
  • CV_PTLOC_OUTSIDE_RECT pt อยู่นอกกอรบ
  • CV_PTLOC_ERROR เกิดข้อผิดพลาดใน subdiv หรือ pt

วิธีที่สอง
เนื่องจากเราสร้าง Delaunay ด้วยการกำหนดจุด external มาก่อน (เพื่อสร้าง external triangle) เราสามารถเข้าถึงข้อมุลเหล่านี้ได้
วิธีหาค่า vertices ของ external triangle

CvSubdiv2DPoint* outer_vtx[3];
    for( i = 0; i < 3; i++ ) {
        outer_vtx[i] = (CvSubdiv2DPoint*)cvGetSeqElem( (CvSeq*)subdiv, I );
}


วิธีหาค่า edge  ของ external triangle

CvQuadEdge2D* outer_qedges[3];
    for( i = 0; i < 3; i++ ) {
        outer_qedges[i] = (CvQuadEdge2D*)cvGetSeqElem( (CvSeq*)(my_subdiv->edges), I );
}



Learning OpenCV: Inpainting and Mean-shift segmentation

Inpainting

เป็นวิธีในการกู้ image ที่เกิด noise เช่น ฝุ่นในภาพถ่าย


void cvInpaint(
    const CvArr* src,
    const CvArr* mask,
    CvArr* dst,
    double inpaintRadius,
    int flags
);

src คือ image ต้นฉบับ
mask ระบุถึงตำแหน่งที่ damaged ใน image (ตำแหน่งที่ต้องการซ่อม)
dst คือ image ผลลัพธ์
inpaintRadiuse กำหนดความขนาดพื้นที่รอบๆ จุดที่ต้องการ inpaint (ถ้ากว้างมากจุดจะโดนเฉลี่ยมากทำให้เกิดการ blur ได้)
flags คือวิธีการใน inpanting เลือกได้ระหว่า

  • CV_INPAINT_NS(Navier-Stokes method)
  • CV_INPAINT_TELEA (A. Telea’s method)
Mean-Shift Segmentation

ในตอนก่อนๆ ได้อธิบายเรื่องการ segmentation โดยใช้ image pyramids (cvPyrSegmentation) วิธีนี้จะใช้ color merge เพื่อ segment image. วิธีนี้จะใช้การ minimizing energy ของ image (energy กำหนดโดย link strength ซึงกำหนดด้วย ความเหมือนของสี อีกที)  cvPyrMeanShiftFiltering ก็ใช้แนวคิดนี้ในการทำ color segmentation ภาพ


void cvPyrMeanShiftFiltering(
    const CvArr* src,
    CvArr* dst,
    double spatialRadius,
    double colorRadius,
    int max_level = 1,
    CvTermCriteria termcrit = cvTermCriteria(CV_TERMCRIT_ITER | CV_TERMCRIT_EPS,5,1)
);

src คือ image ต้นฉบับ
dst คือ image ผลลัพธ์
saptialRadius, colorRadius คือรัศมีสำหรับ spatial และ color ตามลำดับ
max_level คือ level ในการทำ pyramid scale
termcrit  เป็น iterative algorithm ที่กำหนดว่าการ iteration จะหยุดอย่างไรลองดูการประกาศ

cvTermCriteria(
    int type; // CV_TERMCRIT_ITER, CV_TERMCRIT_EPS,
    int max_iter,
    double epsilon
);

int คือเป็นตัวบอกว่า iteration จะหยุดใด
  • CV_TERMCRIT_ITER จะหยุดเมื่อ iteration จนครบจำนวนครั้ง ค่านี้กำหนดใน max_iter
  • CV_TERMCRIT_EPS จะหยุดเมื่อ convergence metrix มีค่าเล็กลงถึงขนาดหนึ่ง กำหนดค่านี้ใน epsilon 
ค่า CV_TERMCRIT_ITER กับ CV_TERMCRIT_EPS สามารถใช้ด้วยกันได้



กลับมาที่ cvPyrMeanShiftFiltering กันต่อ ในกรณีที่ข้อมูลเราอยู่ในระนาบ x,y และ r,g,b  mean-shift จะ  หา ก้อนของข้อมูล(segment) ที่รวมกันอยู่อย่างหนาแน่นโดยกา้ร scan ตัว window ไปทั่ว space แต่ range ของ spatial กับ range ของ color จะแตกต่างกัน  ดังนั้น mean-shift ต้องการรัศมีที่ต่างกันในการ scan ระหว่าง spatial space (กำหนดใน spatialRadius) และ color space (colorRadius) ตลอดทางที่ mean-shift window เคลื่อนที่ (scan) จุดที่ converge (โดยสี) ไปยัง peak จะถูกจัดอยู่ในกลุ่มเดียว (owned by) กับ peak นั้นๆ

จุด peak และส่วนที่ owned by peak นั้นๆ จะถูกนับเป็น segment เดียวกัน จริงๆ แล้วการทำ color clustering จะทำใน scale pyramids (จากคำสั่ง cvPyrUp, cvPyrDown) ดังนั้น max_level จะเป็นตัวกำหนดจำนวน level ในการทำ image pyramids 

Learning OpenCV: Watershed Algorithm

ก่อนที่จะเข้าเรื่อง segmentation ที่เกี่ยวกับภาพเคลื่อนไหวมาเข้าเรื่องการ segmentation ที่ไม่ต้องใช้ภาพจากหลายๆ เวลากันก่อน


Watershed Algorithm
เป็นวิธี segmentation ที่ไม่ได้ใช้ concept ของการ remove background (เพราะในบางครั้งมันไม่มีหรือว่าสำคัญเท่ากันหมด)  คำอธิบายในหนังสืออ่านแล้วก็ไม่สื่อเท่าไร ลองไปค้นมาจาก wiki ก็ได้ความถึง กระบวนการของ watershed คือ เราสามารถมอง ภาพ intensity image(ก็ gray scale นั่นแหละ) ให้เปรียบเสมือนแผนที่ในโหมด topography (ไอ้โหมดที่สีเข้มแทนที่สูง สีน้ำเิงินแทนน้ำทะเล) เราก็จะเห็นภาพเปรียบเสมือนภูเขาและแอ่งน้ำได้ ทีนี้เราก็กำหนดจุด fill ซึ่งก็มีหลายวิธี ในหน้งสือจะอธิบายวิธี fill โดยใช้ flooding ง่ายๆ คือกำหนดจุดปล่อยน้ำให้น้ำไหลจากที่สูงไปที่ต่ำแล้วก็ขังเกิดเป็นลุ่มน้ำ (watershed) ซึ่งลุ่มน้ำนี้อาจจะถือได้ว่าเป็นการ segment ประเภทหนึ่ง (background)

 watershed ยังอนุญาติให้ ผู้ใช้คือ algorithm อื่นกำหนด background และ บอกว่า พื้นที่เหล่านี้เป็นพื้นที่ๆ เดียวกัน(ในกรณีที่มี noise แยกพื้นที่สองที่ๆ ควรจะเป็นที่เดียวกันออก)





ในภาพซ้ายเราเส้นสีขาวเป็นตัวบอกว่าพื้นที่ๆ ต่อกันด้วยเส้นเป็นพื้นที่เดียวกัน ผลลัพธ์ที่ได้ออกมาดังภาพขวา


void cvWatershed(
    const CvArr* image,

    CvArr* markers
);

image เป็นภาพที่ต้องการทำ watershed
markers มีขนาดเท่า image และมีค่าเป็น 0s ยกเว้นจะต้องการจะบอก cvWatershed ว่าพื้นที่ในส่วนต่างๆ เป็นพื้นที่เดียวกัน โดยกำหนดค่า ตัวเลขใดๆ ใน maskers ตัวเลขที่เท่ากันจะบอกถึงความเป็นพื้นที่เดียวกัน

Learning OpenCV: Contour Convexity and Pairwise Geometrical Histograms

ในตอนนี้จะรวมสองเรื่องที่เหลือในการ matching contour


Contour Convexity and Convexity Defects
เป็นวิธีในการหา convex hull (เปลือกที่นูน น่าจะหมายถึงเส้นรอบของ วัตถุที่ไม่มีส่วนเว้าเข้า) ของวัตถุ หลังจากนั้นคำนวณหา convexity defects เราจะนำ convexity defects เหล่านี้มาเป็นส่วนหนึั่งของ characteristic ของ complex object ได้

จากรูปมือที่่เห็น เส้นกรอปนอก(สีดำ)คือ convex hull พื้นที่ ตั้งแต่ a-h คือ convexity defects

ฟังก์ชันที่ใช้ในการคำนวณ convexity defects มีสามตัวคือ

#define CV_CLOCKWISE 1
#define CV_COUNTER_CLOCKWISE 2

CvSeq* cvConvexHull2(
    const CvArr* input,
    void* hull_storage = NULL,
    int orientation = CV_CLOCKWISE,
    int return_points = 0
);

int cvCheckContourConvexity(
    const CvArr* contour
);

CvSeq* cvConvexityDefects(
    const CvArr* contour,
    const CvArr* convexhull,
    CvMemStorage* storage = NULL
);


คำสั่ง cvConvexHull2 จะเป็นการสร้าง hull ของ contour คำสั่งที่สองจะเป็นการตรวจสอบว่า hull นั้น convex หรือไม่ คำสั่งสุดท้ายจะเป็นการหา convexity defects จาก contour และ convex hull

cvConvexHull2 จะเอา input array เป็น พารามิเตอร์แรก พารามิเตอร์ต่อมาคือ memory storage พารามิเตอร์ที่สามจะกำหนดว่า hull ที่ได้จะวนหรือตามเข็ม พารามิเตอร์สุดท้ายถ้าเป็น 1 จุดต่างๆ จะคืนค่ากลับมาใน array ไม่เช่นนั้น จะคืนค่ามาแต่ indices ที่อ้างไปยัง input

cvCheckContourConvexity รับค่า contour เข้าไปและสามารถตรวจสอบได้ว่า convex หรือไม่ วิธีนี้รวดเร็วและง่าย แต่ถ้า contour ตัดกัน วิธีนี้จะใช้ไม่ได้

cvConvexityDefects จะรับค่า contour, convex hull (จาก cvConvexHull2) มาคำนวณหา convexity defects



Pairwise Geometrical Histograms

เราได้อธิบายถึงการกำหนด contour โดยวิธี freeman chain codes ไปแล้ว ต่อไปนี้จะกล่าวถึงประโยชน์ที่กล่างถึงมากอันหนึ่งคือการนำไปใช้ใน Pairwise Geometrical Histograms (PGH) ซึ่งตัว PGH เป็น extension ชอง chain code histogram (CCH)

CCH เป็น histogram ที่คำนวณโดยนับความถี่ที่เกิดขึ้นในแต่ละ step (ทั้ง 8 steps) ที่ represent contour คุณสมบัิติอย่างหนึ่งของ histogram แบบนี้คือการหมุนวัตถุทุกๆ 45 องศา จะทำให้ histogram เกิดการ cyclic transformation เท่านั้น


รูปซ้าย เป็นรูป contour, freeman chain code และ CCH ของรูปรถถัง
รูปขวา เป็นรูป contour, freeman chain code และ CCH ของรูปรถถังที่หมุน 45 องศา


PGH จะใช้แนวคิดคล้ายๆ วิธีการสร้าง PGH คือให้ เลือก edge อันหนึ่งเป็น base edge แล้วหลังจากนั้นจับคู่กับ edge ที่เหลือทุกอัน เพื่อคำนวณหาค่า dmin, dmax, θ   โดยที่ dmin คือระยะที่สั้นที่สุดระหว่าง edge ทั้งสอง dmax คือระยะที่ยาวที่สุด θ คือมุมระหว่าง edge ทั้งสอง แล้วเปลี่ยน edge อันใหม่ให้เป็น base edge จนครบทุกคู่  PGH จะเป็น histogram สองมิติ ที่มิติแรกเป็น θ มิติที่สองเป็น d ในทุกๆ การจับคู่ของ edge จะมีค่า (dmax, θ) และ (dmin, θ) เกิดขึ้น bins ทั้งสองและระหว่างนั้น จะถูกเพิ่มค่า



ในภาพซ้ายแสดง edge ทั้งสองเส้น(เส้นทึบ)พร้อมค่า dmin, dmax, θ 
ในภาพขวาแสดงถึง histogram ที่จะถูกเพิ่มค่าจาก คู่ edge ทางด้านซ้าย

ฟังก์ชันในการคำนวณค่า PGH คือ

void cvCalcPGH(
    const CvSeq* contour,
    CvHistogram* hist
);

Learning OpenCV: Hierarchical Matching

การ matching ด้วย moments ไม่เพียงพอ (แต่เร็ว) ก่อนอื่นมาทำความรู้จักกับ contour tree ก่อน

Contour tree
contour tree ไม่ใช่การ represent contour ด้วย tree (อย่างที่ได้จากค่า cvFindContours) ลองมาดูวิธีการสร้าง contour tree กัน ก่อนอื่นเริ่มต้อนด้วยรูป


ขั้นตอนเริ่มจากการหาบริเวณที่มีส่วนยื่นออกมาหรือส่วนที่เว้าเข้าไปเป็นสามเหลี่ยม (จริงๆ ก็หาจุดสองจุดที่ไม่ติดกันอย่างในรูป) ส่วนที่ยื่นออกไป(A,B,D) จะถูกตัดทิ้ง ส่วนที่หดลงไปจะโดนถม (จะกลายเป็นรูปในเส้นประ) ทุกครั้งที่ิถมหรือตัดสามเหลี่ยมจะเป็นการลดจำนวนจุดใน contour และนำพื้นที่เหล่านี้ใสเข้าไปใน contour tree

ถ้าสามเหลี่ยมที่ถูกตัดหรือถูกถมนั้น มีด้านสองด้านอยู่ใน contour เดิมสามเหลี่ยมนั้นจะเป็น leaf node และถ้าด้านใดด้านหนึ่งของสามเหลี่ยมนั้น เป็นด้านในด้านหนึ่งของสามเหลี่ยมใน tree แล้ว สามเหลี่ยมนั้นจะเข้ามาเป็น parent ของ node นั้น ตัดไปเรื่อยๆ จนเหลือสี่เหลี่ยม (มี vertices เหลือ สี่อัน) แล้วก็หั่นเป็นสองท่อน เป็น child node ของ root

note อย่าลืมว่าทรีสร้างจากใบไปราก, สามเหลี่ยมหนึ่งๆ จะมีสามด้าน ด้านที่ถูกตัดทิ้งแรกๆ (คือมีสองด้านอยู่ในcontour ต้นฉบับ มีแนวโน้มเป็นใบ) parent ของมันจะโดนตัดเป็นอันดับถัดมา(y เป็น parent ของ c,d) ซึ่งแน่นอนว่า parent มีลูกได้แค่สอง(เพราะต่อกับสามเหลี่ยมได้อีกสองอัน อีกอันหนึ่งไว้ต่อกับ parent ของตัวเอง

หลังจาก contour tree สร้างแล้ว ก็จะมาเปรียบเทียบ contour tree ทั้งสองอัน โดยดูจากโหนดในทรีที่ตรงกันแล้วเปรียบเทียบระหว่างโหนดที่ตรงกันนั้นด้วย คุณลักษณะต่างๆ (เช่น ความยาว, moments, etc)

มีข้อควรระวังคือการสร้าง contour tree ของ OpenCV ไม่ค่อย robust การเปลี่ยนแปลงจุดเล็กน้อยใน contour อาจจะทำให้มีผลเปลี่ยนแปลงอย่างมากใน contour tree และ root ของทรี ก็เลือกแบบไม่มีหลักการเท่าใด ดังนั้นจึงแนะนำให้ เรียก cvApproxPoly และทำ cyclic shift

ฟังก์ชันในการเปรียบเทียบ contour มีดังนี้

  • cvCreateContourTree
  • cvContourFromContourTree
  • cvMatchContourTrees


Friday, April 16, 2010

Learning OpenCV: Moments

Moments เป็นวิธีหนึ่งที่ง่ายที่สุดในการเปรียบเทียบ contour โดยการเปรียบเทียบ contour moments
ลองมาดูนิยามของคำว่า moment กันก่อน

m(p,q) = ผลรวมของ (I(x,y)*x^p*y^q) ในทุกจุด

ในกรณีที่ p=q=0 m(0,0) และเป็นรูป binary จะหมายถึงความยาวของ contour ถ้าเป็นรูปปิดจะหมายถึงพื้นที่ (จำนวนจุดทุกจุดในเส้นรอบปิด เท่ากับพื้นที่)


ฟังก์ชันที่ใช้ในการหา moment คือ


void cvContoursMoments(
    CvSeq* contour,
    CvMoments* moments
)



โดย CvMoments นิยามดังนี้


typedef struct CvMoments {
    // spatial moments
    double m00, m10, m01, m20, m11, m02, m30, m21, m12, m03;
    // central moments
    double mu20, mu11, mu02, mu30, mu21, mu12, mu03;
    // m00 != 0 ? 1/sqrt(m00) : 0
    double inv_sqrt_m00;
} CvMoments;

cvContoursMoments จะคำนวนเฉพาะ spatial moments ให้เท่านั้น เราสามารถหา moment ได้โดยใช้คำสั่ง
cvGetSpatialMoment


moments ที่กล่าวมายังใช้ัในการเปรียบเทียบ contour ได้ไม่ดีนัก ปกติแล้วเรามักจะต้องการ normalized moments (contour ที่รูปร่างเหมือนกันแต่ขนาดต่างกันจะได้เปรียบเทียบกันได้) และ moment ที่คำนวณได้จะขึ้นกับ ระนาบที่เลือกใช้ เพราะฉะนั้นถ้า contour เกิดการหมุน moments ก็จะไม่ match กัน

OpenCV มีทางเลือกในการหา moments คือ

  • cvMoments เหมือน cvContoursMoments แต่รับ input เป็นภาพ สามารถตั้งให้เป็น binary image ได้
  • cvGetCentralMoment การคำนวณคล้ายกับ cvMoments เพียงแต่ ลบค่า เฉลี่ยในแกน x,y ลงไป มีผลทำให้เกิดคุณสมบัติ transition invariant (ไม่เปลี่ยนแปลงไปตามการเลื่อน)
  • cvGetNormalizedCentralMoment เหมือน cvGetCentralMoment ยกเว้นมีการ normalized การ normalized นี้ทำให้เกิด invariant to scale (moment ของ contour ที่คล้ายๆ กัน แต่ขนาดไม่เท่ากันจะถูก normalized ให้ใกล้ๆ กัน)
  • cvGetHuMoments เกิดจากผลรวมของ normalized central moment ผลที่ได้ จะได้ค่าที่ invariant to scale, rotation, and reflection (ผลจาก Hu Moment เลเวลหนึ่งจะ invariant to rotate  เลเวลเจ็ด จะ invariant to skew จึงสามารถนำมาใช้ในการเปรียบเทียบได้ดีกว่า moment ธรรมดา)

Matching with Hu Moments

การ matching contours ใน OpenCV ใช้คำสั่ง


double cvMatchShapes(
    const void* object1,
    const void* object2,
    int method,
    double parameter = 0
);

object1, object2 สามารถเป็น image หรือ contour ได้ ถ้าเป็น image ฟังก์ชันนี้จะคำนวณ moments ให้ก่อน แล้วนำมาเปรียบเทียบ
parameter ตอนนี้ไม่ได้ใช้
method เลือกได้ระหว่าง


  • CV_CONTOURS_MATCH_I1
  • CV_CONTOURS_MATCH_I2
  • CV_CONTOURS_MATCH_I3

รายละเอียดดูได้จากในหนังสือ (เพราะไม่ได้บอกถึงผลที่ได้จากการ คำนวณแต่ละแบบ)

Learning OpenCV: Contour Summary Characteristics and Geometry

คุณลักษณะของ contour ที่สำคัญๆ ที่ OpenCV สามารถหาได้

  • cvArcLength ใช้หาความยาวของ contour
  • cvContourArea หาพื้นที่ของ contour
  • cvBoundingRect หา bounding box ของ contour เป็นสี่เหลี่ยมแนวขนานกับแกน x
  • cvMinAreaRect2 หา bounding box โดยสี่เหลี่ยมที่ bound จะมีมุมใดๆ ก็ได้
  • cvMinEnclosingCircle หา bounding circle
  • cvFitEllipse2 หา fitting ellipse

Geometry
เป็นฟังกขันในการคำนวณเกี่ยวกับ bounding box ทั้งหลาย


  • cvMaxRect หา bounding box ระหว่าง CvRect สองตัว
  • cvBoxPoints หาจุดทั้งสี่มุมจาก CvRect 
  • cvPointSeqFromMat สร้าง sequence ของจุดจาก metric
  • cvPointPolygonTest ใช้ในการทดสอบว่าจุด อยู่ใน polygon หรือไม่ 




Learning OpenCV: Freeman Chain Codes, Polygon Approximations and Dominant point

Freeman Chain Codes
ปกติวิธีในการเก็บ contour จาก cvFindContours (ค่า default คือ CV_CHAIN_APPROX_SIMPLE) อีกวิธีหนึ่งคือ Freeman chains โดยกำหนดใน mode  CV_CHAIN_CODE ลองดูภาพตัวอย่าง

จากรูป (a) เราจะแบ่งทิศทางออกเป็นแปดทิศตามรูป ส่วนใน (b) แสดงการใช้ freeman codes ในการลากแทนที่ contour ของรถถัง โดยเริ่มจากท้ายตัวถัง

ในการอ่านค่า freeman chains ใช้ฟังก์ชันดังนี้
  • cvStrarReadChainPoints
  • cvReadChainPoint
 คำสั่งแรกเป็นการเริ่มอ่านค่า chain (เหมือน fopen) แล้วทำการอ่านค่า chain ต่อไปเรื่อยๆ ด้วย cvReadChainPoint จนกว่าจะเจอค่า NULL ในทำนองเดียวกับ CvSeqReader



Polygon Approximations

ปกติ Polygon Approximations จะใช้ในการลดจำนวน vertics OpenCV ใช้วิธี Douglas-Peucker (DP) approximation ในฟังก์ชันที่เรียกว่า cvApproxPoly()

CvSeq* cvApproxPoly(
    const void* src_seq,
    int header_size,
    CvMemStorage* storage,
    int method,
    double parameter,
    int recursive = 0
);


ผลลัพธ์ที่ได้จะเป็น CvSeq* ที่ชี้ไปยัง contour แรก (เราสามารถหา contour ต่อไปได้เอง) 

src_seq คือ sequence ของ contour ที่จะใช้ในการหา approximation
header_size = sizeof(CvContour)
storage เป็น memory storage ที่ต้องใช้ในการคำนวณ
method  เป็น CV_POLY_APPROX_DP (อย่างที่กล่าวไว้ในข้างต้น และตอนนี้มีวิธีเดียว)
recursive จะบอกว่าให้ค่า  approximation เฉพาะ contour แรกที่ชี้(head) หรือจะให้หาทุก contour
parameter เป็นค่าที่กำหนดเป็น threshold สำหรับวิธีการหา approximation

ก่อนอื่นลองดูวิธีการหา approximation เพื่อที่จะเข้าใจถึง parameter


(a) เป็น image, (b) คือ contour ของ (a) ต่อไปเราจะหา extreme point แล้วเชื่อมด้วยเช่น (c)  หลังจากนั้นจะหาจุดใน contour ที่ไกลจากเส้นที่สุด (ดูใน (c)) หลังจากนั้นลากเส้นเชื่อม (d) แล้วหาจุดที่ไกลจากสามเหลี่ยมใน (d)  ทำไปเรื่อยๆ จนในที่สุด เส้นที่หาได้น้อยกว่าค่าทีระบุใน parameter ก็หยุด


Dominant point

คือจุดที่มีข้อมูลเกี่ยวกับ curve มากกว่า จุดอื่น(นัยว่าสำคัญกว่าจุดอื่นเพราะมีข้อมูลเยอะกว่า ถ้าจะลดจำนวน vertices จุดทีเป็น dominant point ไม่น่าโดน) ฟังก์ชันที่ใช้หา dominant point ใน OpenCV คือ 

CvSeq* cvFindDominantPoints(
    CvSeq* contour,
    CvMemStorage* storage,
    int method = CV_DOMINANT_IPAN,
    double parameter1 = 0,
    double parameter2 = 0,
    double parameter3 = 0,
    double parameter4 = 0
);


contour คือ contour ที่จะหา dominant point
storage คือ memory storage ที่ใช้ในการคำนวณ
method ตอนนี้มีค่า CV_DOMINANT_IPAN หมายถึง algorithm IPAN

แนวคิดคือ IPAN จะพยายาม scan ไปทั่ว contour เพื่อจะสร้างสามเหลี่ยมโดยใช้จุดใน contour ลักษณะของสามเหลี่ยมจะกำหนดโดย ขนาดสองด้าน และ มุม (ด้านมุมด้าน เพียงพอที่จะกำหนดสามเหลี่ยมได้ ยังจำได้ไหม) 

parameter1, parameter2 หมายถึง ระยะทางที่สั้นสุด และระยะทางที่ยาวที่สุด
parameter3 หมายถึง neighbor distance
parameter4 หมายถึง maximum angle θ

หลังจากที่ IPAN สร้างสามเหลี่ยมที่แบบที่ ด้านสองด้าน มีค่าระหว่าง paramter1และ parameter2 โดยที่มุมระหว่างสองด้านน้อยกว่า parameter4 หลังจากนั้น จุดที่มีมุมที่น้อยที่สุดในระหว่างจุดที่ใกล้เคียงกัน (กำหนดความใกล้เคียงกันโดย parameter3) จะคงอยู่ที่เหลือจะถูกกำจัดออกไป
โดยปกติค่า parameter1-4 จะกำหนดเป็น 7,9,9,150 ตามลำดับ






Learning OpenCV: Contour

จากตอนก่อนๆ เรื่องเกี่ยวกับการ detect edge โดยใช้ canny ขั้นตอนต่อไปคือการรวมจุดที่ประกอบเป็น edge นั้นรวมกัน เรียกว่า contour แน่นอน OpenCV มีฟังก์ชันสำหรับหา contour ไว้ให้

ก่อนอื่นมาดูนิยามของ contour ก่อน contour คือ list ของจุดที่แทน curve ในรูปภาพ ในกรณีของ Contour ใน OpenCV จะเก็บไว้ใน sequence ลองดูภาพประกอบ



ในภาพบนเป็นภาพที่เราต้องการหา contour ซึ่งประกอบด้วยส่วน A-E ส่วนด้านล่างคือ contour ที่ OpenCV หามาได้ (โดยฟังกชัน cvFindContours) ซึ่งกำกับไว้ด้วยคำว่า cX หรือ hX โดย c หมายถึง contour, h หมายถึง hole ตัวที่เป็นเส้นประ เป็น exterior boundaries ของพื้นที่สีขาว (ที่ไม่ใช่ 0 ก็คือกรอบของพื้นที่ขาว)   ตัวเส้นจุดนั้นเป็น interior boundaries ของพื้นที่สีขาว หรือ exterior boundary ของพื้นที่ดำ (คืออยู่ในพื้นที่ขาว, หรือว่าเป็นกรอบของพื้นที่สีดำ) ซึ่ง OpenCV จะแยกความแตกต่างระหว่างสองตัวนี้

*notes ในความเป็นจริงระหว่างภาพ binary (ภาพขาวดำ) และ edge ที่ detect ได้จาก canny จะมีความแตกต่างกันเล็กน้อยในผลลัพธ์ เพราะจริงๆ แล้ว cvFindContours ไม่รู้จักว่า edge คืออะไร แต่จะมองเห็นเป็นพื้นที่สีขาวบางๆ (ไม่ได้เข้าใจว่ามันเป็น edge แต่อย่างไร) ผลลัพธ์ที่ได้ก็คือ ใน edge วงรอบปิดหนึ่งๆ จะได้ exterior contour และ interior contour มาสองอันที่ขนาดใกล้กันมาก

ลองมาดูฟังก์ชันกันซะที

int cvFindContours(
    IplImage* img,
    CvMemStorage* storage,
    CvSeq** firstContour,
    int headerSize = sizeof(CvContour),
    CvContourRetrievalMode mode = CV_RETR_LIST,
    CvChainApproxMethod method = CV_CHAIN_APPROX_SIMPLE
);

img คือรูปภาพซึ่ง OpenCV จะใช้พื้นที่นี้ในการคำนวณด้วย ถ้าต้องการเก็บข้อมูลส่วนนี้ให้ copy ไว้ก่อน
storage เป็น memory storage ที่เราจองไว้
firstContour เป็นผลลัพธ์ที่ได้จากฟังก์ชัน
headerSize ใช้ในการบอกถึงขนาด object ที่จะ allocate ปกติเป็น sizeof(CvContour) หรือ sizeof(CvChain)
mode มีตัวเลือกอยู่ 4 ตัว

  • CV_RETR_EXTERNAL จะหาเฉพาะ extrme outer contours
  • CV_RETR_LIST หาทุก contour และแทนด้วย list
  • CV_RETR_CCOMP หาทุก contour และแทนด้วยต้น list สอบระนาบ ระนาบบนมีแต่ cX ระนาบล่างเป็น hX
  • CV_RETR_TREE  หาทุก contour และจัดเรียงเป็น tree


ลองดูตัวอย่างผลที่ได้จากการหา contour ในรูปแรกกัน



ตัวเลือกเหล่านี้จะบอกว่าเราต้องการหา contour แบบไหนและผลลัพธ์แบบไหน ซึ่งจริงๆ วิธีการลิงก์ linked list ของ sequence ด้วย h_prev, h_next, v_prev, v_next นั้นกำหนดได้จาก mode ในการหา contour

method คือวิธีการประมาณค่า contour

  • CV_CHAIN_CODE ผลลัพธ์ contour จะอยู่ในรูปแบบ Freeman chain code วิธีอื่นจะเป็น polygons (sequence of vertics)
  • CV_CHAIN_APPROX_NONE แปลง chian code ให้เป็น จุด
  • CV_CHAIN_APPROX_SIMPLE 
  • CV_CHAIN_APPROX_TC89_L1  CV_CHAIN_APPROX_TC89_KCOS ใช้วิธี Teh-Chin approximation algorithm
  • CV_LINK_RUN  method นี้จะใช้ได้กับ mode CV_RETR_LIST เท่านั้น

ซึ่งวิธีการหา contour นอกจาก cvFindContours แล้ว ยังสามารถใช้กลุ่มฟังก์ชันเหล่านี้ในการหา contour ได้

  • cvStartFindContours
  • cvFindNextContour
  • cvSubstituteContour
  • cvEndFindContour
  • cvApproxChains
แนวคิดของวิธีนี้คือเราจะไม่หา contour ออกมาทีเดียว เราจะออกมาทีละตัว โดยตัวแรกเราจะใช้คำสั่ง cvStartFindContours ซึ่งจะให้ผลลัพธ์มาเป็น CvSequenceScanner (คล้ายๆ กับการ เปิด file ก่อนด้วย fopen)
ซึ่งใน CvSequenceScanner เก็บ state เกี่ยวกับ contour ที่เราหาไปแล้ว (อย่าสับสนระหว่าง CvSequenceScanner กับ CvSeqReader, CvSeqReader ไว้ใช้อ่าน elements ที่เรียงเป็น sequence แต่ CvSequenceScanner  เป็น list ของ contour (ซึ่งเป็น list ของ sequece อีกที)

เราสามารถหา contour ต่อไปได้เรื่อยๆ ด้วยคำสั่ง cvFindNextContour จนกว่าจะเจอค่า NULL หรือสั่งจบการหาเองด้วย cvEndFindContour (เหมือน fclose)

ฟังก์ชัน cvSubstituteContour ใช้ในการแทน contour ในตำแหน่งที่ปัจจุบันด้วย contour ใหม่ หรือจะใช้ลบ contour ปัจจุบันอยู่ก็ได้ (โดยใช้ค่า NULL ไปใน contour ที่จะแทนที่)

ฟังก์ชัน  cvApproxChains ใช้ในการแปลง Freeman chians ไปเป็น polygon (จริงๆ มีการประมาณค่าด้วย)


Drawing Contours
ฟังก์ชันในการวาด contour คือ

void cvDrawContours(
    CvArr* img,
    CvSeq* contour,
    CvScalar external_color,
    CvScalar hole_color,
    int max_level,
    int thickness = 1,
    int line_type = 8,
    CvPoint offset = cvPoint(0,0)
);

img คือ image ที่จะวาด contour
contour คือ root node ของ contour tree
external_color, hole_color เป็นสี่ที่จะใช้ fill ระหว่าง external กับ hole
max_level จะเป็นการสั่งให้วาด contour ใน contour tree เท่ากับจำนวนชั้นที่กำหนด(ในกรณีของ CV_RETR_LIST มีชั้นเดียว ค่า 0 ก็พอ ในกรณี CV_RETR_CCOMP มีสองชั้นถ้าใส่หมดก็จะต้องให้ค่าเป็น 1 ยังมีค่าที่ติดลบสำหรับใช้ในกรณี CV_RETR_CCOMP และ CV_RETR_TREE)
thickness คือความกว้างของเส้นที่จะวาด ถ้าให้ค่าเป็น
line_type ประเภทของเส้นที่จะวาด
offset ใช้ในการกำหนดตำแหน่งการวาดแทนที่จะวาดเป็น absolute coordinate สามารถใช้ได้ในกรณีที่เรา หา contour โดยใช้ ROI


Learning OpenCV: Template Matching



วิธีนี้ไม่ใช้ histogram แต่ใช้วิธีการ match patch ที่มีรูปภาพ (ในกรณีของ back project patch นั้นจะเป็นแค่กรอบ ภาพนั้นใช้ภาพจากภาพ test) การ match patch ของรูปภาพ กับ ภาพที่จะหานั้นจะใช้วิธีที่จะกล่าวถึงต่อไปในตอนนี้

ก่อนอื่นมาดูฟังก์ชันในการ match template กันก่อน

void cvMatchTemplate(
    const CvArr* image,
    const CvArr* templ,
    CvArr* result,
    int method
);

image คือภาพที่จะหา template
templ คือ template ที่จะหาในภาพ
result คือผลลัพธ์
method คือวิธีในการคำนวณการ matching มีดังนี้

  • Square difference matching method (method = CV_TM_SQDIFF)(หาค่าผลต่างกำลังสอง)
  • Correlation matching methods (method = CV_TM_CCORR)(หา correlation)
  • Correlation coefficient matching methods (method = CV_TM_CCOEFF)

ยังมี normalized version ของสามวิธีนี้ ซึ่งจะช่วยลดผลการผิดพลาดเนื่องจากสภาพแสงไม่เท่ากัน (ในตัวอย่าง histogram matching EMD) คือ

  • CV_TM_SQDIFF_NORMED
  • CV_TM_CCORR_NORMED
  • CV_TM_CCOEFF_NORMED


วิธี CV_TM_SQDIFF จะใช้เวลาน้อยสุดแต่ผลที่ได้จะค่อนข้างแย่ตรงช้ามกับ CV_TM_CCORR_NORMED แต่ก็ต้อง trade-off

เช่นเดียวกับ cvCalcBackProjectPatch เราจะหาตำแหน่งของ วัตถุได้โดยใช้คำสั่ง cvMinMaxLoc (แต่ต้องระวังหน่อยเพราะว่า mothod ที่ต่างกันค่าที่ต้องการก็จะต่างกันเช่น CV_TM_SQDIFF_NORMED ต้องหาค่า min ส่วน CV_TM_CCORR_NORMED หรือ CV_TM_CCOEFF_NORMED ต้องหาค่า max)

เพื่อต้องการหาค่า match ที่ถูกต้อง(ไม่บังเอิญเจอ error) จึงมีแนวคิดที่ว่า บริเวิณรอบๆ จุดที่ match เจอน่าจะมีค่าค่อนข้างสูง(เพราะภาพความมีความต่อเนื่อง) ดังนั้นวิธีการ smooth ภาพผลลัพธ์ก่อนที่จะใช้ cvMinMaxLoc ในการหาจุดสุดสุดต่ำสุดน่าจะช่วยได้

Learning OpenCV: Back Projection

back projection
เป็นวิธีในการบันทึกว่า pixel หรือว patch ของ pixel เข้ากันได้กับ histogram ของ
model (ตัวอย่าง)  อย่างเช่นถ้าเรามี histogram ของสีผิวคน เราสามารถใช้ back project
หาตแหน่งของสีผิวคนในภาพได้

void cvCalcBackProject(
    IplImage** image,
    CvArr* back_project,
    const CvHistogram* hist
);
image คือภาพ ที่จะหาว่ามีข้อมูลใน histogram อยู่หรือไม่
hist คือ histogram ที่จะหา
back_project เป็น CvArr ขนาดเท่า image

ค่าใน back_project จะเป็นค่าที่อยู่ใน bin (ใน hist ที่ associated กับจุดนั้นๆ) ในกรณีที่ hist นั้น normalized ค่านี้อาจถือได้ว่า เป็นค่า ความน่าจะเป็นแบบมีเงื่อนไข (conditional probability value) อธิบายได้ว่าคือความน่าจะเป็นที่ pixel นั้นๆ จะอยู่ในเงื่อนไขเดียวกับ hist


ซ้ายบนคือ histogram ของมือ(ไม่ใช่ของขวาบน)
ขวาบนคือภาพที่เราจะทดสอบ
ซ้ายล่างคือ histogram ของภาพที่จะทดสอบ(ภาพขวาบน)
ขวาล่างคือค่าความน่าจะเป็น p(C|F)

ลองดูตัวอย่าง ถ้า C เป็นสีของ pixel และ F คือความน่าจะเป็นที่สีนั้นจะเป็นสีผิวหนัง ดังนั้น probability map(back_project) อันนี้ ให้ค่า p(C|F)

เพราะว่าค่าตรงนี้คือค่าใน bin หรือความถี่ ของ histogram ของผิว ที่ associate กับจุดนั้น ความหมายของความถี่คือจำนวน ครั้งที่เกิดขึ้นของ สีนั้นๆ ใน histogram p(C)เมื่อ histogram นี่เป็น histogram ที่คำนวณจากภาพผิวหนัง(ในตัวอย่างคือภาพมือ) เพราะฉะนั้น ค่าที่ได้คือ p(C|F)

ความน่าจะเป็นที่จะเป็นเป็น C เมื่อวัตถุนั้นเป็นผิวหนัง(F)  ในทางกลับกันเราต้องการหา ความน่าจะเป็นที่ pixel ตรงนั้นจะเป็นผิวหนัง(F) ในเงื่อนไขที่ว่า pixel ตรงนั้นมีสี C หรือ p(F|C) แต่ความน่าจะเป็นทั้งสองนี้สัมพันธ์กันโดย bayes's theorem  ดังนั้น ถ้าเรารู้ความน่าจะเป็นที่จะเจอสีผิวในภาพ p(C)  และความน่าจะป็นในการจะเจอบริเวณส่วนที่เป็นเนื้อในภาพ p(F) เราสามารถคำนวณหา p(F|C) ได้

จริงๆ ตรงนี้เห็นว่ามันแปรผันตรงกัน ค่า p(C|F) มากก็น่าจะหมายถึง ค่า p(F|C) มาก และน่าจะเป็นจริงในทางกลับกัน


ข้อควรระวังในการใช้ฟังก์ชันนี้คือถ้า back_project เป็น bytes image(ไม่ใช่ float) hist จะต้องไม่ถูก normalized หรือไม่ก็ scale ขึ้นมา เพราะการ normailized ทำให้ค่าสูงสุดใน hist คือหนึ่งค่าที่ต่ำกว่านั้นจะถูกปัดเป็น 0 ในภาพแบบ bytes



patch based back projection

เราใช้ back projection ในการดูว่า pixel น่าจะเป็นวัตถุที่เราสนใจขนาดไหน (วัตถุที่เราสนใจถูกแทนด้วย histogram) ซึ่งไม่ตรง(กันทีเดียว)กับการคำนวณความน่าจะเป็นที่วัตถุนั้นจะปรากฏในภาพ ทางเลือกอีกทางคือ กำหนด sub region ของภาพ แล้วคำนวณหา feature histogram ของ sub region  แล้วดูว่า match กับ histogram ของ model (ตัวอย่าง, วัตถุที่ต้องการหา) หลังจากนั้น เราจะ associate แต่ละ sub region ด้วยความน่าจะเป็นที่ sub region นั้นจะเป็น วัตถุที่เราสนใจ

ในขณะที่ cvCalcBackProject คำนวนความน่าจะเป็นของ pixel จะเป็นส่วนหนึ่งของ object ที่สนใจ  cvCalcBackProjectPatch จะคำนวณความน่าจะเป็น ที่ patch จะมี object ที่สนใจอยู่ โดย cvCalcBackProjectPatch จะคำนวณพื้นที่ patch นั้นไปทั่ว image แล้วคำนวณค่าใน patch นั้นเพื่อมาใส่ในผลลัพธ์(คล้ายๆ กับการทำ filter) โดยเหตุผลคือ ณ จุดนั้นๆ เพียงจุดเดียวไม่สามารถ ระบุถึง feature บางอย่างบริเวณจุดนั้นได้(เช่น texture ไม่สามารถระบุได้โดยใช้จุดเพียงจุดเดียว)

ลองดูพารามิเตอร์ของฟังก์ชัน

void cvCalcBackProjectPatch(
    IplImage** images,
    CvArr* dst,
    CvSize patch_size,
    CvHistogram* hist,
    int method,
    float factor
);

hist คือ histogram ของ object
dst เป็นผลลัพธ์ ที่มี 1 channel ขนาดลดลงตามขอบเนื่องจากการรันของ patch
patch_size คือ ขนาดของ patch
method เป็น method ที่เดียวกับที่ใช้เปรียบเทียบ histogram ในฟังกชัน cvCompareHist
factor คือระดับการ normalized ถ้าต้องการให้ภาพผลลัพธ์เห็นชัดขึ้น(ขยายความสว่าง) ก็สามารถปรับค่านี้ได้

ผลที่ได้คือความน่าจะเป็นของวัตถุ ในตำแหน่งต่างๆ หลังจากนั้น เราอาจจะใช้ คำสั่ง cvMinMaxLoc ในการหาตำแหน่งของภาพที่มีค่า สุงสุดหรือต่ำสุด

Learning OpenCV: Comparing Two Histograms

เรื่องสำคัญใน CV เรื่องหนึ่งคือการเปรียบเทียบ histogram ซึ่ง OpenCV มีฟังก์ชัน


double cvCompareHist(
    const CvHistogram* hist1,
    const CvHistogram* hist2,
    int method
);

method หมายถึง algorithm ที่ใช้ในการคำนวณเปรียบเทียบความแตกต่างของ histogram มีสี่วิธีคือ

  1. Correlation (method = CV_COMP_CORREL)
  2. Chi-square (method = CV_COMP_CHISQR)
  3. Intersection (method = CV_COMP_INTERSECT)
  4. Bhattacharyya distance (method = CV_COMP_BHATTACHARYYA)
histogram ต้อง normalized ก่อนจะมาเปรีบบเทียบ

ผลที่ได้ขึ่นอยู่กับ method ที่ใช้ในฟังก์ชัน เช่น exact match CV_COMP_CORREL, CV_COMP_INTERSECT จะได้ค่า 1, CV_COMP_CHISQR, CV_COMP_BATTACHARYYA ได้ค่า 0

ลองดูผลการคำนวณ เปรีบยเทียบ สี่วิธีกับ EMD (Earth Mover's Distance)


ปัญหาที่เกิดขึ้นคือจาก model ต้นแบบ(แถวแรก) กับ model ทดสอบที่สาม (total mis-match) ในแง่ของ CV จริงๆ แล้วอาจจะสื่อถึงสิ่งเดียวกันก็ได้ เช่น ภาพถ่ายมือ ในที่ๆ แสดงไม่เท่ากัน จึงการเลื่อนของ histogram   ถ้าเลื่อน histogram ทดสอบอันที่สามมาทางซ้าย จะได้ผล exact match  ซึ่ง EMD จะให้ค่าของจำนวน bins ที่ต้อง shift ให้ histogram เพื่อให้เกิด exact match ในกรณีตารางตัวอย่างนี้ จะเห็นได้ว่า EMD สามารถ ทายค่าของ Total mis-match ได้ถูกต้อง ในแง่ของความคล้ายระหว่างสอง histogram ในขณะที่ อีกสี่วิธีบอกไม่ได้ การคำนวณหา EMD มีฟังก์ขันแบบง่ายดังนี้ (ในหนังสือไม่ได้อธิบายพารามิเตอร์ของแบบยากไว้ และคิดว่าไม่น่าจำเป็น)

float cvCalcEMD2(
    const CvArr* signature1,
    const CvArr* signature2,
    int distance_type
);

distance_type ในที่นี้มีสองประเภทคือ Manhattan distance (CV_DIST_L1) หรือ Euclidean distance (CV_DIST_L2)
signature1, signature2 คือ CvArr ที่จะเปรียบเทียบหา EMD โดย เป็น Array ที่ประกอบด้วยค่าตามด้วยพิกัด
เช่นในกรณีที่มีมิติเดียว(ตังตัวอย่าง)  ตัว model จะได้ signature เป็น [1,0:0,1] ([ค่าหนึ่ง, ในตำแหน่งที่ศูนย์ และ : ค่าศูนย์, ในตำแหน่งที่หนึ่ง]) half match จะได้ signature เป็น  [0.5,0:0.5,1] ([ค่า 0.5, ในตำแหน่งที่ศูนย์ และ : ค่า 0.5, ในตำแหน่งที่หนึ่ง])  ในกรณีของ 3 มิติ จะได้ signature เป็น [value, x : y,z]


ลองดูตัวอย่าง 7-3 ในหนังสือ Learning OpenCV ซึ่งจะแสดงขันตอน การเปรียบเทียบ histogram โดยใช้ EMD เริ่มตั้งแต่การสร้าง signature และคำนวณค่า EMD