Commit 091de338 authored by Niels-Oliver Walkowski's avatar Niels-Oliver Walkowski
Browse files

Merge branch 'dev'

parents a334c077 0badb2c3
# Itten
Itten
=====
Itten is a python library to analyse and visualize color dynamics within moving
image files or frames that were extracted from moving image files.
Optimization
============
Eventuell kann ich einen ganzen Film oder zumindest die Kontrastwerte eines
ganzen Films speichern, wenn ich nur die Histogramme speicher (256 * frames)
ToDo
====
[] `MultivariatePlot` muss noch auf die selbe Simplizität wie `UnivariatePlot`
umgestellt werden
[] gettter und setter für Frames.start setzen und das Rekursionsproblem lösen
[x] Bisher funktioniert nur der LightDark Contrast, andere Kontraste
implementieren
[] frame_range aus UnivariatePlot in Frames outsourcen und Folgen in allen
Klassen beseitigen. (wieder rekursives Aufrufproblem in __init__
[] Möglichkeit view instanzen zu picklen (siehe Abschnitt)
[] Visualisierung innerhalb derer die Pixel des Ursprungsbildes entlang der
Achse des zu Grunde liegenden Kontrastwertes sortiert werden
[] helper function zur Umrechnung von Frames in Zeit und umgekehrt (mit Aufruf
von mplayer, Bildbetrachter an der entsprechenden Stell)
[] Sequence Daten (Plot) Clustern mit K-Means statt descriptiv zu evaluieren
[] Mittels Fancy Indexing, die Bereiche in eine View isolieren, die ich für
ein bestimmtes Muster identifiziert habe und dann statistisch beschreiben
Möglichkeiten View instanzen zu picklen
---------------------------------------
bereits ausprobiert, jedoch kan die Frames Instanz `_frames` die von der view
Instanz benötigt wird nach dem unpickling nicht aufgerufen werden, so dass
meine Methoden nicht mehr funktionieren. Zur Lösung der Problematik:
<http://stackoverflow.com/questions/3614379/attributeerror-when-unpickling-an-object>
<http://stackoverflow.com/questions/3363281/attributeerror-module-object-has-no-attribute-when-using-cpickle>
let SessionLoad = 1
let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0
let v:this_session=expand("<sfile>:p")
silent only
cd ~/Research/Projects/Cinemetrics/Itten
if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == ''
let s:wipebuf = bufnr('%')
endif
set shortmess=aoO
badd +60 testing/ittennb.py
badd +17 itten/movie.py
badd +25 itten/contrasts.py
badd +16 itten/helpers.py
badd +1023 term://.//20282:/usr/bin/zsh\ ;\#neoterm-1
argglobal
silent! argdel *
edit testing/ittennb.py
set splitbelow splitright
wincmd t
set winheight=1 winwidth=1
argglobal
setlocal fdm=manual
setlocal fde=0
setlocal fmr={{{,}}}
setlocal fdi=#
setlocal fdl=0
setlocal fml=1
setlocal fdn=20
setlocal fen
silent! normal! zE
let s:l = 1 - ((0 * winheight(0) + 34) / 68)
if s:l < 1 | let s:l = 1 | endif
exe s:l
normal! zt
1
normal! 0
tabnext 1
if exists('s:wipebuf') && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal'
silent exe 'bwipe ' . s:wipebuf
endif
unlet! s:wipebuf
set winheight=1 winwidth=20 shortmess=filnxtToO
let s:sx = expand("<sfile>:p:r")."x.vim"
if file_readable(s:sx)
exe "source " . fnameescape(s:sx)
endif
let &so = s:so_save | let &siso = s:siso_save
let g:this_session = v:this_session
let g:this_obsession = v:this_session
let g:this_obsession_status = 2
doautoall SessionLoadPost
unlet SessionLoad
" vim: set ft=vim :
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import numpy as np
import cv2 as cv
# TODO create generic class
# TODO import as cv
import cv2
import numpy as np
from .helpers import luminance
from copy import deepcopy
# subclassing numpy ndarray
# Vorgehen: https://docs.scipy.org/doc/numpy/user/basics.subclassing.html
# resize Probleme https://sourceforge.net/p/numpy/mailman/message/12594801/
# andere ownership Probleme könne angeblich mit out= gelöst werden
# "Use __new__ when you need to control the creation of a new instance.
# Use __init__ when you need to control initialization of a new instance."
class View(np.ndarray):
"""Core class for the representation of colour contrasts in Movies
View is the basis class for specific ways to represent colour contrasts.
It does hold the definitions of contrasts itself. Objects of class View
subclass the numpy array class and hence inherit numpy methods. However,
it is not recommended to use functions which manipulate the array in
terms of structure. In this case some of the additional functions which
are implemented by this class and its subclasses might not lead to
reasonable results.
Attributes:
TODO Docstring komplettieren und Verfahren überprüfen
"""
def __new__(cls, frames, input_array=None):
"""instantiates the view class
instantiation complies with the recommendation for subclassing
numpy.ndarray
Parameters
----------
frames : itten.movie.Frames
input_array : itten.contrasts.View # TODO wie schaffte np lower case object
contrast : String # Modifizieren Channel Integer to String
frame_step : Int
savefig : Boolean
Returns
-------
Object : View
Empty numpy.ndarray of type View
"""
obj = input_array
if type(obj) == np.ndarray:
obj = np.asarray(input_array, dtype=np.uint8).view(cls).copy()
else:
input_array = np.zeros((0), dtype=np.uint8)
obj = np.asarray(input_array).view(cls).copy()
obj._frames = frames
obj._contrast = 2
obj._frame_step = 10
obj._bins = 256
return obj
def __array_finalize__(self, obj):
if obj is None: return
self._frames = getattr(obj, '_frames', None)
self._contrast = getattr(obj, '_contrast', None)
self._frame_step = getattr(obj, '_frame_step', None)
self._bins = getattr(obj, '_bins', None)
def __array_wrap__(self, out_arr, context=None):
return np.ndarray.__array_wrap__(self, out_arr, context)
# subclassing subclass of numpy http://stackoverflow.com/questions/7342637/how-to-subclass-a-subclass-of-numpy-ndarray
# TODO es gibt noch das Problem, dass numpy nach mehreren Berechnungen von drive eine max recursion Warnung ausgiebt, warum? Brauche ich __del__
class VHistStack(View):
def __new__(cls, frames, input_array=None):
"""Represents a movie contrast in terms of stacked histograms
For each defined frame a histogram is calculated and each bin that
exceeds threshold is considered in the output array
Parameters
----------
bins : Int
Number of bins for the calculation of the histogram
thrsh : Int
Threshhold defining the number of pixels a bin needs to contain
to be considered in the result
Returns
-------
Object : VHistStack
VHistStack is a 2-dimensional numpy array which contains
the frame number, the bin number and a quantifier which
represents the relative weight of the bin in the frame
"""
class Contrast(object):
"""Base class for color contrasts"""
def __init__(self, img):
self._img = img
class LightDark(Contrast):
"""docstring for LightDark"""
def __init__(self, img, method='luminance'):
super(LightDark, self).__init__(img)
obj = View.__new__(cls, frames, input_array=input_array)
obj._threshold = 60000
return obj
meth = getattr(LightDark, method)
self.ctrst = meth(self)
def __array_finalize__(self, obj):
if obj is None: return
View.__array_finalize__(self, obj)
self._threshold = getattr(obj, '_threshold', None)
def luminance(self):
"""Creates light/dark values using luminance quantifiers for RGB
# TODO jetzt ausschließlich mit self numpy rechnen statt mit contrast_points liste
def populate(self, ctrst=2, frm_stp=10, bins=16, thrsh=60000, start=1, end=0):
"""doc (aus __new__ zusammentragen)
The array has the same dimensions as the image. However the third
does only have the size 1 which contains the luminance value
"""
# Luminance Faktoren nach http://introcs.cs.princeton.edu/python/31datatype/luminance.py.html
luminance_factors = np.array([.114, .587, .299])
# Erzeugung eines eindimensionalen Arrays für die effizientere Berechnung
self._img = np.multiply(self._img, luminance_factors)
# addiert alle Werte auf einer bestimmten Achse
luminances = np.sum(self._img, axis=2)
luminances = luminances.astype(np.uint8, copy=True)
return luminances
def value(self):
"""Creates light/dark values using the value channel in HSV"""
img = cv.cvtColor(self._img, cv.COLOR_BGR2HSV)
values = img[:, :, 2].copy()
return values
def lightness(self):
"""Creates light/dark values using the value channel in HSV"""
img = cv.cvtColor(self._img, cv.COLOR_BGR2HLS)
lightness = img[:, :, 1].copy()
return lightness
class Saturation(Contrast):
"""docstring for Saturation"""
def __init__(self, img, method='saturation'):
super(Saturation, self).__init__(img)
meth = getattr(Saturation, method)
self.ctrst = meth(self)
def saturation(self):
"""docstring for saturation"""
img = cv.cvtColor(self._img, cv.COLOR_BGR2HSV)
saturations = img[:, :, 1].copy()
return saturations
def chroma(self):
"""docstring for chroma"""
pass
class Hue(Contrast):
"""docstring for Saturation"""
def __init__(self, img, method='hue'):
super(Hue, self).__init__(img)
meth = getattr(Hue, method)
self.ctrst = meth(self)
def hue(self):
"""docstring for hue"""
img = cv.cvtColor(self._img, cv.COLOR_BGR2HSV)
hues = img[:, :, 0].copy()
return hues
# set class properties
self._contrast = ctrst
self._frame_step = frm_stp
self._bins = bins
self._threshold = thrsh
# TODO dafür müssen erst getters und setters in movie.frames definiert werden
# slef._frames.start = start
# slef._frames.end = end
contrast_points = []
# pwd list sollte in Frames sein und hier nur durchlaufen werden
for frm_nr in range(self._frames.start, self._frames.end, self._frame_step):
pwd = self._frames.folder + self._frames.prefix + str(frm_nr) + '.png'
img = cv2.imread(pwd) # BGR
# TODO Dictionary um Parameter auf Kontrast Funktion zu mappen
# so wie hier für 2 das ist die Referznimplementierung
# luminances gibt ein Bild mit den luminanz werten in der
# 3. Dimension zurück
if self._contrast == 2:
luminances = luminance(img)
hist_value, _ = np.histogram(luminances.flatten(), bins=self._bins, range=(0, 256))
else:
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV_FULL)
hist_value = cv2.calcHist([img_hsv], [self._contrast], None, [16], [0, 256])
for bin_index, point in enumerate(hist_value):
if point > self._threshold:
contrast_points.append((frm_nr, bin_index, int(point)))
contrast_points = np.asarray(contrast_points, np.uint8)
shape = contrast_points.shape
self.resize(shape, refcheck=False)
self[:, :] = contrast_points
return deepcopy(self) # TODO does not create a new object
......@@ -2,10 +2,13 @@
# -*- coding: utf-8 -*-
from pathlib import Path # TODO wie kann ich third-party module nach außen verstecken
import pickle
# TODO Ist der import aus dem selben Pakte so korrekt?
from . import helpers
from . import contrasts
from . import views
from . import visuals
import numpy as np
# zum Problem mit privaten und öffentlichen Eigenschaften http://www.python-course.eu/python3_properties.php und 'Fluent Python' relativ weit vorne
# numpy gibt beim Versuch zB. size zu schreiben auch ein AttributeError() aus.
......@@ -15,14 +18,125 @@ class Movie(object):
self._frames = Frames(folder, prefix)
self.fsize = self._frames.frm_cnt
# TODO Warum ist die hier?
def light_dark(self):
"""compute the light/dark contrast of the movie """
light_dark_ctrst = contrasts.LightDark(self._frames)
light_dark_ctrst = views.LightDark(self._frames)
return light_dark_ctrst.hist_vstack()
def describe(self, desc=False, start=1, end=0, stp=5):
"""Calculates each feature provided by Itten for the movie instance
Summary values will be returned in a python dictionary. Contrast
sequences will not be returned due to save memory. The method which
will be used to compute each contrast is the contrast class' default
method. Instead the will be plotted using Itten Sequence Plots. More
precisely MultivariatePlot and UnivariatePlot will be stored on disk
for each contrast type. The filename follows the Pattern prefix +
contrast.
Parameters
----------
start: Int which defines the start frame for the description
end: Int which defines the end frame for the description (0 defines
the unknown end frame)
Returns
-------
Dictionary: Dictionary which contains summarizing statistics The
Dictionary keys carry the names of the type of
summarization. For each contrast it contains: min, max,
mean, median, 25, 75, standard deviation, variance. And for
each of this values the same set of values are computed.
ToDo
----
- outsource summary/feature loop to function which can be accessed
by the view class
- pickle view data
"""
# set properties of the current function call
self._frames.start = start
self._frames.end = end
prefix = self._frames.prefix
if not(desc):
desc = 'Frame ' + str(start) + ' bis ' + str(end)
# Summary Dictionary
title = prefix[:-1]
summary = {title: {}}
# available contrasts
contrasts = ['saturation', 'light_dark']
# compute all available statistics for each contrast
for ctrst in contrasts:
summary[title][ctrst] = {}
# compute multivariatee contrast representation
multivariate = views.MultivariateSequence(self._frames)
multivariate.populate(ctrst=ctrst, frm_stp=stp+2)
# TODO: pickle instance instead of just deleating it!
# plot multivariate view
vis = visuals.MultivariatePlot(multivariate)
vis.plot(multivariate)
header = prefix[:-1] + ' MultivariatePlot for ' + ctrst + ' - ' + desc
filename = prefix + 'multivariateplot_' + ctrst + '_' + desc
vis.saveplt(fname=filename, title=header)
# compute summarizations for current contrast
univariate = views.UnivariateSequence(self._frames)
# TODO Workaround, becaus frm_stp is not part of Frames classbut
# view class so when I instantiate vis below it defaults to the
# __init__ value 10 and not the value set by the call of describe.
# This leads to a matplotlib error
univariate._frame_step = stp
# instantiate plot for all univariate summarizations
vis = visuals.UnivariatePlot(univariate)
# summarizing methods of univariate class
summarizations = ['seqmean', 'seqmad'] # TODO Varianz funktioniert so nicht
for feature in summarizations:
# compute summarizations for given univariate value
getattr(univariate, feature)(ctrst=ctrst, frm_stp=stp)
# TODO Pickle current instance state
# describe current feature by summarizing statistics
summary[title][ctrst][feature[3:]] = {
'minv': int(univariate.min()),
'maxv': int(univariate.max()),
'mean': int(univariate.mean()),
'median': int(np.median(univariate)),
'perc25': int(np.percentile(univariate, 25)),
'perc75': int(np.percentile(univariate, 75)),
'std': int(univariate.std()),
'var': int(univariate.var())
}
# plot current summarization current univariate contrast plot
vis.plot(univariate)
# save univariate plot
header = prefix[:-1] + ' UnivariatePlot for ' + ctrst + ' - ' + desc
filename = self._frames.prefix + 'univariateplot_' + ctrst + '_' + desc
vis.saveplt(fname=filename, title=header)
with open(prefix[:-1] + '_summary.pickle', 'wb') as f:
pickle.dump(summary, f)
return summary
class Frames(object):
# TODO getters und setters für start und end setzen
# TODO frm_stp sollte definitiv Teil der Frames Klasse werden
"""Parses movie frames properties"""
def __init__(self, folder, prefix, start=1, end=0):
self.folder = folder
......@@ -30,8 +144,40 @@ class Frames(object):
self.frames = self.get_frame_list()
self.frm_length = self.count_total_frames()
self.start = start
self.end = self._get_end_frame(end)
self.frm_cnt = self.count_frames()
self.end = end
self.frm_cnt = 0
# TODO start als property erzeugt bisher eine Endlosschleife wei
# count_frames sart und end brauchen und so gegenseitig sart und end nicht
# gesetzt wird
@property
def end(self):
return self.__end
@end.setter
def end(self, nr):
if nr == 0:
self.__end = self.frm_length
self.frm_cnt = self.count_frames()
elif nr > self.frm_length:
self.__end = self.frm_length - 1
self.frm_cnt = self.count_frames()
elif nr < self.start:
self.__end = self.start + 1
self.frm_cnt = self.count_frames()
else:
self.__end = nr
self.frm_cnt = self.count_frames()
@property
def frm_cnt(self):
return self.__frm_cnt
@frm_cnt.setter
def frm_cnt(self, nr):
count = self.count_frames()
self.__frm_cnt = count
def get_frame_list(self):
frm_path = Path(self.folder)
......
This diff is collapsed.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import matplotlib.ticker as plticker
import numpy as np
class SequencePlot(object):
"""Key class for visualizations with two axis"""
def __init__(self, view, width=40, height=3):
self.width = width
self.height = height
self._x = self.get_xpos(view) # outsourcen in Frames Class und dann hier löschen
def get_xpos(self, view):
"""calculate frame numbers for x-ticks"""
# TODO Die Frame No. Range könnte ich auch besser zu einer Methode von Frames machen
return [nr for nr in range(view._frames.start, view._frames.end,
view._frame_step)] # um X-Achse Minuten anzeigen zu lassen
def _timelabels(self, val, pos):
min, sec = divmod(int(val), 60)
timelabel = "{0}:{1:02d}".format(min, sec)
return timelabel
# TODO styling und plotten sind hier noch etwas zusammengemischt
def ittenstyle(self, ax, view):
plt.style.use('ggplot')
fig_coef = self.width / self.height
tick_cnt = fig_coef / 0.3605405405
tick_step = int(view._frames.frm_cnt / tick_cnt)
# loc = plticker.MultipleLocator(base=tick_freq) # this locator puts ticks at regular intervals (0.0005)
loc = plticker.FixedLocator(range(0, view._frames.end, tick_step))
fmt = plticker.FuncFormatter(self._timelabels)
ax.xaxis.set_major_locator(loc)
ax.xaxis.set_major_formatter(fmt)
ax.set_xlim(view._frames.start - 20, view._frames.end + 20)
# Beschriftung der Y-Achse
# TODO funktioniert nicht richtig
ax.set_ylim(-1, view._bins + 1)
loc = plticker.FixedLocator(range(0, view._bins + 1, int(view._bins / 8)))
ax.yaxis.set_major_locator(loc)
# TODO mit iter_ticks evtl noch die angegebenen Sekunden auf base 60 setzen
# obere x-achse mit zeitlich versetzten werten
axt = ax.twiny()
axt.set_xlim(ax.get_xlim())
axt.set_ylim(ax.get_ylim())
# loc = plticker.MultipleLocator(base=tick_freq)
# loc = plticker.LinearLocator(20)
loc = plticker.FixedLocator(range(int(tick_step / 2), view._frames.end, tick_step))
fmt = plticker.FuncFormatter(self._timelabels)
axt.xaxis.set_major_locator(loc)
axt.xaxis.set_major_formatter(fmt)
ax.set_axis_bgcolor((1, 1, 1)) # TODO: gradient
chn_label = view._contrast
ax.set_ylabel(chn_label, {'fontsize': 8})
ax.set_xlabel('Time', {'fontsize': 8}, y=0.5)
ax.yaxis.grid(False)
axt.yaxis.grid(False)
ax.tick_params(length=0)
axt.tick_params(length=0)
ax.xaxis.grid(c=(0.90, 0.90, 0.90))
axt.xaxis.grid(c=(0.90, 0.90, 0.90))
return (ax, axt)
# TODO Überführbar in Superclass?
# evtl. auch eher feature drer View Klasse
# TODO self._x sollte eine array sein
def _vlines(self, view, mark, mark_gt, mark_lt):
npx = np.array(self._x)
if mark_gt:
poss = npx[view > mark_gt]
for pos in poss:
self._ax.axvline(pos, color='#a6e22e', alpha=0.4, linewidth=3)
if mark_lt:
poss = npx[view < mark_lt]
for pos in poss:
self._ax.axvline(pos, color='#f92672', alpha=0.4, linewidth=3)
if mark:
for pos in mark:
print(self._ax)
self._ax.axvline(pos, color='#66d9ef', alpha=0.4, linewidth=3)
def saveplt(self, title=False, fname='plot.png'):
if title:
self._ax.set_title(title, {'fontsize': 14}, y=1.18)
self.fig.set_size_inches(self.width, self.height)
self.fig.tight_layout()
self.fig.savefig(fname, dpi=400)