Skip to content

Commit 7a9715e

Browse files
author
eduardotogpi
committed
Implemented multi video pipeline, among other tweaks
1 parent 92f037c commit 7a9715e

File tree

10 files changed

+235
-40
lines changed

10 files changed

+235
-40
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
*.wmv
22
*.csv
33
*.png
4+
*.mp4
5+
*.npz
6+
clear_data.py
47

58
# Byte-compiled / optimized / DLL files
69
__pycache__/

LICENSE

100644100755
File mode changed.

Plots/README.md

100644100755
File mode changed.

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
# pyCRTScripts
22

33
User-friendly scripts designed to make pyCRT accessible to non-programmers.
4+
5+
# TODO
6+
* Add option to view the plots and CRT before saving;
7+
* Try to implement interactive fromTime, toTime and algorithm selection;
8+
* Implement PCA results as a channel;
9+
* Develop algorithm to find appropriate fromTime and toTime;
10+
* Add fromTime and toTime to avgIntensPlot;
11+
* Add monochrome as a possible channel;
12+
* Add tests for pyCRTScripts;
13+
* Add 9010 as an algorithm;
14+
* Add timestamp to csv;
15+
* Add input "Test another video?";
16+
* Add tests;

configuration.toml

100644100755
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
plotPath = "Plots"
44
csvPath = "results_sheet.csv"
55
npzPath = "Npz"
6-
overwrite = true
6+
overwrite = false
7+
ignoreDuplicates = true
78

89
[Video]
910

1011
rescaleFactor = 0.5
11-
livePlot = true
12+
livePlot = false # Leave as false if using Windows
1213
displayVideo = true
1314
playbackSpeed = "fast"
1415

@@ -17,12 +18,14 @@ playbackSpeed = "fast"
1718
sliceMethod = "from local max"
1819
fromTime = 0
1920
toTime = inf
20-
exclusionCriteria = 0.20
21+
exclusionCriteria = inf
2122
exclusionMethod = "first positive peak"
2223
channel = "g"
2324
initialGuesses = [1.0, -0.3, 0.0]
2425
roi = -1
2526

2627
[General]
2728

28-
logLevel = "INFO"
29+
logLevel = "DEBUG"
30+
showPlots = true
31+
askConfirmation = true

defaults.toml

100644100755
File mode changed.

functions.py

100644100755
Lines changed: 109 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
from __future__ import annotations
22

33
import logging
4-
import tkinter as tk
54
from pathlib import Path
6-
from tkinter import filedialog
75
from typing import Any
6+
import gui
87

98
import numpy as np
109
import pandas as pd
11-
import tomli
12-
from pyCRT.simpleUI import PCRT, RoiTuple
10+
11+
try:
12+
import tomllib as tomli # Python 3.11+
13+
except ModuleNotFoundError:
14+
import tomli # fallback for older Python
15+
16+
from pyCRT.simpleUI import DATETIME_FORMAT, PCRT, RoiTuple
1317

1418
PLAYBACK_FPS_SPEED = {
1519
"fast": np.inf,
1620
"normal": 0,
1721
"slow": 25,
1822
}
1923

24+
NORM_LEVEL = 25
25+
logging.addLevelName(NORM_LEVEL, "NORM")
26+
VIDEO_FORMATS = (".mp4", ".wmv", ".avi", ".mov", ".mkv")
27+
2028

2129
def getLogger(name: str):
2230
# {{{
@@ -35,21 +43,6 @@ def getLogger(name: str):
3543
LOGGER = getLogger("mauricio")
3644

3745

38-
def selectFile() -> Path:
39-
# {{{
40-
root = tk.Tk()
41-
root.withdraw() # hide main window
42-
root.attributes("-topmost", True)
43-
selection = filedialog.askopenfilename()
44-
if not selection:
45-
raise RuntimeError("No video selected. Aborting...")
46-
filePath = Path(selection)
47-
root.destroy()
48-
return filePath
49-
50-
51-
# }}}
52-
5346

