Skip to content
Snippets Groups Projects
Commit d77ef348 authored by Manuel Günther's avatar Manuel Günther
Browse files

Added extractors from FaceRecLib

parent 97ef6a7c
No related branches found
No related tags found
No related merge requests found
Showing
with 716 additions and 1 deletion
from . import preprocessor
from . import extractor
from . import algorithm
from . import test
......
#!/usr/bin/env python
import bob.bio.face
extractor = bob.bio.face.extractor.DCTBlocks(
block_size = 12,
block_overlap = 11,
number_of_dct_coefficients = 45
)
#!/usr/bin/env python
import bob.bio.face
# compute eigenfaces using the training database
extractor = bob.bio.face.extractor.Eigenface(
subspace_dimension = 100
)
#!/usr/bin/env python
import bob.bio.base
import bob.bio.face
import math
# load the face cropping parameters
cropper = bob.bio.base.load_resource("face-crop-eyes", "preprocessor")
extractor = bob.bio.face.extractor.GridGraph(
# Gabor parameters
gabor_sigma = math.sqrt(2.) * math.pi,
# what kind of information to extract
normalize_gabor_jets = True,
# setup of the fixed grid
node_distance = (4, 4),
first_node = (6, 6),
image_resolution = cropper.cropped_image_size
)
#!/usr/bin/env python
import bob.bio.face
import math
# feature extraction
extractor = bob.bio.face.extractor.LGBPHS(
# block setup
block_size = 10,
block_overlap = 4,
# Gabor parameters
gabor_sigma = math.sqrt(2.) * math.pi,
# LBP setup (we use the defaults)
# histogram setup
sparse_histogram = True
)
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
# Laurent El Shafey <Laurent.El-Shafey@idiap.ch>
"""Features for face recognition"""
import bob.ip.base
import numpy
from bob.bio.base.extractor import Extractor
class DCTBlocks (Extractor):
"""Extracts DCT blocks"""
def __init__(
self,
block_size = 12, # 1 or two parameters for block size
block_overlap = 11, # 1 or two parameters for block overlap
number_of_dct_coefficients = 45,
normalize_blocks = True,
normalize_dcts = True,
auto_reduce_coefficients = False
):
# call base class constructor
Extractor.__init__(
self,
block_size = block_size,
block_overlap = block_overlap,
number_of_dct_coefficients = number_of_dct_coefficients,
normalize_blocks = normalize_blocks,
normalize_dcts = normalize_dcts,
auto_reduce_coefficients = auto_reduce_coefficients
)
# block parameters
block_size = block_size if isinstance(block_size, (tuple, list)) else (block_size, block_size)
block_overlap = block_overlap if isinstance(block_overlap, (tuple, list)) else (block_overlap, block_overlap)
if block_size[0] < block_overlap[0] or block_size[1] < block_overlap[1]:
raise ValueError("The overlap '%s' is bigger than the block size '%s'. This won't work. Please check your setup!"%(block_overlap, block_size))
if block_size[0] * block_size[1] <= number_of_dct_coefficients:
if auto_reduce_coefficients:
number_of_dct_coefficients = block_size[0] * block_size[1] - 1
else:
raise ValueError("You selected more coefficients %d than your blocks have %d. This won't work. Please check your setup!"%(number_of_dct_coefficients, block_size[0] * block_size[1]))
self.dct_features = bob.ip.base.DCTFeatures(number_of_dct_coefficients, block_size, block_overlap, normalize_blocks, normalize_dcts)
def __call__(self, image):
"""Computes and returns DCT blocks for the given input image"""
assert isinstance(image, numpy.ndarray)
assert image.ndim == 2
assert image.dtype == numpy.float64
# Computes DCT features
return self.dct_features(image)
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
# Manuel Guenther <Manuel.Guenther@idiap.ch>
import numpy
import bob.learn.linear
import bob.io.base
from bob.bio.base.extractor import Extractor
import logging
logger = logging.getLogger("bob.bio.face")
class Eigenface (Extractor):
"""Extracts grid graphs from the images"""
def __init__(self, subspace_dimension):
# We have to register that this function will need a training step
Extractor.__init__(self, requires_training = True, subspace_dimension = subspace_dimension)
self.subspace_dimension = subspace_dimension
def _check_data(self, data):
assert isinstance(data, numpy.ndarray)
assert data.ndim == 2
assert data.dtype == numpy.float64
def train(self, image_list, extractor_file):
"""Trains the eigenface extractor using the given list of training images"""
[self._check_data(image) for image in image_list]
# Initializes an array for the data
data = numpy.vstack([image.flatten() for image in image_list])
logger.info(" -> Training LinearMachine using PCA (SVD)")
t = bob.learn.linear.PCATrainer()
self.machine, __eig_vals = t.train(data)
# Machine: get shape, then resize
self.machine.resize(self.machine.shape[0], self.subspace_dimension)
self.machine.save(bob.io.base.HDF5File(extractor_file, "w"))
def load(self, extractor_file):
# read PCA projector
self.machine = bob.learn.linear.Machine(bob.io.base.HDF5File(extractor_file))
def __call__(self, image):
"""Projects the data using the stored covariance matrix"""
self._check_data(image)
# Projects the data
return self.machine(image.flatten())
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
# Manuel Guenther <Manuel.Guenther@idiap.ch>
import bob.ip.gabor
import bob.io.base
import numpy
import math
from bob.bio.base.extractor import Extractor
class GridGraph (Extractor):
"""Extracts grid graphs from the images"""
def __init__(
self,
# Gabor parameters
gabor_directions = 8,
gabor_scales = 5,
gabor_sigma = 2. * math.pi,
gabor_maximum_frequency = math.pi / 2.,
gabor_frequency_step = math.sqrt(.5),
gabor_power_of_k = 0,
gabor_dc_free = True,
# what kind of information to extract
normalize_gabor_jets = True,
# setup of the aligned grid
eyes = None, # if set, the grid setup will be aligned to the eye positions {'leye' : LEFT_EYE_POS, 'reye' : RIGHT_EYE_POS},
nodes_between_eyes = 4,
nodes_along_eyes = 2,
nodes_above_eyes = 3,
nodes_below_eyes = 7,
# setup of static grid
node_distance = None, # one or two integral values
image_resolution = None, # always two integral values
first_node = None, # one or two integral values, or None -> automatically determined
):
# call base class constructor
Extractor.__init__(
self,
gabor_directions = gabor_directions,
gabor_scales = gabor_scales,
gabor_sigma = gabor_sigma,
gabor_maximum_frequency = gabor_maximum_frequency,
gabor_frequency_step = gabor_frequency_step,
gabor_power_of_k = gabor_power_of_k,
gabor_dc_free = gabor_dc_free,
normalize_gabor_jets = normalize_gabor_jets,
eyes = eyes,
nodes_between_eyes = nodes_between_eyes,
nodes_along_eyes = nodes_along_eyes,
nodes_above_eyes = nodes_above_eyes,
nodes_below_eyes = nodes_below_eyes,
node_distance = node_distance,
image_resolution = image_resolution,
first_node = first_node
)
# create Gabor wavelet transform class
self.gwt = bob.ip.gabor.Transform(
number_of_scales = gabor_scales,
number_of_directions = gabor_directions,
sigma = gabor_sigma,
k_max = gabor_maximum_frequency,
k_fac = gabor_frequency_step,
power_of_k = gabor_power_of_k,
dc_free = gabor_dc_free
)
# create graph extractor
if eyes is not None:
self.graph = bob.ip.gabor.Graph(
righteye = [int(e) for e in eyes['reye']],
lefteye = [int(e) for e in eyes['leye']],
between = int(nodes_between_eyes),
along = int(nodes_along_eyes),
above = int(nodes_above_eyes),
below = int(nodes_below_eyes)
)
else:
if node_distance is None or image_resolution is None:
raise ValueError("Please specify either 'eyes' or the grid parameters 'first_node', 'last_node', and 'node_distance'!")
if isinstance(node_distance, (int, float)):
node_distance = (int(node_distance), int(node_distance))
if first_node is None:
first_node = [0,0]
for i in (0,1):
offset = int((image_resolution[i] - int(image_resolution[i]/node_distance[i])*node_distance[i]) / 2)
if offset < node_distance[i]//2: # This is not tested, but should ALWAYS be the case.
offset += node_distance[i]//2
first_node[i] = offset
last_node = tuple([int(image_resolution[i] - max(first_node[i],1)) for i in (0,1)])
# take the specified nodes
self.graph = bob.ip.gabor.Graph(
first = first_node,
last = last_node,
step = node_distance
)
self.normalize_jets = normalize_gabor_jets
self.trafo_image = None
def __call__(self, image):
assert image.ndim == 2
assert isinstance(image, numpy.ndarray)
assert image.dtype == numpy.float64
if self.trafo_image is None or self.trafo_image.shape[1:3] != image.shape:
# create trafo image
self.trafo_image = numpy.ndarray((self.gwt.number_of_wavelets, image.shape[0], image.shape[1]), numpy.complex128)
# perform Gabor wavelet transform
self.gwt.transform(image, self.trafo_image)
# extract face graph
jets = self.graph.extract(self.trafo_image)
# normalize the Gabor jets of the graph only
if self.normalize_jets:
[j.normalize() for j in jets]
# return the extracted face graph
return jets
def write_feature(self, feature, feature_file):
feature_file = feature_file if isinstance(feature_file, bob.io.base.HDF5File) else bob.io.base.HDF5File(feature_file, 'w')
bob.ip.gabor.save_jets(feature, feature_file)
def read_feature(self, feature_file):
return bob.ip.gabor.load_jets(bob.io.base.HDF5File(feature_file))
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
# Manuel Guenther <Manuel.Guenther@idiap.ch>
import bob.ip.gabor
import bob.ip.base
import numpy
import math
from bob.bio.base.extractor import Extractor
class LGBPHS (Extractor):
"""Extractor for local Gabor binary pattern histogram sequences"""
def __init__(
self,
# Block setup
block_size, # one or two parameters for block size
block_overlap = 0, # one or two parameters for block overlap
# Gabor parameters
gabor_directions = 8,
gabor_scales = 5,
gabor_sigma = 2. * math.pi,
gabor_maximum_frequency = math.pi / 2.,
gabor_frequency_step = math.sqrt(.5),
gabor_power_of_k = 0,
gabor_dc_free = True,
use_gabor_phases = False,
# LBP parameters
lbp_radius = 2,
lbp_neighbor_count = 8,
lbp_uniform = True,
lbp_circular = True,
lbp_rotation_invariant = False,
lbp_compare_to_average = False,
lbp_add_average = False,
# histogram options
sparse_histogram = False,
split_histogram = None
):
"""Initializes the local Gabor binary pattern histogram sequence tool chain with the given file selector object"""
# call base class constructor
Extractor.__init__(
self,
block_size = block_size,
block_overlap = block_overlap,
gabor_directions = gabor_directions,
gabor_scales = gabor_scales,
gabor_sigma = gabor_sigma,
gabor_maximum_frequency = gabor_maximum_frequency,
gabor_frequency_step = gabor_frequency_step,
gabor_power_of_k = gabor_power_of_k,
gabor_dc_free = gabor_dc_free,
use_gabor_phases = use_gabor_phases,
lbp_radius = lbp_radius,
lbp_neighbor_count = lbp_neighbor_count,
lbp_uniform = lbp_uniform,
lbp_circular = lbp_circular,
lbp_rotation_invariant = lbp_rotation_invariant,
lbp_compare_to_average = lbp_compare_to_average,
lbp_add_average = lbp_add_average,
sparse_histogram = sparse_histogram,
split_histogram = split_histogram
)
# block parameters
self.block_size = block_size if isinstance(block_size, (tuple, list)) else (block_size, block_size)
self.block_overlap = block_overlap if isinstance(block_overlap, (tuple, list)) else (block_overlap, block_overlap)
if self.block_size[0] < self.block_overlap[0] or self.block_size[1] < self.block_overlap[1]:
raise ValueError("The overlap is bigger than the block size. This won't work. Please check your setup!")
# Gabor wavelet transform class
self.gwt = bob.ip.gabor.Transform(
number_of_scales = gabor_scales,
number_of_directions = gabor_directions,
sigma = gabor_sigma,
k_max = gabor_maximum_frequency,
k_fac = gabor_frequency_step,
power_of_k = gabor_power_of_k,
dc_free = gabor_dc_free
)
self.trafo_image = None
self.use_phases = use_gabor_phases
self.lbp = bob.ip.base.LBP(
neighbors = lbp_neighbor_count,
radius = float(lbp_radius),
circular = lbp_circular,
to_average = lbp_compare_to_average,
add_average_bit = lbp_add_average,
uniform = lbp_uniform,
rotation_invariant = lbp_rotation_invariant,
border_handling = 'wrap'
)
self.split = split_histogram
self.sparse = sparse_histogram
if self.sparse and self.split:
raise ValueError("Sparse histograms cannot be split! Check your setup!")
def _fill(self, lgbphs_array, lgbphs_blocks, j):
"""Copies the given array into the given blocks"""
# fill array in the desired shape
if self.split is None:
start = j * self.n_bins * self.n_blocks
for b in range(self.n_blocks):
lgbphs_array[start + b * self.n_bins : start + (b+1) * self.n_bins] = lgbphs_blocks[b][:]
elif self.split == 'blocks':
for b in range(self.n_blocks):
lgbphs_array[b, j * self.n_bins : (j+1) * self.n_bins] = lgbphs_blocks[b][:]
elif self.split == 'wavelets':
for b in range(self.n_blocks):
lgbphs_array[j, b * self.n_bins : (b+1) * self.n_bins] = lgbphs_blocks[b][:]
elif self.split == 'both':
for b in range(self.n_blocks):
lgbphs_array[j * self.n_blocks + b, 0 : self.n_bins] = lgbphs_blocks[b][:]
def _sparsify(self, array):
"""This function generates a sparse histogram from a non-sparse one."""
if not self.sparse:
return array
if len(array.shape) == 2 and array.shape[0] == 2:
# already sparse
return array
assert len(array.shape) == 1
indices = []
values = []
for i in range(array.shape[0]):
if array[i] != 0.:
indices.append(i)
values.append(array[i])
return numpy.array([indices, values], dtype = numpy.float64)
def __call__(self, image):
"""Extracts the local Gabor binary pattern histogram sequence from the given image"""
assert image.ndim == 2
assert isinstance(image, numpy.ndarray)
assert image.dtype == numpy.float64
# perform GWT on image
if self.trafo_image is None or self.trafo_image.shape[1:3] != image.shape:
# create trafo image
self.trafo_image = numpy.ndarray((self.gwt.number_of_wavelets, image.shape[0], image.shape[1]), numpy.complex128)
# perform Gabor wavelet transform
self.gwt.transform(image, self.trafo_image)
jet_length = self.gwt.number_of_wavelets * (2 if self.use_phases else 1)
lgbphs_array = None
# iterate through the layers of the trafo image
for j in range(self.gwt.number_of_wavelets):
# compute absolute part of complex response
abs_image = numpy.abs(self.trafo_image[j])
# Computes LBP histograms
abs_blocks = bob.ip.base.lbphs(abs_image, self.lbp, self.block_size, self.block_overlap)
# Converts to Blitz array (of different dimensionalities)
self.n_bins = abs_blocks.shape[1]
self.n_blocks = abs_blocks.shape[0]
if self.split is None:
shape = (self.n_blocks * self.n_bins * jet_length,)
elif self.split == 'blocks':
shape = (self.n_blocks, self.n_bins * jet_length)
elif self.split == 'wavelets':
shape = (jet_length, self.n_bins * self.n_blocks)
elif self.split == 'both':
shape = (jet_length * self.n_blocks, self.n_bins)
else:
raise ValueError("The split parameter must be one of ['blocks', 'wavelets', 'both'] or None")
# create new array if not done yet
if lgbphs_array is None:
lgbphs_array = numpy.ndarray(shape, 'float64')
# fill the array with the absolute values of the Gabor wavelet transform
self._fill(lgbphs_array, abs_blocks, j)
if self.use_phases:
# compute phase part of complex response
phase_image = numpy.angle(self.trafo_image[j])
# Computes LBP histograms
phase_blocks = bob.ip.base.lbphs(phase_image, self.lbp, self.block_size, self.block_overlap)
# fill the array with the phases at the end of the blocks
self._fill(lgbphs_array, phase_blocks, j + self.gwt.number_of_wavelets)
# return the concatenated list of all histograms
return self._sparsify(lgbphs_array)
from .DCTBlocks import DCTBlocks
from .GridGraph import GridGraph
from .LGBPHS import LGBPHS
from .Eigenface import Eigenface
from . import dummy
File added
File added
File added
File added
File added
File added
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
# @author: Manuel Guenther <Manuel.Guenther@idiap.ch>
# @date: Thu May 24 10:41:42 CEST 2012
#
# Copyright (C) 2011-2012 Idiap Research Institute, Martigny, Switzerland
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import bob.bio.base
import bob.bio.face
import unittest
import os
import numpy
import math
from nose.plugins.skip import SkipTest
import bob.io.base.test_utils
from bob.bio.base.test import utils
import pkg_resources
regenerate_refs = False
def _compare(data, reference, write_function = bob.bio.base.save, read_function = bob.bio.base.load):
# write reference?
if regenerate_refs:
write_function(data, reference)
# compare reference
reference = read_function(reference)
assert numpy.allclose(data, reference, atol=1e-5)
def _data():
return bob.bio.base.load(pkg_resources.resource_filename('bob.bio.face.test', 'data/cropped.hdf5'))
def test_dct_blocks():
# read input
data = _data()
dct = bob.bio.base.load_resource('dct-blocks', 'extractor')
assert isinstance(dct, bob.bio.face.extractor.DCTBlocks)
assert isinstance(dct, bob.bio.base.extractor.Extractor)
assert not dct.requires_training
# generate smaller extractor, using mixed tuple and int input for the block size and overlap
dct = bob.bio.face.extractor.DCTBlocks(8, (0,0), 15)
# extract features
feature = dct(data)
assert feature.ndim == 2
# feature dimension is one lower than the block size, since blocks are normalized by default
assert feature.shape == (80, 14)
reference = pkg_resources.resource_filename('bob.bio.face.test', 'data/dct_blocks.hdf5')
_compare(feature, reference, dct.write_feature, dct.read_feature)
def test_graphs():
data = _data()
graph = bob.bio.base.load_resource('grid-graph', 'extractor')
assert isinstance(graph, bob.bio.face.extractor.GridGraph)
assert isinstance(graph, bob.bio.base.extractor.Extractor)
assert not graph.requires_training
# generate smaller extractor, using mixed tuple and int input for the node distance and first location
graph = bob.bio.face.extractor.GridGraph(node_distance = 24, image_resolution = data.shape)
# extract features
feature = graph(data)
reference = pkg_resources.resource_filename('bob.bio.face.test', 'data/graph_regular.hdf5')
# write reference?
if regenerate_refs:
graph.write_feature(feature, reference)
# compare reference
reference = graph.read_feature(reference)
assert len(reference) == len(feature)
assert all(isinstance(f, bob.ip.gabor.Jet) for f in feature)
assert all(numpy.allclose(r.jet, f.jet) for r,f in zip(reference, feature))
# get reference face graph extractor
cropper = bob.bio.base.load_resource('face-crop-eyes', 'preprocessor')
eyes = cropper.cropped_positions
# generate aligned graph extractor
graph = bob.bio.face.extractor.GridGraph(
# setup of the aligned grid
eyes = eyes,
nodes_between_eyes = 4,
nodes_along_eyes = 2,
nodes_above_eyes = 2,
nodes_below_eyes = 7
)
nodes = graph.graph.nodes
assert len(nodes) == 100
assert numpy.allclose(nodes[22], eyes['reye'])
assert numpy.allclose(nodes[27], eyes['leye'])
assert nodes[0] < eyes['reye']
assert nodes[-1] > eyes['leye']
def test_lgbphs():
data = _data()
lgbphs = bob.bio.base.load_resource('lgbphs', 'extractor')
assert isinstance(lgbphs, bob.bio.face.extractor.LGBPHS)
assert isinstance(lgbphs, bob.bio.base.extractor.Extractor)
assert not lgbphs.requires_training
# in this test, we use a smaller setup of the LGBPHS features
lgbphs = bob.bio.face.extractor.LGBPHS(
block_size = 8,
block_overlap = 0,
gabor_directions = 4,
gabor_scales = 2,
gabor_sigma = math.sqrt(2.) * math.pi,
sparse_histogram = True
)
# extract feature
feature = lgbphs(data)
assert feature.ndim == 2
reference = pkg_resources.resource_filename('bob.bio.face.test', 'data/lgbphs_sparse.hdf5')
_compare(feature, reference, lgbphs.write_feature, lgbphs.read_feature)
# generate new non-sparse extractor including Gabor phases
lgbphs = bob.bio.face.extractor.LGBPHS(
block_size = 8,
block_overlap = 0,
gabor_directions = 4,
gabor_scales = 2,
gabor_sigma = math.sqrt(2.) * math.pi,
use_gabor_phases = True
)
feature = lgbphs(data)
assert feature.ndim == 1
reference = pkg_resources.resource_filename('bob.bio.face.test', 'data/lgbphs_with_phase.hdf5')
_compare(feature, reference, lgbphs.write_feature, lgbphs.read_feature)
def test_eigenface():
temp_file = bob.io.base.test_utils.temporary_filename()
data = _data()
eigen1 = bob.bio.base.load_resource('eigenface', 'extractor')
assert isinstance(eigen1, bob.bio.face.extractor.Eigenface)
assert isinstance(eigen1, bob.bio.base.extractor.Extractor)
assert eigen1.requires_training
# create extractor with a smaller number of kept eigenfaces
train_data = utils.random_training_set(data.shape, 400, 0., 255.)
eigen2 = bob.bio.face.extractor.Eigenface(subspace_dimension = 5)
reference = pkg_resources.resource_filename('bob.bio.face.test', 'data/eigenface_extractor.hdf5')
try:
# train the projector
eigen2.train(train_data, temp_file)
assert os.path.exists(temp_file)
if regenerate_refs: shutil.copy(temp_file, reference_file)
# check projection matrix
eigen1.load(reference)
eigen2.load(temp_file)
assert eigen1.machine.shape == eigen2.machine.shape
for i in range(5):
assert numpy.abs(eigen1.machine.weights[:,i] - eigen2.machine.weights[:,i] < 1e-5).all() or numpy.abs(eigen1.machine.weights[:,i] + eigen2.machine.weights[:,i] < 1e-5).all()
finally:
if os.path.exists(temp_file): os.remove(temp_file)
# now, we can execute the extractor and check that the feature is still identical
feature = eigen1(data)
assert feature.ndim == 1
reference = pkg_resources.resource_filename('bob.bio.face.test', 'data/eigenface_feature.hdf5')
_compare(feature, reference, eigen1.write_feature, eigen1.read_feature)
"""
def test05_sift_key_points(self):
# check if VLSIFT is available
import bob.ip.base
if not hasattr(bob.ip.base, "VLSIFT"):
raise SkipTest("VLSIFT is not part of bob.ip.base; maybe SIFT headers aren't installed in your system?")
# we need the preprocessor tool to actually read the data
preprocessor = facereclib.preprocessing.Keypoints()
data = preprocessor.read_data(self.input_dir('key_points.hdf5'))
# now, we extract features from it
extractor = self.config('sift')
feature = self.execute(extractor, data, 'sift.hdf5', epsilon=1e-4)
self.assertEqual(len(feature.shape), 1)
"""
......@@ -120,6 +120,10 @@ setup(
],
'bob.bio.extractor': [
'dct-blocks = bob.bio.face.config.extractor.dct_blocks:extractor', # DCT blocks
'grid-graph = bob.bio.face.config.extractor.grid_graph:extractor', # Grid graph
'lgbphs = bob.bio.face.config.extractor.lgbphs:extractor', # LGBPHS
'eigenface = bob.bio.face.config.extractor.eigenface:extractor', # Eigenface
],
'bob.bio.algorithm': [
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment