Skip to content

Commit 935fdc0

Browse files
author
Adrian Roth
committed
new pyqtgraph viewer
1 parent 36437bf commit 935fdc0

4 files changed

Lines changed: 208 additions & 58 deletions

File tree

pycine/cli/pfs_raw.py

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
#!/usr/bin/env python3
22
import os
3+
import functools
34

45
import click
56
import cv2
67
import numpy as np
78

8-
from pycine.color import color_pipeline, resize
9+
10+
from pycine.color import color_pipeline
911
from pycine.raw import read_frames
12+
from pycine.viewer import view_cine
13+
14+
15+
def image_post_processing(image, setup, bpp):
16+
if setup.CFA in [3, 4]:
17+
image = color_pipeline(image, setup=setup, bpp=bpp)
18+
elif setup.CFA == 0:
19+
pass
20+
else:
21+
raise ValueError("Sensor not supported")
1022

23+
if setup.EnableCrop:
24+
image = image[setup.CropRect.top : setup.CropRect.bottom + 1, setup.CropRect.left : setup.CropRect.right + 1]
1125

12-
def display(image_8bit):
13-
cv2.imshow("image", image_8bit)
14-
cv2.waitKey(0)
15-
cv2.destroyAllWindows()
26+
if setup.EnableResample:
27+
image = cv2.resize(image, (setup.ResampleWidth, setup.ResampleHeight))
28+
return image
1629

1730

1831
@click.command()
@@ -29,40 +42,24 @@ def cli(
2942
out_path: str,
3043
cine_file: str,
3144
):
32-
raw_images, setup, bpp = read_frames(cine_file, start_frame=start_frame, count=count)
33-
34-
if setup.CFA in [3, 4]:
35-
# FIXME: the color pipeline is not at all ready for production!
36-
images = (color_pipeline(raw_image, setup=setup, bpp=bpp) for raw_image in raw_images)
37-
38-
elif setup.CFA == 0:
39-
images = raw_images
45+
images, setup, bpp = read_frames(cine_file, start_frame=start_frame, count=count)
46+
images.post_processing = functools.partial(image_post_processing, setup=setup, bpp=bpp)
4047

41-
else:
42-
raise ValueError("Sensor not supported")
48+
if not out_path:
49+
# cine count is not used here
50+
view_cine(images)
51+
return
4352

4453
for i, rgb_image in enumerate(images):
4554
frame_number = start_frame + i
4655

47-
if setup.EnableCrop:
48-
rgb_image = rgb_image[
49-
setup.CropRect.top : setup.CropRect.bottom + 1, setup.CropRect.left : setup.CropRect.right + 1
50-
]
51-
52-
if setup.EnableResample:
53-
rgb_image = cv2.resize(rgb_image, (setup.ResampleWidth, setup.ResampleHeight))
54-
55-
if out_path:
56-
ending = file_format.strip(".")
57-
name = os.path.splitext(os.path.basename(cine_file))[0]
58-
out_name = f"{name}-{frame_number:06d}.{ending}"
59-
out_file = os.path.join(out_path, out_name)
60-
print(f"Writing File {out_file}")
61-
interpolated = np.interp(rgb_image, [0, 2 ** bpp - 1], [0, 2 ** 16 - 1]).astype(np.uint16)
62-
cv2.imwrite(out_file, interpolated)
63-
64-
else:
65-
display(resize(rgb_image, 720))
56+
ending = file_format.strip(".")
57+
name = os.path.splitext(os.path.basename(cine_file))[0]
58+
out_name = f"{name}-{frame_number:06d}.{ending}"
59+
out_file = os.path.join(out_path, out_name)
60+
print(f"Writing File {out_file}")
61+
interpolated = np.interp(rgb_image, [0, 2 ** bpp - 1], [0, 2 ** 16 - 1]).astype(np.uint16)
62+
cv2.imwrite(out_file, interpolated)
6663

6764

6865
if __name__ == "__main__":

pycine/raw.py

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,104 @@
1212
logger = logging.getLogger()
1313

1414

