From 18b7c03cc66104327bba269d53ada22a8f25b4a3 Mon Sep 17 00:00:00 2001 From: Cowwan Date: Tue, 6 Feb 2024 08:26:22 -0500 Subject: [PATCH] Added files to enable using EmilianaVT's parameter calculations Still need to hook them up and make a command line option for them --- .emilianaFeatureExtractor.py.kate-swp | Bin 0 -> 116 bytes Licenses/Remedian.txt | 19 +++++ emilianaFeature.py | 79 ++++++++++++++++++++ emilianaFeatureExtractor.py | 102 ++++++++++++++++++++++++++ remedian.py | 82 +++++++++++++++++++++ 5 files changed, 282 insertions(+) create mode 100644 .emilianaFeatureExtractor.py.kate-swp create mode 100644 Licenses/Remedian.txt create mode 100644 emilianaFeature.py create mode 100644 emilianaFeatureExtractor.py create mode 100644 remedian.py diff --git a/.emilianaFeatureExtractor.py.kate-swp b/.emilianaFeatureExtractor.py.kate-swp new file mode 100644 index 0000000000000000000000000000000000000000..72dbb6f98e2b893b28446a61faede45e858d0e8b GIT binary patch literal 116 zcmZQzU=Z?7EJ;-eE>A2_aLdd|RWQ;sU|?VnvDzcP;i@N#FOT#N=Ld%$r02anClVYE ulmP)D*I-W|#R%pwFvzAA<>x9CrRJulWG3b*Wabv+7nQ*IuE9abYFz;sW*un& literal 0 HcmV?d00001 diff --git a/Licenses/Remedian.txt b/Licenses/Remedian.txt new file mode 100644 index 0000000..3d14fcf --- /dev/null +++ b/Licenses/Remedian.txt @@ -0,0 +1,19 @@ +Copyright (c) 2016, Tim Menzies tim@menzies.us, MIT license v2. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/emilianaFeature.py b/emilianaFeature.py new file mode 100644 index 0000000..a943d4a --- /dev/null +++ b/emilianaFeature.py @@ -0,0 +1,79 @@ +from remedian import remedian + + + +class Feature(): + def __init__(self, threshold=0.15, alpha=0.2, hard_factor=0.15, decay=0.001, max_feature_updates=0): + self.median = remedian() + self.min = None + self.max = None + self.hard_min = None + self.hard_max = None + self.threshold = threshold + self.alpha = alpha + self.hard_factor = hard_factor + self.decay = decay + self.last = 0 + self.current_median = 0 + self.update_count = 0 + self.max_feature_updates = max_feature_updates + self.first_seen = -1 + self.updating = True + + def update(self, x, now=0): + if self.max_feature_updates > 0: + if self.first_seen == -1: + self.first_seen = now; + new = self.update_state(x, now=now) + filtered = self.last * self.alpha + new * (1 - self.alpha) + self.last = filtered + return filtered + + def update_state(self, x, now=0): + updating = self.updating and (self.max_feature_updates == 0 or now - self.first_seen < self.max_feature_updates) + if updating: + self.median + x + self.current_median = self.median.median() + else: + self.updating = False + median = self.current_median + + if self.min is None: + if x < median and (median - x) / median > self.threshold: + if updating: + self.min = x + self.hard_min = self.min + self.hard_factor * (median - self.min) + return -1 + return 0 + else: + if x < self.min: + if updating: + self.min = x + self.hard_min = self.min + self.hard_factor * (median - self.min) + return -1 + if self.max is None: + if x > median and (x - median) / median > self.threshold: + if updating: + self.max = x + self.hard_max = self.max - self.hard_factor * (self.max - median) + return 1 + return 0 + else: + if x > self.max: + if updating: + self.max = x + self.hard_max = self.max - self.hard_factor * (self.max - median) + return 1 + + if updating: + if self.min < self.hard_min: + self.min = self.hard_min * self.decay + self.min * (1 - self.decay) + if self.max > self.hard_max: + self.max = self.hard_max * self.decay + self.max * (1 - self.decay) + + if x < median: + return - (1 - (x - self.min) / (median - self.min)) + elif x > median: + return (x - median) / (self.max - median) + + return 0 diff --git a/emilianaFeatureExtractor.py b/emilianaFeatureExtractor.py new file mode 100644 index 0000000..3abc17b --- /dev/null +++ b/emilianaFeatureExtractor.py @@ -0,0 +1,102 @@ +import numpy as np + + +class FeatureExtractor(): + def __init__(self, max_feature_updates=0): + self.eye_l = Feature(max_feature_updates=max_feature_updates) + self.eye_r = Feature(max_feature_updates=max_feature_updates) + self.eyebrow_updown_l = Feature(max_feature_updates=max_feature_updates) + self.eyebrow_updown_r = Feature(max_feature_updates=max_feature_updates) + self.eyebrow_quirk_l = Feature(threshold=0.05, max_feature_updates=max_feature_updates) + self.eyebrow_quirk_r = Feature(threshold=0.05, max_feature_updates=max_feature_updates) + self.eyebrow_steepness_l = Feature(threshold=0.05, max_feature_updates=max_feature_updates) + self.eyebrow_steepness_r = Feature(threshold=0.05, max_feature_updates=max_feature_updates) + self.mouth_corner_updown_l = Feature(max_feature_updates=max_feature_updates) + self.mouth_corner_updown_r = Feature(max_feature_updates=max_feature_updates) + self.mouth_corner_inout_l = Feature(threshold=0.02, max_feature_updates=max_feature_updates) + self.mouth_corner_inout_r = Feature(threshold=0.02, max_feature_updates=max_feature_updates) + self.mouth_open = Feature(max_feature_updates=max_feature_updates) + self.mouth_wide = Feature(threshold=0.02, max_feature_updates=max_feature_updates) + + def align_points(self, a, b, pts): + a = tuple(a) + b = tuple(b) + alpha = angle(a, b) + alpha = np.rad2deg(alpha) + if alpha >= 90: + alpha = - (alpha - 180) + if alpha <= -90: + alpha = - (alpha + 180) + alpha = np.deg2rad(alpha) + aligned_pts = [] + for pt in pts: + aligned_pts.append(np.array(rotate(a, pt, alpha))) + return alpha, np.array(aligned_pts) + + def update(self, pts, full=True): + features = {} + now = time.perf_counter() + + norm_distance_x = np.mean([pts[0, 0] - pts[16, 0], pts[1, 0] - pts[15, 0]]) + norm_distance_y = np.mean([pts[27, 1] - pts[28, 1], pts[28, 1] - pts[29, 1], pts[29, 1] - pts[30, 1]]) + + a1, f_pts = self.align_points(pts[42], pts[45], pts[[43, 44, 47, 46]]) + f = abs((np.mean([f_pts[0,1], f_pts[1,1]]) - np.mean([f_pts[2,1], f_pts[3,1]])) / norm_distance_y) + features["eye_l"] = self.eye_l.update(f, now) + + a2, f_pts = self.align_points(pts[36], pts[39], pts[[37, 38, 41, 40]]) + f = abs((np.mean([f_pts[0,1], f_pts[1,1]]) - np.mean([f_pts[2,1], f_pts[3,1]])) / norm_distance_y) + features["eye_r"] = self.eye_r.update(f, now) + + if full: + a3, _ = self.align_points(pts[0], pts[16], []) + a4, _ = self.align_points(pts[31], pts[35], []) + norm_angle = np.mean(list(map(np.rad2deg, [a1, a2, a3, a4]))) + + a, f_pts = self.align_points(pts[22], pts[26], pts[[22, 23, 24, 25, 26]]) + features["eyebrow_steepness_l"] = self.eyebrow_steepness_l.update(-np.rad2deg(a) - norm_angle, now) + f = np.max(np.abs(np.array(f_pts[1:4]) - f_pts[0, 1])) / norm_distance_y + features["eyebrow_quirk_l"] = self.eyebrow_quirk_l.update(f, now) + + a, f_pts = self.align_points(pts[17], pts[21], pts[[17, 18, 19, 20, 21]]) + features["eyebrow_steepness_r"] = self.eyebrow_steepness_r.update(np.rad2deg(a) - norm_angle, now) + f = np.max(np.abs(np.array(f_pts[1:4]) - f_pts[0, 1])) / norm_distance_y + features["eyebrow_quirk_r"] = self.eyebrow_quirk_r.update(f, now) + else: + features["eyebrow_steepness_l"] = 0. + features["eyebrow_steepness_r"] = 0. + features["eyebrow_quirk_l"] = 0. + features["eyebrow_quirk_r"] = 0. + + f = (np.mean([pts[22, 1], pts[26, 1]]) - pts[27, 1]) / norm_distance_y + features["eyebrow_updown_l"] = self.eyebrow_updown_l.update(f, now) + + f = (np.mean([pts[17, 1], pts[21, 1]]) - pts[27, 1]) / norm_distance_y + features["eyebrow_updown_r"] = self.eyebrow_updown_r.update(f, now) + + upper_mouth_line = np.mean([pts[49, 1], pts[50, 1], pts[51, 1]]) + center_line = np.mean([pts[50, 0], pts[60, 0], pts[27, 0], pts[30, 0], pts[64, 0], pts[55, 0]]) + + f = (upper_mouth_line - pts[62, 1]) / norm_distance_y + features["mouth_corner_updown_l"] = self.mouth_corner_updown_l.update(f, now) + if full: + f = abs(center_line - pts[62, 0]) / norm_distance_x + features["mouth_corner_inout_l"] = self.mouth_corner_inout_l.update(f, now) + else: + features["mouth_corner_inout_l"] = 0. + + f = (upper_mouth_line - pts[58, 1]) / norm_distance_y + features["mouth_corner_updown_r"] = self.mouth_corner_updown_r.update(f, now) + if full: + f = abs(center_line - pts[58, 0]) / norm_distance_x + features["mouth_corner_inout_r"] = self.mouth_corner_inout_r.update(f, now) + else: + features["mouth_corner_inout_r"] = 0. + + f = abs(np.mean(pts[[59,60,61], 1], axis=0) - np.mean(pts[[63,64,65], 1], axis=0)) / norm_distance_y + features["mouth_open"] = self.mouth_open.update(f, now) + + f = abs(pts[58, 0] - pts[62, 0]) / norm_distance_x + features["mouth_wide"] = self.mouth_wide.update(f, now) + + return features diff --git a/remedian.py b/remedian.py new file mode 100644 index 0000000..2e92781 --- /dev/null +++ b/remedian.py @@ -0,0 +1,82 @@ +""" +Watch over a stream of numbers, incrementally learning their median. + +Implemented via nested lists. New numbers are added to `lst[i]` and +when it fills up, it posts its median to `lst[i+1]`. Wen `lst[i+1]` +fills up, it posts the medians of its medians to `lst[i+2]`. Etc. +When a remedian is queried for the current median, it returns the +median of the last list with any numbers. + +This approach is quite space efficient . E.g. four nested lists, +each with 64 items, require memory for 4*64 items yet can hold the +median of the median of the median of the median of over 17 million +numbers. + +Example usage: + + z=remedian() + for i in range(1000): + z + i + if not i % 100: + print(i, z.median()) + +Based on [The Remedian:A Robust Averaging Method for Large Data +Sets](http://web.ipac.caltech.edu/staff/fmasci/home/astro_refs/Remedian.pdf). +by Peter J. Rousseeuw and Gilbert W. Bassett Jr. Journal of the +American Statistical Association March 1990, Vol. 85, No. 409, +Theory and Methods + +The code [remedianeg.py](remedianeg.py) compares this rig to just +using Python's built-in sort then reporing the middle number. +Assuming lists of length 64 and use of pypy3: + +- Remedian is getting nearly as fast (within 20%) as raw sort after 500 items; +- While at the same time, avoids having to store all the numbers in RAM; +- Further, remedian's computed median is within 1% (or less) of the medians found via Python's sort. + +_____ + +## Programmer's Guide + +""" + +# If `ordered` is `False`, do not sort `lst` +def median(lst,ordered=False): + assert lst,"median needs a non-empty list" + n = len(lst) + p = q = n//2 + if n < 3: + p,q = 0, n-1 + else: + lst = lst if ordered else sorted(lst) + if not n % 2: # for even-length lists, use mean of mid 2 nums + q = p -1 + return lst[p] if p==q else (lst[p]+lst[q])/2 + +class remedian: + + # Initialization + def __init__(i,inits=[], k=64, # after some experimentation, 64 works ok + about = None): + i.all,i.k = [],k + i.more,i._median=None,None + [i + x for x in inits] + + # When full, push the median of current values to next list, then reset. + def __add__(i,x): + i._median = None + i.all.append(x) + if len(i.all) == i.k: + i.more = i.more or remedian(k=i.k) + i.more + i._medianPrim(i.all) + i.all = [] # reset + + # If there is a next list, ask its median. Else, work it out locally. + def median(i): + return i.more.median() if i.more else i._medianPrim(i.all) + + # Only recompute median if we do not know it already. + def _medianPrim(i,all): + if i._median == None: + i._median = median(all,ordered=False) + return i._median