5447
def loadConfigFile(
5548
tomlPath, defaultsPath: Path | str = "defaults.toml"
@@ -221,7 +214,7 @@ def saveCSV(
221214
overwrite: bool = False,
222215
) -> None:
223216
# {{{
224-
colLabels = ["pCRT", "Unc", "RelUnc", "CT", "ExcCri", "ExcMet"]
217+
colLabels = ["pCRT", "Unc", "RelUnc", "CT", "ExcCri", "ExcMet", "Time"]
225218

226219
row = {
227220
"pCRT": round(float(pcrtObj.pCRT[0]), 3),
@@ -230,6 +223,7 @@ def saveCSV(
230223
"CT": round(float(pcrtObj.criticalTime), 3),
231224
"ExcCri": pcrtObj.exclusionCriteria,
232225
"ExcMet": pcrtObj.exclusionMethod,
226+
"Time": pcrtObj.dateTime.strftime(DATETIME_FORMAT),
233227
}
234228

235229
csvPath = Path(csvPath)
@@ -275,21 +269,11 @@ def saveNpz(
275269
npzFilePath = npzPath / f"{videoName}.npz"
276270
if not overwrite:
277271
npzFilePath = findUniquePath(npzFilePath)
278-
np.savez(
279-
str(npzFilePath),
280-
fullTimeScdsArr=pcrtObj.fullTimeScdsArr,
281-
channelsAvgIntensArr=pcrtObj.channelsAvgIntensArr,
282-
channel=pcrtObj.channel,
283-
fromTime=pcrtObj.fromTime,
284-
toTime=pcrtObj.toTime,
285-
expTuple=np.array(pcrtObj.expTuple),
286-
polyTuple=np.array(pcrtObj.polyTuple),
287-
pCRTTuple=np.array(pcrtObj.pCRTTuple),
288-
criticalTime=pcrtObj.criticalTime,
289-
exclusionMethod=pcrtObj.exclusionMethod,
290-
exclusionCriteria=pcrtObj.exclusionCriteria,
291-
)
272+
pcrtObj.name = videoName
273+
pcrtObj.save(npzFilePath)
292274
LOGGER.info(f"pCRT object saved in {npzFilePath}.")
275+
276+
293277
# }}}
294278

295279

@@ -340,16 +324,106 @@ def singleVideoPipeline(
340324

341325
LOGGER.setLevel(configDict["General"]["logLevel"])
342326
LOGGER.info(f"Config loaded from {configPath}.")
343-
crtVideoPath = selectFile()
327+
crtVideoPath = gui.selectFile()
344328
videoName = crtVideoPath.stem
345329
pcrtObj, roi = measureCRTVideoFromConfig(crtVideoPath, configDict)
346330

331+
LOGGER.log(NORM_LEVEL, f"{pcrtObj.plotTitle}: {pcrtObj}")
332+
347333
plotPath = configDict["Files"]["plotPath"]
348334
csvPath = configDict["Files"]["csvPath"]
349335
npzPath = configDict["Files"]["npzPath"]
350336
overwrite = configDict["Files"]["overwrite"]
351337

338+
showPlots = configDict["General"]["showPlots"]
339+
if showPlots:
340+
pcrtObj.showAvgIntensPlot()
341+
pcrtObj.showPCRTPlot()
342+
352343
savePlots(pcrtObj, videoName, plotPath, overwrite)
353344
saveCSV(pcrtObj, videoName, csvPath, overwrite)
354345
saveNpz(pcrtObj, videoName, npzPath, overwrite)
346+
347+
348+
# }}}
349+
350+
351+
def multiVideoPipeline(
352+
configPath: Path | str,
353+
defaultsPath: Path | str,
354+
) -> list[PCRT]:
355+
# {{{
356+
configDict = loadConfigFile(configPath, defaultsPath)
357+
358+
LOGGER.setLevel(configDict["General"]["logLevel"])
359+
LOGGER.info(f"Config loaded from {configPath}.")
360+
361+
askConfirmation = configDict["General"]["askConfirmation"]
362+
# Just in case the user decides to read the videos out of order
363+
processedPaths = []
364+
365+
plotPath = configDict["Files"]["plotPath"]
366+
csvPath = configDict["Files"]["csvPath"]
367+
npzPath = configDict["Files"]["npzPath"]
368+
overwrite = configDict["Files"]["overwrite"]
369+
showPlots = configDict["General"]["showPlots"]
370+
371+
dirPath = gui.selectDirectory()
372+
for candidatePath in dirPath.iterdir():
373+
if not candidatePath.is_file():
374+
LOGGER.debug(
375+
f"Skipping {candidatePath.stem}.{candidatePath.suffix} "
376+
"(not a file)..."
377+
)
378+
continue
379+
if candidatePath.suffix not in VIDEO_FORMATS:
380+
LOGGER.debug(
381+
f"Skipping {candidatePath.stem}.{candidatePath.suffix} "
382+
"(not a video)..."
383+
)
384+
continue
385+
if candidatePath in processedPaths:
386+
LOGGER.debug(
387+
f"Skipping {candidatePath.stem}.{candidatePath.suffix} "
388+
"(already processed)..."
389+
)
390+
continue
391+
if askConfirmation:
392+
answer = gui.askNextVideo(candidatePath)
393+
if answer == "skip":
394+
processedPaths.append(candidatePath)
395+
continue
396+
elif answer == "abort":
397+
break
398+
elif answer == "select":
399+
actualPath = gui.selectFile()
400+
else:
401+
actualPath = candidatePath
402+
else:
403+
actualPath = candidatePath
404+
405+
LOGGER.info(f"Processing video {actualPath}.")
406+
407+
videoName = actualPath.stem
408+
try:
409+
pcrtObj, _ = measureCRTVideoFromConfig(actualPath, configDict)
410+
processedPaths.append(actualPath)
411+
except (RuntimeError, TypeError, ValueError):
412+
LOGGER.error("CRT calculation failed on {actualPath}")
413+
processedPaths.append(actualPath)
414+
continue
415+
416+
LOGGER.log(NORM_LEVEL, f"{pcrtObj.plotTitle}: {pcrtObj}")
417+
418+
if showPlots:
419+
pcrtObj.showAvgIntensPlot()
420+
pcrtObj.showPCRTPlot()
421+
savePlots(pcrtObj, videoName, plotPath, overwrite)
422+
saveCSV(pcrtObj, videoName, csvPath, overwrite)
423+
saveNpz(pcrtObj, videoName, npzPath, overwrite)
424+
425+
LOGGER.info(f"Finished processing videos in {dirPath}")
426+
427+
428+
355429
# }}}

gui.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import tkinter as tk
2+
from pathlib import Path
3+
from tkinter import filedialog, ttk
4+
5+
6+
def askNextVideo(videoName: str) -> str:
7+
# {{{
8+
"""
9+
Open a dialog asking whether to continue to the next video. By chatGPT
10+
11+
Parameters
12+
----------
13+
videoName : str
14+
Name of the next video.
15+
16+
Returns
17+
-------
18+
str
19+
One of: "yes", "skip", "select", "abort"
20+
"""
21+
result: str | None = None
22+
23+
def set_result(value: str) -> None:
24+
nonlocal result
25+
result = value
26+
root.destroy()
27+
28+
root = tk.Tk()
29+
root.title("Continue?")
30+
root.resizable(False, False)
31+
root.attributes("-topmost", True)
32+
33+
frame = ttk.Frame(root, padding=12)
34+
frame.pack(fill="both", expand=True)
35+
36+
label = ttk.Label(
37+
frame,
38+
text=f"Continue to next video:\n{videoName}?",
39+
justify="center",
40+
)
41+
label.pack(pady=(0, 12))
42+
43+
btn_frame = ttk.Frame(frame)
44+
btn_frame.pack()
45+
46+
ttk.Button(btn_frame, text="Yes", command=lambda: set_result("yes")).grid(
47+
row=0, column=0, padx=5
48+
)
49+
ttk.Button(
50+
btn_frame, text="Skip", command=lambda: set_result("skip")
51+
).grid(row=0, column=1, padx=5)
52+
ttk.Button(
53+
btn_frame, text="Select Video", command=lambda: set_result("select")
54+
).grid(row=0, column=2, padx=5)
55+
ttk.Button(
56+
btn_frame, text="Abort", command=lambda: set_result("abort")
57+
).grid(row=0, column=3, padx=5)
58+
59+
root.protocol("WM_DELETE_WINDOW", lambda: set_result("abort"))
60+
root.mainloop()
61+
62+
assert result is not None
63+
return result
64+
65+
66+
# }}}
67+
68+
69+
def selectFile() -> Path:
70+
# {{{
71+
root = tk.Tk()
72+
root.withdraw() # hide main window
73+
root.attributes("-topmost", True)
74+
selection = filedialog.askopenfilename()
75+
if not selection:
76+
raise RuntimeError("No video selected. Aborting...")
77+
filePath = Path(selection)
78+
root.destroy()
79+
return filePath
80+
81+
82+
# }}}
83+
84+
85+
def selectDirectory() -> Path:
86+
# {{{
87+
root = tk.Tk()
88+
root.withdraw() # hide main window
89+
root.attributes("-topmost", True)
90+
selection = filedialog.askdirectory()
91+
if not selection:
92+
raise RuntimeError("No directory selected. Aborting...")
93+
dirPath = Path(selection)
94+
root.destroy()
95+
return dirPath
96+
97+
98+
# }}}

measure_crt_dir.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env python3
2+
from functions import multiVideoPipeline
3+
4+
multiVideoPipeline("configuration.toml", "defaults.toml")

requirements.txt

100644100755
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
numpy>=2.0.0
22
pandas
33
pyCRT>=2.2.1
4-
tomli
4+
tomli; python_version < "3.11"

0 commit comments

Comments
 (0)