You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
255 lines
9.8 KiB
255 lines
9.8 KiB
''' 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)
|
|
|