15-
def frame_reader(
16-
cine_file: Union[str, bytes, PathLike],
17-
header: Header,
18-
start_frame: int = 1,
19-
count: int = None,
20-
) -> Generator[np.ndarray, Any, None]:
21-
frame = start_frame
22-
if not count:
23-
count = header["cinefileheader"].ImageCount
15+
class Frame_reader:
16+
def __init__(self, cine_file, header=None, start_index=0, count=None, post_processing=None):
17+
"""
18+
Create an object for reading frames from a cine file. It can either be used to get specific frames with __getitem__ or as an iterator.
19+
20+
Parameters
21+
----------
22+
cine_file : str or file-like object
23+
A string containing a path to a cine file
24+
header : dict (optional)
25+
A dictionary contains header information of the cine file
26+
start_index : int
27+
First image in a pile of images in cine file. Only used with the iterator in the object.
28+
count : int
29+
Maximum number of frames to get if this object is used as an iterator.
30+
post_processing : function
31+
Function that takes one image parameter and returns a new processed image
32+
If provided this function will be applied to the raw image before returning.
33+
34+
Returns
35+
-------
36+
the created object
37+
"""
38+
self.cine_file = cine_file
39+
self.cine_file_stream = open(cine_file, "rb")
40+
self.post_processing = post_processing
41+
42+
if header is None:
43+
header = read_header(cine_file)
44+
self.header = header
45+
46+
self.start_index = start_index
47+
self.i = self.start_index
48+
49+
self.full_size = self.header["cinefileheader"].ImageCount
50+
if not count:
51+
count = self.full_size - self.start_index
52+
self.end_index = self.start_index + count
53+
if self.end_index > self.full_size:
54+
raise ValueError("end_index {} is larger than the maximum {}".format(self.end_index, self.full_size))
55+
self.size = count
56+
57+
def __getitem__(self, frame_index):
58+
"""
59+
Dunder method to be able to use [] for retrieving images from the object.
60+
61+
Parameters
62+
----------
63+
frame_index : int
64+
the index for the image in cine file to retrieve.
65+
"""
66+
f = self.cine_file_stream
67+
try:
68+
f.seek(self.header["pImage"][frame_index])
69+
except IndexError:
70+
raise IndexError(
71+
"Index {} is out of bounds for cine_file with size {}.".format(frame_index, self.full_size)
72+
)
2473

