Skip to content

Commit 119df34

Browse files
committed
Added "Line cross section" feature
1 parent 7c7572c commit 119df34

13 files changed

Lines changed: 247 additions & 90 deletions

File tree

doc/features/panels/overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ The built-in panels are:
2525
* :py:data:`plotpy.constants.ID_XCS`: `X-axis cross section` panel
2626
* :py:data:`plotpy.constants.ID_YCS`: `Y-axis cross section` panel
2727
* :py:data:`plotpy.constants.ID_OCS`: `oblique cross section` panel
28+
* :py:data:`plotpy.constants.ID_LCS`: `line cross section` panel

doc/features/tools/overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ The `tools` module provides the following tools:
6464
* :py:class:`.tools.YCSPanelTool`
6565
* :py:class:`.tools.CrossSectionTool`
6666
* :py:class:`.tools.AverageCrossSectionTool`
67+
* :py:class:`.tools.LineCrossSectionTool`
6768
* :py:class:`.tools.SaveAsTool`
6869
* :py:class:`.tools.CopyToClipboardTool`
6970
* :py:class:`.tools.OpenFileTool`

doc/features/tools/reference.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Reference
7777
:members:
7878
.. autoclass:: plotpy.tools.AverageCrossSectionTool
7979
:members:
80+
.. autoclass:: plotpy.tools.LineCrossSectionTool
81+
:members:
8082
.. autoclass:: plotpy.tools.SaveAsTool
8183
:members:
8284
.. autoclass:: plotpy.tools.CopyToClipboardTool

plotpy/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ class PlotType(enum.Enum):
7171
ID_YCS = "y_cross_section"
7272
#: ID of the `oblique averaged cross section` panel
7373
ID_OCS = "oblique_cross_section"
74+
#: ID of the `line cross section` panel
75+
ID_LCS = "line_cross_section"
7476

7577

7678
# ===============================================================================

plotpy/images/csection_line.png

452 Bytes
Loading

plotpy/panels/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class from which all panels must derived from) and identifiers for each kind
1616
.. autodata:: plotpy.constants.ID_XCS
1717
.. autodata:: plotpy.constants.ID_YCS
1818
.. autodata:: plotpy.constants.ID_OCS
19+
.. autodata:: plotpy.constants.ID_LCS
1920
2021
.. autoclass:: PanelWidget
2122
:members:

plotpy/panels/csection/csitem.py

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# -*- coding: utf-8 -*-
2+
3+
from __future__ import annotations
4+
25
import sys
36
import weakref
7+
from typing import TYPE_CHECKING
48

59
import numpy as np
610
from qtpy import QtCore as QC
@@ -12,6 +16,9 @@
1216
from plotpy.items.image.misc import get_image_from_qrect
1317
from plotpy.mathutils.geometry import rotate, translate, vector_angle, vector_norm
1418

19+
if TYPE_CHECKING:
20+
from plotpy.items import AnnotatedObliqueRectangle, AnnotatedSegment
21+
1522
try:
1623
from plotpy._scaler import INTERP_LINEAR, _scale_tr
1724
except ImportError:
@@ -434,14 +441,8 @@ class ObliqueCrossSectionItem(CrossSectionItem):
434441

435442
DEBUG = False
436443

