580 lines
17 KiB
Python
580 lines
17 KiB
Python
import os
|
|
from PIL import Image, ImageDraw,ImageTk
|
|
import argparse
|
|
import json
|
|
import time
|
|
import glob
|
|
import numpy as np
|
|
import datetime
|
|
import Tkinter
|
|
import moviepy.editor as mpy
|
|
import sys
|
|
import wave
|
|
|
|
FPS = 1
|
|
|
|
facialParameters = [
|
|
"smile",
|
|
"innerBrowRaise",
|
|
"browRaise",
|
|
"browFurrow",
|
|
"noseWrinkle",
|
|
"upperLipRaise",
|
|
"lipCornerDepressor",
|
|
"chinRaise",
|
|
"lipPucker",
|
|
"lipPress",
|
|
"lipSuck",
|
|
"mouthOpen",
|
|
"smirk",
|
|
#~ "attention",
|
|
"eyeClosure",
|
|
"eyeWiden",
|
|
"cheekRaise",
|
|
"lidTighten",
|
|
"dimpler",
|
|
"lipStretch",
|
|
"jawDrop",
|
|
]
|
|
|
|
defaultFacialParameters = [
|
|
"smile",
|
|
"innerBrowRaise",
|
|
"browRaise",
|
|
"browFurrow",
|
|
"noseWrinkle",
|
|
"upperLipRaise",
|
|
"lipCornerDepressor",
|
|
"chinRaise",
|
|
#~ "lipPucker",
|
|
"lipPress",
|
|
"lipSuck",
|
|
"mouthOpen",
|
|
#~ "smirk",
|
|
#~ "attention",
|
|
"eyeClosure",
|
|
"eyeWiden",
|
|
"cheekRaise", #
|
|
"lidTighten",
|
|
"dimpler", #
|
|
"lipStretch",
|
|
"jawDrop", #
|
|
]
|
|
|
|
parser = argparse.ArgumentParser(description='Parses opencv-webcam-demo json output files and collects statistics')
|
|
parser.add_argument('--frameOutput', '-o', required=True, help='directory to look for frames & json')
|
|
parser.add_argument('--targetDir', help='Directory in which files are stored')
|
|
parser.add_argument('--status', action='store_true', help='Keep status of last frame')
|
|
parser.add_argument('--stats', action='store_true', help='Show statistics for all frames')
|
|
parser.add_argument('--cutAllFaces', action='store_true', help='Cut out all faces from all frames')
|
|
parser.add_argument('--sum', action='store_true', help='Get total scores over all time')
|
|
parser.add_argument('--unique', action='store_true', help='Get most unique window')
|
|
parser.add_argument('--avg', action='store_true', help='Get most average window')
|
|
parser.add_argument('--json', action='store_true', help='Get output as JSON for graphs (graphs.php)')
|
|
parser.add_argument('--wav', '-w', required=False, default=False, help='split wave file into windows')
|
|
parser.add_argument('--disonant', action='store_true', help='Get most disonant faces over time')
|
|
parser.add_argument('--window-size', '-s', type=int, default=10, help='The nr of frames to group in one sliding window for analysis')
|
|
parser.add_argument("--params", "--metrics", "-p", type=str, nargs='+', default=defaultFacialParameters, choices=facialParameters, help="The parameters used to calculate the statistics")
|
|
|
|
args = parser.parse_args()
|
|
|
|
faces = []
|
|
class Face:
|
|
def __init__(self, frame, data):
|
|
self.id = data['id']
|
|
self.frame = frame # Frame class
|
|
self.data = data # json data
|
|
self.disonanceScore = None # a first attempt, can be deprecated?
|
|
self.anomalyScore = None
|
|
|
|
def getFaceImg(self):
|
|
r = self.data['rect']
|
|
return self.frame.getImg().crop((int(r['x']), int(r['y']), int(r['x']+r['w']), int(r['y']+r['h'])))
|
|
|
|
def getCharacteristicVector(self, params):
|
|
self.vector = [self.data[p] for p in params]
|
|
return self.vector
|
|
|
|
def setAnomalyScore(self, score):
|
|
self.anomalyScore = score
|
|
|
|
class Frame:
|
|
"""
|
|
Everything for an analysed frame
|
|
"""
|
|
def __init__(self, outputPath, nr):
|
|
self.outputPath = outputPath
|
|
self.nr = nr
|
|
self.name = "frame%06d" % nr
|
|
self.jsonPath = os.path.join(outputPath, ("frame%06d" % (nr)) + ".json")
|
|
self.imgPath = os.path.join(outputPath, self.name + ".jpg")
|
|
self.faces = None # init with getFaces
|
|
|
|
def getTime(self):
|
|
"""
|
|
@return datetime
|
|
"""
|
|
with Image.open(self.imgPath) as i:
|
|
#~ print(i._getexif()[36867])
|
|
return datetime.datetime.strptime(i._getexif()[36867], "%Y:%m:%d %H:%M:%S")
|
|
#~ return datetime.datetime.fromtimestamp(os.path.getmtime(self.imgPath))
|
|
|
|
def getJson(self):
|
|
#~ try:
|
|
with open(self.jsonPath) as fp:
|
|
return json.load(fp)
|
|
#~ except Exception as e:
|
|
#~ # no json file yet?
|
|
#~ return None
|
|
|
|
def getImg(self, markFaces = False):
|
|
img = Image.open(self.imgPath)
|
|
if not markFaces:
|
|
return img
|
|
|
|
draw = ImageDraw.Draw(img)
|
|
for f in self.faces:
|
|
xy1 = (int(f.data['rect']['x']), int(f.data['rect']['y']))
|
|
xy2 = (int(f.data['rect']['x'] + f.data['rect']['w']), int(f.data['rect']['y'] + f.data['rect']['h']))
|
|
draw.rectangle([xy1, xy2], outline="#ff0000")
|
|
return img
|
|
|
|
def getFaces(self):
|
|
if self.faces is None:
|
|
j = self.getJson()
|
|
|
|
self.faces = [Face(self, f) for f in j['faces']]
|
|
faces.extend(self.faces)
|
|
|
|
return self.faces
|
|
|
|
def updateDisonanceScores(self):
|
|
totalValence = 0.0
|
|
totalFaces = 0
|
|
for face in self.getFaces():
|
|
totalValence += face.data['valence']
|
|
totalFaces += 1
|
|
|
|
if totalFaces == 0:
|
|
return
|
|
|
|
avgValence = totalValence / totalFaces
|
|
|
|
for face in self.getFaces():
|
|
face.disonanceScore = abs(face.data['valence'] - avgValence)
|
|
|
|
def getAverageV(self, params):
|
|
vectors = [face.getCharacteristicVector(params) for face in self.getFaces()]
|
|
vAvg = np.mean(vectors, axis=0)
|
|
return vAvg
|
|
|
|
def getStdDev(self, params):
|
|
"""
|
|
Get standard deviation of the faces within the frame for given params
|
|
"""
|
|
vectors = [f.getCharacteristicVector(params) for f in self.getFaces()]
|
|
return np.std(vectors)
|
|
|
|
def updateAnomalyScores(self, params):
|
|
vAvg = self.getAverageV(params)
|
|
for face in self.getFaces():
|
|
face.setAnomalyScore(np.linalg.norm(face.getCharacteristicVector() - vAvg))
|
|
|
|
|
|
def exists(self):
|
|
return os.path.exists(self.jsonPath) and os.path.exists(self.imgPath)
|
|
|
|
frames = {}
|
|
|
|
class Window:
|
|
def __init__(self, frameSubset):
|
|
"""
|
|
Init a sliding window for given Frame-s
|
|
"""
|
|
self.frames = frameSubset
|
|
self.deviation = None
|
|
self.standardDeviation = None
|
|
|
|
def getStartTime(self):
|
|
"""get time of first frame in window. Returns datetime"""
|
|
return self.frames[0].getTime()
|
|
|
|
def getEndTime(self):
|
|
"""get time of last frame in window. Returns datetime"""
|
|
return self.frames[-1].getTime()
|
|
|
|
def getFaces(self):
|
|
faces = []
|
|
for frame in self.frames:
|
|
faces.extend(frame.getFaces())
|
|
return faces
|
|
|
|
def getStdDev(self, params):
|
|
"""
|
|
Get standard deviation of the faces within the window for given params
|
|
"""
|
|
vectors = [f.getCharacteristicVector(params) for f in self.getFaces()]
|
|
return np.std(vectors)
|
|
|
|
def getAverageV(self, params):
|
|
vectors = [f.getCharacteristicVector(params) for f in self.getFaces()]
|
|
vAvg = np.mean(vectors, axis=0)
|
|
return vAvg
|
|
|
|
@staticmethod
|
|
def createWindows(windowSize, frames):
|
|
"""
|
|
Give a full list of frames and tunrn it into a collection of sliding windows
|
|
"""
|
|
frames = sorted(frames.items(), key=lambda f: f[0])
|
|
frames = [f[1] for f in frames]
|
|
windows = []
|
|
windowCount = len(frames) / windowSize
|
|
#~ windowCount = len(frames) - windowSize + 1
|
|
if windowCount < 1:
|
|
raise Exception("Not enough frames ({}) for a window of size {}".format(len(frames), windowSize))
|
|
|
|
for offset in range(0, windowCount):
|
|
frameSubset = frames[offset*windowSize:(offset+1)*windowSize]
|
|
#~ frameSubset = frames[offset:offset+windowSize]
|
|
windows.append(Window(frameSubset))
|
|
|
|
return windows
|
|
|
|
class WindowCollection:
|
|
def __init__(self, windowSize, frames):
|
|
self.windows = Window.createWindows(windowSize, frames)
|
|
self.frames = frames
|
|
#~ self.faces = [face for face in frame.getFaces() for frame in frames]
|
|
|
|
#~ def getMostWindowsClosestToMedian(self, nr = 5):
|
|
#~ """
|
|
#~ Get windows with the faces closest to the median
|
|
#~ """
|
|
#~ self.faces
|
|
|
|
def getWindowVectors(self, params):
|
|
return [window.getAverageV(params) for window in self.windows]
|
|
|
|
def getWindowsByDeviation(self, params):
|
|
vectors = self.getWindowVectors(params)
|
|
vAvg = np.mean(vectors, axis=0)
|
|
#~ diffs = [numpy.linalg.norm(v-vAvg) for v in vectors]
|
|
#~ min_index, min_value = min(enumerate(diffs), key=lambda p: p[1])
|
|
#~ max_index, max_value = max(enumerate(diffs), key=lambda p: p[1])
|
|
|
|
return sorted(self.windows, key=lambda w: np.linalg.norm(w.getAverageV(params)-vAvg))
|
|
|
|
def getDeviationForWindow(self, window, params):
|
|
vectors = self.getWindowVectors(params)
|
|
vAvg = np.mean(vectors, axis=0)
|
|
return np.linalg.norm(w.getAverageV(params)-vAvg)
|
|
|
|
def getUniqueWindows(self, params, nr=5):
|
|
windows = self.getWindowsByDeviation(params)
|
|
return windows[0: nr]
|
|
|
|
def getMostAvgWindows(self, params, nr=5):
|
|
windows = self.getWindowsByDeviation(params)
|
|
windows.reverse()
|
|
return windows[0:nr]
|
|
|
|
def getMostContrastingWindows(self, params, nr=5):
|
|
sortedWindows = sorted(self.windows, key=lambda w: w.getStdDev(params), reverse=True)
|
|
return sortedWindows[0:nr]
|
|
|
|
def loadFrames(frameDir):
|
|
global frames
|
|
nr = 2
|
|
nextFrame = Frame(frameDir, nr)
|
|
# TODO; make threaded and infinite loop that updates global frames
|
|
while nextFrame.exists():
|
|
frames[nr] = nextFrame
|
|
nr+=1
|
|
nextFrame = Frame(frameDir, nr)
|
|
return frames
|
|
|
|
def getLastFrame(frameDir):
|
|
jsons = sorted(glob.glob(os.path.join(frameDir, "*.json")))
|
|
if len(jsons):
|
|
lastJson = jsons[-1]
|
|
lastNr = int(lastJson[-11:-5])
|
|
frame = Frame(frameDir, lastNr)
|
|
return frame
|
|
return None
|
|
|
|
def cutOutFaces(frame, targetDir):
|
|
for faceNr, face in enumerate(frame.getFaces()):
|
|
print(faceNr, face)
|
|
img = face.getFaceImg()
|
|
faceImgPath = os.path.join(targetDir, frame.name + "-%s.jpg" % face.id)
|
|
print(faceImgPath)
|
|
img.save(faceImgPath)
|
|
pass
|
|
|
|
def validateJsonTimes():
|
|
lastTime = None
|
|
for frameNr, frame in loadFrames(args.frameOutput).items():
|
|
thisTime = frame.getJson()['t']
|
|
#print(frameNr, thisTime)
|
|
if not (lastTime is None) and lastTime > thisTime:
|
|
sys.stderr.write("ERRROR!! Time error at %s. Restarted scanner there?\n" % frameNr)
|
|
lastTime = thisTime
|
|
|
|
def sumEmotions():
|
|
total = 0.
|
|
summed = 0.
|
|
items = 0
|
|
for frameNr, frame in loadFrames(args.frameOutput).items():
|
|
for face in frame.getFaces():
|
|
total += abs(face.data['valence'])
|
|
summed += face.data['valence']
|
|
items += 1
|
|
|
|
average = summed / items
|
|
print ("Total emotion %d, positivity score %d (average: %s)" % (total, summed, average))
|
|
|
|
def getMostDisonant(nr = 5):
|
|
for frameNr, frame in loadFrames(args.frameOutput).items():
|
|
frame.updateDisonanceScores()
|
|
faces.sort(key=lambda x: x.disonanceScore, reverse=True)
|
|
|
|
mostDisonantFaces = faces[:nr]
|
|
for face in mostDisonantFaces:
|
|
print("Frame %d, face %d, score %d, valence %d" % (face.frame.nr, face.id, face.disonanceScore, face.data['valence']))
|
|
face.getFaceImg().show()
|
|
|
|
def getAnomalies(params, nr = 5):
|
|
for frameNr, frame in loadFrames(args.frameOutput).items():
|
|
frame.updateAnomalyScores(params)
|
|
faces.sort(key=lambda x: x.anomalyScore, reverse=True)
|
|
|
|
anomalies = faces[:nr]
|
|
for face in anomalies:
|
|
print("Frame %d, face %d, score %d" % (face.frame.nr, face.id, face.anomalyScore))
|
|
#~ getCharacteristicVector
|
|
face.getFaceImg().show()
|
|
|
|
def printFrameStats(frame, params):
|
|
os.system('clear')
|
|
print(time.time())
|
|
print( ("Nr: %d" % frame.nr).ljust(40) + ("t: {}".format(frame.getJson()['t'])) )
|
|
#~ print
|
|
faces = frame.getFaces()
|
|
print("Faces: %d" % len(faces))
|
|
|
|
if len(faces) < 1:
|
|
return
|
|
|
|
print " ".ljust(20), "0%".rjust(13), "q1".rjust(13), "median".rjust(13), "q3".rjust(13), "100%".rjust(13)
|
|
|
|
for p in params:
|
|
q0 = np.percentile(np.array([f.data[p] for f in faces]),0)
|
|
q1 = np.percentile(np.array([f.data[p] for f in faces]),25)
|
|
q2 = np.percentile(np.array([f.data[p] for f in faces]),50)
|
|
q3 = np.percentile(np.array([f.data[p] for f in faces]),75)
|
|
q4 = np.percentile(np.array([f.data[p] for f in faces]),100)
|
|
print p.ljust(20), ("%f%%" % q0).rjust(13), ("%f%%" % q1).rjust(13),("%f%%" % q2).rjust(13),("%f%%" % q3).rjust(13),("%f%%" % q4).rjust(13)
|
|
|
|
|
|
#~ TODO: speaker stats
|
|
|
|
frame.updateDisonanceScores()
|
|
dissonantFace = max(faces,key=lambda f: f.disonanceScore)
|
|
#~ dissonantFace.getFaceImg()
|
|
|
|
|
|
def monitorStatus(frameDir, params):
|
|
while True:
|
|
frame = getLastFrame(frameDir)
|
|
if not frame is None:
|
|
printFrameStats(frame, params)
|
|
|
|
# don't check too often
|
|
time.sleep(.5)
|
|
|
|
def printStats(frameDir, params):
|
|
frames = loadFrames(frameDir)
|
|
vAvg = np.mean([f.getAverageV(params) for i,f in frames.items()], axis=0)
|
|
for nr, frame in frames.items():
|
|
max_index, max_value = max(enumerate(frame.getAverageV(params)), key=lambda a: a[1])
|
|
distance = np.linalg.norm(vAvg - frame.getAverageV(params))
|
|
print("{:06d}: {: >4} faces (d: {: 6.2f}, internal s {: 6.2f}, max: {})".format(frame.nr, len(frame.getFaces()),distance, frame.getStdDev(params), params[max_index]))
|
|
|
|
print("{} frames".format(len(frames)))
|
|
|
|
def playWindowStopmotion(window, params):
|
|
"""
|
|
Play a set of sliding window frames as stop motion video
|
|
"""
|
|
root = Tkinter.Tk()
|
|
root.geometry('%dx%d+%d+%d' % (1000,1000,0,0))
|
|
canvas = Tkinter.Canvas(root,width=1000,height=1000)
|
|
canvas.pack()
|
|
|
|
old_label_image = None
|
|
for frame in window.frames:
|
|
image = frame.getImg(markFaces=True)
|
|
basewidth = 1000
|
|
wpercent = (basewidth / float(image.size[0]))
|
|
hsize = int((float(image.size[1]) * float(wpercent)))
|
|
image = image.resize((basewidth, hsize), Image.ANTIALIAS)
|
|
|
|
tkpi = ImageTk.PhotoImage(image)
|
|
canvas.delete("IMG")
|
|
imagesprite = canvas.create_image(500,500,image=tkpi, tags="IMG")
|
|
root.update()
|
|
time.sleep(1)
|
|
|
|
def createWindowVideo(window, params, targetDir):
|
|
global FPS
|
|
basewidth = 1920
|
|
i=0
|
|
images = []
|
|
for frame in window.frames:
|
|
image = frame.getImg(markFaces=True)
|
|
wpercent = (basewidth / float(image.size[0]))
|
|
hsize = int((float(image.size[1]) * float(wpercent)))
|
|
image = image.resize((basewidth, hsize), Image.ANTIALIAS)
|
|
imgName = os.path.join("/tmp/","renderframe{:06d}.jpg".format(i))
|
|
print("(VIDEO) Create frame: {}".format(imgName))
|
|
image.save(imgName)
|
|
images.append(imgName)
|
|
i+=1
|
|
|
|
clip = mpy.ImageSequenceClip(images, fps=FPS)
|
|
#~ audio = mpy.AudioFileClip(filename="")
|
|
#~ audio.set_start(...)
|
|
#~ audio.set_duration(len(images)/FPS)
|
|
#~ clip = clip.set_audio(audio)
|
|
|
|
targetName = "video_" + "-".join(params) + ".mp4"
|
|
print("(VIDEO) Write video: {}".format(targetName))
|
|
clip.write_videofile(os.path.join(targetDir,targetName), fps=FPS)
|
|
|
|
def getGraphData(frames, params, window_size, outputNr):
|
|
"""
|
|
Get data to generate graph with
|
|
"""
|
|
collection = WindowCollection(window_size, frames)
|
|
windows = collection.windows
|
|
jsonOutput = {'windows':[], 'outputNr': outputNr}
|
|
for window in windows:
|
|
w = {
|
|
'start': window.getStartTime().strftime('%Y-%m-%d %H:%M:%S'),
|
|
'end': window.getEndTime().strftime('%Y-%m-%d %H:%M:%S'),
|
|
'startFrame': window.frames[0].name,
|
|
'params': {},
|
|
'param_values': {},
|
|
}
|
|
|
|
pVs = []
|
|
for param in params:
|
|
paramV = window.getAverageV([param])
|
|
value = paramV[0]
|
|
pVs.append(value)
|
|
w['params'][param] = value
|
|
#~ values = []
|
|
#~ for frame in window.frames:
|
|
#~ for face in frame.getFaces():
|
|
#~ values.append(face.data[param])
|
|
#~ w['param_values'][param] = values
|
|
|
|
w['avg'] = np.mean(pVs)
|
|
w['stddev'] = window.getStdDev(params)
|
|
jsonOutput['windows'].append(w)
|
|
|
|
jsonOutput['range'] = [
|
|
jsonOutput['windows'][0]['start'],
|
|
jsonOutput['windows'][-1]['end'],
|
|
]
|
|
return jsonOutput
|
|
|
|
|
|
def splitAudio(frames, window_size, srcWav, targetDir):
|
|
"""
|
|
Get audio cllips
|
|
"""
|
|
global FPS
|
|
if not os.path.exists(srcWav):
|
|
raise Exception("Wav does not exist")
|
|
if not os.path.exists(targetDir):
|
|
raise Exception("Target dir does not exist")
|
|
|
|
collection = WindowCollection(window_size, frames)
|
|
origAudio = wave.open(srcWav,'r')
|
|
|
|
frameRate = origAudio.getframerate()
|
|
nChannels = origAudio.getnchannels()
|
|
sampWidth = origAudio.getsampwidth()
|
|
|
|
windows = collection.windows
|
|
for i, window in enumerate(windows):
|
|
print("Window {}, {}".format(i, window.frames[0].name))
|
|
start = i * window_size * FPS
|
|
end = i+1 * window_size * FPS
|
|
origAudio.setpos(start*frameRate)
|
|
print(origAudio.tell())
|
|
chunkData = origAudio.readframes(int((end-start)*frameRate))
|
|
print(origAudio.tell())
|
|
targetFile = os.path.join(targetDir, window.frames[0].name + '.wav')
|
|
print("\t{}".format(targetFile))
|
|
chunkAudio = wave.open(targetFile,'w')
|
|
chunkAudio.setnchannels(nChannels)
|
|
chunkAudio.setsampwidth(sampWidth)
|
|
chunkAudio.setframerate(frameRate)
|
|
chunkAudio.writeframes(chunkData)
|
|
chunkAudio.close()
|
|
|
|
#~ def createGraphPage(windows, params, targetFilename):
|
|
#~ jsonOutput = {'windows':[]}
|
|
#~ for window in windows:
|
|
#~ window
|
|
|
|
|
|
validateJsonTimes()
|
|
|
|
if args.sum:
|
|
sumEmotions()
|
|
|
|
if args.disonant:
|
|
getMostDisonant()
|
|
|
|
if args.cutAllFaces:
|
|
faceDir = os.path.join(args.frameOutput, 'faces')
|
|
|
|
if not os.path.exists(faceDir):
|
|
os.mkdir(faceDir)
|
|
for frameNr, frame in loadFrames(args.frameOutput).items():
|
|
cutOutFaces(faceDir)
|
|
|
|
if args.unique:
|
|
collection = WindowCollection(args.window_size, frames)
|
|
windows = collection.getUniqueWindows(args.params)
|
|
#~ print(windows)
|
|
if args.targetDir:
|
|
createWindowVideo(windows[0], args.params, args.targetDir)
|
|
else:
|
|
playWindowStopmotion(windows[0], args.params)
|
|
|
|
if args.avg:
|
|
collection = WindowCollection(args.window_size, frames)
|
|
windows = collection.getMostAvgWindows(args.params)
|
|
#~ print(windows)
|
|
if args.targetDir:
|
|
createWindowVideo(windows[0], args.params, args.targetDir)
|
|
else:
|
|
playWindowStopmotion(windows[0], args.params)
|
|
|
|
if args.status:
|
|
monitorStatus(args.frameOutput, args.params)
|
|
|
|
if args.stats:
|
|
printStats(args.frameOutput, args.params)
|
|
|
|
if args.json:
|
|
nr = int(args.frameOutput[-1])
|
|
print json.dumps(getGraphData(frames, args.params, args.window_size, nr))
|
|
|
|
if args.wav:
|
|
print splitAudio(frames, args.window_size, args.wav, args.targetDir)
|