Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b067e21
format: frontend css
raymondwjang Nov 10, 2025
4d3c320
feat: add downsample
raymondwjang Nov 11, 2025
9c35757
tests: benchmarking moved out of tests
raymondwjang Nov 11, 2025
c988dc7
debug: retain older traces than peek_size
raymondwjang Nov 11, 2025
6bdd3d3
feat: asset save / natsorting videos
raymondwjang Nov 11, 2025
76a60bc
feat: long_recording.yaml
raymondwjang Nov 11, 2025
7d648e4
init_order
raymondwjang Nov 11, 2025
730b934
format: ruff
raymondwjang Nov 11, 2025
22e8838
debug: handle sparse in zarr
raymondwjang Nov 11, 2025
46caa9a
stop fucking switching the import order
raymondwjang Nov 11, 2025
c351f76
feat: trace interval flushing to zarr functionality
raymondwjang Nov 11, 2025
1a148f5
feat: trace mostly supporting new grammar (except zarr component update)
raymondwjang Nov 11, 2025
4d0e5f4
feat: implement zarr and in-memory caching in traces
raymondwjang Nov 11, 2025
8edf5c4
format: ruff
raymondwjang Nov 11, 2025
1555a2b
debug: new component concat
raymondwjang Nov 11, 2025
d7dffc9
debug: new component concat
raymondwjang Nov 11, 2025
93653aa
debug: update noob for gather compatibility
raymondwjang Nov 11, 2025
a369cd1
debug: asset zarr saving
raymondwjang Nov 11, 2025
f9f89d1
test: motion correction crisp score
raymondwjang Nov 12, 2025
bea7406
debug: allow non-zarr
raymondwjang Nov 12, 2025
59c708e
feat: continue for gui failure
raymondwjang Nov 12, 2025
d903419
format: ruff
raymondwjang Nov 21, 2025
dd306e4
rename: detect to segment
raymondwjang Nov 21, 2025
eb077b9
refactor: restructure filetree to group omf
raymondwjang Nov 22, 2025
f7b301f
chore: cleanup unused codes
raymondwjang Nov 26, 2025
3e4b857
feat: separate out segment loop
raymondwjang Nov 26, 2025
fb21044
feat: recurse cala
raymondwjang Dec 2, 2025
c1b2373
feat: scripts
raymondwjang Dec 2, 2025
0a7a4da
mypy
raymondwjang Dec 2, 2025
c8168b3
chore: intermediate changes to assets
raymondwjang Dec 3, 2025
c5b8a2e
feat: clearer asset names
raymondwjang Dec 4, 2025
3fdcc9e
feat: unify axis copy
raymondwjang Dec 4, 2025
0c104b2
feat: import cleanup
raymondwjang Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repos:
name: pdm-format
entry: pdm format
language: system
types: [python]
types: [ python ]
pass_filenames: false
always_run: true
# - repo: https://github.com/pre-commit/mirrors-mypy
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
> [!CAUTION]
> **NOT READY FOR USE: In active development in version alpha. Beta release scheduled by the end of 2025.**

Cala is a neural endoscope image processing tool designed for neuroscience research, with a focus on long-term massive recordings. It features a no-code approach through configuration files, making it accessible to researchers of all programming backgrounds.
Cala is a neural endoscope image processing tool designed for neuroscience research, with a focus on long-term massive
recordings. It features a no-code approach through configuration files, making it accessible to researchers of all
programming backgrounds.

## Requirements

Expand All @@ -20,11 +22,10 @@ Cala is a neural endoscope image processing tool designed for neuroscience resea

## Architecture