437-
def __init__(self, curveparam=None, errorbarparam=None):
438-
CrossSectionItem.__init__(self, curveparam, errorbarparam)
439-
440-
def update_curve_data(self, obj):
441-
"""
442-
443-
:param obj:
444-
"""
444+
def update_curve_data(self, obj: AnnotatedObliqueRectangle) -> None:
445+
"""Update curve data"""
445446
source = self.get_source_image()
446447
rect = obj.get_bounding_rect_coords()
447448
if rect is not None and source.data is not None:
@@ -455,3 +456,51 @@ def update_curve_data(self, obj):
455456
def update_scale(self):
456457
""" """
457458
pass
459+
460+
461+
def compute_line_section(
462+
data: np.ndarray, row0, col0, row1, col1
463+
) -> tuple[np.ndarray, np.ndarray]:
464+
"""Return intensity profile of data along a line
465+
466+
Args:
467+
data: 2D array
468+
row0, col0: start point
469+
row1, col1: end point
470+
"""
471+
# Keep coordinates inside the image
472+
row0 = max(0, min(row0, data.shape[0] - 1))
473+
col0 = max(0, min(col0, data.shape[1] - 1))
474+
row1 = max(0, min(row1, data.shape[0] - 1))
475+
col1 = max(0, min(col1, data.shape[1] - 1))
476+
# Keep coordinates in the right order
477+
row0, row1 = min(row0, row1), max(row0, row1)
478+
col0, col1 = min(col0, col1), max(col0, col1)
479+
# Extract the line
480+
line = np.zeros((2, max(abs(row1 - row0), abs(col1 - col0)) + 1), dtype=np.float64)
481+
line[0, :] = np.linspace(row0, row1, line.shape[1])
482+
line[1, :] = np.linspace(col0, col1, line.shape[1])
483+
# Interpolate the line
484+
return line[1, :], np.array([data[int(r), int(c)] for r, c in line.T])
485+
486+
487+
# Line cross section item
488+
class LineCrossSectionItem(CrossSectionItem):
489+
"""A Qwt item representing line cross section data"""
490+
491+
def update_curve_data(self, obj: AnnotatedSegment) -> None:
492+
"""Update curve data"""
493+
source = self.get_source_image()
494+
rect = obj.get_rect()
495+
if rect is not None and source.data is not None:
496+
x0, y0, x1, y1 = obj.get_rect()
497+
c0, r0 = source.get_closest_pixel_indexes(x0, y0)
498+
c1, r1 = source.get_closest_pixel_indexes(x1, y1)
499+
sectx, secty = compute_line_section(source.data, r0, c0, r1, c1)
500+
if secty.size == 0 or np.all(np.isnan(secty)):
501+
sectx, secty = np.array([]), np.array([])
502+
self.process_curve_data(sectx, secty, None, None)
503+
504+
def update_scale(self):
505+
""" """
506+
pass

