Skip to content

Commit 5465016

Browse files
Merge pull request #38 from ttngu207/dev_optimize_facemapSVD
Major changes and updates to facemapSVD pipeline
2 parents 6649e06 + 1a9d060 commit 5465016

File tree

4 files changed

+148
-132
lines changed

4 files changed

+148
-132
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
Observes [Semantic Versioning](https://semver.org/spec/v2.0.0.html) standard and
44
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) convention.
55

6+
## [0.3.0] - 2024-08-20
7+
8+
+ Update - Attribute names in `FacialSignal` table
9+
+ Update - FacemapSVD handles all ROIs and FullSVD analysis
10+
611
## [0.2.2] - 2024-05-12
712

813
+ Fix - Fix docs by updating `mkdocs`

element_facemap/facial_behavior_estimation.py

+139-130
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import importlib
22
import inspect
33
from datetime import datetime
4-
from glob import glob
54
from pathlib import Path
65
from typing import List, Tuple
76

8-
import cv2
97
import datajoint as dj
108
import numpy as np
11-
from element_interface.utils import find_full_path, find_root_directory
9+
from element_interface.utils import find_full_path, find_root_directory, memoized_result
1210

1311
schema = dj.schema()
1412

@@ -185,6 +183,7 @@ def key_source(self):
185183

186184
def make(self, key):
187185
"""Populates the RecordingInfo table."""
186+
import cv2
188187

189188
file_paths = (VideoRecording.File & key).fetch("file_path")
190189

@@ -301,33 +300,37 @@ def make(self, key):
301300
# update processing_output_dir
302301
FacemapTask.update1({**key, "facemap_output_dir": output_dir.as_posix()})
303302

303+
output_dir = find_full_path(get_facemap_root_data_dir(), output_dir)
304+
304305
if task_mode == "trigger":
305306
from facemap.process import run as facemap_run
306307

307308
params = (FacemapTask & key).fetch1("facemap_params")
308309

310+
valid_args = inspect.getfullargspec(facemap_run).args
311+
params = {k: v for k, v in params.items() if k in valid_args}
312+
309313
video_files = (FacemapTask * VideoRecording.File & key).fetch("file_path")
314+
# video files are sequentially acquired, not simultaneously
310315
video_files = [
311-
[
312-
find_full_path(get_facemap_root_data_dir(), video_file).as_posix()
313-
for video_file in video_files
314-
]
316+
[find_full_path(get_facemap_root_data_dir(), video_file).as_posix()]
317+
for video_file in video_files
315318
]
316319

317-
output_dir = find_full_path(get_facemap_root_data_dir(), output_dir)
318-
facemap_run(
319-
video_files,
320-
sbin=params["sbin"],
321-
proc=params,
322-
savepath=output_dir.as_posix(),
323-
motSVD=params["motSVD"],
324-
movSVD=params["movSVD"],
325-
)
320+
@memoized_result(uniqueness_dict=params, output_directory=output_dir)
321+
def _run_facemap_process():
322+
facemap_run(
323+
filenames=video_files,
324+
savepath=output_dir.as_posix(),
325+
**params,
326+
)
326327

327-
_, creation_time = get_loader_result(key, FacemapTask)
328-
key = {**key, "processing_time": creation_time}
328+
_run_facemap_process()
329329

330-
self.insert1(key)
330+
results_proc_fp = next(output_dir.glob("*_proc.npy"))
331+
creation_time = datetime.fromtimestamp(results_proc_fp.stat().st_ctime)
332+
333+
self.insert1({**key, "processing_time": creation_time})
331334

332335

333336
@schema
@@ -358,54 +361,54 @@ class Region(dj.Part):
358361