Schematics of the architecture can be found [here](https://lucid.app/documents/embedded/808097f9-bf66-4ea8-9df0-e957e6bd0931).


3. **API Reference**: Available on [Read the Docs](https://cala.readthedocs.io/en/latest/)
Schematics of the architecture can be
found [here](https://lucid.app/documents/embedded/808097f9-bf66-4ea8-9df0-e957e6bd0931).

1. **API Reference**: Available on [Read the Docs](https://cala.readthedocs.io/en/latest/)

## Contact

Expand Down
17 changes: 14 additions & 3 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 18 additions & 23 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ dependencies = [
"pyyaml>=6.0.2",
"typer>=0.15.3",
"xarray-validate>=0.0.2",
"noob @ git+https://github.com/miniscope/noob.git",
"noob @ git+https://github.com/miniscope/noob.git@scheduler-optimize",
"natsort>=8.4.0",
]
keywords = [
"pipeline",
Expand Down Expand Up @@ -138,6 +139,7 @@ test = "pytest"
docs = "sphinx-autobuild docs docs/_build/html"
start.cmd = "npm run dev"
start.working_dir = "frontend"
mypy = "mypy"


[tool.pytest.ini_options]
Expand All @@ -146,10 +148,10 @@ pythonpath = ["src"]
addopts = [
"-ra",
"-q",
# "--cov=cala",
# "--cov-append",
# "--cov-report=term-missing",
# "--cov-report=html"
# "--cov=cala",
# "--cov-append",
# "--cov-report=term-missing",
# "--cov-report=html"
]
markers = [
"timeout: marks tests that need a timeout failure to prevent falling into an infinite loop"
Expand Down Expand Up @@ -206,8 +208,6 @@ select = [
"S608", "S701",
]
ignore = [
# needing to annotate `self` is ridiculous
"ANN101",
#"special" methods like `__init__` don't need to be annotated
"ANN204",
# any types are semantically valid actually sometimes
Expand All @@ -234,22 +234,6 @@ ignore = [
"F401"
]

[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_optional = true
exclude = ["tests/.*"]

[tool.coverage.run]
source = ["src/cala"]
omit = [
Expand All @@ -276,3 +260,14 @@ omit = [
"**/__init__.py",
"**/conftest.py",
]
[tool.mypy]
mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
packages = ["cala"]
warn_redundant_casts = true
warn_unused_ignores = true
show_error_context = true
show_column_numbers = true
show_error_code_links = true
pretty = true
color_output = true
plugins = ['pydantic.mypy']
File renamed without changes.
204 changes: 204 additions & 0 deletions scripts/benchmarking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
from datetime import datetime

import cv2
import matplotlib
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
from noob import SynchronousRunner, Tube

from cala.nodes.io import stream
from cala.nodes.prep import Anchor, blur, butter, package_frame, remove_mean
from cala.testing.util import total_gradient_magnitude

sns.set_style("whitegrid")
font = {"family": "normal", "weight": "regular", "size": 15}

matplotlib.rc("font", **font)


VIDEOS = [
"minian/msCam1.avi",
"minian/msCam2.avi",
"minian/msCam3.avi",
"minian/msCam4.avi",
"minian/msCam5.avi",
"minian/msCam6.avi",
"minian/msCam7.avi",
"minian/msCam8.avi",
"minian/msCam9.avi",
"minian/msCam10.avi",
# "long_recording/0.avi",
# "long_recording/1.avi",
# "long_recording/2.avi",
# "long_recording/3.avi",
# "long_recording/4.avi",
]


def preprocess(arr, idx):
frame = package_frame(arr, idx)
frame = blur(frame, method="median", kwargs={"ksize": 3})
frame = butter(frame, {})
return remove_mean(frame, orient="both")


def test_encode():
gen = stream(VIDEOS)

fourcc = cv2.VideoWriter_fourcc(*"FFV1")
out = cv2.VideoWriter("encode_test.avi", fourcc, 60.0, (600, 600))

for arr in gen:
frame_bgr = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_GRAY2BGR)
out.write(frame_bgr)

out.release()


def test_motion_movie():
"""
For testing how well the motion correction performs with real movie

"""
gen = stream(VIDEOS)

fourcc = cv2.VideoWriter_fourcc(*"mp4v")
out = cv2.VideoWriter("motion_test.avi", fourcc, 60.0, (600, 1200))

stab = Anchor()

for idx, arr in enumerate(gen):
frame = preprocess(arr, idx)
matched = stab.stabilize(frame)
combined = np.concat([frame.array.values, matched.array.values], axis=0)

frame_bgr = cv2.cvtColor(combined.astype(np.uint8), cv2.COLOR_GRAY2BGR)
out.write(frame_bgr)

out.release()


def test_motion_crisp_pics():
"""
For generating a mean summary frame across raw vs. motion-corrected video.
The motion-corrected video should have a much crisper summary picture.

"""

gen = stream(VIDEOS)

stab = Anchor()
raws = []
stabs = []

for idx, arr in enumerate(gen):
frame = preprocess(arr, idx)
matched = stab.stabilize(frame)
raws.append(frame.array.values)
stabs.append(matched.array.values)

raw = np.stack(raws)
stab = np.stack(stabs)

raw_mean = np.mean(raw, axis=0)
stab_mean = np.mean(stab, axis=0)

crisp_raw = total_gradient_magnitude(raw_mean)
crisp_stab = total_gradient_magnitude(stab_mean)

print(f"{crisp_raw = }, {crisp_stab = }")

mean = np.concatenate((raw_mean, stab_mean), axis=0)
plt.imsave("motion_crisp_pics.png", mean, cmap="gray")


def test_motion_mean_corr():
"""
For testing how well the motion correction performs with real movie
"""
gen = stream(VIDEOS)

stab = Anchor()
raws = []
stabs = []

for idx, arr in enumerate(gen):
frame = preprocess(arr, idx)
matched = stab.stabilize(frame)
raws.append(frame.array.values)
stabs.append(matched.array.values)

raw = np.stack(raws)
stab = np.stack(stabs)

raw_mean = np.mean(raw[:, 20:-20, 20:-20], axis=0)
stab_mean = np.mean(stab[:, 20:-20, 20:-20], axis=0)

raw_cms = [np.corrcoef(r[20:-20, 20:-20].flatten(), raw_mean.flatten())[0, 1] for r in raws]
stab_cms = [np.corrcoef(s[20:-20, 20:-20].flatten(), stab_mean.flatten())[0, 1] for s in stabs]

fig, ax = plt.subplots(figsize=(24, 10))
plt.plot(raw_cms)
plt.plot(stab_cms)
plt.legend(["raw", "stabilized"], loc="upper right")
plt.title("Mean Correlation")
plt.xlabel("frame")
plt.ylabel("correlation")
plt.tight_layout()
plt.savefig("mc.png")

assert False


def test_with_movie():
tube = Tube.from_specification("with-minian")
runner = SynchronousRunner(tube=tube)
processed_vid = runner.run()
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
out = cv2.VideoWriter("motion_test.avi", fourcc, 20.0, (100, 100))

for arr in processed_vid:
frame_bgr = cv2.cvtColor(arr.array.values.astype(np.uint8), cv2.COLOR_GRAY2BGR)
out.write(frame_bgr)
out.release()


def test_processing_speed():
tube = Tube.from_specification("with-minian")
runner = SynchronousRunner(tube=tube)
gen = runner.iter()
frame_speed = []
i = 0
while True:
try:
start = datetime.now()
next(gen)
duration = datetime.now() - start
frame_speed.append(round(duration.total_seconds(), 2))
i += 1
except RuntimeError:
break
fig, ax = plt.subplots(figsize=(20, 4))
ax.set_yscale("log")
plt.plot(frame_speed)
plt.xlabel("frame", fontsize=20)
plt.ylabel("time taken (s)", fontsize=20)
plt.tight_layout()
plt.savefig("frame_speed.png")


def test_deglow():

gen = stream(VIDEOS[:3])

stab = Anchor()
stabs = []

for idx, arr in enumerate(gen):
frame = preprocess(arr, idx)
matched = stab.stabilize(frame)
stabs.append(matched.array.values)

deglowed = stabs[-1] - np.min(stabs, axis=0)
plt.imsave("deglowed.png", deglowed, cmap="gray")
35 changes: 35 additions & 0 deletions scripts/memory_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os

import psutil
from matplotlib import pyplot as plt
from noob import Tube, SynchronousRunner


def main():
process = psutil.Process(os.getpid())
tube = Tube.from_specification("test-memory")
runner = SynchronousRunner(tube=tube)
gen = runner.iter()
ram_use_frame = []
i = 0
while True:
try:
next(gen)
ram_used = process.memory_info().rss / (1024 * 1024) # in MB
ram_use_frame.append(round(ram_used, 2))
i += 1
if i % 100 == 0:
print(f"{i} frames processed")
except RuntimeError as e:
print(e)
break
fig, ax = plt.subplots(figsize=(40, 8))
plt.plot(ram_use_frame)
plt.xlabel("frame")
plt.ylabel("memory used (MB)")
plt.tight_layout()
plt.savefig("ram_use.svg", format="svg")


if __name__ == "__main__":
main()
Loading