25-
with open(cine_file, "rb") as f:
26-
while count:
27-
frame_index = frame - 1
28-
logger.debug(f"Reading frame {frame}")
74+
annotation_size = struct.unpack("I", f.read(4))[0]
75+
annotation = struct.unpack("{}B".format(annotation_size - 8), f.read((annotation_size - 8) // 8))
76+
self.header["Annotation"] = annotation
2977

30-
f.seek(header["pImage"][frame_index])
78+
image_size = struct.unpack("I", f.read(4))[0]
79+
data = f.read(image_size)
80+
image = create_raw_array(data, self.header)
81+
if not self.post_processing is None:
82+
image = self.post_processing(image)
83+
return image
3184

32-
annotation_size = struct.unpack("I", f.read(4))[0]
33-
annotation = struct.unpack(f"{annotation_size - 8}B", f.read((annotation_size - 8) // 8))
34-
# TODO: Save annotations
85+
def __iter__(self):
86+
""" Object of this class is an iterator """
87+
return self
3588

36-
image_size = struct.unpack("I", f.read(4))[0]
89+
def __next__(self):
90+
""" When iterating get the next image if more are left """
91+
if self.i >= self.end_index:
92+
raise StopIteration
93+
logger.debug("Reading frame {}".format(self.i))
94+
raw_image = self.__getitem__(self.i)
95+
self.i += 1
96+
return raw_image
3797

38-
data = f.read(image_size)
98+
def __len__(self):
99+
return self.size
39100

40-
raw_image = create_raw_array(data, header)
101+
def __del__(self):
102+
self.cine_file_stream.close()
41103

42-
yield raw_image
43-
frame += 1
44-
count -= 1
104+
105+
# def frame_reader(cine_file, header=None, start_frame=1, count=None):
106+
def frame_reader(
107+
cine_file: Union[str, bytes, PathLike],
108+
header: Header,
109+
start_frame: int = 1,
110+
count: int = None,
111+
) -> Generator[np.ndarray, Any, None]:
112+
return Frame_reader(cine_file, header, start_frame - 1, count)
45113

46114

47115
def read_bpp(header):

pycine/viewer.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from PyQt5 import QtCore, QtWidgets
2+
import pyqtgraph as pg
3+
# backward compatibility default is col-major in pyqtgraph so change
4+
pg.setConfigOptions(imageAxisOrder="row-major")
5+
6+
7+
class Cine_viewer(QtWidgets.QMainWindow):
8+
def __init__(self, frame_reader):
9+
super(Cine_viewer, self).__init__()
10+
self.frame_reader = frame_reader
11+
12+
self.setWindowTitle("Cine viewer")
13+
size = (640, 480)
14+
self.resize(*size)
15+
16+
layout = QtWidgets.QGridLayout()
17+
18+
image_view = pg.ImageView()
19+
self.image_view = image_view
20+
layout.addWidget(image_view, 0, 0)
21+
22+
if frame_reader.full_size == 1:
23+
image = self.frame_reader[0]
24+
self.image_view.setImage(image)
25+
else:
26+
frame_slider = QtWidgets.QSlider()
27+
frame_slider.setOrientation(QtCore.Qt.Horizontal)
28+
frame_slider.setMinimum(1) # maybe change to FirstImageNo in cine file
29+
frame_slider.setMaximum(frame_reader.full_size)
30+
frame_slider.setTracking(True) # call update on mouse drag
31+
frame_slider.setSingleStep(1)
32+
frame_slider.setValue(frame_reader.start_index + 1)
33+
self.frame_slider = frame_slider
34+
layout.addWidget(frame_slider, 1, 0)
35+
36+
slider_label_left = QtWidgets.QLabel()
37+
slider_label_left.setText("{}".format(frame_slider.minimum()))
38+
layout.addWidget(slider_label_left, 2, 0)
39+
40+
slider_label = QtWidgets.QLabel()
41+
slider_label.setAlignment(QtCore.Qt.AlignCenter)
42+
self.slider_label = slider_label
43+
layout.addWidget(slider_label, 2, 0)
44+
45+
slider_label_right = QtWidgets.QLabel()
46+
slider_label_right.setText("{}".format(self.frame_slider.maximum()))
47+
slider_label_right.setAlignment(QtCore.Qt.AlignRight)
48+
layout.addWidget(slider_label_right, 2, 0)
49+
50+
self.update_frame(frame_reader.start_index + 1, autoLevels=True)
51+
# do this last to avoid double update with slider setValue
52+
frame_slider.valueChanged.connect(self.update_frame)
53+
54+
widget = QtWidgets.QWidget()
55+
widget.setLayout(layout)
56+
self.setCentralWidget(widget)
57+
58+
def set_frame(self, frame_index):
59+
# indirectly call update frame
60+
self.frame_slider.setValue(frame_index)
61+
62+
def update_frame(self, frame_index, autoLevels=False):
63+
image = self.frame_reader[frame_index - 1]
64+
self.image_view.setImage(image, autoLevels=autoLevels)
65+
self.slider_label.setText("Frame: {}".format(frame_index))
66+
67+
68+
def view_cine(frame_reader):
69+
"""Start an interactive viewer for a cine file
70+
71+
Parameters
72+
----------
73+
frame_reader : pycine.raw.Frame_reader
74+
Object for the cine_file to be viewed
75+
76+
Returns
77+
-------
78+
None
79+
"""
80+
app = QtWidgets.QApplication([])
81+
window = Cine_viewer(frame_reader)
82+
window.show()
83+
84+
# Start the event loop.
85+
app.exec_()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
description="This package allows handling of .cine files created by Vision Research Phantom® cameras.",
1010
entry_points={"console_scripts": ["pfs_meta = pycine.cli.pfs_meta:cli", "pfs_raw = pycine.cli.pfs_raw:cli"]},
1111
include_package_data=True,
12-
install_requires=["click", "docopt", "opencv-python", "colorama", "timecode"],
12+
install_requires=["click", "docopt", "opencv-python", "colorama", "timecode", "pyqt5", "pyqtgraph"],
1313
long_description=long_description,
1414
long_description_content_type="text/markdown",
1515
name="pycine",

0 commit comments

Comments
 (0)