359362
definition = """
360363
-> master
361-
roi_no : int # Region number
364+
roi_no : int # Region number (roi_no=0 is FullSVD if exists)
362365
---
363-
roi_name='' : varchar(16) # user-friendly name of the roi
364-
xrange : longblob # 1d np.array - x pixel indices
365-
yrange : longblob # 1d np.array - y pixel indices
366-
xrange_bin : longblob # 1d np.array - binned x pixel indices
367-
yrange_bin : longblob # 1d np.array - binned y pixel indices
368-
motion : longblob # 1d np.array - absolute motion energies (nframes)
366+
roi_name='' : varchar(16) # user-friendly name of the roi
367+
xrange=null : longblob # 1d np.array - x pixel indices
368+
yrange=null : longblob # 1d np.array - y pixel indices
369+
xrange_bin=null : longblob # 1d np.array - binned x pixel indices
370+
yrange_bin=null : longblob # 1d np.array - binned y pixel indices
371+
motion=null : longblob # 1d np.array - absolute motion energies (nframes)
369372
"""
370373

371374
class MotionSVD(dj.Part):
372375
"""Components of the SVD from motion video.
373376
374377
Attributes:
375378
master.Region (foreign key): Primary key from FacialSignal.Region.
376-
pc_no (int): Principle component (PC) number.
377-
singular_value (float, optional): singular value corresponding to the PC.
378-
motmask (longblob): PC (y, x).
379-
projection (longblob): projections onto the principle component (nframes).
379+
component_id (int): component number.
380+
singular_value (float, optional): singular value corresponding to the component.
381+
motmask (longblob): (y, x).
382+
projection (longblob): projections onto the component (nframes).
380383
"""
381384

382385
definition = """
383386
-> master.Region
384-
pc_no : int # principle component (PC) number
387+
component_id : int # component number
385388
---
386-
singular_value=null : float # singular value corresponding to the PC
387-
motmask : longblob # PC (y, x)
388-
projection : longblob # projections onto the principle component (nframes)
389+
singular_value=null : float # singular value corresponding to the component
390+
motmask : longblob # (y, x)
391+
projection : longblob # projections onto the component (nframes)
389392
"""
390393

391394
class MovieSVD(dj.Part):
392395
"""Components of the SVD from movie video.
393396
394397
Attributes:
395398
master.Region (foreign key): Primary key of the FacialSignal.Region table.
396-
pc_no (int): principle component (PC) number.
397-
singular_value (float, optional): Singular value corresponding to the PC.
398-
movmask (longblob): PC (y, x)
399-
projection (longblob): Projections onto the principle component (nframes).
399+
component_id (int): component number.
400+
singular_value (float, optional): Singular value corresponding to the component.
401+
movmask (longblob): (y, x)
402+
projection (longblob): Projections onto the component (nframes).
400403
"""
401404

402405
definition = """
403406
-> master.Region
404-
pc_no : int # principle component (PC) number
407+
component_id : int # component number
405408
---
406-
singular_value=null : float # singular value corresponding to the PC
407-
movmask : longblob # PC (y, x)
408-
projection : longblob # projections onto the principle component (nframes)
409+
singular_value=null : float # singular value corresponding to the component
410+
movmask : longblob # (y, x)
411+
projection : longblob # projections onto the component (nframes)
409412
"""
410413

