Holger Frey
7 years ago
2 changed files with 303 additions and 1 deletions
@ -1,2 +1,49 @@
@@ -1,2 +1,49 @@
|
||||
# contactangle |
||||
collecting data from measured contact angles |
||||
============================================ |
||||
|
||||
While measuring contact angles, for each drop three pictures are recorded |
||||
for the static, advancing and the receeding contact angle. |
||||
|
||||
Usually the file name of this pictures consists of four parts: |
||||
|
||||
1. an identifier for the measurement |
||||
2. what type of angle was measured, e.g. RCA for receeding contact angle |
||||
3. the measured contact angle on the left side of the drop |
||||
4. the measured contact angle on the right side of the drop |
||||
|
||||
So a typical file name looks quite cyptic like: 'PS1 RCA 12,5 15,6' |
||||
|
||||
In adittion to cryptic file names with abbreviations, there is normally |
||||
quite an amount of files: 5 measurements x 3 types = 15 files for one |
||||
substrate or coating alone. |
||||
|
||||
This script provides means to rename these files to a more verbose version |
||||
and to collect the data into a text file that can be imported in excel. |
||||
|
||||
Since the naming of the type of measurement and the contact angles themselves |
||||
are somehow fixed, the 'user defined action' is renaming the identifier. To |
||||
do such a rename, a function accepting the identifier as string and returning |
||||
a verbose version as a string needs to be provided |
||||
|
||||
An Example |
||||
>>> import conactangle as ca |
||||
>>> def verbose_id(identifier): |
||||
... if identifier.startswith('PS'): |
||||
... verbose_name = 'Polystyrol' |
||||
... else: |
||||
... verbose_name = 'Unknown Substrate' |
||||
... number = identifier[-1] |
||||
... return '{}, Measurement {}'.format(verbose_name, number) |
||||
... |
||||
>>> ca.rename(verbose_id, path='/example/directory') |
||||
|
||||
This will change the cryptic filename 'PS1 RCA 12,5 15,6.bmp' to |
||||
'Polystyrol, Measurement 1, receeding, L12,5 R15,6.bmp' |
||||
|
||||
To not tinker with the original files, the 'renaming' is done on a copy of |
||||
the orignal data. The results file is called 'results.txt' and is created |
||||
in the same directory where the raw data resides. |
||||
|
||||
This is actually a two step process: first the files are processed and the |
||||
future result of the rename is printed to stdout. If no exception occurs, |
||||
the renaming of the files is done in a second round. |
||||
|
@ -0,0 +1,255 @@
@@ -0,0 +1,255 @@
|
||||
''' collecting data from measured contact angles |
||||
|
||||
While measuring contact angles, for each drop three pictures are recorded |
||||
for the static, advancing and the receeding contact angle. |
||||
|
||||
Usually the file name of this pictures consists of four parts: |
||||
1. an identifier for the measurement |
||||
2. what type of angle was measured, e.g. RCA for receeding contact angle |
||||
3. the measured contact angle on the left side of the drop |
||||
4. the measured contact angle on the right side of the drop |
||||
|
||||
So a typical file name looks quite cyptic like: 'PS1 RCA 12,5 15,6' |
||||
|
||||
In adittion to cryptic file names with abbreviations, there is normally |
||||
quite an amount of files: 5 measurements x 3 types = 15 files for one |
||||
substrate or coating alone. |
||||
|
||||
This script provides means to rename these files to a more verbose version |
||||
and to collect the data into a text file that can be imported in excel. |
||||
|
||||
Since the naming of the type of measurement and the contact angles themselves |
||||
are somehow fixed, the 'user defined action' is renaming the identifier. To |
||||
do such a rename, a function accepting the identifier as string and returning |
||||
a verbose version as a string needs to be provided |
||||
|
||||
An Example |
||||
>>> import conactangle as ca |
||||
>>> def verbose_id(identifier): |
||||
... if identifier.startswith('PS'): |
||||
... verbose_name = 'Polystyrol' |
||||
... else: |
||||
... verbose_name = 'Unknown Substrate' |
||||
... number = identifier[-1] |
||||
... return '{}, Measurement {}'.format(verbose_name, number) |
||||
... |
||||
>>> ca.rename(verbose_id, path='/example/directory') |
||||
This will change the cryptic filename 'PS1 RCA 12,5 15,6.bmp' to |
||||
'Polystyrol, Measurement 1, receeding, L12,5 R15,6.bmp' |
||||
|
||||
To not tinker with the original files, the 'renaming' is done on a copy of |
||||
the orignal data. The results file is called 'results.txt' and is created |
||||
in the same directory where the raw data resides. |
||||
|
||||
This is actually a two step process: first the files are processed and the |
||||
future result of the rename is printed to stdout. If no exception occurs, |
||||
the renaming of the files is done in a second round. |
||||
''' |
||||
|
||||
import os |
||||
import shutil |
||||
from collections import namedtuple, OrderedDict |
||||
|
||||
# constants |
||||
VERBOSE_TEMPLATE = '{i}, {m.type}, L{m.left} R{m.right}' |
||||
TYPE_ABBREVIATIONS = [(a[0], a) for a in ['static', 'receeding', 'advancing']] |
||||
NAN_STRING = '-' |
||||
|
||||
# constants for excel output |
||||
XLS_HEADLINE1 = ('Measurement', 'Position', 'Static', 'Advancing', 'Receeding') |
||||
XLS_HEADLINE2 = ('Measurement', '', 'Static', 'Advancing', 'Receeding') |
||||
XLS_CELLS = ('=A2', '{h}', '={f}(C2:C{x})', '={f}(D2:D{x})', '={f}(E2:E{x})') |
||||
XLS_AGGREGATIONS = ( |
||||
('n', 'ANZAHL'), ('Mittelwert', 'MITTELWERT'), ('Std.Abw.', 'STABW')) |
||||
|
||||
|
||||
# records the name of a source file |
||||
FileInfo = namedtuple('FileInfo', ['path', 'dir', 'name', 'ext']) |
||||
|
||||
# records the data found in the name of a source file |
||||
Measurement = namedtuple('Measurement', ['id', 'type', 'left', 'right']) |
||||
|
||||
|
||||
def rename(rename_func, path='.'): |
||||
''' renames contact angle source files in a folder |
||||
|
||||
rename_func: function that accepts an abbreviated identifier |
||||
and returns a verbose version |
||||
path: path to the folder to process |
||||
xls_dec_sep: decimal separator for the resulting xls file |
||||
''' |
||||
entries = sorted(os.listdir(path)) |
||||
files = [f for f in entries if not f.startswith('.')] |
||||
bitmaps = [f for f in files if f.endswith('.bmp')] |
||||
avis = [f for f in files if f.endswith('.avi')] |
||||
|
||||
# first a test run and if no exception occurs, rename the files |
||||
process_pictures(rename_func, bitmaps, rename=False) |
||||
process_movies(rename_func, avis, rename=False) |
||||
process_pictures(rename_func, bitmaps, rename=True) |
||||
process_movies(rename_func, avis, rename=True) |
||||
|
||||
|
||||
def process_movies(rename_func, avis, rename=False): |
||||
''' renames all given movies |
||||
|
||||
Movie file names only consist of the abbreviated identifer |
||||
|
||||
rename_func: function that accepts an abbreviated identifier |
||||
and returns a verbose version |
||||
avis: list of paths to movie files |
||||
rename: should the file be renamed or is this a test run |
||||
''' |
||||
for avi in avis: |
||||
source = get_file_info(avi) |
||||
verbose_name = rename_func(source.name) |
||||
mock_or_rename(source, verbose_name, rename) |
||||
|
||||
|
||||
def process_pictures(rename_func, bitmaps, rename=False): |
||||
''' renames all given pictures and collects the measured data |
||||
|
||||
Movie file names only consist of the abbreviated identifer |
||||
|
||||
rename_func: function that accepts an abbreviated identifier |
||||
and returns a verbose version |
||||
bitmaps: list of paths to bitmap files |
||||
rename: should the file be renamed or is this a test run |
||||
''' |
||||
# prepare a container for the measurement results |
||||
measurements = OrderedDict() |
||||
for bmp in bitmaps: |
||||
# retrive the measured value and the verbose identification |
||||
source = get_file_info(bmp) |
||||
measurement = extract_measurement(source.name) |
||||
verbose_id = rename_func(measurement.id) |
||||
# record the measured values |
||||
if verbose_id not in measurements: |
||||
measurements[verbose_id] = {'left': {}, 'right':{}} |
||||
measurements[verbose_id]['left'][measurement.type] = measurement.left |
||||
measurements[verbose_id]['right'][measurement.type] = measurement.right |
||||
# renaming the file |
||||
verbose_name = VERBOSE_TEMPLATE.format(i=verbose_id, m=measurement) |
||||
mock_or_rename(source, verbose_name, rename) |
||||
write_results(measurements, source.dir) |
||||
|
||||
|
||||
def extract_measurement(filename): |
||||
''' extract the abbr. id, type and result of a measurement in a filename |
||||
|
||||
A measurement is made up from the abbeviated id, type, left and right |
||||
contact angle separated by a space. |
||||
|
||||
filename: the file name without extension |
||||
''' |
||||
# use rsplit, the raw_id might contain a space itself |
||||
raw_id, raw_type, raw_left, raw_right = filename.rsplit(' ', 3) |
||||
measurement_type = get_measurement_type(raw_type) |
||||
left = get_contact_angle(raw_left) |
||||
right = get_contact_angle(raw_right) |
||||
return Measurement(raw_id, measurement_type, left, right) |
||||
|
||||
|
||||
def get_measurement_type(abbreviation): |
||||
''' returns the verbose measurement type |
||||
|
||||
Measurement types are 'advancing', 'receeding' or 'static'. Mostly they |
||||
will be abbreviated like aca, rca or sca. Since the letter 'a' is present |
||||
in all of them, first test is on 's' then on 'r'. |
||||
|
||||
abbreviation: the abbreviated measurement type |
||||
''' |
||||
abbr = abbreviation.lower() |
||||
for type_id, measurement_type in TYPE_ABBREVIATIONS: |
||||
if type_id in abbr: |
||||
return measurement_type |
||||
raise ValueError('Unknown measurement type: ' + abbreviation) |
||||
|
||||
|
||||
def get_contact_angle(raw_angle, decimal_separator=','): |
||||
''' returns a verified contact angle or a nice NaN representation |
||||
|
||||
raw_angle: raw notation of the angle value as found in the |
||||
file name |
||||
decimal_separator: character used as decimal separator. |
||||
Since I work in Germany, it defaults to a colon''' |
||||
# the conversion to and from a float is to verify it's a number |
||||
try: |
||||
angle = float(raw_angle.replace(decimal_separator, '.')) |
||||
except ValueError: |
||||
return NAN_STRING |
||||
angle_as_str = '{:.1f}'.format(angle) |
||||
return angle_as_str.replace('.', decimal_separator) |
||||
|
||||
|
||||
def get_file_info(path): |
||||
''' returns a named tuple with path, filename, extension, etc. |
||||
|
||||
path: path of the file |
||||
''' |
||||
folder = os.path.dirname(path) |
||||
filename_with_ext = os.path.basename(path) |
||||
filename, extension = os.path.splitext(filename_with_ext) |
||||
return FileInfo(path, folder, filename, extension) |
||||
|
||||
def mock_or_rename(source, destination_name, rename=False): |
||||
''' copies a source file to a new destination name or mocks such a 'rename' |
||||
|
||||
the new 'renamed' file will be in the same directory as the source file |
||||
|
||||
source: named tuple (FileInfo) |
||||
destination_name: name of the new file without extension |
||||
rename: is this a 'real rename' or just a test |
||||
''' |
||||
destination_path = os.path.join(source.dir, destination_name + source.ext) |
||||
if rename: |
||||
shutil.copyfile(source.path, destination_path) |
||||
else: |
||||
print('{} -> {}'.format(source.path, destination_path)) |
||||
|
||||
|
||||
def write_results(measurements, source_dir): |
||||
''' write the results of the measurements in a text file |
||||
|
||||
measurements: OrderedDict with the measurement results |
||||
source_dir: directory to store the results file |
||||
''' |
||||
results_path = os.path.join(source_dir, 'results.xls') |
||||
xls_result_line = 1 |
||||
with open(results_path, 'w') as filehandle: |
||||
filehandle.write(tabbed_line(*XLS_HEADLINE1)) |
||||
for idx, positions in measurements.items(): |
||||
for position, values in positions.items(): |
||||
s = values.get('static', NAN_STRING) |
||||
a = values.get('advancing', NAN_STRING) |
||||
r = values.get('receeding', NAN_STRING) |
||||
filehandle.write(tabbed_line(idx, position, s, a, r)) |
||||
xls_result_line += 1 |
||||
# two blank lines and data aggregation fields |
||||
filehandle.write('\n\n') |
||||
filehandle.write(tabbed_line(*XLS_HEADLINE2)) |
||||
formular_tpl = tabbed_line(*XLS_CELLS) |
||||
for human, function in XLS_AGGREGATIONS: |
||||
line = formular_tpl.format(h=human, f=function, x=xls_result_line) |
||||
filehandle.write(line) |
||||
|
||||
def tabbed_line(*args): |
||||
''' small helper function, returns the argumends a tab separated string ''' |
||||
return '\t'.join(args) + '\n' |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
|
||||
def nice_identity(raw_id): |
||||
if len(raw_id) != 3: |
||||
raise ValueError('unparsable identity: ' + raw_id) |
||||
substrate, slide, point = raw_id |
||||
if substrate in '125': |
||||
substrate = 'P(DMAA-{}%MABP)'.format(substrate) |
||||
elif substrate in 'ps': |
||||
substrate = 'P(S-5%MABP)' |
||||
else: |
||||
raise ValueError('Unknown raw id: ' + raw_id) |
||||
return '{}, Slide {}, Measurement {}'.format(substrate, slide, point) |
||||
|
||||
rename(nice_identity) |
Loading…
Reference in new issue