plotpy/panels/csection/csplot.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from plotpy.constants import LUT_MAX, PlotType
1313
from plotpy.interfaces import ICSImageItemType
1414
from plotpy.panels.csection.csitem import (
15+
LineCrossSectionItem,
1516
ObliqueCrossSectionItem,
1617
XCrossSectionItem,
1718
YCrossSectionItem,
@@ -430,6 +431,7 @@ class ObliqueCrossSectionPlot(HorizontalCrossSectionPlot):
430431
PLOT_TITLE = _("Oblique averaged cross section")
431432
CURVE_LABEL = _("Oblique averaged cross section")
432433
LABEL_TEXT = _("Activate the oblique cross section tool")
434+
SHADE = 0.0
433435

434436
def __init__(self, parent=None):
435437
super().__init__(parent)
@@ -446,3 +448,26 @@ def create_cross_section_item(self):
446448
def axis_dir_changed(self, plot, axis_id):
447449
"""An axis direction has changed"""
448450
pass
451+
452+
453+
# Line cross section plot
454+
class LineCrossSectionPlot(HorizontalCrossSectionPlot):
455+
"""Line cross section plot"""
456+
457+
PLOT_TITLE = _("Line cross section")
458+
CURVE_LABEL = _("Line cross section")
459+
LABEL_TEXT = _("Activate the line cross section tool")
460+
SHADE = 0.0
461+
462+
def __init__(self, parent=None):
463+
super().__init__(parent)
464+
self.set_title(self.PLOT_TITLE)
465+
self.single_source = True
466+
467+
def create_cross_section_item(self) -> LineCrossSectionItem:
468+
"""Create cross section item"""
469+
return LineCrossSectionItem(self.param)
470+
471+
def axis_dir_changed(self, plot, axis_id):
472+
"""An axis direction has changed"""
473+
pass

plotpy/panels/csection/cswidget.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
from qtpy import QtWidgets as QW
88

99
from plotpy.config import _
10-
from plotpy.constants import ID_OCS, ID_XCS, ID_YCS
10+
from plotpy.constants import ID_LCS, ID_OCS, ID_XCS, ID_YCS
1111
from plotpy.interfaces import IPanel
1212
from plotpy.panels.base import PanelWidget
1313
from plotpy.panels.csection.csplot import (
1414
CrossSectionPlot,
15+
LineCrossSectionPlot,
1516
ObliqueCrossSectionPlot,
1617
XCrossSectionPlot,
1718
YCrossSectionPlot,
@@ -397,3 +398,18 @@ def setup_actions(self):
397398
super().setup_actions()
398399
self.lockscales_ac.setChecked(False)
399400
self.autoscale_ac.setChecked(True)
401+
402+
403+
class LineCrossSection(CrossSectionWidget):
404+
"""Line cross section panel
405+
406+
Args:
407+
parent: parent widget
408+
"""
409+
410+
PANEL_ID = ID_LCS
411+
CrossSectionPlotKlass = LineCrossSectionPlot
412+
413+
def __init__(self, parent=None):
414+
super().__init__(parent)
415+
self.cs_plot.set_axis_direction("bottom", reverse=False)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Licensed under the terms of the BSD 3-Clause
4+
# (see plotpy/LICENSE for details)
5+
6+
"""Line cross section test"""
7+
8+
# guitest: show
9+
10+
from guidata.qthelpers import qt_app_context
11+
12+
from plotpy.builder import make
13+
from plotpy.panels.csection.cswidget import LineCrossSection
14+
from plotpy.plot import PlotDialog, PlotOptions
15+
from plotpy.tests import get_path
16+
from plotpy.tools import ImageMaskTool, LCSPanelTool, LineCrossSectionTool
17+
18+
19+
class BaseCSImageDialog(PlotDialog):
20+
"""Base cross section test
21+
22+
This class is used to test the LineCrossSection and ObliqueCrossSection
23+
"""
24+
25+
TOOLCLASSES = ()
26+
PANELCLASS = None
27+
28+
def __init__(self, parent=None, toolbar=True, title=None, options=None):
29+
super().__init__(
30+
parent=parent,
31+
toolbar=toolbar,
32+
title=title,
33+
options=options,
34+
auto_tools=True,
35+
)
36+
for tool in self.TOOLCLASSES:
37+
self.manager.add_tool(tool)
38+
39+
def populate_plot_layout(self):
40+
"""Populate the plot layout"""
41+
super().populate_plot_layout()
42+
cs_panel = self.PANELCLASS(self)
43+
splitter = self.plot_widget.xcsw_splitter
44+
splitter.addWidget(cs_panel)
45+
splitter.setSizes([0, 1, 0])
46+
self.manager.add_panel(cs_panel)
47+
48+
49+
class LCSImageDialog(BaseCSImageDialog):
50+
"""Line cross section test"""
51+
52+
TOOLCLASSES = (LineCrossSectionTool, LCSPanelTool, ImageMaskTool)
53+
PANELCLASS = LineCrossSection
54+
55+
56+
def generic_cross_section_dialog(title, dialogclass):
57+
"""Generic function used to test the cross section tools"""
58+
with qt_app_context(exec_loop=True):
59+
win = dialogclass(
60+
toolbar=True,
61+
title=title,
62+
options=PlotOptions(type="image"),
63+
)
64+
win.resize(600, 600)
65+
filename = get_path("brain_cylinder.png")
66+
image = make.maskedimage(filename=filename, colormap="bone")
67+
plot = win.manager.get_plot()
68+
plot.add_item(image)
69+
plot.set_active_item(image)
70+
image.unselect()
71+
win.show()
72+
73+
74+
def test_cross_section_line():
75+
"""Test cross section oblique"""
76+
generic_cross_section_dialog("Line cross section test", LCSImageDialog)
77+
78+
79+
if __name__ == "__main__":
80+
test_cross_section_line()

0 commit comments

Comments
 (0)