411414
class Summary(dj.Part):
@@ -414,121 +417,127 @@ class Summary(dj.Part):
414417
Attributes:
415418
master (foreign key): Primary key from FacialSignal.
416419
sbin (int): Spatial bin size.
417-
avgframe (longblob): 2d np.array - average binned frame.
418-
avgmotion (longblob): 2d nd.array - average binned motion frame.
420+
avgframe (longblob): 2d np.array (y, x) - average binned frame
421+
avgmotion (longblob): 2d nd.array (y, x) - average binned motion frame
419422
"""
420423

421424
definition = """
422425
-> master
423426
---
424427
sbin : int # spatial bin size
425-
avgframe : longblob # 2d np.array - average binned frame
426-
avgmotion : longblob # 2d nd.array - average binned motion frame
428+
avgframe : longblob # 2d np.array (y, x) - average binned frame
429+
avgmotion : longblob # 2d nd.array (y, x) - average binned motion frame
427430
"""
428431

429432
def make(self, key):
430433
"""Populates the FacialSignal table by transferring the results from default
431434
Facemap outputs to the database."""
432435

433-
dataset, _ = get_loader_result(key, FacemapTask)
434-
# Only motion SVD region type is supported.
435-
dataset["rois"] = [x for x in dataset["rois"] if x["rtype"] == "motion SVD"]
436+
output_dir = (FacemapTask & key).fetch1("facemap_output_dir")
437+
output_dir = find_full_path(get_facemap_root_data_dir(), output_dir)
438+
results_proc_fp = next(output_dir.glob("*_proc.npy"))
439+
dataset = np.load(results_proc_fp, allow_pickle=True).item()
436440

437-
self.insert1(key)
441+
region_entries, motion_svd_entries, movie_svd_entries = [], [], []
442+
motions = dataset["motion"].copy()
438443

439-
self.Region.insert(
440-
[
444+
motion_svd_rois = []
445+
if dataset["fullSVD"]:
446+
region_entries.append(
441447
dict(
442448
key,
443-
roi_no=i,
444-
xrange=dataset["rois"][i]["xrange"],
445-
yrange=dataset["rois"][i]["yrange"],
446-
xrange_bin=(
447-
dataset["rois"][i]["xrange_bin"]
448-
if "xrange_bin" in dataset["rois"][i]
449-
else None
450-
),
451-
yrange_bin=(
452-
dataset["rois"][i]["yrange_bin"]
453-
if "yrange_bin" in dataset["rois"][i]
454-
else None
455-
),
456-
motion=dataset["motion"][i + 1],
449+
roi_no=0,
450+
roi_name="FullSVD",
451+
xrange=np.arange(dataset["Lx"][0]),
452+
yrange=np.arange(dataset["Ly"][0]),
453+
motion=motions.pop(),
454+
)
455+
)
456+
motion_svd_rois.append(0)
457+
# Region
458+
if dataset["rois"] is not None:
459+
for i, roi in enumerate(dataset["rois"]):
460+
roi_no = i + int(dataset["fullSVD"])
461+
roi_name = f"{roi['rtype']}_{roi['iROI']}"
462+
if roi["rtype"] == "motion SVD":
463+
motion_svd_rois.append(roi_no)
464+
motion = motions.pop()
465+
else:
466+
motion = None
467+
region_entries.append(
468+
dict(
469+
key,
470+
roi_no=roi_no,
471+
roi_name=roi_name,
472+
xrange=roi["xrange"],
473+
yrange=roi["yrange"],
474+
xrange_bin=roi.get("xrange_bin"),
475+
yrange_bin=roi.get("yrange_bin"),
476+
motion=motion,
477+
)
457478
)
458-
for i in range(len(dataset["rois"]))
459-
if dataset["rois"][i]["rtype"] == "motion SVD"
460-
]
461-
)
462-
463479
# MotionSVD
464480
if any(np.any(x) for x in dataset.get("motSVD", [False])):
465-
entry = [
466-
dict(
467-
key,
468-
roi_no=roi_no,
469-
pc_no=i,
470-
singular_value=(
471-
dataset["motSv"][roi_no][i] if "motSv" in dataset else None
472-
),
473-
motmask=dataset["motMask_reshape"][roi_no + 1][:, :, i],
474-
projection=dataset["motSVD"][roi_no + 1][i],
481+
for roi_idx, roi_no in enumerate(motion_svd_rois):
482+
roi_idx += int(
483+
not dataset["fullSVD"]
484+
) # skip the first entry if fullSVD is False
485+
motSVD = dataset["motSVD"][roi_idx]
486+
motMask = dataset["motMask_reshape"][roi_idx]
487+
motSv = (
488+
dataset["motSv"][roi_idx]
489+
if "motSv" in dataset
490+
else np.full(motSVD.shape[-1], np.nan)
491+
)
492+
motion_svd_entries.extend(
493+
[
494+
dict(
495+
key,
496+
roi_no=roi_no,
497+
component_id=idx,
498+
singular_value=s,
499+
motmask=m,
500+
projection=p,
501+
)
502+
for idx, (s, m, p) in enumerate(zip(motSv, motMask, motSVD))
503+
]
475504
)
476-
for roi_no in range(len(dataset["rois"]))
477-
for i in range(dataset["motSVD"][roi_no + 1].shape[1])
478-
]
479-
self.MotionSVD.insert(entry)
480-
481505
# MovieSVD
482506
if any(np.any(x) for x in dataset.get("movSVD", [False])):
483-
entry = [
484-
dict(
485-
key,
486-
roi_no=roi_no,
487-
pc_no=i,
488-
singular_value=(
489-
dataset["movSv"][roi_no][i] if "movSv" in dataset else None
490-
),
491-
movmask=dataset["movMask_reshape"][roi_no + 1][:, :, i],
492-
projection=dataset["movSVD"][roi_no + 1][i],
507+
for roi_idx, roi_no in enumerate(motion_svd_rois):
508+
roi_idx += int(
509+
not dataset["fullSVD"]
510+
) # skip the first entry if fullSVD is False
511+
movSVD = dataset["movSVD"][roi_idx]
512+
movMask = dataset["movMask_reshape"][roi_idx]
513+
movSv = (
514+
dataset["movSv"][roi_idx]
515+
if "movSv" in dataset
516+
else np.full(movSVD.shape[-1], np.nan)
517+
)
518+
motion_svd_entries.extend(
519+
[
520+
dict(
521+
key,
522+
roi_no=roi_no,
523+
component_id=idx,
524+
singular_value=s,
525+
motmask=m,
526+
projection=p,
527+
)
528+
for idx, (s, m, p) in enumerate(zip(movSv, movMask, movSVD))
529+
]
493530
)
494-
for roi_no in range(len(dataset["rois"]))
495-
for i in range(dataset["movSVD"][roi_no + 1].shape[1])
496-
]
497-
self.MovieSVD.insert(entry)
498531

499-
# Summary
532+
self.insert1(key)
533+
self.Region.insert(region_entries)
534+
self.MotionSVD.insert(motion_svd_entries)
535+
self.MovieSVD.insert(movie_svd_entries)
500536
self.Summary.insert1(
501537
dict(
502538
key,
503539
sbin=dataset["sbin"],
504-
avgframe=dataset["avgframe"][0],
505-
avgmotion=dataset["avgmotion"][0],
540+
avgframe=dataset["avgframe_reshape"],
541+
avgmotion=dataset["avgmotion_reshape"],
506542
)
507543
)
508-
509-
510-
# ---------------- HELPER FUNCTIONS ----------------
511-
512-
513-
def get_loader_result(
514-
key: dict, table: dj.user_tables.TableMeta
515-
) -> Tuple[np.array, datetime]:
516-
"""Retrieve the facemap analysis results.
517-
518-
Args:
519-
key (dict): A primary key for an entry in the provided table.
520-
table (dj.Table): DataJoint user table from which loaded results are retrieved (i.e. FacemapTask).
521-
522-
Returns:
523-
loaded_dataset (np.array): The results of the facemap analysis.
524-
creation_time (datetime): Date and time that the results files were created.
525-
"""
526-
output_dir = (table & key).fetch1("facemap_output_dir")
527-
528-
output_path = find_full_path(get_facemap_root_data_dir(), output_dir)
529-
output_file = glob(output_path.as_posix() + "/*_proc.npy")[0]
530-
531-
loaded_dataset = np.load(output_file, allow_pickle=True).item()
532-
creation_time = datetime.fromtimestamp(Path(output_file).stat().st_ctime)
533-
534-
return loaded_dataset, creation_time

element_facemap/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Package metadata."""
22

3-
__version__ = "0.2.2"
3+
__version__ = "0.3.0"

0 commit comments

Comments
 (0)