diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2201d97f..9b368f78 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,12 +21,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.12"] + python-version: ["3.9", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.vscode/launch.json b/.vscode/launch.json index 0204abbd..fe32f36e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,7 @@ ], "env": { // "DEBUG": "1", - "LANG": "en", + // "LANG": "en", // "QT_COLOR_MODE": "light", } }, @@ -61,7 +61,7 @@ // "1000" ], "env": { - // "DEBUG": "1", + "DEBUG": "1", // "TEST_SEGFAULT_ERROR": "1", "LANG": "en", "QT_COLOR_MODE": "light", @@ -84,7 +84,7 @@ "--unattended", ], "env": { - // "DEBUG": "1", + "DEBUG": "1", // The `CDL_DATA` environment variable is set here just for checking // that the data path is not added twice to the test data path list: "CDL_DATA": "${workspaceFolder}/cdl/data/tests", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b3405649..7f7cf7d6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -288,34 +288,6 @@ "clear": false } }, - { - "label": "Upgrade PlotPyStack packages (Python 3.8)", - "type": "shell", - "command": "cmd", - "args": [ - "/c", - "upgrade_stack.bat" - ], - "options": { - "cwd": "scripts", - "env": { - "PYTHON": "${env:CDL_PYTHONEXE_PY38}", - "UNATTENDED": "1" - } - }, - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared", - "showReuseMessage": true, - "clear": false - } - }, { "label": "Clean Up", "type": "shell", @@ -638,73 +610,6 @@ "Create installer" ] }, - { - "label": "Create Windows 7 executable", - "type": "shell", - "command": "cmd", - "options": { - "cwd": "scripts", - "env": { - "PYTHON": "${env:CDL_PYTHONEXE_PY38}", - "RELEASE": "1", - "UNATTENDED": "1" - } - }, - "args": [ - "/c", - "build_exe.bat" - ], - "problemMatcher": [], - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared", - "showReuseMessage": true, - "clear": true - } - }, - { - "label": "New Windows 7 release", - "type": "shell", - "command": "cmd", - "args": [ - "/c", - "release_win7.bat" - ], - "options": { - "cwd": "scripts", - "env": { - "PYTHON": "${env:CDL_PYTHONEXE_PY38}", - "RELEASE": "1", - "UNATTENDED": "1" - } - }, - "problemMatcher": [], - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared", - "showReuseMessage": true, - "clear": true - }, - "dependsOrder": "sequence", - "dependsOn": [ - "Clean Up", - "Upgrade PlotPyStack packages (Python 3.8)", - "Create Windows 7 executable", - "Create installer" - ] - }, { "label": "Run _tests_.bat", "type": "shell", diff --git a/CHANGELOG.md b/CHANGELOG.md index e2847ff8..1804ba22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ See DataLab [roadmap page](https://datalab-platform.com/en/contributing/roadmap.html) for future and past milestones. +## DataLab Version 0.18.0 ## + +ℹ️ General information: + +* PlotPy v2.7 is required for this release. +* Dropped support for Python 3.8. +* Python 3.13 is not supported yet, due to the fact that some dependencies are not compatible with this version. + +💥 New features and enhancements: + +* New operation mode feature: + * Added "Operation mode" feature to the "Processing" tab in the "Settings" dialog box + * This feature allows to choose between "single" and "pairwise" operation modes for all basic operations (addition, subtraction, multiplication, division, etc.): + * "Single" mode: single operand mode (default mode: the operation is done on each object independently) + * "Pairwise" mode: pairwise operand mode (the operation is done on each pair of objects) + * This applies to both signals and images, and to computations taking *N* inputs + * Computations taking *N* inputs are the ones where: + * *N(>=2)* objects in give *N* objects out + * *N(>=1)* object(s) + 1 object in give N objects out + +* New ROI (Region Of Interest) features: + * New polygonal ROI feature + * Complete redesign of the ROI editor user interfaces, improving ergonomics and consistency with the rest of the application + * Major internal refactoring of the ROI system to make it more robust (more tests) and easier to maintain + +* Implemented [Issue #102](https://github.com/DataLab-Platform/DataLab/issues/102) - Launch DataLab using `datalab` instead of `cdl`. Note that the `cdl` command is still available for backward compatibility. + +* Implemented [Issue #101](https://github.com/DataLab-Platform/DataLab/issues/101) - Configuration: set default image interpolation to anti-aliasing (`5` instead of `0` for nearest). This change is motivated by the fact that a performance improvement was made in PlotPy v2.7 on Windows, which allows to use anti-aliasing interpolation by default without a significant performance impact. + +* Implemented [Issue #100](https://github.com/DataLab-Platform/DataLab/issues/100) - Use the same installer and executable on Windows 7 SP1, 8, 10, 11. Before this change, a specific installer was required for Windows 7 SP1, due to the fact that Python 3.9 and later versions are not supported on this platform. A workaround was implemented to make DataLab work on Windows 7 SP1 with Python 3.9. + +🛠️ Bug fixes: + +* Fixed [Issue #103](https://github.com/DataLab-Platform/DataLab/issues/103) - `proxy.add_annotations_from_items`: circle shape color seems to be ignored. + ## DataLab Version 0.17.1 ## ℹ️ PlotPy v2.6.2 is required for this release. @@ -492,7 +527,7 @@ NumPy 2.0 support has been added with this release. * Circle and ellipse results now also show area (A) * Added "Plot results" entry in "Analysis" menu: * This feature allows to plot analysis results (1D or 2D) - * It creates a new signal with X and Y axes corresponding to user-defined parameters (e.g. X = indexes and Y = radius for circle results) + * It creates a new signal with X and Y axes corresponding to user-defined parameters (e.g. X = indices and Y = radius for circle results) * Increased default width of the object selection dialog box: * The object selection dialog box is now wider by default, so that the full signal/image/group titles may be more easily readable * Delete metadata feature: @@ -544,9 +579,9 @@ NumPy 2.0 support has been added with this release. * Fixed [Issue #29](https://github.com/DataLab-Platform/DataLab/issues/29) - Polynomial fit error: `QDialog [...] argument 1 has an unexpected type 'SignalProcessor'` * Image ROI extraction feature: - * Before this release, when extracting a single circular ROI from an image with the "Extract all regions of interest into a single image object" option enabled, the result was a single image without the ROI mask (the ROI mask was only available when extracting ROI with the option disabled) + * Before this release, when extracting a single circular ROI from an image with the "Extract all ROIs into a single image object" option enabled, the result was a single image without the ROI mask (the ROI mask was only available when extracting ROI with the option disabled) * This was leading to an unexpected behavior, because one could interpret the result (a square image without the ROI mask) as the result of a single rectangular ROI - * Now, when extracting a single circular ROI from an image with the "Extract all regions of interest into a single image object" option enabled, the result is a single image with the ROI mask (as if the option was disabled) + * Now, when extracting a single circular ROI from an image with the "Extract all ROIs into a single image object" option enabled, the result is a single image with the ROI mask (as if the option was disabled) * This fixes [Issue #31](https://github.com/DataLab-Platform/DataLab/issues/31) - Single circular ROI extraction: automatically switch to `extract_single_roi` function * Analysis on circular ROI: * Before this release, when running computations on a circular ROI, the results were unexpected in terms of coordinates (results seemed to be computed in a region located above the actual ROI). @@ -640,7 +675,7 @@ NumPy 2.0 support has been added with this release. 🛠️ Bug fixes: * Region of interest (ROI) extraction feature for images: - * ROI extraction was not working properly when the "Extract all regions of interest into a single image object" option was enabled if there was only one defined ROI. The result was an image positioned at the origin (0, 0) instead of the expected position (x0, y0) and the ROI rectangle itself was not removed as expected. This is now fixed (see [Issue #6](https://github.com/DataLab-Platform/DataLab/issues/6) - 'Extract multiple ROI' feature: unexpected result for a single ROI) + * ROI extraction was not working properly when the "Extract all ROIs into a single image object" option was enabled if there was only one defined ROI. The result was an image positioned at the origin (0, 0) instead of the expected position (x0, y0) and the ROI rectangle itself was not removed as expected. This is now fixed (see [Issue #6](https://github.com/DataLab-Platform/DataLab/issues/6) - 'Extract multiple ROI' feature: unexpected result for a single ROI) * ROI rectangles with negative coordinates were not properly handled: ROI extraction was raising a `ValueError` exception, and the image mask was not displayed properly. This is now fixed (see [Issue #7](https://github.com/DataLab-Platform/DataLab/issues/7) - Image ROI extraction: `ValueError: zero-size array to reduction operation minimum which has no identity`) * ROI extraction was not taking into account the pixel size (dx, dy) and the origin (x0, y0) of the image. This is now fixed (see [Issue #8](https://github.com/DataLab-Platform/DataLab/issues/8) - Image ROI extraction: take into account pixel size) * Macro-command console is now read-only: diff --git a/DataLab.bat b/DataLab.bat index cee90b27..ec2286e7 100644 --- a/DataLab.bat +++ b/DataLab.bat @@ -3,5 +3,7 @@ for %%a in ("%CDL_PYTHONEXE%") do set "p_dir=%%~dpa" for %%a in (%p_dir:~0,-1%) do set "WINPYDIRBASE=%%~dpa" call %WINPYDIRBASE%scripts\env_for_icons.bat %* cd/D %~dp0 -set PYTHONPATH=%cd% +set ORIGINAL_PYTHONPATH=%PYTHONPATH% +for /F "tokens=*" %%A in (.env) do (set %%A) +set PYTHONPATH=%PYTHONPATH%;%ORIGINAL_PYTHONPATH% start "" "%WINPYDIR%\pythonw.exe" cdl\start.pyw %* \ No newline at end of file diff --git a/DataLab.spec b/DataLab.spec index dee27533..81f6c406 100644 --- a/DataLab.spec +++ b/DataLab.spec @@ -7,7 +7,6 @@ from PyInstaller.utils.hooks import collect_submodules, collect_data_files all_hidden_imports = collect_submodules('cdl') datas = collect_data_files('cdl') + [('cdl\\plugins', 'cdl\\plugins')] datas += collect_data_files('guidata') + collect_data_files('plotpy') -datas += collect_data_files('skimage') a = Analysis( ['cdl\\start.pyw'], diff --git a/cdl/__init__.py b/cdl/__init__.py index df1cf316..7fd54709 100644 --- a/cdl/__init__.py +++ b/cdl/__init__.py @@ -13,7 +13,7 @@ import os -__version__ = "0.17.1" +__version__ = "0.18.0" __docurl__ = __homeurl__ = "https://datalab-platform.com/" __supporturl__ = "https://github.com/DataLab-Platform/DataLab/issues/new/choose" diff --git a/cdl/algorithms/image.py b/cdl/algorithms/image.py index b567e83d..e5aac55b 100644 --- a/cdl/algorithms/image.py +++ b/cdl/algorithms/image.py @@ -258,14 +258,10 @@ def get_centroid_fourier(data: np.ndarray) -> tuple[float, float]: row = (np.arctan(b / a) + rphi) * (rows - 1) / (2 * np.pi) + 1 col = (np.arctan(d / c) + cphi) * (cols - 1) / (2 * np.pi) + 1 - try: - row = int(row) - except ma.MaskError: - row = np.nan - try: - col = int(col) - except ma.MaskError: - col = np.nan + + row = np.nan if row is np.ma.masked else row + col = np.nan if col is np.ma.masked else col + return row, col diff --git a/cdl/algorithms/signal.py b/cdl/algorithms/signal.py index a197d1fa..3d5bbf37 100644 --- a/cdl/algorithms/signal.py +++ b/cdl/algorithms/signal.py @@ -174,7 +174,7 @@ def sort_frequencies(x: np.ndarray, y: np.ndarray) -> np.ndarray: # MARK: Peak detection ----------------------------------------------------------------- -def peak_indexes( +def peak_indices( y, thres: float = 0.3, min_dist: int = 1, thres_abs: bool = False ) -> np.ndarray: # Copyright (c) 2014 Lucas Hermann Negri @@ -202,7 +202,7 @@ def peak_indexes( Returns ------- ndarray - Array containing the numeric indexes of the peaks that were detected + Array containing the numeric indices of the peaks that were detected """ if isinstance(y, np.ndarray) and np.issubdtype(y.dtype, np.unsignedinteger): raise ValueError("y must be signed") @@ -222,11 +222,11 @@ def peak_indexes( return np.array([]) if len(zeros): - # compute first order difference of zero indexes + # compute first order difference of zero indices zeros_diff = np.diff(zeros) # check when zeros are not chained together (zeros_diff_not_one,) = np.add(np.where(zeros_diff != 1), 1) - # make an array of the chained zero indexes + # make an array of the chained zero indices zero_plateaus = np.split(zeros, zeros_diff_not_one) # fix if leftmost value in dy is zero @@ -239,7 +239,7 @@ def peak_indexes( dy[zero_plateaus[-1]] = dy[zero_plateaus[-1][0] - 1] zero_plateaus.pop(-1) - # for each chain of zero indexes + # for each chain of zero indices for plateau in zero_plateaus: median = np.median(plateau) # set leftmost values to leftmost non zero values @@ -281,7 +281,7 @@ def xpeak(x: np.ndarray, y: np.ndarray) -> float: Returns: Peak X-position """ - peaks = peak_indexes(y) + peaks = peak_indices(y) if peaks.size == 1: return x[peaks[0]] return np.average(x, weights=y) @@ -527,13 +527,13 @@ def fwhm(cls, amp, sigma): def find_nearest_zero_point_idx(y: np.ndarray) -> np.ndarray: - """Find the x indexes where the corresponding y is the closest to zero + """Find the x indices where the corresponding y is the closest to zero Args: y: Y data Returns: - Indexes of the points right before or at zero crossing + Indices of the points right before or at zero crossing """ xi = np.where((y[:-1] >= 0) & (y[1:] <= 0) | (y[:-1] <= 0) & (y[1:] >= 0))[0] return xi @@ -856,13 +856,13 @@ def fwhm( dx, dy, base = np.max(x) - np.min(x), np.max(y) - np.min(y), np.min(y) sigma, mu = dx * 0.1, xpeak(x, y) if isinstance(xmin, float): - indexes = np.where(x >= xmin)[0] - x = x[indexes] - y = y[indexes] + indices = np.where(x >= xmin)[0] + x = x[indices] + y = y[indices] if isinstance(xmax, float): - indexes = np.where(x <= xmax)[0] - x = x[indexes] - y = y[indexes] + indices = np.where(x <= xmax)[0] + x = x[indices] + y = y[indices] if method == "zero-crossing": hmax = dy * 0.5 + np.min(y) diff --git a/cdl/computation/base.py b/cdl/computation/base.py index ad7f0182..6f430393 100644 --- a/cdl/computation/base.py +++ b/cdl/computation/base.py @@ -153,28 +153,6 @@ def get_suffix(self, data: np.ndarray) -> str: upper = gds.FloatItem(_("Upper limit"), default=None, check=False) -class ROIDataParam(gds.DataSet): - """ROI Editor data""" - - roidata = gds.FloatArrayItem( - _("ROI data"), - help=_( - "For convenience, this item accepts a 2D NumPy array, a list of list " - "of numbers, or None. In the end, the data is converted to a 2D NumPy " - "array of integers (if not None)." - ), - ) - singleobj = gds.BoolItem( - _("Single object"), - help=_("Whether to extract the ROI as a single object or not."), - ) - - @property - def is_empty(self) -> bool: - """Return True if there is no ROI""" - return self.roidata is None or np.array(self.roidata).size == 0 - - class FFTParam(gds.DataSet): """FFT parameters""" @@ -304,8 +282,7 @@ def calc_resultproperties( raise ValueError("Values of labeledfuncs must be functions") res = [] - roi_nb = 0 if obj.roi is None else obj.roi.shape[0] - for i_roi in [None] + list(range(roi_nb)): + for i_roi in [None] + list(obj.iterate_roi_indices()): data_roi = obj.get_data(i_roi) val_roi = -1 if i_roi is None else i_roi res.append([val_roi] + [fn(data_roi) for fn in labeledfuncs.values()]) diff --git a/cdl/computation/image/__init__.py b/cdl/computation/image/__init__.py index ddd9730a..80fdf1ee 100644 --- a/cdl/computation/image/__init__.py +++ b/cdl/computation/image/__init__.py @@ -49,11 +49,9 @@ from cdl.obj import ( BaseProcParam, ImageObj, - ImageRoiDataItem, ResultProperties, ResultShape, ROI2DParam, - RoiDataGeometries, SignalObj, ) @@ -752,15 +750,15 @@ def extract_multiple_roi(src: ImageObj, group: gds.DataSetGroup) -> ImageObj: Output image object """ # Initialize x0, y0 with maximum values: - y0, x0 = src.data.shape + y0, x0 = ymax, xmax = src.data.shape # Initialize x1, y1 with minimum values: - y1, x1 = 0, 0 + y1, x1 = ymin, xmin = 0, 0 for p in group.datasets: p: ROI2DParam - x0i, y0i, x1i, y1i = p.get_rect_indexes() + x0i, y0i, x1i, y1i = p.get_bounding_box_indices() x0, y0, x1, y1 = min(x0, x0i), min(y0, y0i), max(x1, x1i), max(y1, y1i) - x0, y0 = max(x0, 0), max(y0, 0) - x1, y1 = min(x1, src.data.shape[1]), min(y1, src.data.shape[0]) + x0, y0 = max(x0, xmin), max(y0, ymin) + x1, y1 = min(x1, xmax), min(y1, ymax) suffix = None if len(group.datasets) == 1: @@ -772,7 +770,7 @@ def extract_multiple_roi(src: ImageObj, group: gds.DataSetGroup) -> ImageObj: dst.roi = None src2 = src.copy() - src2.roi = src2.params_to_roidata(group) + src2.roi = src2.roi.from_params(src2, group) src2.data[src2.maskdata] = 0 dst.data = src2.data[y0:y1, x0:x1] return dst @@ -790,13 +788,10 @@ def extract_single_roi(src: ImageObj, p: ROI2DParam) -> ImageObj: """ dst = dst_11(src, "extract_single_roi", p.get_suffix()) dst.data = p.get_data(src).copy() - x0, y0, _x1, _y1 = p.get_rect_indexes() + dst.roi = p.get_extracted_roi(src) + x0, y0, _x1, _y1 = p.get_bounding_box_indices() dst.x0 += x0 * src.dx dst.y0 += y0 * src.dy - dst.roi = None - if p.geometry is RoiDataGeometries.CIRCLE: - # Circular ROI - dst.roi = p.get_single_roi() return dst @@ -1393,7 +1388,7 @@ def calc_resultshape( """ res = [] num_cols = [] - for i_roi in obj.iterate_roi_indexes(): + for i_roi in obj.iterate_roi_indices(): data_roi = obj.get_data(i_roi) if args is None: coords: np.ndarray = func(data_roi) @@ -1427,13 +1422,13 @@ def calc_resultshape( else: # Circle [x0, y0, r] or ellipse coordinates [x0, y0, a, b, theta] colx, coly = 0, 1 + coords[:, colx] = obj.dx * coords[:, colx] + obj.x0 + coords[:, coly] = obj.dy * coords[:, coly] + obj.y0 if obj.roi is not None: - x0, y0, _x1, _y1 = ImageRoiDataItem(obj.roi[i_roi]).get_rect() + x0, y0, _x1, _y1 = obj.roi.get_single_roi(i_roi).get_bounding_box(obj) coords[:, colx] += x0 coords[:, coly] += y0 - coords[:, colx] = obj.dx * coords[:, colx] + obj.x0 - coords[:, coly] = obj.dy * coords[:, coly] + obj.y0 - idx = np.ones((coords.shape[0], 1)) * i_roi + idx = np.ones((coords.shape[0], 1)) * (0 if i_roi is None else i_roi) coords = np.hstack([idx, coords]) res.append(coords) num_cols.append(coords.shape[1]) diff --git a/cdl/computation/signal.py b/cdl/computation/signal.py index edff13a8..12726c5d 100644 --- a/cdl/computation/signal.py +++ b/cdl/computation/signal.py @@ -534,7 +534,7 @@ class PeakDetectionParam(gds.DataSet): def compute_peak_detection(src: SignalObj, p: PeakDetectionParam) -> SignalObj: - """Peak detection with :py:func:`cdl.algorithms.signal.peak_indexes` + """Peak detection with :py:func:`cdl.algorithms.signal.peak_indices` Args: src: source signal @@ -547,8 +547,8 @@ def compute_peak_detection(src: SignalObj, p: PeakDetectionParam) -> SignalObj: src, "peak_detection", f"threshold={p.threshold}%, min_dist={p.min_dist}pts" ) x, y = src.get_data() - indexes = alg.peak_indexes(y, thres=p.threshold * 0.01, min_dist=p.min_dist) - dst.set_xydata(x[indexes], y[indexes]) + indices = alg.peak_indices(y, thres=p.threshold * 0.01, min_dist=p.min_dist) + dst.set_xydata(x[indices], y[indices]) dst.metadata["curvestyle"] = "Sticks" return dst @@ -1266,7 +1266,7 @@ def calc_resultshape( or a tuple) containing the result of the computation. """ res = [] - for i_roi in obj.iterate_roi_indexes(): + for i_roi in obj.iterate_roi_indices(): data_roi = obj.get_data(i_roi) if args is None: results: np.ndarray = func(data_roi) @@ -1282,7 +1282,7 @@ def calc_resultshape( raise ValueError( "The computation function must return a 1D NumPy array" ) - results = np.array([i_roi] + results.tolist()) + results = np.array([0 if i_roi is None else i_roi] + results.tolist()) res.append(results) if res: return ResultShape(title, np.vstack(res), shape, add_label=add_label) diff --git a/cdl/config.py b/cdl/config.py index ae68c732..927953ca 100644 --- a/cdl/config.py +++ b/cdl/config.py @@ -94,7 +94,7 @@ class MainSection(conf.Section, metaclass=conf.SectionMeta): Each class attribute is an option (metaclass is automatically affecting option names in .INI file based on class attribute names).""" - color_mode = conf.EnumOption(["auto", "dark", "light"]) + color_mode = conf.EnumOption(["auto", "dark", "light"], default="auto") process_isolation_enabled = conf.Option() rpc_server_enabled = conf.Option() rpc_server_port = conf.Option() @@ -151,6 +151,11 @@ class ProcSection(conf.Section, metaclass=conf.SectionMeta): Each class attribute is an option (metaclass is automatically affecting option names in .INI file based on class attribute names).""" + # Operation mode: + # - "single": single operand mode + # - "pairwise": pairwise operation mode + operation_mode = conf.EnumOption(["single", "pairwise"], default="single") + # ROI extraction strategy: # - True: extract all ROIs in a single signal or image # - False: extract each ROI in a separate signal or image @@ -301,6 +306,7 @@ def initialize(): Conf.io.h5_fname_in_title.get(True) Conf.io.imageio_formats.get(()) # Proc section + Conf.proc.operation_mode.get("single") Conf.proc.fft_shift_enabled.get(True) Conf.proc.extract_roi_singleobj.get(False) Conf.proc.ignore_warnings.get(False) @@ -316,7 +322,7 @@ def initialize(): Conf.view.sig_def_baseline.get(0.0) Conf.view.ima_def_colormap.get("viridis") Conf.view.ima_def_invert_colormap.get(False) - Conf.view.ima_def_interpolation.get(0) + Conf.view.ima_def_interpolation.get(5) Conf.view.ima_def_alpha.get(1.0) Conf.view.ima_def_alpha_function.get(LUTAlpha.NONE.value) diff --git a/cdl/core/gui/objectmodel.py b/cdl/core/gui/objectmodel.py index 78b4f3e7..2bba9c33 100644 --- a/cdl/core/gui/objectmodel.py +++ b/cdl/core/gui/objectmodel.py @@ -272,6 +272,23 @@ def get_group_from_number(self, number: int) -> ObjectGroup: return group raise IndexError(f"Group with number {number} not found") + def get_group_from_object(self, obj: SignalObj | ImageObj) -> ObjectGroup: + """Return group containing object + + Args: + obj: object to find group for + + Returns: + Group + + Raises: + KeyError: if object not found in any group + """ + for group in self._groups: + if obj in group: + return group + raise KeyError(f"Object with uuid '{obj.uuid}' not found in any group") + def get_groups(self, uuids: list[str] | None = None) -> list[ObjectGroup]: """Return groups""" if uuids is None: @@ -289,11 +306,18 @@ def add_group(self, title: str) -> ObjectGroup: return group def get_object_group_id(self, obj: SignalObj | ImageObj) -> str | None: - """Return group id of object""" - for group in self._groups: - if obj in group: - return group.uuid - return None + """Return group id of object + + Args: + obj: object to get group id from + + Returns: + group id or None if object is not in any group + """ + try: + return self.get_group_from_object(obj).uuid + except KeyError: + return None def get_group_object_ids(self, group_id: str) -> list[str]: """Return object ids in group""" diff --git a/cdl/core/gui/objectview.py b/cdl/core/gui/objectview.py index 2123a474..b647b489 100644 --- a/cdl/core/gui/objectview.py +++ b/cdl/core/gui/objectview.py @@ -23,7 +23,7 @@ Get object dialog ----------------- -.. autoclass:: GetObjectDialog +.. autoclass:: GetObjectsDialog Object view ----------- @@ -156,6 +156,50 @@ def get_current_group_id(self) -> str: return selected_item.data(0, QC.Qt.UserRole) return selected_item.parent().data(0, QC.Qt.UserRole) + def get_sel_group_items(self) -> list[QW.QTreeWidgetItem]: + """Return selected group items""" + return [item for item in self.selectedItems() if item.parent() is None] + + def get_sel_group_uuids(self) -> list[str]: + """Return selected group uuids""" + return [item.data(0, QC.Qt.UserRole) for item in self.get_sel_group_items()] + + def get_sel_object_items(self) -> list[QW.QTreeWidgetItem]: + """Return selected object items""" + return [item for item in self.selectedItems() if item.parent() is not None] + + def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]: + """Return selected objects uuids. + + Args: + include_groups: If True, also return objects from selected groups. + + Returns: + List of selected objects uuids. + """ + sel_items = self.get_sel_object_items() + if not sel_items: + cur_item = self.currentItem() + if cur_item is not None and cur_item.parent() is not None: + sel_items = [cur_item] + uuids = [item.data(0, QC.Qt.UserRole) for item in sel_items] + if include_groups: + for group_id in self.get_sel_group_uuids(): + uuids.extend(self.objmodel.get_group_object_ids(group_id)) + return uuids + + def get_sel_objects( + self, include_groups: bool = False + ) -> list[SignalObj | ImageObj]: + """Return selected objects. + + If include_groups is True, also return objects from selected groups.""" + return [self.objmodel[oid] for oid in self.get_sel_object_uuids(include_groups)] + + def get_sel_groups(self) -> list[ObjectGroup]: + """Return selected groups""" + return self.objmodel.get_groups(self.get_sel_group_uuids()) + @staticmethod def __update_item( item: QW.QTreeWidgetItem, obj: SignalObj | ImageObj | ObjectGroup @@ -241,13 +285,15 @@ def contextMenuEvent(self, event: QG.QContextMenuEvent) -> None: # pylint: disa self.SIG_CONTEXT_MENU.emit(event.globalPos()) -class GetObjectDialog(QW.QDialog): - """Get object dialog box +class GetObjectsDialog(QW.QDialog): + """Dialog box showing groups and objects (signals or images) to select one, or more. Args: parent: parent widget panel: data panel title: dialog title + comment: optional dialog comment + nb_objects: number of objects to select (default: 1) minimum_size: minimum size (width, height) """ @@ -256,10 +302,13 @@ def __init__( parent: QW.QWidget, panel: BaseDataPanel, title: str, - minimum_size: tuple[int, int] = None, + comment: str = "", + nb_objects: int = 1, + minimum_size: tuple[int, int] | None = None, ) -> None: super().__init__(parent) - self.__current_object_uuid: str | None = None + self.__nb_objects = nb_objects + self.__selected_objects: list[SignalObj | ImageObj] = [] self.setWindowTitle(title) vlayout = QW.QVBoxLayout() self.setLayout(vlayout) @@ -267,9 +316,17 @@ def __init__( self.tree = SimpleObjectTree(parent, panel.objmodel) self.tree.initialize_from(panel.objview) self.tree.SIG_ITEM_DOUBLECLICKED.connect(lambda oid: self.accept()) - self.tree.itemSelectionChanged.connect(self.__current_object_changed) + self.tree.itemSelectionChanged.connect(self.__item_selection_changed) + if nb_objects > 1: + self.tree.setSelectionMode(QW.QAbstractItemView.ExtendedSelection) vlayout.addWidget(self.tree) + if comment: + lbl = QW.QLabel(comment) + lbl.setWordWrap(True) + vlayout.addSpacing(10) + vlayout.addWidget(lbl) + bbox = QW.QDialogButtonBox(QW.QDialogButtonBox.Ok | QW.QDialogButtonBox.Cancel) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) @@ -277,21 +334,22 @@ def __init__( vlayout.addSpacing(10) vlayout.addWidget(bbox) # Update OK button state: - self.__current_object_changed() + self.__item_selection_changed() if minimum_size is not None: self.setMinimumSize(*minimum_size) else: self.setMinimumWidth(400) - def __current_object_changed(self) -> None: + def __item_selection_changed(self) -> None: """Item selection has changed""" - self.__current_object_uuid = self.tree.get_current_item_id(object_only=True) - self.ok_btn.setEnabled(bool(self.__current_object_uuid)) + nobj = self.__nb_objects + self.__selected_objects = self.tree.get_sel_objects(include_groups=nobj > 1) + self.ok_btn.setEnabled(len(self.__selected_objects) == nobj) - def get_current_object_uuid(self) -> str: - """Return current object uuid""" - return self.__current_object_uuid + def get_selected_objects(self) -> list[SignalObj | ImageObj]: + """Return selected objects""" + return self.__selected_objects class ObjectView(SimpleObjectTree): @@ -511,50 +569,6 @@ def set_current_object(self, obj: SignalObj | ImageObj) -> None: """Set current object""" self.set_current_item_id(obj.uuid) - def get_sel_group_items(self) -> list[QW.QTreeWidgetItem]: - """Return selected group items""" - return [item for item in self.selectedItems() if item.parent() is None] - - def get_sel_group_uuids(self) -> list[str]: - """Return selected group uuids""" - return [item.data(0, QC.Qt.UserRole) for item in self.get_sel_group_items()] - - def get_sel_object_items(self) -> list[QW.QTreeWidgetItem]: - """Return selected object items""" - return [item for item in self.selectedItems() if item.parent() is not None] - - def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]: - """Return selected objects uuids. - - Args: - include_groups: If True, also return objects from selected groups. - - Returns: - List of selected objects uuids. - """ - sel_items = self.get_sel_object_items() - if not sel_items: - cur_item = self.currentItem() - if cur_item is not None and cur_item.parent() is not None: - sel_items = [cur_item] - uuids = [item.data(0, QC.Qt.UserRole) for item in sel_items] - if include_groups: - for group_id in self.get_sel_group_uuids(): - uuids.extend(self.objmodel.get_group_object_ids(group_id)) - return uuids - - def get_sel_objects( - self, include_groups: bool = False - ) -> list[SignalObj | ImageObj]: - """Return selected objects. - - If include_groups is True, also return objects from selected groups.""" - return [self.objmodel[oid] for oid in self.get_sel_object_uuids(include_groups)] - - def get_sel_groups(self) -> list[ObjectGroup]: - """Return selected groups""" - return self.objmodel.get_groups(self.get_sel_group_uuids()) - def item_selection_changed(self) -> None: """Refreshing the selection of objects and groups, emitting the SIG_SELECTION_CHANGED signal which triggers the update of the diff --git a/cdl/core/gui/panel/base.py b/cdl/core/gui/panel/base.py index e1c9687e..407ada56 100644 --- a/cdl/core/gui/panel/base.py +++ b/cdl/core/gui/panel/base.py @@ -13,7 +13,7 @@ import os.path as osp import re import warnings -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generic, Type import guidata.dataset as gds import guidata.dataset.qtwidgets as gdq @@ -30,10 +30,16 @@ from qtpy import QtWidgets as QW from qtpy.compat import getopenfilename, getopenfilenames, getsavefilename -import cdl.computation.base from cdl.config import APP_NAME, Conf, _ -from cdl.core.gui import actionhandler, objectmodel, objectview, roieditor -from cdl.core.model.base import ResultProperties, ResultShape, items_to_json +from cdl.core.gui import actionhandler, objectmodel, objectview +from cdl.core.gui.roieditor import TypeROIEditor +from cdl.core.model.base import ( + ResultProperties, + ResultShape, + TypeObj, + TypeROI, + items_to_json, +) from cdl.core.model.signal import create_signal from cdl.env import execenv from cdl.utils.qthelpers import ( @@ -242,12 +248,12 @@ def create_resultdata_dict(objs: list[SignalObj | ImageObj]) -> dict[str, Result return rdatadict -class BaseDataPanel(AbstractPanel): +class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]): """Object handling the item list, the selected item properties and plot""" PANEL_STR = "" # e.g. "Signal Panel" PANEL_STR_ID = "" # e.g. "signal" - PARAMCLASS: SignalObj | ImageObj = None # Replaced in child object + PARAMCLASS: TypeObj = None # Replaced in child object ANNOTATION_TOOLS = () MINDIALOGSIZE = (800, 600) MAXDIALOGSIZE = 0.95 # % of DataLab's main window size @@ -256,8 +262,11 @@ class BaseDataPanel(AbstractPanel): SIG_STATUS_MESSAGE = QC.Signal(str) # emitted by "qt_try_except" decorator SIG_REFRESH_PLOT = QC.Signal(str, bool) # Connected to PlotHandler.refresh_plot ROIDIALOGOPTIONS = {} - # Replaced in child object: - ROIDIALOGCLASS: roieditor.SignalROIEditor | roieditor.ImageROIEditor | None = None + + @staticmethod + @abc.abstractmethod + def get_roieditor_class() -> Type[TypeROIEditor]: + """Return ROI editor class""" @abc.abstractmethod def __init__(self, parent: QW.QWidget) -> None: @@ -273,7 +282,7 @@ def __init__(self, parent: QW.QWidget) -> None: self.acthandler: actionhandler.BaseActionHandler = None self.__metadata_clipboard = {} self.context_menu = QW.QMenu() - self.__separate_views: dict[QW.QDialog, SignalObj | ImageObj] = {} + self.__separate_views: dict[QW.QDialog, TypeObj] = {} def closeEvent(self, event): """Reimplement QMainWindow method""" @@ -312,9 +321,7 @@ def plot_item_moved( """ self.plothandler.update_resultproperty_from_plot_item(item) - def serialize_object_to_hdf5( - self, obj: SignalObj | ImageObj, writer: NativeH5Writer - ) -> None: + def serialize_object_to_hdf5(self, obj: TypeObj, writer: NativeH5Writer) -> None: """Serialize object to HDF5 file""" # Before serializing, update metadata from plot item parameters, in order to # save the latest visualization settings: @@ -354,7 +361,7 @@ def __len__(self) -> int: """Return number of objects""" return len(self.objmodel) - def __getitem__(self, nb: int) -> SignalObj | ImageObj: + def __getitem__(self, nb: int) -> TypeObj: """Return object from its number (1 to N)""" return self.objmodel.get_object_from_number(nb) @@ -362,7 +369,7 @@ def __iter__(self): """Iterate over objects""" return iter(self.objmodel) - def create_object(self) -> SignalObj | ImageObj: + def create_object(self) -> TypeObj: """Create object (signal or image) Returns: @@ -373,7 +380,7 @@ def create_object(self) -> SignalObj | ImageObj: @qt_try_except() def add_object( self, - obj: SignalObj | ImageObj, + obj: TypeObj, group_id: str | None = None, set_current: bool = True, ) -> None: @@ -576,13 +583,13 @@ def delete_metadata( refresh_plot: Refresh plot. Defaults to True. keep_roi: Keep regions of interest, if any. Defaults to None (ask user). """ - if execenv.unattended: - keep_roi = False sel_objs = self.objview.get_sel_objects(include_groups=True) # Check if there are regions of interest first: - roi_backup: dict[SignalObj | ImageObj, np.ndarray] = {} + roi_backup: dict[TypeObj, np.ndarray] = {} if any(obj.roi is not None for obj in sel_objs): - if keep_roi is None: + if execenv.unattended and keep_roi is None: + keep_roi = False + elif keep_roi is None: answer = QW.QMessageBox.warning( self, _("Delete metadata"), @@ -602,6 +609,8 @@ def delete_metadata( # Delete metadata: for index, obj in enumerate(sel_objs): obj.reset_metadata_to_defaults() + if not keep_roi: + obj.invalidate_maskdata_cache() if obj in roi_backup: obj.roi = roi_backup[obj] if index == 0: @@ -640,16 +649,30 @@ def new_group(self) -> None: if ok: self.add_group(group_name) - def rename_group(self) -> None: - """Rename a group""" - # Open a message box to enter the group name - group = self.objview.get_sel_groups()[0] - group_name, ok = QW.QInputDialog.getText( - self, _("Rename group"), _("Group name:"), QW.QLineEdit.Normal, group.title - ) - if ok: - group.title = group_name - self.objview.update_item(group.uuid) + def rename_group(self, new_name: str | None = None) -> None: + """Rename a group + + Args: + new_name: new group name. Defaults to None (ask user). + """ + sel_groups = self.objview.get_sel_groups() + if not sel_groups or len(sel_groups) > 1: + # Won't happen in the application, but could happen in tests or using the + # API directly + raise ValueError("Select one group to rename") + group = sel_groups[0] + if new_name is None: + new_name, ok = QW.QInputDialog.getText( + self, + _("Rename group"), + _("Group name:"), + QW.QLineEdit.Normal, + group.title, + ) + if not ok: + return + group.title = new_name + self.objview.update_item(group.uuid) @abc.abstractmethod def get_newparam_from_current( @@ -671,7 +694,7 @@ def new_object( addparam: gds.DataSet | None = None, edit: bool = True, add_to_panel: bool = True, - ) -> SignalObj | ImageObj | None: + ) -> TypeObj | None: """Create a new object (signal/image). Args: @@ -710,7 +733,7 @@ def __load_from_file(self, filename: str) -> list[SignalObj] | list[ImageObj]: self.selection_changed() return objs - def __save_to_file(self, obj: SignalObj | ImageObj, filename: str) -> None: + def __save_to_file(self, obj: TypeObj, filename: str) -> None: """Save object to file (signal/image). Args: @@ -719,9 +742,7 @@ def __save_to_file(self, obj: SignalObj | ImageObj, filename: str) -> None: """ self.IO_REGISTRY.write(filename, obj) - def load_from_files( - self, filenames: list[str] | None = None - ) -> list[SignalObj | ImageObj]: + def load_from_files(self, filenames: list[str] | None = None) -> list[TypeObj]: """Open objects from file (signals/images), add them to DataLab and return them. Args: @@ -862,11 +883,38 @@ def properties_changed(self) -> None: """The properties 'Apply' button was clicked: update object properties, refresh plot and update object view.""" obj = self.objview.get_current_object() + # if obj is not None: # XXX: Is it necessary? + obj.invalidate_maskdata_cache() update_dataset(obj, self.objprop.properties.dataset) self.objview.update_item(obj.uuid) self.SIG_REFRESH_PLOT.emit("selected", True) # ------Plotting data in modal dialogs---------------------------------------------- + def add_plot_items_to_dialog(self, dlg: PlotDialog, oids: list[str]) -> None: + """Add plot items to dialog + + Args: + dlg: Dialog + oids: Object IDs + """ + objs = self.objmodel.get_objects(oids) + plot = dlg.get_plot() + with create_progress_bar( + self, _("Creating plot items"), max_=len(objs) + ) as progress: + for index, obj in enumerate(objs): + progress.setValue(index + 1) + QW.QApplication.processEvents() + if progress.wasCanceled(): + return None + item = obj.make_item(update_from=self.plothandler[obj.uuid]) + item.set_readonly(True) + plot.add_item(item, z=0) + plot.set_active_item(item) + item.unselect() + plot.replot() + return dlg + def open_separate_view( self, oids: list[str] | None = None, edit_annotations: bool = False ) -> PlotDialog | None: @@ -880,20 +928,23 @@ def open_separate_view( Returns: Instance of PlotDialog """ - title = _("Annotations") if oids is None: oids = self.objview.get_sel_object_uuids(include_groups=True) obj = self.objmodel[oids[0]] + + # Create a new dialog and add plot items to it dlg = self.create_new_dialog( - oids, + title=obj.title if len(oids) == 1 else None, edit=True, - name="new_window", + name=f"{obj.PREFIX}_new_window", options={"show_itemlist": edit_annotations}, ) if dlg is None: return None + self.add_plot_items_to_dialog(dlg, oids) + mgr = dlg.get_manager() - toolbar = QW.QToolBar(title, self) + toolbar = QW.QToolBar(_("Annotations"), self) dlg.button_layout.insertWidget(0, toolbar) mgr.add_toolbar(toolbar, id(toolbar)) toolbar.setToolButtonStyle(QC.Qt.ToolButtonTextUnderIcon) @@ -955,35 +1006,24 @@ def manual_refresh(self) -> None: def create_new_dialog( self, - oids: list[str], edit: bool = False, toolbar: bool = True, title: str | None = None, - tools: list[GuiTool] | None = None, name: str | None = None, options: dict | None = None, ) -> PlotDialog | None: """Create new pop-up signal/image plot dialog. Args: - oids: Object IDs edit: Edit mode toolbar: Show toolbar title: Dialog title - tools: list of tools to add to the toolbar - name: Dialog name + name: Dialog object name options: Plot options Returns: - QDialog instance + Plot dialog instance """ - if title is not None or len(oids) == 1: - if title is None: - title = self.objview.get_sel_objects(include_groups=True)[0].title - title = f"{title} - {APP_NAME}" - else: - title = APP_NAME - plot_options = self.plothandler.get_current_plot_options() if options is not None: plot_options = plot_options.copy(options) @@ -998,121 +1038,72 @@ def create_new_dialog( # pylint: disable=not-callable dlg = PlotDialog( parent=self.parent(), - title=title, + title=APP_NAME if title is None else f"{title} - {APP_NAME}", edit=edit, options=plot_options, toolbar=toolbar, size=size, ) dlg.setWindowIcon(get_icon("DataLab.svg")) - - if tools is not None: - for tool in tools: - dlg.get_manager().add_tool(tool) - plot = dlg.get_plot() - - objs = self.objmodel.get_objects(oids) - dlg.setObjectName(f"{objs[0].PREFIX}_{name}") - - with create_progress_bar( - self, _("Creating plot items"), max_=len(objs) - ) as progress: - for index, obj in enumerate(objs): - progress.setValue(index + 1) - QW.QApplication.processEvents() - if progress.wasCanceled(): - return None - item = obj.make_item(update_from=self.plothandler[obj.uuid]) - item.set_readonly(True) - plot.add_item(item, z=0) - plot.set_active_item(item) - item.unselect() - plot.replot() + dlg.setObjectName(name) return dlg - def create_new_dialog_for_selection( - self, - title: str, - name: str, - options: dict[str, any] = None, - toolbar: bool = False, - tools: list[GuiTool] = None, - ) -> tuple[PlotDialog | None, SignalObj | ImageObj]: - """Create new pop-up dialog for the currently selected signal/image. - - Args: - title: Dialog title - name: Dialog name - options: Plot options - toolbar: Show toolbar - tools: list of tools to add to the toolbar - - Returns: - QDialog instance, selected object - """ - obj = self.objview.get_sel_objects(include_groups=True)[0] - dlg = self.create_new_dialog( - [obj.uuid], - edit=True, - toolbar=toolbar, - title=f"{title} - {obj.title}", - tools=tools, - name=name, - options=options, - ) - return dlg, obj - - def get_roi_editor_output( - self, extract: bool, singleobj: bool, add_roi: bool = False - ) -> tuple[cdl.computation.base.ROIDataParam, bool] | None: + def get_roi_editor_output(self, extract: bool) -> tuple[TypeROI, bool] | None: """Get ROI data (array) from specific dialog box. Args: extract: Extract ROI from data - singleobj: Single object - add_roi: Add ROI immediately after opening the dialog (default: False) Returns: - ROI data + A tuple containing the ROI object and a boolean indicating whether the + dialog was accepted or not. """ roi_s = _("Regions of interest") options = self.ROIDIALOGOPTIONS - dlg, obj = self.create_new_dialog_for_selection( - roi_s, "roi_dialog", options, toolbar=True + obj = self.objview.get_sel_objects(include_groups=True)[0] + + # Create a new dialog + dlg = self.create_new_dialog( + edit=True, + toolbar=True, + title=f"{roi_s} - {obj.title}", + name=f"{obj.PREFIX}_roi_dialog", + options=options, ) if dlg is None: return None - plot = dlg.get_plot() - for item in plot.items: - item.set_selectable(False) + + # Create ROI editor (and add it to the dialog) # pylint: disable=not-callable - roi_editor: roieditor.SignalROIEditor | roieditor.ImageROIEditor = ( - self.ROIDIALOGCLASS(dlg, obj, extract, singleobj) - ) + item = obj.make_item(update_from=self.plothandler[obj.uuid]) + roi_editor = self.get_roieditor_class()(dlg, obj, extract, item=item) dlg.button_layout.insertWidget(0, roi_editor) - if add_roi: - roi_editor.add_roi() + if exec_dialog(dlg): return roi_editor.get_roieditor_results() return None - def get_object_with_dialog( - self, title: str, parent: QW.QWidget | None = None - ) -> SignalObj | ImageObj | None: + def get_objects_with_dialog( + self, + title: str, + comment: str = "", + nb_objects: int = 1, + parent: QW.QWidget | None = None, + ) -> TypeObj | None: """Get object with dialog box. Args: title: Dialog title + comment: Optional dialog comment + nb_objects: Number of objects to select parent: Parent widget - Returns: - Object (signal or image, or None if dialog was canceled) + Object(s) (signal(s) or image(s), or None if dialog was canceled) """ parent = self if parent is None else parent - dlg = objectview.GetObjectDialog(parent, self, title) + dlg = objectview.GetObjectsDialog(parent, self, title, comment, nb_objects) if exec_dialog(dlg): - obj_uuid = dlg.get_current_object_uuid() - return self.objmodel[obj_uuid] + return dlg.get_selected_objects() return None def __new_objprop_button( @@ -1208,7 +1199,7 @@ def plot_results(self) -> None: rdatadict = create_resultdata_dict(objs) if rdatadict: for category, rdata in rdatadict.items(): - xchoices = (("indexes", _("Indexes")),) + xchoices = (("indices", _("Indices")),) for xlabel in rdata.xlabels: xchoices += ((xlabel, xlabel),) ychoices = xchoices[1:] @@ -1252,7 +1243,7 @@ class PlotResultParam(gds.DataSet): ), default=default_kind, ) - xaxis = gds.ChoiceItem(_("X axis"), xchoices, default="indexes") + xaxis = gds.ChoiceItem(_("X axis"), xchoices, default="indices") yaxis = gds.ChoiceItem( _("Y axis"), ychoices, default=ychoices[0][0] ) @@ -1274,7 +1265,7 @@ class PlotResultParam(gds.DataSet): for title, results in grouped_results.items(): # title x, y = [], [] for index, result in enumerate(results): # object - if param.xaxis == "indexes": + if param.xaxis == "indices": x.append(index) else: i_xaxis = rdata.xlabels.index(param.xaxis) @@ -1288,7 +1279,7 @@ class PlotResultParam(gds.DataSet): roi_idx = np.array(np.unique(result.array[:, 0]), dtype=int) for i_roi in roi_idx: # ROI mask = result.array[:, 0] == i_roi - if param.xaxis == "indexes": + if param.xaxis == "indices": x = np.arange(result.array.shape[0])[mask] else: i_xaxis = rdata.xlabels.index(param.xaxis) diff --git a/cdl/core/gui/panel/image.py b/cdl/core/gui/panel/image.py index bc2dcb82..70b4aaa3 100644 --- a/cdl/core/gui/panel/image.py +++ b/cdl/core/gui/panel/image.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Type from plotpy.tools import ( AnnotatedCircleTool, @@ -29,6 +29,7 @@ from cdl.core.model.image import ( ImageDatatypes, ImageObj, + ImageROI, create_image_from_param, new_image_param, ) @@ -42,7 +43,7 @@ from cdl.core.model.image import NewImageParam -class ImagePanel(BaseDataPanel): +class ImagePanel(BaseDataPanel[ImageObj, ImageROI, roieditor.ImageROIEditor]): """Object handling the item list, the selected item properties and plot, specialized for Image objects""" @@ -66,10 +67,14 @@ class ImagePanel(BaseDataPanel): IO_REGISTRY = ImageIORegistry H5_PREFIX = "DataLab_Ima" ROIDIALOGOPTIONS = {"show_itemlist": True, "show_contrast": False} - ROIDIALOGCLASS = roieditor.ImageROIEditor # pylint: disable=duplicate-code + @staticmethod + def get_roieditor_class() -> Type[roieditor.ImageROIEditor]: + """Return ROI editor class""" + return roieditor.ImageROIEditor + def __init__( self, parent: QW.QWidget, @@ -83,13 +88,6 @@ def __init__( self.acthandler = ImageActionHandler(self, panel_toolbar, view_toolbar) # ------Refreshing GUI-------------------------------------------------------------- - def properties_changed(self) -> None: - """The properties 'Apply' button was clicked: updating signal""" - obj = self.objview.get_current_object() - if obj is not None: - obj.invalidate_maskdata_cache() - super().properties_changed() - def plot_lut_changed(self, plot: BasePlot) -> None: """The LUT of the plot has changed: updating image objects accordingly @@ -155,19 +153,6 @@ def new_object( self.add_object(image) return image - def delete_metadata( - self, refresh_plot: bool = True, keep_roi: bool | None = None - ) -> None: - """Delete metadata of selected objects - - Args: - refresh_plot: Refresh plot. Defaults to True. - keep_roi: Keep regions of interest, if any. Defaults to None (ask user). - """ - for obj in self.objview.get_sel_objects(include_groups=True): - obj.invalidate_maskdata_cache() - super().delete_metadata(refresh_plot, keep_roi) - def toggle_show_contrast(self, state: bool) -> None: """Toggle show contrast option""" Conf.view.show_contrast.set(state) diff --git a/cdl/core/gui/panel/signal.py b/cdl/core/gui/panel/signal.py index b1b3af2e..6f4559dc 100644 --- a/cdl/core/gui/panel/signal.py +++ b/cdl/core/gui/panel/signal.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Type from plotpy.tools import ( HCursorTool, @@ -30,6 +30,7 @@ from cdl.core.model.signal import ( CURVESTYLES, SignalObj, + SignalROI, create_signal_from_param, new_signal_param, ) @@ -42,7 +43,7 @@ from cdl.core.model.signal import NewSignalParam -class SignalPanel(BaseDataPanel): +class SignalPanel(BaseDataPanel[SignalObj, SignalROI, roieditor.SignalROIEditor]): """Object handling the item list, the selected item properties and plot, specialized for Signal objects""" @@ -65,10 +66,14 @@ class SignalPanel(BaseDataPanel): IO_REGISTRY = SignalIORegistry H5_PREFIX = "DataLab_Sig" - ROIDIALOGCLASS = roieditor.SignalROIEditor # pylint: disable=duplicate-code + @staticmethod + def get_roieditor_class() -> Type[roieditor.SignalROIEditor]: + """Return ROI editor class""" + return roieditor.SignalROIEditor + def __init__( self, parent: QW.QWidget, diff --git a/cdl/core/gui/plothandler.py b/cdl/core/gui/plothandler.py index 5f4cf642..62a69661 100644 --- a/cdl/core/gui/plothandler.py +++ b/cdl/core/gui/plothandler.py @@ -29,25 +29,26 @@ import hashlib from collections.abc import Iterator -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, Generic, TypeVar from weakref import WeakKeyDictionary import numpy as np from plotpy.constants import PlotType -from plotpy.items import GridItem, LegendBoxItem +from plotpy.items import CurveItem, GridItem, LegendBoxItem, MaskedImageItem from plotpy.plot import PlotOptions from qtpy import QtWidgets as QW from cdl.config import Conf, _ +from cdl.core.model.base import TypeObj, TypePlotItem +from cdl.core.model.image import ImageObj +from cdl.core.model.signal import SignalObj from cdl.utils.qthelpers import block_signals, create_progress_bar if TYPE_CHECKING: - from plotpy.items import CurveItem, LabelItem, MaskedImageItem + from plotpy.items import LabelItem from plotpy.plot import BasePlot, PlotWidget from cdl.core.gui.panel.base import BaseDataPanel - from cdl.core.model.image import ImageObj - from cdl.core.model.signal import SignalObj def calc_data_hash(obj: SignalObj | ImageObj) -> str: @@ -55,7 +56,10 @@ def calc_data_hash(obj: SignalObj | ImageObj) -> str: return hashlib.sha1(np.ascontiguousarray(obj.data)).hexdigest() -class BasePlotHandler: +TypePlotHandler = TypeVar("TypePlotHandler", bound="BasePlotHandler") + + +class BasePlotHandler(Generic[TypeObj, TypePlotItem]): """Object handling plot items associated to objects (signals/images)""" PLOT_TYPE: PlotType | None = None # Replaced in subclasses @@ -70,10 +74,10 @@ def __init__( self.plot = plotwidget.get_plot() # Plot items: key = object uuid, value = plot item - self.__plotitems: dict[str, CurveItem | MaskedImageItem] = {} + self.__plotitems: dict[str, TypePlotItem] = {} self.__shapeitems = [] - self.__cached_hashes: WeakKeyDictionary[SignalObj | ImageObj, list[int]] = ( + self.__cached_hashes: WeakKeyDictionary[TypeObj, list[int]] = ( WeakKeyDictionary() ) self.__auto_refresh = False @@ -86,7 +90,7 @@ def __len__(self) -> int: """Return number of items""" return len(self.__plotitems) - def __getitem__(self, oid: str) -> CurveItem | MaskedImageItem: + def __getitem__(self, oid: str) -> TypePlotItem: """Return item associated to object uuid""" try: return self.__plotitems[oid] @@ -100,17 +104,13 @@ def __getitem__(self, oid: str) -> CurveItem | MaskedImageItem: # Item does not exist and auto refresh is enabled: this should not happen raise exc - def get( - self, key: str, default: CurveItem | MaskedImageItem | None = None - ) -> CurveItem | MaskedImageItem | None: + def get(self, key: str, default: TypePlotItem | None = None) -> TypePlotItem | None: """Return item associated to object uuid. If the key is not found, default is returned if given, otherwise None is returned.""" return self.__plotitems.get(key, default) - def get_obj_from_item( - self, item: CurveItem | MaskedImageItem - ) -> SignalObj | ImageObj | None: + def get_obj_from_item(self, item: TypePlotItem) -> TypeObj | None: """Return object associated to plot item Args: @@ -124,11 +124,11 @@ def get_obj_from_item( return obj return None - def __setitem__(self, oid: str, item: CurveItem | MaskedImageItem) -> None: + def __setitem__(self, oid: str, item: TypePlotItem) -> None: """Set item associated to object uuid""" self.__plotitems[oid] = item - def __iter__(self) -> Iterator[CurveItem | MaskedImageItem]: + def __iter__(self) -> Iterator[TypePlotItem]: """Return an iterator over plothandler values (plot items)""" return iter(self.__plotitems.values()) @@ -201,33 +201,33 @@ def remove_all_shape_items(self) -> None: self.plot.del_items(self.__shapeitems) self.__shapeitems = [] - def __add_item_to_plot(self, oid: str) -> CurveItem | MaskedImageItem: + def __add_item_to_plot(self, oid: str) -> TypePlotItem: """Make plot item and add it to plot. Args: - oid (str): object uuid + oid: object uuid Returns: - CurveItem | MaskedImageItem: plot item + Plot item """ obj = self.panel.objmodel[oid] self.__cached_hashes[obj] = calc_data_hash(obj) - item: CurveItem | MaskedImageItem = obj.make_item() + item: TypePlotItem = obj.make_item() item.set_readonly(True) self[oid] = item self.plot.add_item(item) return item def __update_item_on_plot( - self, oid: str, ref_item: CurveItem | MaskedImageItem, just_show: bool = False + self, oid: str, ref_item: TypePlotItem, just_show: bool = False ) -> None: """Update plot item. Args: - oid (str): object uuid - ref_item (CurveItem | MaskedImageItem): reference item - just_show (bool | None): if True, only show the item (do not update it, - except regarding the reference item). Defaults to False. + oid: object uuid + ref_item: reference item + just_show: if True, only show the item (do not update it, except regarding + the reference item). Defaults to False. """ if not just_show: obj = self.panel.objmodel[oid] @@ -240,7 +240,7 @@ def __update_item_on_plot( @staticmethod def update_item_according_to_ref_item( - item: MaskedImageItem, ref_item: MaskedImageItem + item: TypePlotItem, ref_item: TypePlotItem ) -> None: # pylint: disable=unused-argument """Update plot item according to reference item""" # For now, nothing to do here: it's only used for images (contrast) @@ -274,7 +274,7 @@ def reduce_shown_oids(self, oids: list[str]) -> list[str]: oids: list of object uuids Returns: - list[str]: reduced list of object uuids + Reduced list of object uuids """ if self.__show_first_only: return oids[:1] @@ -449,7 +449,7 @@ def get_current_plot_options(self) -> PlotOptions: ) -class SignalPlotHandler(BasePlotHandler): +class SignalPlotHandler(BasePlotHandler[SignalObj, CurveItem]): """Object handling signal plot items, plot dialogs, plot options""" PLOT_TYPE = PlotType.CURVE @@ -470,7 +470,7 @@ def get_current_plot_options(self) -> PlotOptions: return options -class ImagePlotHandler(BasePlotHandler): +class ImagePlotHandler(BasePlotHandler[ImageObj, MaskedImageItem]): """Object handling image plot items, plot dialogs, plot options""" PLOT_TYPE = PlotType.IMAGE @@ -494,7 +494,7 @@ def reduce_shown_oids(self, oids: list[str]) -> list[str]: oids: list of object uuids Returns: - list[str]: reduced list of object uuids + Reduced list of object uuids """ oids = super().reduce_shown_oids(oids) diff --git a/cdl/core/gui/processor/base.py b/cdl/core/gui/processor/base.py index 21d023ec..7c671c21 100644 --- a/cdl/core/gui/processor/base.py +++ b/cdl/core/gui/processor/base.py @@ -14,7 +14,7 @@ import warnings from collections.abc import Callable from multiprocessing.pool import Pool -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, Generic, Union import guidata.dataset as gds import numpy as np @@ -26,10 +26,9 @@ from cdl import env from cdl.algorithms.datatypes import is_complex_dtype -from cdl.computation.base import ROIDataParam from cdl.config import Conf, _ from cdl.core.gui.processor.catcher import CompOut, wng_err_func -from cdl.core.model.base import ResultProperties, ResultShape +from cdl.core.model.base import ResultProperties, ResultShape, TypeROI from cdl.utils.qthelpers import create_progress_bar, qt_try_except from cdl.widgets.warningerror import show_warning_error @@ -158,7 +157,17 @@ def get_result(self) -> CompOut: return self.result -class BaseProcessor(QC.QObject): +def is_pairwise_mode() -> bool: + """Return True if operation mode is pairwise. + + Returns: + bool: True if operation mode is pairwise + """ + state = Conf.proc.operation_mode.get() == "pairwise" + return state + + +class BaseProcessor(QC.QObject, Generic[TypeROI]): """Object handling data processing: operations, processing, analysis. Args: @@ -501,6 +510,63 @@ def compute_10( exec_dialog(dlg) return results + def __get_src_grps_gids_objs_nbobj_valid(self) -> tuple[list, list, dict, int]: + """Get source groups, group ids, objects, and number of objects, + for pairwise mode, and check if the number of objects is valid. + + Returns: + Tuple (source groups, group ids, objects, number of objects, valid) + """ + # In pairwise mode, we need to create a new object for each pair of objects + objs = self.panel.objview.get_sel_objects(include_groups=True) + objmodel = self.panel.objmodel + src_grps = sorted( + {objmodel.get_group_from_object(obj) for obj in objs}, + key=lambda x: x.number, + ) + src_gids = [grp.uuid for grp in src_grps] + + # [src_objs dictionary] keys: old group id, values: list of old objects + src_objs: dict[str, list[Obj]] = {} + for src_gid in src_gids: + src_objs[src_gid] = [ + obj for obj in objs if objmodel.get_object_group_id(obj) == src_gid + ] + + nbobj = len(src_objs[src_gids[0]]) + + valid = len(src_grps) > 1 + if not valid: + # In pairwise mode, we need selected objects in at least two groups. + if env.execenv.unattended: + raise ValueError( + "Pairwise mode: objects must be selected in at least two groups" + ) + QW.QMessageBox.warning( + self.panel.parent(), + _("Warning"), + _( + "In pairwise mode, you need to select objects " + "in at least two groups." + ), + ) + if valid: + valid = all(len(src_objs[src_gid]) == nbobj for src_gid in src_gids) + if not valid: + if env.execenv.unattended: + raise ValueError( + "Pairwise mode: invalid number of objects in each group" + ) + QW.QMessageBox.warning( + self.panel.parent(), + _("Warning"), + _( + "In pairwise mode, you need to select " + "the same number of objects in each group." + ), + ) + return src_grps, src_gids, src_objs, nbobj, valid + def compute_n1( self, name: str, @@ -531,66 +597,123 @@ def compute_n1( return objs = self.panel.objview.get_sel_objects(include_groups=True) + objmodel = self.panel.objmodel + pairwise = is_pairwise_mode() - # [new_objs dictionary] keys: old group id, values: new object - dst_objs: dict[str, Obj] = {} - # [src_dtypes dictionary] keys: old group id, values: old data type - src_dtypes: dict[str, np.dtype] = {} - # [src_objs dictionary] keys: old group id, values: list of old objects - src_objs: dict[str, list[Obj]] = {} - - with create_progress_bar(self.panel, title, max_=len(objs)) as progress: - for index, src_obj in enumerate(objs): - progress.setValue(index + 1) - progress.setLabelText(title) - src_gid = self.panel.objmodel.get_object_group_id(src_obj) - dst_obj = dst_objs.get(src_gid) - if dst_obj is None: - src_dtypes[src_gid] = src_dtype = src_obj.data.dtype + if pairwise: + src_grps, src_gids, src_objs, _nbobj, valid = ( + self.__get_src_grps_gids_objs_nbobj_valid() + ) + if not valid: + return + dst_gname = ( + f"{name}({','.join([grp.short_id for grp in src_grps])})|pairwise" + ) + group_exclusive = len(self.panel.objview.get_sel_groups()) != 0 + if not group_exclusive: + # This is not a group exclusive selection + dst_gname += "[...]" + dst_gid = self.panel.add_group(dst_gname).uuid + n_pairs = len(src_objs[src_gids[0]]) + max_i_pair = min(n_pairs, max(len(src_objs[grp.uuid]) for grp in src_grps)) + with create_progress_bar(self.panel, title, max_=n_pairs) as progress: + for i_pair, src_obj1 in enumerate(src_objs[src_gids[0]][:max_i_pair]): + src_obj1: SignalObj | ImageObj + progress.setValue(i_pair + 1) + progress.setLabelText(title) + src_dtype = src_obj1.data.dtype dst_dtype = complex if is_complex_dtype(src_dtype) else float - dst_objs[src_gid] = dst_obj = src_obj.copy(dtype=dst_dtype) - dst_obj.roi = None - src_objs[src_gid] = [src_obj] - else: - src_objs[src_gid].append(src_obj) - if param is None: - args = (dst_obj, src_obj) - else: - args = (dst_obj, src_obj, param) - result = self.__exec_func(func, args, progress) - if result is None: - break - dst_obj = self.handle_output( - result, _("Calculating: %s") % title, progress - ) - if dst_obj is None: - break - dst_objs[src_gid] = dst_obj - dst_obj.update_resultshapes_from(src_obj) - if src_obj.roi is not None: - if dst_obj.roi is None: - dst_obj.roi = src_obj.roi.copy() - else: - dst_obj.roi = np.vstack((dst_obj.roi, src_obj.roi)) + dst_obj = src_obj1.copy(dtype=dst_dtype) + src_objs_pair = [src_obj1] + for src_gid in src_gids[1:]: + src_obj = src_objs[src_gid][i_pair] + src_objs_pair.append(src_obj) + if param is None: + args = (dst_obj, src_obj) + else: + args = (dst_obj, src_obj, param) + result = self.__exec_func(func, args, progress) + if result is None: + break + dst_obj = self.handle_output( + result, _("Calculating: %s") % title, progress + ) + if dst_obj is None: + break + dst_obj.update_resultshapes_from(src_obj) + if src_obj.roi is not None: + if dst_obj.roi is None: + dst_obj.roi = src_obj.roi.copy() + else: + dst_obj.roi.add_roi(src_obj.roi) + if func_objs is not None: + func_objs(dst_obj, src_objs_pair) + short_ids = [obj.short_id for obj in src_objs_pair] + dst_obj.title = f'{name}({", ".join(short_ids)})' + self.panel.add_object(dst_obj, group_id=dst_gid) - grps = self.panel.objview.get_sel_groups() - if grps: - # (Group exclusive selection) - # At least one group is selected: create a new group - dst_gname = f"{name}({','.join([grp.short_id for grp in grps])})" - dst_gid = self.panel.add_group(dst_gname).uuid else: - # (Object exclusive selection) - # No group is selected: use each object's group - dst_gid = None - - for src_gid, dst_obj in dst_objs.items(): - if func_objs is not None: - func_objs(dst_obj, src_objs[src_gid]) - short_ids = [obj.short_id for obj in src_objs[src_gid]] - dst_obj.title = f'{name}({", ".join(short_ids)})' - group_id = dst_gid if dst_gid is not None else src_gid - self.panel.add_object(dst_obj, group_id=group_id) + # In single operand mode, we create a single object for all selected objects + + # [new_objs dictionary] keys: old group id, values: new object + dst_objs: dict[str, Obj] = {} + # [src_dtypes dictionary] keys: old group id, values: old data type + src_dtypes: dict[str, np.dtype] = {} + # [src_objs dictionary] keys: old group id, values: list of old objects + src_objs: dict[str, list[Obj]] = {} + + with create_progress_bar(self.panel, title, max_=len(objs)) as progress: + for index, src_obj in enumerate(objs): + progress.setValue(index + 1) + progress.setLabelText(title) + src_gid = objmodel.get_object_group_id(src_obj) + dst_obj = dst_objs.get(src_gid) + if dst_obj is None: + src_dtypes[src_gid] = src_dtype = src_obj.data.dtype + dst_dtype = complex if is_complex_dtype(src_dtype) else float + dst_objs[src_gid] = dst_obj = src_obj.copy(dtype=dst_dtype) + dst_obj.roi = None + src_objs[src_gid] = [src_obj] + else: + src_objs[src_gid].append(src_obj) + if param is None: + args = (dst_obj, src_obj) + else: + args = (dst_obj, src_obj, param) + result = self.__exec_func(func, args, progress) + if result is None: + break + dst_obj = self.handle_output( + result, _("Calculating: %s") % title, progress + ) + if dst_obj is None: + break + dst_objs[src_gid] = dst_obj + dst_obj.update_resultshapes_from(src_obj) + if src_obj.roi is not None: + if dst_obj.roi is None: + dst_obj.roi = src_obj.roi.copy() + else: + dst_obj.roi.add_roi(src_obj.roi) + + grps = self.panel.objview.get_sel_groups() + if grps: + # (Group exclusive selection) + # At least one group is selected: create a new group + dst_gname = f"{name}({','.join([grp.short_id for grp in grps])})" + dst_gid = self.panel.add_group(dst_gname).uuid + else: + # (Object exclusive selection) + # No group is selected: use each object's group + dst_gid = None + + for src_gid, dst_obj in dst_objs.items(): + if func_objs is not None: + func_objs(dst_obj, src_objs[src_gid]) + short_ids = [obj.short_id for obj in src_objs[src_gid]] + dst_obj.title = f'{name}({", ".join(short_ids)})' + group_id = dst_gid if dst_gid is not None else src_gid + self.panel.add_object(dst_obj, group_id=group_id) # Select newly created group, if any if dst_gid is not None: @@ -598,7 +721,7 @@ def compute_n1( def compute_n1n( self, - obj2: Obj | None, + obj2: Obj | list[Obj] | None, obj2_name: str, func: Callable, param: gds.DataSet | None = None, @@ -612,7 +735,7 @@ def compute_n1n( Examples: subtract, divide Args: - obj2: second object + obj2: second object (or list of objects in case of pairwise operation mode) obj2_name: name of second object func: function to execute param: parameters. Defaults to None. @@ -623,30 +746,100 @@ def compute_n1n( """ if (edit is None or param is None) and paramclass is not None: edit, param = self.init_param(param, paramclass, title, comment) + + objs = self.panel.objview.get_sel_objects(include_groups=True) + objmodel = self.panel.objmodel + pairwise = is_pairwise_mode() + if obj2 is None: - obj2 = self.panel.get_object_with_dialog(_("Select %s") % obj2_name) - if obj2 is None: - return - if param is not None: - if edit and not param.edit(parent=self.panel.parent()): + objs2 = [] + elif isinstance(obj2, list): + objs2 = obj2 + assert pairwise + else: + objs2 = [obj2] + + dlg_title = _("Select %s") % obj2_name + + if pairwise: + group_exclusive = len(self.panel.objview.get_sel_groups()) != 0 + + src_grps, src_gids, src_objs, nbobj, valid = ( + self.__get_src_grps_gids_objs_nbobj_valid() + ) + if not valid: return - objs = self.panel.objview.get_sel_objects(include_groups=True) - # name = func.__name__.replace("compute_", "") - with create_progress_bar(self.panel, title, max_=len(objs)) as progress: - for index, obj in enumerate(objs): - progress.setValue(index + 1) - progress.setLabelText(title) - args = (obj, obj2) if param is None else (obj, obj2, param) - result = self.__exec_func(func, args, progress) - if result is None: - break - new_obj = self.handle_output( - result, _("Calculating: %s") % title, progress + if not objs2: + objs2 = self.panel.get_objects_with_dialog( + dlg_title, + _( + "Note: operation mode is pairwise: " + "%s object(s) expected (i.e. as many as in the first group)" + ) + % nbobj, + nbobj, ) - if new_obj is None: - continue - group_id = self.panel.objmodel.get_object_group_id(obj) - self.panel.add_object(new_obj, group_id=group_id) + if objs2 is None: + return + + name = func.__name__.replace("compute_", "") + n_pairs = len(src_objs[src_gids[0]]) + max_i_pair = min(n_pairs, max(len(src_objs[grp.uuid]) for grp in src_grps)) + grp2_id = objmodel.get_object_group_id(objs2[0]) + grp2 = objmodel.get_group(grp2_id) + with create_progress_bar(self.panel, title, max_=len(src_gids)) as progress: + for i_group, src_gid in enumerate(src_gids): + progress.setValue(i_group + 1) + progress.setLabelText(title) + if group_exclusive: + # This is a group exclusive selection + src_grp = objmodel.get_group(src_gid) + grp_short_ids = [grp.short_id for grp in (src_grp, grp2)] + dst_gname = f"{name}({','.join(grp_short_ids)})|pairwise" + else: + dst_gname = f"{name}[...]" + dst_gid = self.panel.add_group(dst_gname).uuid + for i_pair in range(max_i_pair): + args = [src_objs[src_gid][i_pair], objs2[i_pair]] + if param is not None: + args.append(param) + result = self.__exec_func(func, tuple(args), progress) + if result is None: + break + new_obj = self.handle_output( + result, _("Calculating: %s") % title, progress + ) + if new_obj is None: + continue + self.panel.add_object(new_obj, group_id=dst_gid) + + else: + if not objs2: + objs2 = self.panel.get_objects_with_dialog( + dlg_title, + _( + "Note: operation mode is single operand: " + "1 object expected" + ), + ) + if objs2 is None: + return + obj2 = objs2[0] + with create_progress_bar(self.panel, title, max_=len(objs)) as progress: + for index, obj in enumerate(objs): + progress.setValue(index + 1) + progress.setLabelText(title) + args = (obj, obj2) if param is None else (obj, obj2, param) + result = self.__exec_func(func, args, progress) + if result is None: + break + new_obj = self.handle_output( + result, _("Calculating: %s") % title, progress + ) + if new_obj is None: + continue + group_id = objmodel.get_object_group_id(obj) + self.panel.add_object(new_obj, group_id=group_id) # ------Data Operations------------------------------------------------------------- @@ -679,48 +872,19 @@ def compute_product(self) -> None: @abc.abstractmethod @qt_try_except() - def compute_difference(self, obj2: Obj | None = None) -> None: + def compute_difference(self, obj2: Obj | list[Obj] | None = None) -> None: """Compute difference""" @abc.abstractmethod @qt_try_except() - def compute_quadratic_difference(self, obj2: Obj | None = None) -> None: + def compute_quadratic_difference(self, obj2: Obj | list[Obj] | None = None) -> None: """Compute quadratic difference""" @abc.abstractmethod @qt_try_except() - def compute_division(self, obj2: Obj | None = None) -> None: + def compute_division(self, obj2: Obj | list[Obj] | None = None) -> None: """Compute division""" - def _get_roidataparam(self, param: ROIDataParam | None = None) -> ROIDataParam: - """Eventually open ROI Editing Dialog, and return ROI editor data. - - Args: - param: ROI data parameters. - Defaults to None. - - Returns: - ROI data parameters. - """ - # Expected behavior: - # ----------------- - # * If param.roidata argument is not None, skip the ROI dialog - # * If first selected obj has a ROI, use this ROI as default but open - # ROI Editor dialog anyway - # * If multiple objs are selected, then apply the first obj ROI to all - if param is None: - param = ROIDataParam() - if param.roidata is None: - param = self.edit_regions_of_interest( - extract=True, singleobj=param.singleobj - ) - return param - - @abc.abstractmethod - @qt_try_except() - def compute_roi_extraction(self, param=None) -> None: - """Extract Region Of Interest (ROI) from data""" - @abc.abstractmethod @qt_try_except() def compute_swap_axes(self) -> None: @@ -818,49 +982,86 @@ def compute_product_constant(self, param: ConstantParam) -> None: def compute_division_constant(self, param: ConstantParam) -> None: """Compute division by a constant""" + @qt_try_except() + def compute_roi_extraction(self, roi: TypeROI | None = None) -> None: + """Extract Region Of Interest (ROI) from data with: + + - :py:func:`cdl.computation.image.extract_single_roi` for single ROI + - :py:func:`cdl.computation.image.extract_multiple_roi` for multiple ROIs""" + # Expected behavior: + # ----------------- + # * If `roi` is not None or not empty, skip the ROI dialog + # * If first selected obj has a ROI, use this ROI as default but open + # ROI Editor dialog anyway + # * If multiple objs are selected, then apply the first obj ROI to all + if roi is None or roi.is_empty(): + roi = self.edit_regions_of_interest(extract=True) + if roi is None or roi.is_empty(): + return + obj = self.panel.objview.get_sel_objects(include_groups=True)[0] + group = roi.to_params(obj) + if roi.singleobj and len(group.datasets) > 1: + # Extract multiple ROIs into a single object (remove all the ROIs), + # if the "Extract all ROIs into a single image object" + # option is checked and if there are more than one ROI + self._extract_multiple_roi_in_single_object(group) + else: + # Extract each ROI into a separate object (keep the ROI in the case of + # a circular ROI), if the "Extract all ROIs into a single image object" + # option is not checked or if there is only one ROI (See Issue #31) + self._extract_each_roi_in_separate_object(group) + + @abc.abstractmethod + @qt_try_except() + def _extract_multiple_roi_in_single_object(self, group: gds.DataSetGroup) -> None: + """Extract multiple Regions Of Interest (ROIs) from data in a single object""" + + @abc.abstractmethod + @qt_try_except() + def _extract_each_roi_in_separate_object(self, group: gds.DataSetGroup) -> None: + """Extract each single Region Of Interest (ROI) from data in a separate + object (keep the ROI in the case of a circular ROI, for example)""" + # ------Analysis------------------------------------------------------------------- def edit_regions_of_interest( self, extract: bool = False, - singleobj: bool | None = None, - add_roi: bool = False, - ) -> ROIDataParam | None: + ) -> TypeROI | None: """Define Region Of Interest (ROI). Args: extract: If True, ROI is extracted from data. Defaults to False. - singleobj: If True, ROI is extracted from first selected object only. - If False, ROI is extracted from all selected objects. If None, ROI is - extracted from all selected objects only if they all have the same ROI. - Defaults to None. - add_roi: If True, add ROI to data immediately after opening the ROI editor. - Defaults to False. Returns: - ROI data parameters or None if ROI dialog has been canceled. + ROI object or None if ROI dialog has been canceled. """ - results = self.panel.get_roi_editor_output( - extract=extract, singleobj=singleobj, add_roi=add_roi - ) + # Expected behavior: + # ----------------- + # * If first selected obj has a ROI, use this ROI as default but open + # ROI Editor dialog anyway + # * If multiple objs are selected, then apply the first obj ROI to all + results = self.panel.get_roi_editor_output(extract=extract) if results is None: return None - roieditordata, modified = results + edited_roi, modified = results obj = self.panel.objview.get_sel_objects(include_groups=True)[0] - roigroup = obj.roidata_to_params(roieditordata.roidata) + group = edited_roi.to_params(obj) if ( - env.execenv.unattended - or roieditordata.roidata.size == 0 - or roigroup.edit(parent=self.panel.parent()) + env.execenv.unattended # Unattended mode (automated unit tests) + or edited_roi.is_empty() # No ROI has been defined + or group.edit(parent=self.panel.parent()) # ROI dialog has been accepted ): - roidata = obj.params_to_roidata(roigroup) if modified: - roieditordata.roidata = roidata # If ROI has been modified, save ROI (even in "extract mode") - obj.roi = roidata + if edited_roi.is_empty(): + obj.roi = None + else: + edited_roi = edited_roi.from_params(obj, group) + obj.roi = edited_roi self.SIG_ADD_SHAPE.emit(obj.uuid) self.panel.selection_changed(update_items=True) - return roieditordata + return edited_roi def delete_regions_of_interest(self) -> None: """Delete Regions Of Interest""" diff --git a/cdl/core/gui/processor/image.py b/cdl/core/gui/processor/image.py index c0410b6e..9f55b48d 100644 --- a/cdl/core/gui/processor/image.py +++ b/cdl/core/gui/processor/image.py @@ -8,6 +8,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np from guidata.qthelpers import exec_dialog from plotpy.widgets.resizedialog import ResizeDialog @@ -27,12 +29,15 @@ from cdl.core.gui.processor.base import BaseProcessor from cdl.core.gui.profiledialog import ProfileExtractionDialog from cdl.core.model.base import ResultProperties, ResultShape -from cdl.core.model.image import ImageObj, ROI2DParam, RoiDataGeometries +from cdl.core.model.image import ImageObj, ImageROI, ROI2DParam, create_image_roi from cdl.utils.qthelpers import create_progress_bar, qt_try_except from cdl.widgets import imagebackground +if TYPE_CHECKING: + import guidata.dataset as gds + -class ImageProcessor(BaseProcessor): +class ImageProcessor(BaseProcessor[ImageROI]): """Object handling image processing: operations, processing, analysis""" # pylint: disable=duplicate-code @@ -239,31 +244,6 @@ def compute_binning(self, param: cdl.param.BinningParam | None = None) -> None: param.dtype_str = input_dtype_str self.compute_11(cpi.compute_binning, param, title=title, edit=edit) - @qt_try_except() - def compute_roi_extraction( - self, param: cdl.param.ROIDataParam | None = None - ) -> None: - """Extract Region Of Interest (ROI) from data with: - - - :py:func:`cdl.computation.image.extract_single_roi` for single ROI - - :py:func:`cdl.computation.image.extract_multiple_roi` for multiple ROIs""" - param = self._get_roidataparam(param) - if param is None or param.is_empty: - return - obj = self.panel.objview.get_sel_objects(include_groups=True)[0] - group = obj.roidata_to_params(param.roidata) - if param.singleobj and len(group.datasets) > 1: - # Extract multiple ROIs into a single object (remove all the ROIs), - # if the "Extract all regions of interest into a single image object" - # option is checked and if there are more than one ROI - self.compute_11(cpi.extract_multiple_roi, group, title=_("Extract ROI")) - else: - # Extract each ROI into a separate object (keep the ROI in the case of - # a circular ROI), if the "Extract all regions of interest into a single - # image object" option is not checked or if there is only one ROI - # (See Issue #31) - self.compute_1n(cpi.extract_single_roi, group.datasets, "ROI", edit=False) - @qt_try_except() def compute_line_profile( self, param: cdl.param.LineProfileParam | None = None @@ -397,7 +377,7 @@ def compute_arithmetic( ) @qt_try_except() - def compute_difference(self, obj2: ImageObj | None = None) -> None: + def compute_difference(self, obj2: ImageObj | list[ImageObj] | None = None) -> None: """Compute difference between two images with :py:func:`cdl.computation.image.compute_difference`""" self.compute_n1n( @@ -421,7 +401,9 @@ def compute_difference_constant( ) @qt_try_except() - def compute_quadratic_difference(self, obj2: ImageObj | None = None) -> None: + def compute_quadratic_difference( + self, obj2: ImageObj | list[ImageObj] | None = None + ) -> None: """Compute quadratic difference between two images with :py:func:`cdl.computation.image.compute_quadratic_difference`""" self.compute_n1n( @@ -432,7 +414,7 @@ def compute_quadratic_difference(self, obj2: ImageObj | None = None) -> None: ) @qt_try_except() - def compute_division(self, obj2: ImageObj | None = None) -> None: + def compute_division(self, obj2: ImageObj | list[ImageObj] | None = None) -> None: """Compute division between two images with :py:func:`cdl.computation.image.compute_division`""" self.compute_n1n( @@ -508,8 +490,9 @@ def compute_offset_correction(self, param: ROI2DParam | None = None) -> None: if param is None: dlg = imagebackground.ImageBackgroundDialog(obj, parent=self.panel.parent()) if exec_dialog(dlg): - param = ROI2DParam.create(geometry=RoiDataGeometries.RECTANGLE) - param.xr0, param.yr0, param.xr1, param.yr1 = dlg.get_index_range() + param = ROI2DParam.create(geometry="rectangle") + x0, y0, x1, y1 = dlg.get_rect_coords() + param.x0, param.y0, param.dx, param.dy = x0, y0, x1 - x0, y1 - y0 else: return self.compute_11(cpi.compute_offset_correction, param) @@ -1103,6 +1086,17 @@ def compute_all_edges(self) -> None: ] self.compute_1n(funcs, None, "Edges") + @qt_try_except() + def _extract_multiple_roi_in_single_object(self, group: gds.DataSetGroup) -> None: + """Extract multiple Regions Of Interest (ROIs) from data in a single object""" + self.compute_11(cpi.extract_multiple_roi, group, title=_("Extract ROI")) + + @qt_try_except() + def _extract_each_roi_in_separate_object(self, group: gds.DataSetGroup) -> None: + """Extract each single Region Of Interest (ROI) from data in a separate + object (keep the ROI in the case of a circular ROI, for example)""" + self.compute_1n(cpi.extract_single_roi, group.datasets, "ROI", edit=False) + # ------Image Analysis @qt_try_except() def compute_stats(self) -> dict[str, ResultProperties]: @@ -1158,17 +1152,13 @@ def compute_peak_detection( assert dist_min > 0 radius = int(0.5 * dist_min / np.sqrt(2) - 1) assert radius >= 1 - roicoords = [] ymax, xmax = obj.data.shape + coords = [] for x, y in result.raw_data: - coords = [ - max(x - radius, 0), - max(y - radius, 0), - min(x + radius, xmax), - min(y + radius, ymax), - ] - roicoords.append(coords) - obj.roi = np.array(roicoords, int) + x0, y0 = max(x - radius, 0), max(y - radius, 0) + dx, dy = min(x + radius, xmax) - x0, min(y + radius, ymax) - y0 + coords.append([x0, y0, dx, dy]) + obj.roi = create_image_roi("rectangle", coords, indices=True) self.SIG_ADD_SHAPE.emit(obj.uuid) self.panel.SIG_REFRESH_PLOT.emit(obj.uuid, True) return results diff --git a/cdl/core/gui/processor/signal.py b/cdl/core/gui/processor/signal.py index faf1419c..254edeef 100644 --- a/cdl/core/gui/processor/signal.py +++ b/cdl/core/gui/processor/signal.py @@ -11,6 +11,7 @@ import re from collections.abc import Callable +import guidata.dataset as gds import numpy as np from guidata.qthelpers import exec_dialog @@ -20,12 +21,12 @@ from cdl.config import Conf, _ from cdl.core.gui.processor.base import BaseProcessor from cdl.core.model.base import ResultProperties, ResultShape -from cdl.core.model.signal import ROI1DParam, SignalObj, create_signal +from cdl.core.model.signal import ROI1DParam, SignalObj, SignalROI, create_signal from cdl.utils.qthelpers import qt_try_except from cdl.widgets import fitdialog, signalbaseline, signalpeak -class SignalProcessor(BaseProcessor): +class SignalProcessor(BaseProcessor[SignalROI]): """Object handling signal processing: operations, processing, analysis""" # pylint: disable=duplicate-code @@ -77,25 +78,6 @@ def compute_product_constant(self, param: cpb.ConstantParam | None = None) -> No title=_("Product with constant"), ) - @qt_try_except() - def compute_roi_extraction( - self, param: cdl.param.ROIDataParam | None = None - ) -> None: - """Extract Region Of Interest (ROI) from data with: - - - :py:func:`cdl.computation.signal.extract_multiple_roi` for single ROI - - :py:func:`cdl.computation.signal.extract_single_roi` for multiple ROIs - """ - param = self._get_roidataparam(param) - if param is None or param.is_empty: - return - obj = self.panel.objview.get_sel_objects(include_groups=True)[0] - group = obj.roidata_to_params(param.roidata) - if param.singleobj: - self.compute_11(cps.extract_multiple_roi, group, title=_("Extract ROI")) - else: - self.compute_1n(cps.extract_single_roi, group.datasets, "ROI", edit=False) - @qt_try_except() def compute_swap_axes(self) -> None: """Swap data axes with :py:func:`cdl.computation.signal.compute_swap_axes`""" @@ -159,7 +141,9 @@ def compute_arithmetic( ) @qt_try_except() - def compute_difference(self, obj2: SignalObj | None = None) -> None: + def compute_difference( + self, obj2: SignalObj | list[SignalObj] | None = None + ) -> None: """Compute difference between two signals with :py:func:`cdl.computation.signal.compute_difference`""" self.compute_n1n( @@ -180,10 +164,13 @@ def compute_difference_constant( param, paramclass=cpb.ConstantParam, title=_("Difference with constant"), + edit=True, ) @qt_try_except() - def compute_quadratic_difference(self, obj2: SignalObj | None = None) -> None: + def compute_quadratic_difference( + self, obj2: SignalObj | list[SignalObj] | None = None + ) -> None: """Compute quadratic difference between two signals with :py:func:`cdl.computation.signal.compute_quadratic_difference`""" self.compute_n1n( @@ -194,7 +181,7 @@ def compute_quadratic_difference(self, obj2: SignalObj | None = None) -> None: ) @qt_try_except() - def compute_division(self, obj2: SignalObj | None = None) -> None: + def compute_division(self, obj2: SignalObj | list[SignalObj] | None = None) -> None: """Compute division between two signals with :py:func:`cdl.computation.signal.compute_division`""" self.compute_n1n( @@ -555,7 +542,7 @@ def compute_multigaussianfit(self) -> None: dlg = signalpeak.SignalPeakDetectionDialog(obj, parent=self.panel) if exec_dialog(dlg): # Computing x, y - peaks = dlg.get_peak_indexes() + peaks = dlg.get_peak_indices() def multigaussianfit(x, y, parent=None): """Multi-Gaussian fit dialog function""" @@ -564,6 +551,17 @@ def multigaussianfit(x, y, parent=None): self.__row_compute_fit(obj, _("Multi-Gaussian fit"), multigaussianfit) + @qt_try_except() + def _extract_multiple_roi_in_single_object(self, group: gds.DataSetGroup) -> None: + """Extract multiple Regions Of Interest (ROIs) from data in a single object""" + self.compute_11(cps.extract_multiple_roi, group, title=_("Extract ROI")) + + @qt_try_except() + def _extract_each_roi_in_separate_object(self, group: gds.DataSetGroup) -> None: + """Extract each single Region Of Interest (ROI) from data in a separate + object (keep the ROI in the case of a circular ROI, for example)""" + self.compute_1n(cps.extract_single_roi, group.datasets, "ROI", edit=False) + # ------Signal Analysis @qt_try_except() def compute_fwhm( diff --git a/cdl/core/gui/roieditor.py b/cdl/core/gui/roieditor.py index 9e2d78f9..6f094f9f 100644 --- a/cdl/core/gui/roieditor.py +++ b/cdl/core/gui/roieditor.py @@ -25,64 +25,249 @@ from __future__ import annotations import abc -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Generic, TypeVar, Union from guidata.configtools import get_icon -from guidata.qthelpers import create_toolbutton +from guidata.qthelpers import add_actions, create_action from plotpy.builder import make from plotpy.interfaces import IImageItemType -from plotpy.items import AnnotatedCircle, ObjectInfo +from plotpy.items import ( + AnnotatedCircle, + AnnotatedPolygon, + AnnotatedRectangle, + CurveItem, + MaskedImageItem, + ObjectInfo, + XRangeSelection, +) +from plotpy.plot import PlotDialog, PlotManager +from plotpy.tools import CircleTool, HRangeTool, PolygonTool, RectangleTool from qtpy import QtCore as QC from qtpy import QtWidgets as QW -from cdl.computation.base import ROIDataParam +import cdl.obj as dlo from cdl.config import Conf, _ -from cdl.core.model.image import RoiDataGeometries +from cdl.core.model.base import ( + TypeObj, + TypePlotItem, + TypeROI, + TypeROIItem, + configure_roi_item, +) +from cdl.core.model.image import CircularROI, PolygonalROI, RectangularROI +from cdl.core.model.signal import SegmentROI +from cdl.env import execenv +from cdl.obj import ImageObj, ImageROI, SignalObj, SignalROI if TYPE_CHECKING: - from plotpy.plot import BasePlot, PlotDialog + from plotpy.plot import BasePlot + from plotpy.tools.base import InteractiveTool - from cdl.obj import ImageObj, SignalObj - AnyObj = Union[SignalObj, ImageObj] +def plot_item_to_single_roi( + item: XRangeSelection | AnnotatedRectangle | AnnotatedCircle | AnnotatedPolygon, +) -> SegmentROI | CircularROI | RectangularROI | PolygonalROI: + """Factory function to create a single ROI object from plot item + + Args: + item: Plot item + + Returns: + Single ROI object + """ + if isinstance(item, XRangeSelection): + cls = SegmentROI + elif isinstance(item, AnnotatedRectangle): + cls = RectangularROI + elif isinstance(item, AnnotatedCircle): + cls = CircularROI + elif isinstance(item, AnnotatedPolygon): + cls = PolygonalROI + else: + raise TypeError(f"Unsupported ROI item type: {type(item)}") + return cls.from_plot_item(item) + + +ROI_EDITOR_TOOLBAR_ID = "roi_editor_toolbar" + + +def configure_roi_item_in_tool(shape, obj: dlo.SignalObj | dlo.ImageObj) -> None: + """Configure ROI item in tool""" + fmt = obj.get_metadata_option("format") + configure_roi_item(shape, fmt, lbl=True, editable=True, option=obj.PREFIX) + + +def tool_activate(tool: InteractiveTool) -> None: + """Tool activate""" + plot = tool.get_active_plot() + plot.select_some_items([]) # Deselect all items + tool.activate() + + +def tool_setup_shape(shape: TypeROIItem, obj: dlo.SignalObj | dlo.ImageObj) -> None: + """Tool setup shape""" + shape.setTitle("ROI") + configure_roi_item_in_tool(shape, obj) + + +class ROISegmentTool(HRangeTool): + """ROI segment tool""" + + TITLE = _("Add ROI") + ICON = "signal_roi.svg" + + def __init__(self, manager: PlotManager, obj: dlo.SignalObj) -> None: + super().__init__( + manager, switch_to_default_tool=True, toolbar_id=ROI_EDITOR_TOOLBAR_ID + ) + self.roi = SegmentROI([0, 1], False) + self.obj = obj + self.activate = tool_activate + + def create_shape(self) -> XRangeSelection: + """Create shape""" + shape = self.roi.to_plot_item(self.obj) + configure_roi_item_in_tool(shape, self.obj) + shape.selected = True + return shape + + +class ROIRectangleTool(RectangleTool): + """ROI rectangle tool""" + + TITLE = _("Add rectangular ROI") + ICON = "roi_new_rectangle.svg" + + def __init__(self, manager: PlotManager, obj: dlo.ImageObj) -> None: + super().__init__( + manager, + switch_to_default_tool=True, + toolbar_id=ROI_EDITOR_TOOLBAR_ID, + setup_shape_cb=tool_setup_shape, + ) + self.roi = RectangularROI([0, 0, 1, 1], True) + self.obj = obj + self.activate = tool_activate + + def create_shape(self) -> tuple[AnnotatedRectangle, int, int]: + """Create shape""" + item = self.roi.to_plot_item(self.obj) + return item, 0, 2 + + def setup_shape(self, shape: AnnotatedRectangle) -> None: + """Setup shape""" + tool_setup_shape(shape, self.obj) + + +class ROICircleTool(CircleTool): + """ROI circle tool""" + + TITLE = _("Add circular ROI") + ICON = "roi_new_circle.svg" + + def __init__(self, manager: PlotManager, obj: dlo.ImageObj) -> None: + super().__init__( + manager, + switch_to_default_tool=True, + toolbar_id=ROI_EDITOR_TOOLBAR_ID, + setup_shape_cb=tool_setup_shape, + ) + self.roi = CircularROI([0, 0, 1], True) + self.obj = obj + self.activate = tool_activate + + def create_shape(self) -> tuple[AnnotatedCircle, int, int]: + """Create shape""" + item = self.roi.to_plot_item(self.obj) + return item, 0, 1 + + def setup_shape(self, shape: AnnotatedCircle) -> None: + """Setup shape""" + tool_setup_shape(shape, self.obj) + + +class ROIPolygonTool(PolygonTool): + """ROI polygon tool""" + + TITLE = _("Add polygonal ROI") + ICON = "roi_new_polygon.svg" + + def __init__(self, manager: PlotManager, obj: dlo.ImageObj) -> None: + super().__init__( + manager, + switch_to_default_tool=True, + toolbar_id=ROI_EDITOR_TOOLBAR_ID, + setup_shape_cb=tool_setup_shape, + ) + self.roi = PolygonalROI([[0, 0], [1, 0], [1, 1], [0, 1]], True) + self.obj = obj + self.activate = tool_activate + + def create_shape(self) -> tuple[AnnotatedPolygon, int, int]: + """Create shape""" + return self.roi.to_plot_item(self.obj) + + def setup_shape(self, shape: AnnotatedPolygon) -> None: + """Setup shape""" + tool_setup_shape(shape, self.obj) + + +TypeROIEditor = TypeVar("TypeROIEditor", bound="BaseROIEditor") class BaseROIEditorMeta(type(QW.QWidget), abc.ABCMeta): """Mixed metaclass to avoid conflicts""" -class BaseROIEditor(QW.QWidget, metaclass=BaseROIEditorMeta): +class BaseROIEditor( + QW.QWidget, + Generic[TypeObj, TypeROI, TypePlotItem, TypeROIItem], + metaclass=BaseROIEditorMeta, +): """ROI Editor""" ICON_NAME = None OBJ_NAME = None + ROI_ITEM_TYPES = () def __init__( self, parent: PlotDialog, - obj: AnyObj, + obj: TypeObj, extract: bool, - singleobj: bool | None = None, + item: TypePlotItem | None = None, ) -> None: super().__init__(parent) self.plot_dialog = parent parent.accepted.connect(self.dialog_accepted) self.plot = parent.get_plot() + self.toolbar = QW.QToolBar(self) self.obj = obj self.extract = extract self.__modified: bool | None = None - self.__data = ROIDataParam.create(singleobj=singleobj) + roi = obj.roi + if roi is None: + roi = self.get_obj_roi_class()() + self.__roi: TypeROI = roi - self.fmt = obj.get_metadata_option("format") - self.roi_items = list(obj.iterate_roi_items(self.fmt, True)) + fmt = obj.get_metadata_option("format") + self.roi_items: list[TypeROIItem] = list( + self.__roi.iterate_roi_items(obj, fmt, True, True) + ) + self.add_tools_to_plot_dialog() + item = obj.make_item() if item is None else item + item.set_selectable(False) + item.set_readonly(True) + self.plot.add_item(item) for roi_item in self.roi_items: self.plot.add_item(roi_item) self.plot.set_active_item(roi_item) - self.remove_all_btn: QW.QToolButton | None = None + self.remove_all_action: QW.QAction | None = None self.singleobj_btn: QW.QToolButton | None = None + self.setup_widget() # force update of ROI titles and remove_all_btn state @@ -97,7 +282,18 @@ def __init__( # when at least one ROI is defined, # whereas in non-extract mode (when editing ROIs) the OK button is by default # disabled (until ROI data is modified) - self.modified = extract + if extract: + self.modified = len(self.roi_items) > 0 + else: + self.modified = False + + @abc.abstractmethod + def get_obj_roi_class(self) -> type[TypeROI]: + """Get object ROI class""" + + @abc.abstractmethod + def add_tools_to_plot_dialog(self) -> None: + """Add tools to plot dialog""" @property def modified(self) -> bool: @@ -105,7 +301,7 @@ def modified(self) -> bool: return self.__modified @modified.setter - def modified(self, value: bool): + def modified(self, value: bool) -> None: """Set dialog modified state""" self.__modified = value if self.extract: @@ -113,18 +309,17 @@ def modified(self, value: bool): value = value and len(self.roi_items) > 0 self.plot_dialog.button_box.button(QW.QDialogButtonBox.Ok).setEnabled(value) - def dialog_accepted(self): + def dialog_accepted(self) -> None: """Parent dialog was accepted: updating ROI Editor data""" - coords = [] + self.__roi.empty() for roi_item in self.roi_items: - coords.append(list(self.get_roi_item_coords(roi_item))) - self.__data.roidata = self.obj.roi_coords_to_indexes(coords) + self.__roi.add_roi(plot_item_to_single_roi(roi_item)) if self.singleobj_btn is not None: singleobj = self.singleobj_btn.isChecked() - self.__data.singleobj = singleobj + self.__roi.singleobj = singleobj Conf.proc.extract_roi_singleobj.set(singleobj) - def get_roieditor_results(self) -> tuple[ROIDataParam, bool]: + def get_roieditor_results(self) -> tuple[TypeROI, bool]: """Get ROI editor results Returns: @@ -132,59 +327,44 @@ def get_roieditor_results(self) -> tuple[ROIDataParam, bool]: ROI modified state is True if the ROI data has been modified within the dialog box. """ - return self.__data, self.modified + return self.__roi, self.modified - def build_roi_buttons(self) -> list[QW.QToolButton | QW.QFrame]: - """Build ROI buttons""" - self.remove_all_btn = create_toolbutton( + def create_actions(self) -> list[QW.QAction]: + """Create actions""" + self.remove_all_action = create_action( self, - get_icon("roi_delete.svg"), _("Remove all ROIs"), - self.remove_all_rois, - autoraise=True, + icon=get_icon("roi_delete.svg"), + triggered=self.remove_all_rois, ) - # Return a vertical bar to separate the buttons in the layout - vert_sep = QW.QFrame(self) - vert_sep.setFrameShape(QW.QFrame.VLine) - vert_sep.setStyleSheet("color: gray") - return [vert_sep, self.remove_all_btn] + return [None, self.remove_all_action] - def setup_widget(self): + def setup_widget(self) -> None: """Setup ROI editor widget""" layout = QW.QHBoxLayout() - for btn in self.build_roi_buttons(): - if isinstance(btn, QW.QToolButton): - btn.setToolButtonStyle(QC.Qt.ToolButtonTextUnderIcon) - layout.addWidget(btn) + self.toolbar.setToolButtonStyle(QC.Qt.ToolButtonTextUnderIcon) + add_actions(self.toolbar, self.create_actions()) + layout.addWidget(self.toolbar) if self.extract: self.singleobj_btn = QW.QCheckBox( - _("Extract all regions of interest into a single %s object") - % self.OBJ_NAME, + _("Extract all ROIs into a single %s object") % self.OBJ_NAME, self, ) layout.addWidget(self.singleobj_btn) - singleobj = self.__data.singleobj - if singleobj is None: - singleobj = Conf.proc.extract_roi_singleobj.get() - self.singleobj_btn.setChecked(singleobj) + self.singleobj_btn.setChecked(self.__roi.singleobj) layout.addStretch() self.setLayout(layout) - def add_roi_item(self, roi_item): - """Add ROI item to plot and refresh titles""" - self.plot.unselect_all() - self.roi_items.append(roi_item) - self.update_roi_titles() - self.modified = True - self.plot.add_item(roi_item) - self.plot.set_active_item(roi_item) - - def remove_all_rois(self): + def remove_all_rois(self) -> None: """Remove all ROIs""" - if QW.QMessageBox.question( - self, - _("Remove all ROIs"), - _("Are you sure you want to remove all ROIs?"), + if ( + execenv.unattended + or QW.QMessageBox.question( + self, + _("Remove all ROIs"), + _("Are you sure you want to remove all ROIs?"), + ) + == QW.QMessageBox.Yes ): self.plot.del_items(self.roi_items) @@ -192,36 +372,43 @@ def remove_all_rois(self): def update_roi_titles(self) -> None: """Update ROI annotation titles""" + def update_roi_items(self) -> None: + """Update ROI items""" + old_nb_items = len(self.roi_items) + self.roi_items = [ + item + for item in self.plot.get_items() + if isinstance(item, self.ROI_ITEM_TYPES) + ] + self.plot.select_some_items([]) + self.update_roi_titles() + if old_nb_items != len(self.roi_items): + self.modified = True + def items_changed(self, _plot: BasePlot) -> None: """Items have changed""" - self.update_roi_titles() - self.remove_all_btn.setEnabled(len(self.roi_items) > 0) + self.update_roi_items() + self.remove_all_action.setEnabled(len(self.roi_items) > 0) - def item_removed(self, item): + def item_removed(self, item) -> None: """Item was removed. Since all items are read-only except ROIs... this must be an ROI.""" assert item in self.roi_items - self.roi_items.remove(item) + self.update_roi_items() self.modified = True - self.update_roi_titles() - def item_moved(self): + def item_moved(self) -> None: """ROI plot item has just been moved""" self.modified = True - @staticmethod - @abc.abstractmethod - def get_roi_item_coords(roi_item): - """Return ROI item coords""" - class ROIRangeInfo(ObjectInfo): """ObjectInfo for ROI selection""" - def __init__(self, roi_items): + def __init__(self, roi_items) -> None: self.roi_items = roi_items - def get_text(self): + def get_text(self) -> str: textlist = [] for index, roi_item in enumerate(self.roi_items): x0, x1 = roi_item.get_range() @@ -229,20 +416,24 @@ def get_text(self): return "
".join(textlist) -class SignalROIEditor(BaseROIEditor): +class SignalROIEditor(BaseROIEditor[SignalObj, SignalROI, CurveItem, XRangeSelection]): """Signal ROI Editor""" ICON_NAME = "signal_roi.svg" OBJ_NAME = _("signal") + ROI_ITEM_TYPES = (XRangeSelection,) - def build_roi_buttons(self) -> list[QW.QToolButton | QW.QFrame]: - """Build ROI buttons""" - add_btn = create_toolbutton( - self, get_icon(self.ICON_NAME), _("Add ROI"), self.add_roi, autoraise=True - ) - return [add_btn] + super().build_roi_buttons() + def get_obj_roi_class(self) -> type[SignalROI]: + """Get object ROI class""" + return SignalROI + + def add_tools_to_plot_dialog(self) -> None: + """Add tools to plot dialog""" + mgr = self.plot_dialog.get_manager() + mgr.add_toolbar(self.toolbar, ROI_EDITOR_TOOLBAR_ID) + mgr.add_tool(ROISegmentTool, self.obj) - def setup_widget(self): + def setup_widget(self) -> None: """Setup ROI editor widget""" super().setup_widget() info = ROIRangeInfo(self.roi_items) @@ -250,68 +441,49 @@ def setup_widget(self): self.plot.add_item(info_label) self.info_label = info_label - def add_roi(self): - """Simply add an ROI""" - roi_item = self.obj.new_roi_item(self.fmt, True, editable=True) - self.add_roi_item(roi_item) - def update_roi_titles(self): """Update ROI annotation titles""" super().update_roi_titles() self.info_label.update_text() - @staticmethod - def get_roi_item_coords(roi_item): - """Return ROI item coords""" - return roi_item.get_range() - -class ImageROIEditor(BaseROIEditor): +class ImageROIEditor( + BaseROIEditor[ + ImageObj, + ImageROI, + MaskedImageItem, + # `Union` is mandatory here for Python 3.9-3.10 compatibility: + Union[AnnotatedPolygon, AnnotatedRectangle, AnnotatedCircle], + ] +): """Image ROI Editor""" ICON_NAME = "image_roi.svg" OBJ_NAME = _("image") + ROI_ITEM_TYPES = (AnnotatedRectangle, AnnotatedCircle, AnnotatedPolygon) - def build_roi_buttons(self) -> list[QW.QToolButton | QW.QFrame]: - """Build ROI buttons""" - rect_btn = create_toolbutton( - self, - get_icon("roi_new_rectangle.svg"), - _("Rectangular ROI"), - lambda: self.add_roi(RoiDataGeometries.RECTANGLE), - autoraise=True, - ) - circ_btn = create_toolbutton( - self, - get_icon("roi_new_circle.svg"), - _("Circular ROI"), - lambda: self.add_roi(RoiDataGeometries.CIRCLE), - autoraise=True, - ) - return [rect_btn, circ_btn] + super().build_roi_buttons() + def get_obj_roi_class(self) -> type[ImageROI]: + """Get object ROI class""" + return ImageROI + + def add_tools_to_plot_dialog(self) -> None: + """Add tools to plot dialog""" + mgr = self.plot_dialog.get_manager() + mgr.add_toolbar(self.toolbar, ROI_EDITOR_TOOLBAR_ID) + mgr.add_tool(ROIRectangleTool, self.obj) + mgr.add_tool(ROICircleTool, self.obj) + mgr.add_tool(ROIPolygonTool, self.obj) - def setup_widget(self): + def setup_widget(self) -> None: """Setup ROI editor widget""" super().setup_widget() - item = self.plot.get_items(item_type=IImageItemType)[0] + item: MaskedImageItem = self.plot.get_items(item_type=IImageItemType)[0] item.set_mask_visible(False) - def add_roi(self, geometry: RoiDataGeometries = RoiDataGeometries.RECTANGLE): - """Add new ROI""" - item = self.obj.new_roi_item(self.fmt, True, editable=True, geometry=geometry) - self.add_roi_item(item) - - def update_roi_titles(self): + def update_roi_titles(self) -> None: """Update ROI annotation titles""" super().update_roi_titles() for index, roi_item in enumerate(self.roi_items): + roi_item: AnnotatedRectangle | AnnotatedCircle | AnnotatedPolygon roi_item.annotationparam.title = f"ROI{index:02d}" roi_item.annotationparam.update_item(roi_item) - - @staticmethod - def get_roi_item_coords(roi_item): - """Return ROI item coords""" - x0, y0, x1, y1 = roi_item.get_rect() - if isinstance(roi_item, AnnotatedCircle): - y0 = y1 = 0.5 * (y0 + y1) - return x0, y0, x1, y1 diff --git a/cdl/core/gui/settings.py b/cdl/core/gui/settings.py index 2441802e..6d0edea6 100644 --- a/cdl/core/gui/settings.py +++ b/cdl/core/gui/settings.py @@ -28,7 +28,7 @@ class MainSettings(gds.DataSet): g0 = gds.BeginGroup(_("Settings for main window and general features")) color_mode = gds.ChoiceItem( _("Color mode"), - zip(Conf.main.color_mode.choices, Conf.main.color_mode.choices), + zip(Conf.main.color_mode.values, Conf.main.color_mode.values), help=_("Color mode for the application"), ) process_isolation_enabled = gds.BoolItem( @@ -95,6 +95,19 @@ class ProcSettings(gds.DataSet): """DataLab processing settings""" g0 = gds.BeginGroup(_("Settings for computations")) + operation_mode = gds.ChoiceItem( + _("Operation mode"), + zip(Conf.proc.operation_mode.values, Conf.proc.operation_mode.values), + help=_( + "Operation mode for computations taking N inputs:" + "" + "
Computations taking N inputs are the ones where:" + "" + ) + % ("→", "→"), + ) fft_shift_enabled = gds.BoolItem("", _("FFT shift")) extract_roi_singleobj = gds.BoolItem("", _("Extract ROI in single object")) ignore_warnings = gds.BoolItem("", _("Ignore warnings")) diff --git a/cdl/core/model/base.py b/cdl/core/model/base.py index 28aa0882..10679c95 100644 --- a/cdl/core/model/base.py +++ b/cdl/core/model/base.py @@ -12,9 +12,9 @@ import enum import json import sys -from collections.abc import Callable, Iterable +from collections.abc import Callable, Generator, Iterable from copy import deepcopy -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Generic, Iterator, Literal, Type, TypeVar import guidata.dataset as gds import numpy as np @@ -22,6 +22,7 @@ from guidata.configtools import get_font from guidata.dataset import update_dataset from guidata.io import JSONReader, JSONWriter +from numpy import ma from plotpy.builder import make from plotpy.io import load_items, save_items from plotpy.items import ( @@ -42,10 +43,12 @@ AbstractShape, AnnotatedCircle, AnnotatedEllipse, + AnnotatedPolygon, AnnotatedRectangle, CurveItem, Marker, MaskedImageItem, + XRangeSelection, ) from plotpy.styles import AnnotationParam, ShapeParam @@ -181,7 +184,7 @@ def config_annotated_shape( lbl: bool, section: str | None = None, option: str | None = None, - cmp: bool | None = None, + show_computations: bool | None = None, ): """Configurate annotated shape. @@ -191,13 +194,13 @@ def config_annotated_shape( lbl: Show label section: Shape style section (e.g. "plot") option: Shape style option (e.g. "shape/drag") - cmp: Show computations + show_computations: Show computations """ param: AnnotationParam = item.annotationparam param.format = fmt param.show_label = lbl - if cmp is not None: - param.show_computations = cmp + if show_computations is not None: + param.show_computations = show_computations if isinstance(item, AnnotatedSegment): item.label.labelparam.anchor = "T" @@ -850,21 +853,17 @@ def label(x, y): ) -def make_roi_item( - func, - coords: list, - title: str, +def configure_roi_item( + item, fmt: str, lbl: bool, editable: bool, option: Literal["s", "i"], ): - """Make ROI item shape. + """Configure ROI plot item. Args: - func: function to create ROI item - coords: coordinates - title: title + item: plot item fmt: numeric format (e.g. "%.3f") lbl: if True, show shape labels editable: if True, make shape editable @@ -873,11 +872,12 @@ def make_roi_item( Returns: Plot item """ - item = func(*coords, title) option += "/" + ("editable" if editable else "readonly") if not editable: if isinstance(item, AnnotatedShape): - config_annotated_shape(item, fmt, lbl, "roi", option, cmp=editable) + config_annotated_shape( + item, fmt, lbl, "roi", option, show_computations=editable + ) item.set_movable(False) item.set_resizable(False) item.set_readonly(True) @@ -920,11 +920,371 @@ def json_to_items(json_str: str | None) -> list: return items +TypeSingleROI = TypeVar("TypeSingleROI", bound="BaseSingleROI") +TypeROI = TypeVar("TypeROI", bound="BaseROI") +TypeROIParam = TypeVar("TypeROIParam", bound="BaseROIParam") +TypeObj = TypeVar("TypeObj", bound="BaseObj") +TypePlotItem = TypeVar("TypePlotItem", bound="CurveItem | MaskedImageItem") +TypeROIItem = TypeVar( + "TypeROIItem", + bound="XRangeSelection | AnnotatedPolygon | AnnotatedRectangle | AnnotatedCircle", +) + + +class BaseROIParamMeta(abc.ABCMeta, gds.DataSetMeta): + """Mixed metaclass to avoid conflicts""" + + +class BaseROIParam( + gds.DataSet, Generic[TypeObj, TypeSingleROI], metaclass=BaseROIParamMeta +): + """Base class for ROI parameters""" + + @abc.abstractmethod + def to_single_roi(self, obj: TypeObj, title: str = "") -> TypeSingleROI: + """Convert parameters to single ROI + + Args: + obj: object (signal/image) + title: ROI title + + Returns: + Single ROI + """ + + +class BaseSingleROI(Generic[TypeObj, TypeROIParam, TypeROIItem], abc.ABC): + """Base class for single ROI + + Args: + coords: ROI edge (physical coordinates for signal) + indices: if True, coords are indices (pixels) instead of physical coordinates + title: ROI title + """ + + def __init__(self, coords: np.ndarray, indices: bool, title: str = "ROI") -> None: + self.coords = np.array(coords, int if indices else float) + self.indices = indices + self.title = title + self.check_coords() + + def __eq__(self, other: BaseSingleROI) -> bool: + """Test equality with another single ROI""" + return ( + np.array_equal(self.coords, other.coords) and self.indices == other.indices + ) + + def get_physical_coords(self, obj: TypeObj) -> np.ndarray: + """Return physical coords + + Args: + obj: object (signal/image) + + Returns: + Physical coords + """ + if self.indices: + return obj.indices_to_physical(self.coords) + return self.coords + + def get_indices_coords(self, obj: TypeObj) -> np.ndarray: + """Return indices coords + + Args: + obj: object (signal/image) + + Returns: + Indices coords + """ + if self.indices: + return self.coords + return obj.physical_to_indices(self.coords) + + def set_indices_coords(self, obj: TypeObj, coords: np.ndarray) -> None: + """Set indices coords + + Args: + obj: object (signal/image) + coords: indices coords + """ + if self.indices: + self.coords = coords + else: + self.coords = obj.indices_to_physical(coords) + + @abc.abstractmethod + def check_coords(self) -> None: + """Check if coords are valid + + Raises: + ValueError: invalid coords + """ + + @abc.abstractmethod + def to_mask(self, obj: TypeObj) -> np.ndarray: + """Create mask from ROI + + Args: + obj: signal or image object + + Returns: + Mask (boolean array where True values are inside the ROI) + """ + + @abc.abstractmethod + def to_param(self, obj: TypeObj, title: str | None = None) -> TypeROIParam: + """Convert ROI to parameters + + Args: + obj: object (signal/image), for physical-indices coordinates conversion + title: ROI title + """ + + @abc.abstractmethod + def to_plot_item(self, obj: TypeObj, title: str | None = None) -> TypeROIItem: + """Make ROI plot item from ROI. + + Args: + obj: object (signal/image), for physical-indices coordinates conversion + title: ROI title + + Returns: + Plot item + """ + + @classmethod + @abc.abstractmethod + def from_plot_item(cls: Type[TypeSingleROI], item: AbstractShape) -> TypeSingleROI: + """Create single ROI from plot item + + Args: + item: plot item + + Returns: + Single ROI + """ + + def to_dict(self) -> dict: + """Convert ROI to dictionary + + Returns: + Dictionary + """ + return { + "coords": self.coords, + "indices": self.indices, + "title": self.title, + "type": type(self).__name__, + } + + @classmethod + def from_dict(cls: Type[TypeSingleROI], dictdata: dict) -> TypeSingleROI: + """Convert dictionary to ROI + + Args: + dictdata: dictionary + + Returns: + ROI + """ + return cls(dictdata["coords"], dictdata["indices"], dictdata["title"]) + + +class BaseROI(Generic[TypeObj, TypeSingleROI, TypeROIParam, TypeROIItem], abc.ABC): + """Abstract base class for ROIs (Regions of Interest) + + Args: + singleobj: if True, when extracting data defined by ROIs, only one object + is created (default to True). If False, one object is created per single ROI. + If None, the value is get from the user configuration + inverse: if True, ROI is outside the region of interest + """ + + PREFIX = "" # This is overriden in children classes + + def __init__(self, singleobj: bool | None = None, inverse: bool = False) -> None: + self.single_rois: list[TypeSingleROI] = [] + if singleobj is None: + singleobj = Conf.proc.extract_roi_singleobj.get() + self.singleobj = singleobj + self.inverse = inverse + + @staticmethod + @abc.abstractmethod + def get_compatible_single_roi_classes() -> list[Type[BaseSingleROI]]: + """Return compatible single ROI classes""" + + def __len__(self) -> int: + """Return number of ROIs""" + return len(self.single_rois) + + def __iter__(self) -> Iterator[TypeSingleROI]: + """Iterate over single ROIs""" + return iter(self.single_rois) + + def get_single_roi(self, index: int) -> TypeSingleROI: + """Return single ROI at index + + Args: + index: ROI index + """ + return self.single_rois[index] + + def is_empty(self) -> bool: + """Return True if no ROI is defined""" + return len(self) == 0 + + @classmethod + def create(cls: Type[BaseROI], single_roi: TypeSingleROI) -> TypeROI: + """Create Regions of Interest object from a single ROI. + + Args: + single_roi: single ROI + + Returns: + Regions of Interest object + """ + roi = cls() + roi.add_roi(single_roi) + return roi + + def copy(self) -> TypeROI: + """Return a copy of ROIs""" + return deepcopy(self) + + def empty(self) -> None: + """Empty ROIs""" + self.single_rois.clear() + + def add_roi(self, roi: TypeSingleROI | TypeROI) -> None: + """Add ROI. + + Args: + roi: ROI + + Raises: + TypeError: if roi type is not supported (not a single ROI or a ROI) + ValueError: if `singleobj` or `inverse` values are incompatible + """ + if isinstance(roi, BaseSingleROI): + self.single_rois.append(roi) + elif isinstance(roi, BaseROI): + self.single_rois.extend(roi.single_rois) + if roi.singleobj != self.singleobj: + raise ValueError("Incompatible `singleobj` values") + if roi.inverse != self.inverse: + raise ValueError("Incompatible `inverse` values") + else: + raise TypeError(f"Unsupported ROI type: {type(roi)}") + + @abc.abstractmethod + def to_mask(self, obj: TypeObj) -> np.ndarray[bool]: + """Create mask from ROI + + Args: + obj: signal or image object + + Returns: + Mask (boolean array where True values are inside the ROI) + """ + + def to_params( + self, obj: TypeObj, title: str | None = None + ) -> TypeROIParam | gds.DataSetGroup: + """Convert ROIs to group of parameters + + Args: + obj: object (signal/image), for physical to pixel conversion + title: group title + """ + return gds.DataSetGroup( + [iroi.to_param(obj, f"ROI{idx:02d}") for idx, iroi in enumerate(self)], + title=_("Regions of interest") if title is None else title, + ) + + @classmethod + def from_params( + cls: Type[BaseROI], obj: TypeObj, params: TypeROIParam | gds.DataSetGroup + ) -> TypeROI: + """Create ROIs from parameters + + Args: + obj: object (signal/image) + params: ROI parameters + + Returns: + ROIs + """ + roi = cls() + if isinstance(params, gds.DataSetGroup): + for param in params.datasets: + param: TypeROIParam + roi.add_roi(param.to_single_roi(obj)) + else: + roi.add_roi(params.to_single_roi(obj)) + return roi + + def iterate_roi_items( + self, obj: TypeObj, fmt: str, lbl: bool, editable: bool = True + ) -> Iterator[TypeROIItem]: + """Iterate over ROI plot items associated to each single ROI composing + the object. + + Args: + obj: object (signal/image), for physical-indices coordinates conversion + fmt: format string + lbl: if True, add label + editable: if True, ROI is editable + + Yields: + Plot item + """ + for index, single_roi in enumerate(self): + title = "ROI" if index is None else f"ROI{index:02d}" + roi_item = single_roi.to_plot_item(obj, title) + yield configure_roi_item(roi_item, fmt, lbl, editable, option=self.PREFIX) + + def to_dict(self) -> dict: + """Convert ROIs to dictionary + + Returns: + Dictionary + """ + return { + "singleobj": self.singleobj, + "inverse": self.inverse, + "single_rois": [roi.to_dict() for roi in self.single_rois], + } + + @classmethod + def from_dict(cls: Type[TypeROI], dictdata: dict) -> TypeROI: + """Convert dictionary to ROIs + + Args: + dictdata: dictionary + + Returns: + ROIs + """ + instance = cls() + instance.singleobj = dictdata["singleobj"] + instance.inverse = dictdata["inverse"] + instance.single_rois = [] + for single_roi in dictdata["single_rois"]: + for single_roi_class in instance.get_compatible_single_roi_classes(): + if single_roi["type"] == single_roi_class.__name__: + instance.single_rois.append(single_roi_class.from_dict(single_roi)) + break + else: + raise ValueError(f"Unsupported single ROI type: {single_roi['type']}") + return instance + + class BaseObjMeta(abc.ABCMeta, gds.DataSetMeta): """Mixed metaclass to avoid conflicts""" -class BaseObj(metaclass=BaseObjMeta): +class BaseObj(Generic[TypeROI, TypePlotItem], metaclass=BaseObjMeta): """Object (signal/image) interface""" PREFIX = "" # This is overriden in children classes @@ -941,15 +1301,21 @@ def __init__(self): self.__onb = 0 self.__roi_changed: bool | None = None self.__metadata_options: dict[str, Any] | None = None + self._maskdata_cache: np.ndarray | None = None self.reset_metadata_to_defaults() + @staticmethod + @abc.abstractmethod + def get_roi_class() -> Type[TypeROI]: + """Return ROI class""" + @property def number(self) -> int: """Return object number (used for short ID)""" return self.__onb @number.setter - def number(self, onb: int): + def number(self, onb: int) -> None: """Set object number (used for short ID). Args: @@ -958,7 +1324,7 @@ def number(self, onb: int): self.__onb = onb @property - def short_id(self): + def short_id(self) -> str: """Short object ID""" return f"{self.PREFIX}{self.__onb:03d}" @@ -990,10 +1356,10 @@ def check_data(self): if self.data.dtype not in self.VALID_DTYPES: raise TypeError(f"Unsupported data type: {self.data.dtype}") - def iterate_roi_indexes(self): - """Iterate over object ROI indexes ([0] if there is no ROI)""" + def iterate_roi_indices(self) -> Generator[int | None, None, None]: + """Iterate over object ROI indices (if there is no ROI, yield None)""" if self.roi is None: - yield 0 + yield None else: yield from range(len(self.roi)) @@ -1011,7 +1377,7 @@ def get_data(self, roi_index: int | None = None) -> np.ndarray: """ @abc.abstractmethod - def copy(self, title: str | None = None, dtype: np.dtype | None = None) -> BaseObj: + def copy(self, title: str | None = None, dtype: np.dtype | None = None) -> TypeObj: """Copy object. Args: @@ -1031,7 +1397,7 @@ def set_data_type(self, dtype): """ @abc.abstractmethod - def make_item(self, update_from=None): + def make_item(self, update_from: TypePlotItem | None = None) -> TypePlotItem: """Make plot item from data. Args: @@ -1042,7 +1408,7 @@ def make_item(self, update_from=None): """ @abc.abstractmethod - def update_item(self, item, data_changed: bool = True) -> None: + def update_item(self, item: TypePlotItem, data_changed: bool = True) -> None: """Update plot item from data. Args: @@ -1051,59 +1417,25 @@ def update_item(self, item, data_changed: bool = True) -> None: """ @abc.abstractmethod - def roi_coords_to_indexes(self, coords: list) -> np.ndarray: - """Convert ROI coordinates to indexes. + def physical_to_indices(self, coords: list) -> np.ndarray: + """Convert coordinates from physical (real world) to (array) indices Args: coords: coordinates Returns: - Indexes + Indices """ @abc.abstractmethod - def get_roi_param(self, title, *defaults: int) -> gds.DataSet: - """Return ROI parameters dataset. - - Args: - title: title - *defaults: default values - """ - - def roidata_to_params( - self, roidata: np.ndarray | list[list[int]] - ) -> gds.DataSetGroup: - """Convert ROI array data to ROI dataset group. - - Args: - roidata: ROI array data (array or list of lists, floating point values - are accepted and will be converted to integers) - - Returns: - ROI dataset group - """ - roi_params = [] - try: - data = np.array(roidata, int) - except (ValueError, TypeError) as exc: - raise TypeError(f"Invalid ROI data: {roidata}") from exc - if len(data.shape) != 2 and data.size != 0: - raise ValueError(f"Invalid ROI data shape: {data.shape}") - for index, parameters in enumerate(data): - roi_param = self.get_roi_param(f"ROI{index:02d}", *parameters) - roi_params.append(roi_param) - group = gds.DataSetGroup(roi_params, title=_("Regions of interest")) - return group - - @abc.abstractmethod - def params_to_roidata(self, params: gds.DataSetGroup) -> np.ndarray: - """Convert ROI dataset group to ROI array data. + def indices_to_physical(self, indices: np.ndarray) -> list: + """Convert coordinates from (array) indices to physical (real world) Args: - params: ROI dataset group + indices: indices Returns: - ROI array data + Coordinates """ def roi_has_changed(self) -> bool: @@ -1124,30 +1456,65 @@ def roi_has_changed(self) -> bool: return returned_value @property - def roi(self) -> np.ndarray | None: - """Return object regions of interest array (one ROI per line). + def roi(self) -> TypeROI | None: + """Return object regions of interest object. Returns: - Regions of interest array + Regions of interest object """ roidata = self.metadata.get(ROI_KEY) - assert roidata is None or isinstance(roidata, np.ndarray) - return roidata + if roidata is None: + return None + if not isinstance(roidata, dict): + # Old or unsupported format: remove it + self.metadata.pop(ROI_KEY) + return None + return self.get_roi_class().from_dict(roidata) @roi.setter - def roi(self, roidata: np.ndarray | None) -> None: - """Set object regions of interest array, using a list or ROI dataset params. + def roi(self, roi: TypeROI | None) -> None: + """Set object regions of interest. Args: - roidata: regions of interest array + roi: regions of interest object """ - if roidata is None: + if roi is None: if ROI_KEY in self.metadata: self.metadata.pop(ROI_KEY) else: - self.metadata[ROI_KEY] = np.array(roidata, int) + self.metadata[ROI_KEY] = roi.to_dict() self.__roi_changed = True + @property + def maskdata(self) -> np.ndarray: + """Return masked data (areas outside defined regions of interest) + + Returns: + Masked data + """ + roi_changed = self.roi_has_changed() + if self.roi is None: + if roi_changed: + self._maskdata_cache = None + elif roi_changed or self._maskdata_cache is None: + self._maskdata_cache = self.roi.to_mask(self) + return self._maskdata_cache + + def get_masked_view(self) -> ma.MaskedArray: + """Return masked view for data + + Returns: + Masked view + """ + self.data: np.ndarray + view = self.data.view(ma.MaskedArray) + view.mask = self.maskdata + return view + + def invalidate_maskdata_cache(self) -> None: + """Invalidate mask data cache: force to rebuild it""" + self._maskdata_cache = None + def iterate_resultshapes(self) -> Iterable[ResultShape]: """Iterate over object result shapes. @@ -1176,7 +1543,7 @@ def delete_results(self) -> None: ): self.metadata.pop(key) - def update_resultshapes_from(self, other: BaseObj) -> None: + def update_resultshapes_from(self, other: TypeObj) -> None: """Update geometric shape from another object (merge metadata). Args: @@ -1231,19 +1598,6 @@ def transform(coords: np.ndarray): if items: self.annotations = items_to_json(items) - @abc.abstractmethod - def iterate_roi_items(self, fmt: str, lbl: bool, editable: bool = True): - """Make plot item representing a Region of Interest. - - Args: - fmt: format string - lbl: if True, add label - editable: if True, ROI is editable - - Yields: - Plot item - """ - def __set_annotations(self, annotations: str | None) -> None: """Set object annotations (JSON string describing annotation plot items) @@ -1307,7 +1661,11 @@ def iterate_shape_items(self, editable: bool = False): lbl = self.get_metadata_option("showlabel") for key, value in self.metadata.items(): if key == ROI_KEY: - yield from self.iterate_roi_items(fmt=fmt, lbl=lbl, editable=False) + roi = self.roi + if roi is not None: + yield from roi.iterate_roi_items( + self, fmt=fmt, lbl=lbl, editable=False + ) elif ResultShape.match(key, value): mshape: ResultShape = ResultShape.from_metadata_entry(key, value) yield from mshape.iterate_plot_items(fmt, lbl, self.PREFIX) @@ -1410,7 +1768,7 @@ def update_metadata_view_settings(self) -> None: """Update metadata view settings from Conf.view""" self.metadata.update(self.__get_def_dict()) - def update_plot_item_parameters(self, item: CurveItem | MaskedImageItem) -> None: + def update_plot_item_parameters(self, item: TypePlotItem) -> None: """Update plot item parameters from object data/metadata Takes into account a subset of plot item parameters. Those parameters may @@ -1429,7 +1787,7 @@ def update_plot_item_parameters(self, item: CurveItem | MaskedImageItem) -> None if item.selected: item.select() - def update_metadata_from_plot_item(self, item: CurveItem | MaskedImageItem) -> None: + def update_metadata_from_plot_item(self, item: TypePlotItem) -> None: """Update metadata from plot item. Takes into account a subset of plot item parameters. Those parameters may diff --git a/cdl/core/model/image.py b/cdl/core/model/image.py index fde124aa..8ad9556f 100644 --- a/cdl/core/model/image.py +++ b/cdl/core/model/image.py @@ -11,19 +11,24 @@ from __future__ import annotations +import abc import enum import re -from collections.abc import ByteString, Iterator, Mapping, Sequence -from typing import TYPE_CHECKING, Any +from collections.abc import ByteString, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Generic, Literal, Type, Union from uuid import uuid4 import guidata.dataset as gds import numpy as np from guidata.configtools import get_icon from guidata.dataset import update_dataset -from numpy import ma from plotpy.builder import make -from plotpy.items import AnnotatedCircle, AnnotatedRectangle, MaskedImageItem +from plotpy.items import ( + AnnotatedCircle, + AnnotatedPolygon, + AnnotatedRectangle, + MaskedImageItem, +) from skimage import draw from cdl.algorithms.datatypes import clip_astype @@ -35,43 +40,6 @@ from qtpy import QtWidgets as QW -def make_roi_rectangle( - x0: int, y0: int, x1: int, y1: int, title: str -) -> AnnotatedRectangle: - """Make and return the annnotated rectangle associated to ROI - - Args: - x0: top left corner X coordinate - y0: top left corner Y coordinate - x1: bottom right corner X coordinate - y1: bottom right corner Y coordinate - title: title - """ - roi_item: AnnotatedRectangle = make.annotated_rectangle(x0, y0, x1, y1, title) - param = roi_item.label.labelparam - param.anchor = "BL" - param.xc, param.yc = 5, -5 - param.update_item(roi_item.label) - return roi_item - - -def make_roi_circle(x0: int, y0: int, x1: int, y1: int, title: str) -> AnnotatedCircle: - """Make and return the annnotated circle associated to ROI - - Args: - x0: top left corner X coordinate - y0: top left corner Y coordinate - x1: bottom right corner X coordinate - y1: bottom right corner Y coordinate - title: title - """ - item = AnnotatedCircle(x0, y0, x1, y1) - item.annotationparam.title = title - item.annotationparam.update_item(item) - item.set_style("plot", "shape/drag") - return item - - def to_builtin(obj) -> str | int | float | list | dict | np.ndarray | None: """Convert an object implementing a numeric value or collection into the corresponding builtin/NumPy type. @@ -92,196 +60,758 @@ def to_builtin(obj) -> str | int | float | list | dict | np.ndarray | None: return None -class RoiDataGeometries(base.Choices): - """ROI data geometry types""" +class ROI2DParam(base.BaseROIParam["ImageObj", "BaseSingleImageROI"]): + """Image ROI parameters""" + + # Note: the ROI coordinates are expressed in pixel coordinates (integers) + # => That is the only way to handle ROI parametrization for image objects. + # Otherwise, we would have to ask the user to systematically provide the + # physical coordinates: that would be cumbersome and error-prone. + + _geometry_prop = gds.GetAttrProp("geometry") + _rfp = gds.FuncProp(_geometry_prop, lambda x: x != "rectangle") + _cfp = gds.FuncProp(_geometry_prop, lambda x: x != "circle") + _pfp = gds.FuncProp(_geometry_prop, lambda x: x != "polygon") + + # Do not declare it as a static method: not supported by Python 3.9 + def _lbl(name: str, index: int): # pylint: disable=no-self-argument + """Returns nameindex""" + return f"{name}{index}" + + _ut = "pixels" + + geometries = ("rectangle", "circle", "polygon") + geometry = gds.ChoiceItem( + _("Geometry"), list(zip(geometries, geometries)), default="rectangle" + ).set_prop("display", store=_geometry_prop, hide=True) + + # Parameters for rectangular ROI geometry: + _tlcorner = gds.BeginGroup(_("Top left corner")).set_prop("display", hide=_rfp) + x0 = gds.IntItem(_lbl("X", 0), unit=_ut).set_prop("display", hide=_rfp) + y0 = gds.IntItem(_lbl("Y", 0), unit=_ut).set_pos(1).set_prop("display", hide=_rfp) + _e_tlcorner = gds.EndGroup(_("Top left corner")) + dx = gds.IntItem("ΔX", unit=_ut).set_prop("display", hide=_rfp) + dy = gds.IntItem("ΔY", unit=_ut).set_pos(1).set_prop("display", hide=_rfp) + + # Parameters for circular ROI geometry: + _cgroup = gds.BeginGroup(_("Center coordinates")).set_prop("display", hide=_cfp) + xc = gds.IntItem(_lbl("X", "C"), unit=_ut).set_prop("display", hide=_cfp) + yc = gds.IntItem(_lbl("Y", "C"), unit=_ut).set_pos(1).set_prop("display", hide=_cfp) + _e_cgroup = gds.EndGroup(_("Center coordinates")) + r = gds.IntItem(_("Radius"), unit=_ut).set_prop("display", hide=_cfp) + + # Parameters for polygonal ROI geometry: + points = gds.FloatArrayItem(_("Coordinates") + f" ({_ut})").set_prop( + "display", hide=_pfp + ) + + def to_single_roi( + self, obj: ImageObj, title: str = "" + ) -> PolygonalROI | RectangularROI | CircularROI: + """Convert parameters to single ROI + + Args: + obj: image object (used for conversion of pixel to physical coordinates) + title: ROI title + + Returns: + Single ROI + """ + if self.geometry == "rectangle": + return RectangularROI.from_param(obj, self) + if self.geometry == "circle": + return CircularROI.from_param(obj, self) + if self.geometry == "polygon": + return PolygonalROI.from_param(obj, self) + raise ValueError(f"Unknown ROI geometry type: {self.geometry}") + + def get_suffix(self) -> str: + """Get suffix text representation for ROI extraction""" + if self.geometry == "rectangle": + return f"x0={self.x0},y0={self.y0},dx={self.dx},dy={self.dy}" + if self.geometry == "circle": + return f"xc={self.xc},yc={self.yc},r={self.r}" + if self.geometry == "polygon": + return "polygon" + raise ValueError(f"Unknown ROI geometry type: {self.geometry}") + + def get_extracted_roi(self, obj: ImageObj) -> ImageROI | None: + """Get extracted ROI, i.e. the remaining ROI after extracting ROI from image. + + Args: + obj: image object (used for conversion of pixel to physical coordinates) + + When extracting ROIs from an image to multiple images (i.e. one image per ROI), + this method returns the ROI that has to be kept in the destination image. This + is not necessary for a rectangular ROI: the destination image is simply a crop + of the source image according to the ROI coordinates. But for a circular ROI or + a polygonal ROI, the destination image is a crop of the source image according + to the bounding box of the ROI. Thus, to avoid any loss of information, a ROI + has to be defined for the destination image: this is the ROI returned by this + method. It's simply the same as the source ROI, but with coordinates adjusted + to the destination image. One may called this ROI the "extracted ROI". + """ + if self.geometry == "rectangle": + return None + single_roi = self.to_single_roi(obj) + x0, y0, _x1, _y1 = self.get_bounding_box_indices() + single_roi.translate(obj, -x0, -y0) + roi = ImageROI() + roi.add_roi(single_roi) + return roi + + def get_bounding_box_indices(self) -> tuple[int, int, int, int]: + """Get bounding box (pixel coordinates)""" + if self.geometry == "circle": + x0, y0 = self.xc - self.r, self.yc - self.r + x1, y1 = self.xc + self.r, self.yc + self.r + elif self.geometry == "rectangle": + x0, y0, x1, y1 = self.x0, self.y0, self.x0 + self.dx, self.y0 + self.dy + else: + self.points: np.ndarray + x0, y0 = self.points[::2].min(), self.points[1::2].min() + x1, y1 = self.points[::2].max(), self.points[1::2].max() + return x0, y0, x1, y1 + + def get_data(self, obj: ImageObj) -> np.ndarray: + """Get data in ROI + + Args: + obj: image object + + Returns: + Data in ROI + """ + x0, y0, x1, y1 = self.get_bounding_box_indices() + x0, y0 = max(0, x0), max(0, y0) + x1, y1 = min(obj.data.shape[1], x1), min(obj.data.shape[0], y1) + return obj.data[y0:y1, x0:x1] + + +class BaseSingleImageROI( + base.BaseSingleROI["ImageObj", ROI2DParam, base.TypeROIItem], + Generic[base.TypeROIItem], + abc.ABC, +): + """Base class for single image ROI + + Args: + coords: ROI edge coordinates (floats) + title: ROI title + + .. note:: + + The image ROI coords are expressed in physical coordinates (floats). The + conversion to pixel coordinates is done in :class:`cdl.obj.ImageObj` + (see :meth:`cdl.obj.ImageObj.physical_to_indices`). Most of the time, + the physical coordinates are the same as the pixel coordinates, but this + is not always the case (e.g. after image binning), so it's better to keep the + physical coordinates in the ROI object: this will help reusing the ROI with + different images (e.g. with different pixel sizes). + """ + + @abc.abstractmethod + def get_bounding_box(self, obj: ImageObj) -> tuple[float, float, float, float]: + """Get bounding box (physical coordinates) + + Args: + obj: image object + """ + + @abc.abstractmethod + def translate(self, obj: ImageObj, dx: int, dy: int) -> None: + """Translate ROI - RECTANGLE = _("Rectangle") - CIRCLE = _("Circle") + Args: + obj: image object + dx: translation along X-axis + dy: translation along Y-axis + """ -class ImageRoiDataItem: - """Object representing an image ROI. +class PolygonalROI(BaseSingleImageROI[AnnotatedPolygon]): + """Polygonal ROI Args: - data: ROI data + coords: ROI edge coordinates + title: title + + Raises: + ValueError: if number of coordinates is odd + + .. note:: The image ROI coords are expressed in physical coordinates (floats) """ - def __init__(self, data: np.ndarray | list | tuple): - self._data = data + def check_coords(self) -> None: + """Check if coords are valid + + Raises: + ValueError: invalid coords + """ + if len(self.coords) % 2 != 0: + raise ValueError("Edge indices must be pairs of X, Y values") + # pylint: disable=unused-argument @classmethod - def from_image(cls, obj: ImageObj, geometry: RoiDataGeometries) -> ImageRoiDataItem: - """Construct roi data item from image object: called for making new ROI items + def from_param(cls: PolygonalROI, obj: ImageObj, param: ROI2DParam) -> PolygonalROI: + """Create ROI from parameters Args: obj: image object - geometry: ROI geometry + param: parameters """ - frac = 0.2 - x0, x1 = obj.x0 + frac * obj.width, obj.x0 + (1 - frac) * obj.width - if geometry is RoiDataGeometries.RECTANGLE: - y0, y1 = obj.y0 + frac * obj.height, obj.y0 + (1 - frac) * obj.height - else: - y0 = y1 = obj.yc - coords = x0, y0, x1, y1 - return cls(coords) + indices = True # ROI coordinates are in pixel coordinates in `ROI2DParam` + return cls(param.points, indices=indices, title=param.get_title()) - @property - def geometry(self) -> RoiDataGeometries: - """ROI geometry""" - _x0, y0, _x1, y1 = self._data - if y0 == y1: - return RoiDataGeometries.CIRCLE - return RoiDataGeometries.RECTANGLE - - def get_rect(self) -> tuple[int, int, int, int]: - """Get rectangle coordinates""" - x0, y0, x1, y1 = self._data - if self.geometry is RoiDataGeometries.CIRCLE: - radius = int(round(0.5 * (x1 - x0))) - y0 -= radius - y1 += radius - return x0, y0, x1, y1 + def get_bounding_box(self, obj: ImageObj) -> tuple[float, float, float, float]: + """Get bounding box (physical coordinates) + + Args: + obj: image object + """ + coords = self.get_physical_coords(obj) + x_edges, y_edges = coords[::2], coords[1::2] + return min(x_edges), min(y_edges), max(x_edges), max(y_edges) - def get_image_masked_view(self, obj: ImageObj) -> np.ndarray: - """Return masked view for data + def translate(self, obj: ImageObj, dx: int, dy: int) -> None: + """Translate ROI Args: obj: image object + dx: translation along X-axis + dy: translation along Y-axis """ - x0, y0, x1, y1 = self.get_rect() - return obj.get_masked_view()[y0:y1, x0:x1] + coords = self.get_indices_coords(obj) + coords[::2] += int(dx) + coords[1::2] += int(dy) + self.set_indices_coords(obj, coords) - def apply_mask(self, data: np.ndarray, yxratio: float) -> np.ndarray: - """Apply ROI to data as a mask and return masked array + def to_mask(self, obj: ImageObj) -> np.ndarray: + """Create mask from ROI Args: - data: data - yxratio: Y/X ratio + obj: image object + + Returns: + Mask (boolean array where True values are inside the ROI) """ - roi_mask = np.ones_like(data, dtype=bool) - x0, y0, x1, y1 = self.get_rect() - if self.geometry is RoiDataGeometries.RECTANGLE: - roi_mask[max(y0, 0) : y1, max(x0, 0) : x1] = False - else: - xc, yc = 0.5 * (x0 + x1), 0.5 * (y0 + y1) - radius = 0.5 * (x1 - x0) - rr, cc = draw.ellipse(yc, xc, radius / yxratio, radius, shape=data.shape) - roi_mask[rr, cc] = False + roi_mask = np.ones_like(obj.data, dtype=bool) + indices = self.get_indices_coords(obj) + rows, cols = indices[1::2], indices[::2] + rr, cc = draw.polygon(rows, cols, shape=obj.data.shape) + roi_mask[rr, cc] = False return roi_mask - def make_roi_item( - self, index: int | None, fmt: str, lbl: bool, editable: bool = True - ): - """Make ROI plot item + def to_param(self, obj: ImageObj, title: str | None = None) -> ROI2DParam: + """Convert ROI to parameters + + Args: + obj: object (image), for physical-indices coordinates conversion + title: ROI title + """ + param = ROI2DParam(title=self.title if title is None else title) + param.geometry = "polygon" + param.points = self.get_indices_coords(obj) + return param + + def to_plot_item(self, obj: ImageObj, title: str | None = None) -> AnnotatedPolygon: + """Make and return the annnotated polygon associated to ROI + + Args: + obj: object (image), for physical-indices coordinates conversion + title: title + """ + item = AnnotatedPolygon(self.get_physical_coords(obj).reshape(-1, 2)) + item.annotationparam.title = self.title if title is None else title + item.annotationparam.update_item(item) + item.set_style("plot", "shape/drag") + return item + + @classmethod + def from_plot_item(cls: PolygonalROI, item: AnnotatedPolygon) -> PolygonalROI: + """Create ROI from plot item + + Args: + item: plot item + """ + return cls(item.get_points().flatten(), False, item.annotationparam.title) + + +class RectangularROI(BaseSingleImageROI[AnnotatedRectangle]): + """Rectangular ROI + + Args: + coords: ROI edge coordinates (x0, y0, dx, dy) + title: title + + .. note:: The image ROI coords are expressed in physical coordinates (floats) + """ + + def check_coords(self) -> None: + """Check if coords are valid + + Raises: + ValueError: invalid coords + """ + if len(self.coords) != 4: + raise ValueError("Rectangle ROI requires 4 coordinates") + + @classmethod + def from_param( + cls: RectangularROI, obj: ImageObj, param: ROI2DParam + ) -> RectangularROI: + """Create ROI from parameters Args: - index: ROI index - fmt: format string - lbl: if True, show label - editable: if True, ROI is editable + obj: image object + param: parameters """ - coords = self._data - if self.geometry is RoiDataGeometries.RECTANGLE: - func = make_roi_rectangle + ix0, iy0, ix1, iy1 = param.get_bounding_box_indices() + coords = [ix0, iy0, ix1 - ix0, iy1 - iy0] + indices = True # ROI coordinates are in pixel coordinates in `ROI2DParam` + return cls(coords, indices=indices, title=param.get_title()) + + def get_bounding_box(self, obj: ImageObj) -> tuple[float, float, float, float]: + """Get bounding box (physical coordinates) + + Args: + obj: image object + """ + x0, y0, dx, dy = self.get_physical_coords(obj) + return x0, y0, x0 + dx, y0 + dy + + def translate(self, obj: ImageObj, dx: int, dy: int) -> None: + """Translate ROI + + Args: + obj: image object + dx: translation along X-axis + dy: translation along Y-axis + """ + coords = self.get_indices_coords(obj) + coords[0] += int(dx) + coords[1] += int(dy) + self.set_indices_coords(obj, coords) + + def get_physical_coords(self, obj: ImageObj) -> np.ndarray: + """Return physical coords + + Args: + obj: image object + + Returns: + Physical coords + """ + if self.indices: + ix0, iy0, idx, idy = self.coords + x0, y0, x1, y1 = obj.indices_to_physical([ix0, iy0, ix0 + idx, iy0 + idy]) + return [x0, y0, x1 - x0, y1 - y0] + return self.coords + + def get_indices_coords(self, obj: ImageObj) -> np.ndarray: + """Return indices coords + + Args: + obj: image object + + Returns: + Indices coords + """ + if self.indices: + return self.coords + ix0, iy0, ix1, iy1 = obj.physical_to_indices(self.get_bounding_box(obj)) + return [ix0, iy0, ix1 - ix0, iy1 - iy0] + + def set_indices_coords(self, obj: ImageObj, coords: np.ndarray) -> None: + """Set indices coords + + Args: + obj: object (signal/image) + coords: indices coords + """ + if self.indices: + self.coords = coords else: - func = make_roi_circle - title = "ROI" if index is None else f"ROI{index:02d}" - return base.make_roi_item(func, coords, title, fmt, lbl, editable, option="i") + ix0, iy0, idx, idy = coords + x0, y0, x1, y1 = obj.indices_to_physical([ix0, iy0, ix0 + idx, iy0 + idy]) + self.coords = [x0, y0, x1 - x0, y1 - y0] + def to_mask(self, obj: ImageObj) -> np.ndarray: + """Create mask from ROI -class ROI2DParam(gds.DataSet): - """Image ROI parameters""" + Args: + obj: image object - _geometry_prop = gds.GetAttrProp("geometry") - _rfp = gds.FuncProp(_geometry_prop, lambda x: x is RoiDataGeometries.RECTANGLE) - _cfp = gds.FuncProp(_geometry_prop, lambda x: x is RoiDataGeometries.CIRCLE) + Returns: + Mask (boolean array where True values are inside the ROI) + """ + roi_mask = np.ones_like(obj.data, dtype=bool) + x0, y0, dx, dy = self.get_indices_coords(obj) + roi_mask[max(y0, 0) : y0 + dy, max(x0, 0) : x0 + dx] = False + return roi_mask - # Do not declare it as a static method: not supported on Python 3.8 - def _lbl(name: str, index: int): # pylint: disable=no-self-argument - """Returns nameindex""" - return f"{name}{index}" + def to_param(self, obj: ImageObj, title: str | None = None) -> ROI2DParam: + """Convert ROI to parameters - geometry = gds.ChoiceItem(_("Geometry"), RoiDataGeometries.get_choices()).set_prop( - "display", store=_geometry_prop, hide=True - ) + Args: + obj: object (image), for physical-indices coordinates conversion + title: ROI title + """ + param = ROI2DParam(title=self.title if title is None else title) + param.geometry = "rectangle" + param.x0, param.y0, param.dx, param.dy = self.get_indices_coords(obj) + return param - # Parameters for rectangular ROI geometry: - _tlcorner = gds.BeginGroup(_("Top left corner")).set_prop("display", hide=_cfp) - xr0 = gds.IntItem(_lbl("X", 0), unit="pixel").set_prop("display", hide=_cfp) - yr0 = ( - gds.IntItem(_lbl("Y", 0), unit="pixel") - .set_pos(1) - .set_prop("display", hide=_cfp) - ) - _e_tlcorner = gds.EndGroup(_("Top left corner")) - _brcorner = gds.BeginGroup(_("Bottom right corner")).set_prop("display", hide=_cfp) - xr1 = gds.IntItem(_lbl("X", 1), unit="pixel").set_prop("display", hide=_cfp) - yr1 = ( - gds.IntItem(_lbl("Y", 1), unit="pixel") - .set_pos(1) - .set_prop("display", hide=_cfp) - ) - _e_brcorner = gds.EndGroup(_("Bottom right corner")) + def to_plot_item( + self, obj: ImageObj, title: str | None = None + ) -> AnnotatedRectangle: + """Make and return the annnotated rectangle associated to ROI - # Parameters for circular ROI geometry: - _cgroup = gds.BeginGroup(_("Center coordinates")).set_prop("display", hide=_rfp) - xc = gds.IntItem(_lbl("X", "C"), unit="pixel").set_prop("display", hide=_rfp) - yc = ( - gds.IntItem(_lbl("Y", "C"), unit="pixel") - .set_pos(1) - .set_prop("display", hide=_rfp) - ) - _e_cgroup = gds.EndGroup(_("Center coordinates")) - r = gds.IntItem(_("Radius"), unit="pixel").set_prop("display", hide=_rfp) + Args: + obj: object (image), for physical-indices coordinates conversion + title: title + """ - def get_suffix(self) -> str: - """Get suffix text representation for ROI extraction""" - if self.geometry is RoiDataGeometries.CIRCLE: - return f"xc={self.xc},yc={self.yc},r={self.r}" - return f"x={self.xr0}:{self.xr1},y={self.yr0}:{self.yr1}" - - def get_coords(self) -> tuple[int, int, int, int]: - """Get ROI coordinates""" - if self.geometry is RoiDataGeometries.CIRCLE: - return self.xc - self.r, self.yc, self.xc + self.r, self.yc - x0, y0, x1, y1 = self.xr0, self.yr0, self.xr1, self.yr1 - x0, x1 = min(x0, x1), max(x0, x1) - y0, y1 = min(y0, y1), max(y0, y1) - x1 = x1 + 1 if x1 == x0 else x1 - y1 = y1 + 1 if y1 == y0 else y1 - return x0, y0, x1, y1 + def info_callback(item: AnnotatedRectangle) -> str: + """Return info string for rectangular ROI""" + x0, y0, x1, y1 = item.get_rect() + if self.indices: + x0, y0, x1, y1 = obj.physical_to_indices([x0, y0, x1, y1]) + x0, y0, dx, dy = self.rect_to_coords(x0, y0, x1, y1) + return "
".join( + [ + f"X0, Y0 = {x0:g}, {y0:g}", + f"ΔX x ΔY = {dx:g} x {dy:g}", + ] + ) - def get_single_roi(self) -> np.ndarray | None: - """Get single ROI, i.e. after extracting ROI from image""" - if self.geometry is RoiDataGeometries.CIRCLE: - return np.array([(0, self.r, 2 * self.r, self.r)], int) - return None + x0, y0, dx, dy = self.get_physical_coords(obj) + x1, y1 = x0 + dx, y0 + dy + title = self.title if title is None else title + roi_item: AnnotatedRectangle = make.annotated_rectangle(x0, y0, x1, y1, title) + roi_item.set_info_callback(info_callback) + param = roi_item.label.labelparam + param.anchor = "BL" + param.xc, param.yc = 5, -5 + param.update_item(roi_item.label) + return roi_item + + @staticmethod + def rect_to_coords( + x0: int | float, y0: int | float, x1: int | float, y1: int | float + ) -> np.ndarray: + """Convert rectangle to coordinates - def get_rect_indexes(self) -> tuple[int, int, int, int]: - """Get rectangle indexes""" - if self.geometry is RoiDataGeometries.CIRCLE: - x0, y0 = self.xc - self.r, self.yc - self.r - x1, y1 = self.xc + self.r, self.yc + self.r + Args: + x0: x0 (top-left corner) + y0: y0 (top-left corner) + x1: x1 (bottom-right corner) + y1: y1 (bottom-right corner) + + Returns: + Rectangle coordinates + """ + return np.array([x0, y0, x1 - x0, y1 - y0], dtype=type(x0)) + + @classmethod + def from_plot_item(cls: RectangularROI, item: AnnotatedRectangle) -> RectangularROI: + """Create ROI from plot item + + Args: + item: plot item + """ + rect = item.get_rect() + return cls(cls.rect_to_coords(*rect), False, item.annotationparam.title) + + +class CircularROI(BaseSingleImageROI[AnnotatedCircle]): + """Circular ROI + + Args: + coords: ROI edge coordinates (xc, yc, r) + title: title + + .. note:: The image ROI coords are expressed in physical coordinates (floats) + """ + + # pylint: disable=unused-argument + @classmethod + def from_param(cls: CircularROI, obj: ImageObj, param: ROI2DParam) -> CircularROI: + """Create ROI from parameters + + Args: + obj: image object + param: parameters + """ + ix0, iy0, ix1, iy1 = param.get_bounding_box_indices() + ixc, iyc = (ix0 + ix1) * 0.5, (iy0 + iy1) * 0.5 + ir = (ix1 - ix0) * 0.5 + indices = True # ROI coordinates are in pixel coordinates in `ROI2DParam` + return cls([ixc, iyc, ir], indices=indices, title=param.get_title()) + + def check_coords(self) -> None: + """Check if coords are valid + + Raises: + ValueError: invalid coords + """ + if len(self.coords) != 3: + raise ValueError("Circle ROI requires 3 coordinates") + + def get_bounding_box(self, obj: ImageObj) -> tuple[float, float, float, float]: + """Get bounding box (physical coordinates) + + Args: + obj: image object + """ + xc, yc, r = self.get_physical_coords(obj) + return xc - r, yc - r, xc + r, yc + r + + def translate(self, obj: ImageObj, dx: int, dy: int) -> None: + """Translate ROI + + Args: + obj: image object + dx: translation along X-axis + dy: translation along Y-axis + """ + coords = self.get_indices_coords(obj) + coords[0] += int(dx) + coords[1] += int(dy) + self.set_indices_coords(obj, coords) + + def get_physical_coords(self, obj: ImageObj) -> np.ndarray: + """Return physical coords + + Args: + obj: image object + + Returns: + Physical coords + """ + if self.indices: + ixc, iyc, ir = self.coords + x0, y0, x1, y1 = obj.indices_to_physical( + [ixc - ir, iyc - ir, ixc + ir, iyc + ir] + ) + return [0.5 * (x0 + x1), 0.5 * (y0 + y1), 0.5 * (x1 - x0)] + return self.coords + + def get_indices_coords(self, obj: ImageObj) -> np.ndarray: + """Return indices coords + + Args: + obj: image object + + Returns: + Indices coords + """ + if self.indices: + return self.coords + ix0, iy0, ix1, iy1 = obj.physical_to_indices(self.get_bounding_box(obj)) + ixc, iyc = int((ix0 + ix1) * 0.5), int((iy0 + iy1) * 0.5) + ir = int((ix1 - ix0) * 0.5) + return [ixc, iyc, ir] + + def set_indices_coords(self, obj: ImageObj, coords: np.ndarray) -> None: + """Set indices coords + + Args: + obj: object (signal/image) + coords: indices coords + """ + if self.indices: + self.coords = coords else: - x0, y0, x1, y1 = self.xr0, self.yr0, self.xr1, self.yr1 - return max(0, x0), max(0, y0), x1, y1 + ixc, iyc, ir = coords + x0, y0, x1, y1 = obj.indices_to_physical( + [ixc - ir, iyc - ir, ixc + ir, iyc + ir] + ) + self.coords = [0.5 * (x0 + x1), 0.5 * (y0 + y1), 0.5 * (x1 - x0)] - def get_data(self, obj: ImageObj) -> np.ndarray: - """Get data in ROI + def to_mask(self, obj: ImageObj) -> np.ndarray: + """Create mask from ROI Args: obj: image object Returns: - Data in ROI + Mask (boolean array where True values are inside the ROI) """ - x0, y0, x1, y1 = self.get_rect_indexes() - x1, y1 = min(obj.data.shape[1], x1), min(obj.data.shape[0], y1) - return obj.data[y0:y1, x0:x1] + roi_mask = np.ones_like(obj.data, dtype=bool) + ixc, iyc, ir = self.get_indices_coords(obj) + yxratio = obj.dy / obj.dx + rr, cc = draw.ellipse(iyc, ixc, ir / yxratio, ir, shape=obj.data.shape) + roi_mask[rr, cc] = False + return roi_mask + + def to_param(self, obj: ImageObj, title: str | None = None) -> ROI2DParam: + """Convert ROI to parameters + + Args: + obj: object (image), for physical-indices coordinates conversion + title: ROI title + """ + param = ROI2DParam(title=self.title if title is None else title) + param.geometry = "circle" + param.xc, param.yc, param.r = self.get_indices_coords(obj) + return param + + def to_plot_item(self, obj: ImageObj, title: str | None = None) -> AnnotatedCircle: + """Make and return the annnotated circle associated to ROI + + Args: + obj: object (image), for physical-indices coordinates conversion + title: title + """ + + def info_callback(item: AnnotatedCircle) -> str: + """Return info string for circular ROI""" + x0, y0, x1, y1 = item.get_rect() + if self.indices: + x0, y0, x1, y1 = obj.physical_to_indices([x0, y0, x1, y1]) + xc, yc, r = self.rect_to_coords(x0, y0, x1, y1) + return "
".join( + [ + f"Center = {xc:g}, {yc:g}", + f"Radius = {r:g}", + ] + ) + + xc, yc, r = self.get_physical_coords(obj) + item = AnnotatedCircle(xc - r, yc, xc + r, yc) + item.set_info_callback(info_callback) + item.annotationparam.title = self.title if title is None else title + item.annotationparam.update_item(item) + item.set_style("plot", "shape/drag") + return item + + @staticmethod + def rect_to_coords( + x0: int | float, y0: int | float, x1: int | float, y1: int | float + ) -> np.ndarray: + """Convert rectangle to circle coordinates + + Args: + x0: x0 (top-left corner) + y0: y0 (top-left corner) + x1: x1 (bottom-right corner) + y1: y1 (bottom-right corner) + + Returns: + Circle coordinates + """ + xc, yc, r = 0.5 * (x0 + x1), 0.5 * (y0 + y1), 0.5 * (x1 - x0) + return np.array([xc, yc, r], dtype=type(x0)) + + @classmethod + def from_plot_item(cls: CircularROI, item: AnnotatedCircle) -> CircularROI: + """Create ROI from plot item + + Args: + item: plot item + """ + rect = item.get_rect() + return cls(cls.rect_to_coords(*rect), False, item.annotationparam.title) -class ImageObj(gds.DataSet, base.BaseObj): +class ImageROI( + base.BaseROI[ + "ImageObj", + BaseSingleImageROI, + ROI2DParam, + # `Union` is mandatory here for Python 3.9-3.10 compatibility: + Union[AnnotatedPolygon, AnnotatedRectangle, AnnotatedCircle], + ] +): + """Image Regions of Interest + + Args: + singleobj: if True, when extracting data defined by ROIs, only one object + is created (default to True). If False, one object is created per single ROI. + If None, the value is get from the user configuration + inverse: if True, ROI is outside the region + """ + + PREFIX = "i" + + @staticmethod + def get_compatible_single_roi_classes() -> list[Type[BaseSingleImageROI]]: + """Return compatible single ROI classes""" + return [RectangularROI, CircularROI, PolygonalROI] + + def to_mask(self, obj: ImageObj) -> np.ndarray[bool]: + """Create mask from ROI + + Args: + obj: image object + + Returns: + Mask (boolean array where True values are inside the ROI) + """ + mask = np.ones_like(obj.data, dtype=bool) + for roi in self.single_rois: + mask &= roi.to_mask(obj) + return mask + + +def create_image_roi( + geometry: Literal["rectangle", "circle", "polygon"], + coords: np.ndarray | list[float] | list[list[float]], + indices: bool = True, + singleobj: bool | None = None, + inverse: bool = False, + title: str = "", +) -> ImageROI: + """Create Image Regions of Interest (ROI) object. + More ROIs can be added to the object after creation, using the `add_roi` method. + + Args: + geometry: ROI type ('rectangle', 'circle', 'polygon') + coords: ROI coords (physical coordinates), `[x0, y0, dx, dy]` for a rectangle, + `[xc, yc, r]` for a circle, or `[x0, y0, x1, y1, ...]` for a polygon (lists or + NumPy arrays are accepted). For multiple ROIs, nested lists or NumPy arrays are + accepted but with a common geometry type (e.g. + `[[xc1, yc1, r1], [xc2, yc2, r2], ...]` for circles). + indices: if True, coordinates are indices, if False, they are physical values + (default to True for images) + singleobj: if True, when extracting data defined by ROIs, only one object + is created (default to True). If False, one object is created per single ROI. + If None, the value is get from the user configuration + inverse: if True, ROI is outside the region + title: title + + Returns: + Regions of Interest (ROI) object + + Raises: + ValueError: if ROI type is unknown or if the number of coordinates is invalid + """ + coords = np.array(coords, float) + if coords.ndim == 1: + coords = coords.reshape(1, -1) + roi = ImageROI(singleobj, inverse) + if geometry == "rectangle": + if coords.shape[1] != 4: + raise ValueError("Rectangle ROI requires 4 coordinates") + for row in coords: + roi.add_roi(RectangularROI(row, indices, title)) + elif geometry == "circle": + if coords.shape[1] != 3: + raise ValueError("Circle ROI requires 3 coordinates") + for row in coords: + roi.add_roi(CircularROI(row, indices, title)) + elif geometry == "polygon": + if coords.shape[1] % 2 != 0: + raise ValueError("Polygon ROI requires pairs of X, Y coordinates") + for row in coords: + roi.add_roi(PolygonalROI(row, indices, title)) + else: + raise ValueError(f"Unknown ROI type: {geometry}") + return roi + + +class ImageObj(gds.DataSet, base.BaseObj[ImageROI, MaskedImageItem]): """Image object""" PREFIX = "i" @@ -309,7 +839,11 @@ def __init__(self, title=None, comment=None, icon=""): base.BaseObj.__init__(self) self.regenerate_uuid() self._dicom_template = None - self._maskdata_cache = None + + @staticmethod + def get_roi_class() -> Type[ImageROI]: + """Return ROI class""" + return ImageROI def regenerate_uuid(self): """Regenerate UUID @@ -476,8 +1010,9 @@ def get_data(self, roi_index: int | None = None) -> np.ndarray: """ if self.roi is None or roi_index is None: return self.data - roidataitem = ImageRoiDataItem(self.roi[roi_index]) - return roidataitem.get_image_masked_view(self) + single_roi = self.roi.get_single_roi(roi_index) + x0, y0, x1, y1 = self.physical_to_indices(single_roi.get_bounding_box(self)) + return self.get_masked_view()[y0:y1, x0:x1] def copy(self, title: str | None = None, dtype: np.dtype | None = None) -> ImageObj: """Copy object. @@ -625,128 +1160,51 @@ def update_item(self, item: MaskedImageItem, data_changed: bool = True) -> None: self.update_plot_item_parameters(item) item.plot().update_colormap_axis(item) - def get_roi_param(self, title, *defaults: int) -> ROI2DParam: - """Return ROI parameters dataset. - - Args: - title: title - *defaults: default values - """ - roidataitem = ImageRoiDataItem(defaults) - xd0, yd0, xd1, yd1 = defaults - param = ROI2DParam(title) - param.geometry = roidataitem.geometry - if roidataitem.geometry is RoiDataGeometries.RECTANGLE: - param.xr0, param.yr0, param.xr1, param.yr1 = xd0, yd0, xd1, yd1 - else: - param.xc = int(0.5 * (xd0 + xd1)) - param.yc = yd0 - param.r = int(0.5 * (xd1 - xd0)) - return param - - def params_to_roidata(self, params: gds.DataSetGroup) -> np.ndarray | None: - """Convert ROI dataset group to ROI array data. - - Args: - params: ROI dataset group - - Returns: - ROI array data - """ - roilist = [] - for roiparam in params.datasets: - roiparam: ROI2DParam - roilist.append(roiparam.get_coords()) - if len(roilist) == 0: - return None - return np.array(roilist, int) - - def new_roi_item( - self, fmt: str, lbl: bool, editable: bool, geometry: RoiDataGeometries - ) -> MaskedImageItem: - """Return a new ROI item from scratch - - Args: - fmt: format string - lbl: if True, add label - editable: if True, ROI is editable - geometry: ROI geometry - """ - roidataitem = ImageRoiDataItem.from_image(self, geometry) - return roidataitem.make_roi_item(None, fmt, lbl, editable) - - def roi_coords_to_indexes(self, coords: list) -> np.ndarray: - """Convert ROI coordinates to indexes. + def physical_to_indices(self, coords: list[float]) -> np.ndarray: + """Convert coordinates from physical (real world) to (array) indices (pixel) Args: coords: coordinates Returns: - Indexes + Indices """ - indexes = np.array(coords) - if indexes.size > 0: - indexes[:, ::2] -= self.x0 + 0.5 * self.dx - indexes[:, ::2] /= self.dx - indexes[:, 1::2] -= self.y0 + 0.5 * self.dy - indexes[:, 1::2] /= self.dy - return np.array(indexes, int) - - def iterate_roi_items(self, fmt: str, lbl: bool, editable: bool = True) -> Iterator: - """Make plot item representing a Region of Interest. + indices = np.array(coords, float) + ndim = indices.ndim + if ndim == 1: + indices = indices.reshape(1, -1) + if indices.size > 0: + indices[:, ::2] -= self.x0 + 0.5 * self.dx + indices[:, ::2] /= self.dx + indices[:, 1::2] -= self.y0 + 0.5 * self.dy + indices[:, 1::2] /= self.dy + if ndim == 1: + indices = indices.flatten() + return np.array(indices, int) + + def indices_to_physical( + self, indices: list[float | int] | np.ndarray + ) -> np.ndarray: + """Convert coordinates from (array) indices to physical (real world) Args: - fmt: format string - lbl: if True, add label - editable: if True, ROI is editable - - Yields: - Plot item - """ - if self.roi is not None: - roicoords = np.array(self.roi, float) - roicoords[:, ::2] *= self.dx - roicoords[:, ::2] += self.x0 - 0.5 * self.dx - roicoords[:, 1::2] *= self.dy - roicoords[:, 1::2] += self.y0 - 0.5 * self.dy - for index, coords in enumerate(roicoords): - roidataitem = ImageRoiDataItem(coords) - yield roidataitem.make_roi_item(index, fmt, lbl, editable) - - @property - def maskdata(self) -> np.ndarray: - """Return masked data (areas outside defined regions of interest) + indices: indices Returns: - Masked data + Coordinates """ - roi_changed = self.roi_has_changed() - if self.roi is None: - if roi_changed: - self._maskdata_cache = None - elif roi_changed or self._maskdata_cache is None: - mask = np.ones_like(self.data, dtype=bool) - for roirow in self.roi: - roidataitem = ImageRoiDataItem(roirow) - roi_mask = roidataitem.apply_mask(self.data, yxratio=self.dy / self.dx) - mask &= roi_mask - self._maskdata_cache = mask - return self._maskdata_cache - - def get_masked_view(self) -> ma.MaskedArray: - """Return masked view for data - - Returns: - Masked view - """ - self.data: np.ndarray - view = self.data.view(ma.MaskedArray) - view.mask = self.maskdata - return view - - def invalidate_maskdata_cache(self) -> None: - """Invalidate mask data cache: force to rebuild it""" - self._maskdata_cache = None + coords = np.array(indices, float) + ndim = coords.ndim + if ndim == 1: + coords = coords.reshape(1, -1) + if coords.size > 0: + coords[:, ::2] *= self.dx + coords[:, ::2] += self.x0 + 0.5 * self.dx + coords[:, 1::2] *= self.dy + coords[:, 1::2] += self.y0 + 0.5 * self.dy + if ndim == 1: + coords = coords.flatten() + return coords def add_label_with_title(self, title: str | None = None) -> None: """Add label with title annotation diff --git a/cdl/core/model/signal.py b/cdl/core/model/signal.py index 7b44505e..e0cd5d02 100644 --- a/cdl/core/model/signal.py +++ b/cdl/core/model/signal.py @@ -12,7 +12,7 @@ from __future__ import annotations from contextlib import contextmanager -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Generator, Type from uuid import uuid4 import guidata.dataset as gds @@ -21,8 +21,8 @@ from guidata.configtools import get_icon from guidata.dataset import restore_dataset, update_dataset from guidata.qthelpers import exec_dialog -from numpy import ma from plotpy.builder import make +from plotpy.items import CurveItem, XRangeSelection from plotpy.tools import EditPointTool from qtpy import QtWidgets as QW @@ -31,7 +31,6 @@ from cdl.core.model import base if TYPE_CHECKING: - from plotpy.items import CurveItem from plotpy.plot import PlotDialog from plotpy.styles import CurveParam @@ -103,12 +102,26 @@ def suspend(self) -> Generator[None, None, None]: CURVESTYLES = CurveStyles() # This is the unique instance of the CurveStyles class -class ROI1DParam(gds.DataSet): +class ROI1DParam(base.BaseROIParam["SignalObj", "SegmentROI"]): """Signal ROI parameters""" + # Note: in this class, the ROI parameters are stored as X coordinates + xmin = gds.FloatItem(_("First point coordinate")) xmax = gds.FloatItem(_("Last point coordinate")) + def to_single_roi(self, obj: SignalObj, title: str = "") -> SegmentROI: + """Convert parameters to single ROI + + Args: + obj: signal object + title: ROI title + + Returns: + Single ROI + """ + return SegmentROI([self.xmin, self.xmax], False, title=title) + def get_data(self, obj: SignalObj) -> np.ndarray: """Get signal data in ROI @@ -122,6 +135,162 @@ def get_data(self, obj: SignalObj) -> np.ndarray: return np.array([obj.x[imin:imax], obj.y[imin:imax]]) +class SegmentROI(base.BaseSingleROI["SignalObj", ROI1DParam, XRangeSelection]): + """Segment ROI + + Args: + coords: ROI coordinates (xmin, xmax) + title: ROI title + """ + + # Note: in this class, the ROI parameters are stored as X indices + + def check_coords(self) -> None: + """Check if coords are valid + + Raises: + ValueError: invalid coords + """ + if len(self.coords) != 2: + raise ValueError("Invalid ROI segment coords (2 values expected)") + if self.coords[0] >= self.coords[1]: + raise ValueError("Invalid ROI segment coords (xmin >= xmax)") + + def get_data(self, obj: SignalObj) -> np.ndarray: + """Get signal data in ROI + + Args: + obj: signal object + + Returns: + Data in ROI + """ + imin, imax = self.get_indices_coords(obj) + return np.array([obj.x[imin:imax], obj.y[imin:imax]]) + + def to_mask(self, obj: SignalObj) -> np.ndarray: + """Create mask from ROI + + Args: + obj: signal object + + Returns: + Mask (boolean array where True values are inside the ROI) + """ + + mask = np.ones_like(obj.xydata, dtype=bool) + imin, imax = self.get_indices_coords(obj) + mask[:, imin:imax] = False + return mask + + # pylint: disable=unused-argument + def to_param(self, obj: SignalObj, title: str | None = None) -> ROI1DParam: + """Convert ROI to parameters + + Args: + obj: object (signal), for physical-indices coordinates conversion + title: ROI title + """ + title = title or self.title + param = ROI1DParam(title) + param.xmin, param.xmax = self.get_physical_coords(obj) + return param + + # pylint: disable=unused-argument + def to_plot_item(self, obj: SignalObj, title: str | None = None) -> XRangeSelection: + """Make and return the annnotated segment associated with the ROI + + Args: + obj: object (signal), for physical-indices coordinates conversion + title: title + """ + xmin, xmax = self.get_physical_coords(obj) + item = make.range(xmin, xmax) + return item + + @classmethod + def from_plot_item(cls: SegmentROI, item: XRangeSelection) -> SegmentROI: + """Create ROI from plot item + + Args: + item: plot item + + Returns: + ROI + """ + if not isinstance(item, XRangeSelection): + raise TypeError("Invalid plot item type") + return cls(item.get_range(), False) + + +class SignalROI(base.BaseROI["SignalObj", SegmentROI, ROI1DParam, XRangeSelection]): + """Signal Regions of Interest + + Args: + singleobj: if True, when extracting data defined by ROIs, only one object + is created (default to True). If False, one object is created per single ROI. + If None, the value is get from the user configuration + inverse: if True, ROI is outside the region + """ + + PREFIX = "s" + + @staticmethod + def get_compatible_single_roi_classes() -> list[Type[SegmentROI]]: + """Return compatible single ROI classes""" + return [SegmentROI] + + def to_mask(self, obj: SignalObj) -> np.ndarray[bool]: + """Create mask from ROI + + Args: + obj: signal object + + Returns: + Mask (boolean array where True values are inside the ROI) + """ + mask = np.ones_like(obj.xydata, dtype=bool) + for roi in self.single_rois: + mask &= roi.to_mask(obj) + return mask + + +def create_signal_roi( + coords: np.ndarray | list[float, float] | list[list[float, float]], + indices: bool = False, + singleobj: bool | None = None, + inverse: bool = False, + title: str = "", +) -> SignalROI: + """Create Signal Regions of Interest (ROI) object. + More ROIs can be added to the object after creation, using the `add_roi` method. + + Args: + coords: single ROI coordinates `[xmin, xmax]`, or multiple ROIs coordinates + `[[xmin1, xmax1], [xmin2, xmax2], ...]` (lists or NumPy arrays) + indices: if True, coordinates are indices, if False, they are physical values + (default to False for signals) + singleobj: if True, when extracting data defined by ROIs, only one object + is created (default to True). If False, one object is created per single ROI. + If None, the value is get from the user configuration + inverse: if True, ROI is outside the region + title: title + + Returns: + Regions of Interest (ROI) object + + Raises: + ValueError: if the number of coordinates is not even + """ + coords = np.array(coords, float) + if coords.ndim == 1: + coords = coords.reshape(1, -1) + roi = SignalROI(singleobj, inverse) + for row in coords: + roi.add_roi(SegmentROI(row, indices=indices, title=title)) + return roi + + def apply_downsampling(item: CurveItem, do_not_update: bool = False) -> None: """Apply downsampling to curve item @@ -142,7 +311,7 @@ def apply_downsampling(item: CurveItem, do_not_update: bool = False) -> None: item.update_data() -class SignalObj(gds.DataSet, base.BaseObj): +class SignalObj(gds.DataSet, base.BaseObj[SignalROI, CurveItem]): """Signal object""" PREFIX = "s" @@ -214,7 +383,11 @@ def __init__(self, title=None, comment=None, icon=""): gds.DataSet.__init__(self, title, comment, icon) base.BaseObj.__init__(self) self.regenerate_uuid() - self._maskdata_cache = None + + @staticmethod + def get_roi_class() -> Type[SignalROI]: + """Return ROI class""" + return SignalROI def regenerate_uuid(self): """Regenerate UUID @@ -353,8 +526,8 @@ def get_data(self, roi_index: int | None = None) -> tuple[np.ndarray, np.ndarray """ if self.roi is None or roi_index is None: return self.x, self.y - i1, i2 = self.roi[roi_index, :] - return self.x[i1:i2], self.y[i1:i2] + single_roi = self.roi.get_single_roi(roi_index) + return single_roi.get_data(self) def update_plot_item_parameters(self, item: CurveItem) -> None: """Update plot item parameters from object data/metadata @@ -388,7 +561,7 @@ def update_metadata_from_plot_item(self, item: CurveItem) -> None: restore_dataset(item.param.line, self.metadata) restore_dataset(item.param.symbol, self.metadata) - def make_item(self, update_from: CurveItem = None) -> CurveItem: + def make_item(self, update_from: CurveItem | None = None) -> CurveItem: """Make plot item from data. Args: @@ -439,134 +612,30 @@ def update_item(self, item: CurveItem, data_changed: bool = True) -> None: apply_downsampling(item) self.update_plot_item_parameters(item) - def roi_coords_to_indexes(self, coords: list) -> np.ndarray: - """Convert ROI coordinates to indexes. + def physical_to_indices(self, coords: list[float] | np.ndarray) -> np.ndarray: + """Convert coordinates from physical (real world) to (array) indices (pixel) Args: coords: coordinates Returns: - Indexes + Indices """ - indexes = np.array(coords, int) - for row in range(indexes.shape[0]): - for col in range(indexes.shape[1]): - x0 = coords[row][col] - indexes[row, col] = np.abs(self.x - x0).argmin() - return indexes + self.x: np.ndarray + return np.array([np.abs(self.x - x).argmin() for x in coords]) - def get_roi_param(self, title: str, *defaults: int) -> ROI1DParam: - """Return ROI parameters dataset (converting ROI point indexes to coordinates) + def indices_to_physical(self, indices: list[int] | np.ndarray) -> np.ndarray: + """Convert coordinates from (array) indices to physical (real world) Args: - title: title - *defaults: default values (first, last point indexes) + indices: indices Returns: - ROI parameters dataset (containing the ROI coordinates: first and last X) - """ - i0, i1 = defaults - param = ROI1DParam(title) - param.xmin = self.x[i0] - param.xmax = self.x[i1] - param.set_global_prop("data", unit=self.xunit) - return param - - def params_to_roidata(self, params: gds.DataSetGroup) -> np.ndarray: - """Convert ROI dataset group to ROI array data. - - Args: - params: ROI dataset group - - Returns: - ROI array data - """ - roilist = [] - for roiparam in params.datasets: - roiparam: ROI1DParam - idx1 = np.searchsorted(self.x, roiparam.xmin) - idx2 = np.searchsorted(self.x, roiparam.xmax) - roilist.append([idx1, idx2]) - if len(roilist) == 0: - return None - return np.array(roilist, int) - - def new_roi_item(self, fmt: str, lbl: bool, editable: bool): - """Return a new ROI item from scratch - - Args: - fmt: format string - lbl: if True, add label - editable: if True, ROI is editable + Coordinates """ # We take the real part of the x data to avoid `ComplexWarning` warnings # when creating and manipulating the `XRangeSelection` shape (`plotpy`) - xmin, xmax = self.x.real.min(), self.x.real.max() - xdelta = (xmax - xmin) * 0.2 - coords = xmin + xdelta, xmax - xdelta - return base.make_roi_item( - lambda x, y, _title: make.range(x, y), - coords, - "ROI", - fmt, - lbl, - editable, - option=self.PREFIX, - ) - - def iterate_roi_items(self, fmt: str, lbl: bool, editable: bool = True): - """Make plot item representing a Region of Interest. - - Args: - fmt: format string - lbl: if True, add label - editable: if True, ROI is editable - - Yields: - Plot item - """ - if self.roi is not None: - # We take the real part of the x data to avoid `ComplexWarning` warnings - # when creating and manipulating the `XRangeSelection` shape (`plotpy`) - for index, coords in enumerate(self.x.real[self.roi]): - yield base.make_roi_item( - lambda x, y, _title: make.range(x, y), - coords, - f"ROI{index:02d}", - fmt, - lbl, - editable, - option=self.PREFIX, - ) - - @property - def maskdata(self) -> np.ndarray: - """Return masked data (areas outside defined regions of interest) - - Returns: - Masked data - """ - roi_changed = self.roi_has_changed() - if self.roi is None: - if roi_changed: - self._maskdata_cache = None - elif roi_changed or self._maskdata_cache is None: - mask = np.ones_like(self.xydata, dtype=bool) - for roirow in self.roi: - mask[:, roirow[0] : roirow[1]] = False - self._maskdata_cache = mask - return self._maskdata_cache - - def get_masked_view(self) -> ma.MaskedArray: - """Return masked view for data - - Returns: - Masked view - """ - self.data: np.ndarray - view = self.data.view(ma.MaskedArray) - view.mask = self.maskdata - return view + return self.x.real[indices] def add_label_with_title(self, title: str | None = None) -> None: """Add label with title annotation diff --git a/cdl/data/icons/edit/roi_new_polygon.svg b/cdl/data/icons/edit/roi_new_polygon.svg new file mode 100644 index 00000000..f5c700e2 --- /dev/null +++ b/cdl/data/icons/edit/roi_new_polygon.svg @@ -0,0 +1,110 @@ + + diff --git a/cdl/data/tests/reordering_test.h5 b/cdl/data/tests/reordering_test.h5 index b16e9c25..fe171920 100644 Binary files a/cdl/data/tests/reordering_test.h5 and b/cdl/data/tests/reordering_test.h5 differ diff --git a/cdl/locale/fr/LC_MESSAGES/cdl.po b/cdl/locale/fr/LC_MESSAGES/cdl.po index fb7344ea..91f97ca8 100644 --- a/cdl/locale/fr/LC_MESSAGES/cdl.po +++ b/cdl/locale/fr/LC_MESSAGES/cdl.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2024-08-02 17:00+0200\n" +"POT-Creation-Date: 2024-11-14 15:42+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -26,7 +26,7 @@ msgstr "Facteur" msgid "Constant" msgstr "Constante" -#: cdl\computation\base.py:70 cdl\computation\image\__init__.py:694 +#: cdl\computation\base.py:70 cdl\computation\image\__init__.py:692 #: cdl\computation\image\threshold.py:52 msgid "Operation" msgstr "Opération" @@ -61,7 +61,7 @@ msgid "Size of the moving window" msgstr "Taille de la fenêtre glissante" #: cdl\computation\base.py:98 cdl\computation\base.py:108 -#: cdl\computation\image\__init__.py:435 cdl\computation\image\__init__.py:630 +#: cdl\computation\image\__init__.py:433 cdl\computation\image\__init__.py:628 #: cdl\computation\image\edges.py:59 cdl\computation\image\restoration.py:93 #: cdl\computation\image\restoration.py:128 cdl\computation\signal.py:1064 msgid "Mode" @@ -95,7 +95,7 @@ msgstr "Energie" #: cdl\computation\base.py:127 msgid "RMS" -msgstr "" +msgstr "RMS" #: cdl\computation\base.py:129 msgid "Normalize with respect to" @@ -114,59 +114,37 @@ msgstr "Limite inférieure" msgid "Upper limit" msgstr "Limite supérieure" -#: cdl\computation\base.py:160 -msgid "ROI data" -msgstr "Données de la ROI" - -#: cdl\computation\base.py:161 -msgid "" -"For convenience, this item accepts a 2D NumPy array, a list of list of " -"numbers, or None. In the end, the data is converted to a 2D NumPy array of " -"integers (if not None)." -msgstr "" -"Pour plus de commodité, cet élément accepte un tableau NumPy 2D, une liste " -"de listes de nombres, ou None. En fin de compte, les données sont converties " -"en un tableau NumPy 2D d'entiers (si non nul)." - -#: cdl\computation\base.py:168 -msgid "Single object" -msgstr "Objet unique" - -#: cdl\computation\base.py:169 -msgid "Whether to extract the ROI as a single object or not." -msgstr "Extrait la ROI comme un objet unique ou non." - -#: cdl\computation\base.py:181 +#: cdl\computation\base.py:159 msgid "Shift" msgstr "Décalage" -#: cdl\computation\base.py:181 +#: cdl\computation\base.py:159 msgid "Shift zero frequency to center" msgstr "Décaler la fréquence nulle au centre" -#: cdl\computation\base.py:187 cdl\core\model\image.py:421 -#: cdl\core\model\image.py:430 cdl\core\model\signal.py:184 -#: cdl\core\model\signal.py:193 +#: cdl\computation\base.py:165 cdl\core\model\image.py:954 +#: cdl\core\model\image.py:963 cdl\core\model\signal.py:353 +#: cdl\core\model\signal.py:362 msgid "Logarithmic scale" msgstr "Echelle logarithmique" -#: cdl\computation\base.py:193 +#: cdl\computation\base.py:171 msgid "Constant value" msgstr "Constante" -#: cdl\computation\image\__init__.py:363 cdl\computation\signal.py:531 +#: cdl\computation\image\__init__.py:361 cdl\computation\signal.py:531 msgid "Threshold" msgstr "Seuil" -#: cdl\computation\image\__init__.py:433 +#: cdl\computation\image\__init__.py:431 msgid "Angle" msgstr "Angle" -#: cdl\computation\image\__init__.py:438 cdl\computation\image\__init__.py:633 +#: cdl\computation\image\__init__.py:436 cdl\computation\image\__init__.py:631 msgid "cval" msgstr "cval" -#: cdl\computation\image\__init__.py:440 cdl\computation\image\__init__.py:635 +#: cdl\computation\image\__init__.py:438 cdl\computation\image\__init__.py:633 msgid "" "Value used for points outside the boundaries of the input if mode is " "'constant'" @@ -174,11 +152,11 @@ msgstr "" "Valeur utilisée pour les points situés en dehors des frontières de l'image " "d'origine (si le mode est 'constant')" -#: cdl\computation\image\__init__.py:447 +#: cdl\computation\image\__init__.py:445 msgid "Reshape the output array" msgstr "Redimensionner l'image de destination" -#: cdl\computation\image\__init__.py:449 +#: cdl\computation\image\__init__.py:447 msgid "" "Reshape the output array so that the input array is contained completely in " "the output" @@ -186,209 +164,209 @@ msgstr "" "Redimensionner l'image de destination de sorte qu'elle puisse contenir la " "totalité de l'image source" -#: cdl\computation\image\__init__.py:455 cdl\computation\image\__init__.py:641 +#: cdl\computation\image\__init__.py:453 cdl\computation\image\__init__.py:639 msgid "Prefilter the input image" msgstr "Prétraitement de l'image d'origine (spline)" -#: cdl\computation\image\__init__.py:459 cdl\computation\image\__init__.py:645 -#: cdl\computation\image\__init__.py:1326 +#: cdl\computation\image\__init__.py:457 cdl\computation\image\__init__.py:643 +#: cdl\computation\image\__init__.py:1321 msgid "Order" msgstr "Ordre" -#: cdl\computation\image\__init__.py:463 cdl\computation\image\__init__.py:649 +#: cdl\computation\image\__init__.py:461 cdl\computation\image\__init__.py:647 msgid "Spline interpolation order" msgstr "Ordre de l'interpolation de type spline" -#: cdl\computation\image\__init__.py:608 +#: cdl\computation\image\__init__.py:606 msgid "columns" msgstr "colonnes" -#: cdl\computation\image\__init__.py:608 +#: cdl\computation\image\__init__.py:606 msgid "rows" msgstr "lignes" -#: cdl\computation\image\__init__.py:609 +#: cdl\computation\image\__init__.py:607 msgid "Distribute over" msgstr "Distribuer selon les" -#: cdl\computation\image\__init__.py:612 +#: cdl\computation\image\__init__.py:610 msgid "Columns" msgstr "Colonnes" -#: cdl\computation\image\__init__.py:615 +#: cdl\computation\image\__init__.py:613 msgid "Rows" msgstr "Lignes" -#: cdl\computation\image\__init__.py:618 +#: cdl\computation\image\__init__.py:616 msgid "Column spacing" msgstr "Espace entre chaque colonne" -#: cdl\computation\image\__init__.py:619 +#: cdl\computation\image\__init__.py:617 msgid "Row spacing" msgstr "Espace entre chaque ligne" -#: cdl\computation\image\__init__.py:628 +#: cdl\computation\image\__init__.py:626 msgid "Zoom" msgstr "Zoom" -#: cdl\computation\image\__init__.py:681 +#: cdl\computation\image\__init__.py:679 msgid "Cluster size (X)" msgstr "Nombre de pixels (X)" -#: cdl\computation\image\__init__.py:684 +#: cdl\computation\image\__init__.py:682 msgid "Number of adjacent pixels to be combined together along X-axis." msgstr "Nombre de pixels adjacents à regrouper le long de l'axe des X." -#: cdl\computation\image\__init__.py:687 +#: cdl\computation\image\__init__.py:685 msgid "Cluster size (Y)" msgstr "Nombre de pixels (Y)" -#: cdl\computation\image\__init__.py:690 +#: cdl\computation\image\__init__.py:688 msgid "Number of adjacent pixels to be combined together along Y-axis." msgstr "Nombre de pixels adjacents à regrouper le long de l'axe des Y." -#: cdl\computation\image\__init__.py:700 cdl\core\model\image.py:844 +#: cdl\computation\image\__init__.py:698 cdl\core\model\image.py:1311 #: cdl\widgets\textimport.py:209 cdl\widgets\textimport.py:232 msgid "Data type" msgstr "Type de données" -#: cdl\computation\image\__init__.py:702 cdl\computation\image\__init__.py:1068 +#: cdl\computation\image\__init__.py:700 cdl\computation\image\__init__.py:1063 #: cdl\computation\signal.py:450 cdl\widgets\textimport.py:234 msgid "Output image data type." msgstr "Type de données de l'image générée." -#: cdl\computation\image\__init__.py:705 +#: cdl\computation\image\__init__.py:703 msgid "Change pixel size" msgstr "Modifier la taille des pixels" -#: cdl\computation\image\__init__.py:707 +#: cdl\computation\image\__init__.py:705 msgid "Change pixel size so that overall image size remains the same." msgstr "" "Modification de la taille des pixels de sorte que les dimensions de l'images " "restent les mêmes après l'opération." -#: cdl\computation\image\__init__.py:807 cdl\computation\image\__init__.py:879 +#: cdl\computation\image\__init__.py:802 cdl\computation\image\__init__.py:874 msgid "horizontal" msgstr "horizontal" -#: cdl\computation\image\__init__.py:807 cdl\computation\image\__init__.py:879 +#: cdl\computation\image\__init__.py:802 cdl\computation\image\__init__.py:874 msgid "vertical" msgstr "vertical" -#: cdl\computation\image\__init__.py:808 cdl\computation\image\__init__.py:880 +#: cdl\computation\image\__init__.py:803 cdl\computation\image\__init__.py:875 msgid "Direction" msgstr "Direction" -#: cdl\computation\image\__init__.py:811 +#: cdl\computation\image\__init__.py:806 msgid "Row" msgstr "Ligne" -#: cdl\computation\image\__init__.py:814 +#: cdl\computation\image\__init__.py:809 msgid "Column" msgstr "Colonne" -#: cdl\computation\image\__init__.py:847 +#: cdl\computation\image\__init__.py:842 msgid "Start row" msgstr "Ligne (début)" -#: cdl\computation\image\__init__.py:848 +#: cdl\computation\image\__init__.py:843 msgid "Start column" msgstr "Colonne (début)" -#: cdl\computation\image\__init__.py:849 +#: cdl\computation\image\__init__.py:844 msgid "End row" msgstr "Ligne (fin)" -#: cdl\computation\image\__init__.py:850 +#: cdl\computation\image\__init__.py:845 msgid "End column" msgstr "Colonne (fin)" -#: cdl\computation\image\__init__.py:881 cdl\computation\image\__init__.py:886 +#: cdl\computation\image\__init__.py:876 cdl\computation\image\__init__.py:881 msgid "Profile rectangular area" msgstr "Zone rectangulaire du profil" -#: cdl\computation\image\__init__.py:882 +#: cdl\computation\image\__init__.py:877 msgid "Row 1" msgstr "Ligne 1" -#: cdl\computation\image\__init__.py:883 +#: cdl\computation\image\__init__.py:878 msgid "Row 2" msgstr "Ligne 2" -#: cdl\computation\image\__init__.py:884 +#: cdl\computation\image\__init__.py:879 msgid "Column 1" msgstr "Colonne 1" -#: cdl\computation\image\__init__.py:885 +#: cdl\computation\image\__init__.py:880 msgid "Column 2" msgstr "Colonne 2" -#: cdl\computation\image\__init__.py:945 +#: cdl\computation\image\__init__.py:940 msgid "Center position" msgstr "Position du centre" -#: cdl\computation\image\__init__.py:947 +#: cdl\computation\image\__init__.py:942 msgid "Image centroid" msgstr "Barycentre de l'image" -#: cdl\computation\image\__init__.py:948 +#: cdl\computation\image\__init__.py:943 msgid "Image center" msgstr "Centre de l'image" -#: cdl\computation\image\__init__.py:949 +#: cdl\computation\image\__init__.py:944 msgid "User-defined" msgstr "Défini par l'utilisateur" -#: cdl\computation\image\__init__.py:955 +#: cdl\computation\image\__init__.py:950 msgid "Center" msgstr "Centre" -#: cdl\computation\image\__init__.py:1004 cdl\computation\signal.py:1003 +#: cdl\computation\image\__init__.py:999 cdl\computation\signal.py:1003 msgid "Counts" msgstr "Coups" -#: cdl\computation\image\__init__.py:1066 cdl\computation\signal.py:448 +#: cdl\computation\image\__init__.py:1061 cdl\computation\signal.py:448 msgid "Destination data type" msgstr "Type de données de destination" -#: cdl\computation\image\__init__.py:1229 -#: cdl\computation\image\__init__.py:1230 -#: cdl\computation\image\__init__.py:1271 -#: cdl\computation\image\__init__.py:1287 -#: cdl\computation\image\__init__.py:1306 cdl\computation\signal.py:885 +#: cdl\computation\image\__init__.py:1224 +#: cdl\computation\image\__init__.py:1225 +#: cdl\computation\image\__init__.py:1266 +#: cdl\computation\image\__init__.py:1282 +#: cdl\computation\image\__init__.py:1301 cdl\computation\signal.py:885 #: cdl\computation\signal.py:925 cdl\computation\signal.py:944 -#: cdl\computation\signal.py:966 cdl\core\model\signal.py:694 +#: cdl\computation\signal.py:966 cdl\core\model\signal.py:764 #: cdl\widgets\fitdialog.py:308 msgid "Frequency" msgstr "Fréquence" -#: cdl\computation\image\__init__.py:1314 -#: cdl\computation\image\__init__.py:1318 +#: cdl\computation\image\__init__.py:1309 +#: cdl\computation\image\__init__.py:1313 msgid "Cut-off frequency ratio" msgstr "Fréquence de coupure relative" -#: cdl\computation\image\__init__.py:1321 cdl\core\gui\actionhandler.py:878 -#: cdl\core\gui\processor\signal.py:371 +#: cdl\computation\image\__init__.py:1316 cdl\core\gui\actionhandler.py:878 +#: cdl\core\gui\processor\signal.py:358 msgid "High-pass filter" msgstr "Filtre passe-haut" -#: cdl\computation\image\__init__.py:1323 +#: cdl\computation\image\__init__.py:1318 msgid "If True, apply high-pass filter instead of low-pass" msgstr "Si vrai, appliquer un filtre passe-haut au lieu d'un filtre passe-bas" -#: cdl\computation\image\__init__.py:1329 +#: cdl\computation\image\__init__.py:1324 msgid "Order of the Butterworth filter" msgstr "Ordre du filtre de Butterworth" -#: cdl\computation\image\__init__.py:1518 +#: cdl\computation\image\__init__.py:1513 msgid "Radiusmin" msgstr "Rayonmin" -#: cdl\computation\image\__init__.py:1521 +#: cdl\computation\image\__init__.py:1516 msgid "Radiusmax" msgstr "Rayonmax" -#: cdl\computation\image\__init__.py:1523 +#: cdl\computation\image\__init__.py:1518 msgid "Minimal distance" msgstr "Distance minimale" @@ -413,7 +391,7 @@ msgstr "" "Taille de la fenêtre glissante utilisée dans l'algorithme de filtrage " "maximum/minimum" -#: cdl\computation\image\detection.py:52 cdl\core\gui\processor\image.py:1148 +#: cdl\computation\image\detection.py:52 cdl\core\gui\processor\image.py:1142 msgid "Create regions of interest" msgstr "Créer des régions d'intérêt" @@ -421,7 +399,7 @@ msgstr "Créer des régions d'intérêt" msgid "Ellipse" msgstr "Ellipse" -#: cdl\computation\image\detection.py:78 cdl\core\model\image.py:99 +#: cdl\computation\image\detection.py:78 msgid "Circle" msgstr "Cercle" @@ -781,7 +759,7 @@ msgstr "" msgid "Footprint (disk) radius." msgstr "Rayon de l'ouverture circulaire (disque)." -#: cdl\computation\image\morphology.py:29 cdl\core\model\image.py:237 +#: cdl\computation\image\morphology.py:29 cdl\core\model\image.py:101 #: cdl\tests\data.py:423 msgid "Radius" msgstr "Rayon" @@ -842,18 +820,18 @@ msgstr "Méthode" msgid "Manual" msgstr "Manuel" -#: cdl\computation\image\threshold.py:33 cdl\core\gui\processor\image.py:634 +#: cdl\computation\image\threshold.py:33 cdl\core\gui\processor\image.py:617 #: cdl\tests\data.py:75 cdl\widgets\fitdialog.py:125 #: cdl\widgets\fitdialog.py:153 cdl\widgets\fitdialog.py:181 #: cdl\widgets\fitdialog.py:345 msgid "Mean" msgstr "Moyenne" -#: cdl\computation\image\threshold.py:34 cdl\core\gui\processor\image.py:640 +#: cdl\computation\image\threshold.py:34 cdl\core\gui\processor\image.py:623 msgid "Minimum" msgstr "Minimum" -#: cdl\computation\image\threshold.py:36 cdl\core\gui\processor\image.py:652 +#: cdl\computation\image\threshold.py:36 cdl\core\gui\processor\image.py:635 msgid "Triangle" msgstr "Triangle" @@ -881,19 +859,19 @@ msgstr "Puissance" msgid "Minimum distance" msgstr "Distance minimale" -#: cdl\computation\signal.py:608 cdl\core\model\image.py:399 -#: cdl\core\model\image.py:402 cdl\core\model\image.py:420 -#: cdl\core\model\image.py:428 cdl\core\model\signal.py:166 -#: cdl\core\model\signal.py:169 cdl\core\model\signal.py:183 -#: cdl\core\model\signal.py:191 +#: cdl\computation\signal.py:608 cdl\core\model\image.py:932 +#: cdl\core\model\image.py:935 cdl\core\model\image.py:953 +#: cdl\core\model\image.py:961 cdl\core\model\signal.py:335 +#: cdl\core\model\signal.py:338 cdl\core\model\signal.py:352 +#: cdl\core\model\signal.py:360 msgid "X-axis" msgstr "Axe des X" -#: cdl\computation\signal.py:608 cdl\core\model\image.py:403 -#: cdl\core\model\image.py:406 cdl\core\model\image.py:429 -#: cdl\core\model\image.py:437 cdl\core\model\signal.py:170 -#: cdl\core\model\signal.py:173 cdl\core\model\signal.py:192 -#: cdl\core\model\signal.py:200 +#: cdl\computation\signal.py:608 cdl\core\model\image.py:936 +#: cdl\core\model\image.py:939 cdl\core\model\image.py:962 +#: cdl\core\model\image.py:970 cdl\core\model\signal.py:339 +#: cdl\core\model\signal.py:342 cdl\core\model\signal.py:361 +#: cdl\core\model\signal.py:369 msgid "Y-axis" msgstr "Axe des Y" @@ -1011,16 +989,16 @@ msgid "Cosine" msgstr "Cosinus" #: cdl\computation\signal.py:1158 cdl\core\gui\actionhandler.py:690 -#: cdl\core\gui\processor\image.py:382 cdl\core\gui\processor\signal.py:134 +#: cdl\core\gui\processor\image.py:362 cdl\core\gui\processor\signal.py:116 msgid "Exponential" msgstr "Exponentielle" #: cdl\computation\signal.py:1159 msgid "Flat top" -msgstr "" +msgstr "Sommet plat" -#: cdl\computation\signal.py:1160 cdl\core\model\signal.py:925 -#: cdl\tests\scenarios\common.py:216 cdl\tests\scenarios\demo.py:83 +#: cdl\computation\signal.py:1160 cdl\core\model\signal.py:995 +#: cdl\tests\scenarios\common.py:217 cdl\tests\scenarios\demo.py:83 msgid "Gaussian" msgstr "Gaussienne" @@ -1045,17 +1023,17 @@ msgid "Zero-crossing" msgstr "Passage par zéro" #: cdl\computation\signal.py:1297 cdl\core\gui\actionhandler.py:899 -#: cdl\tests\scenarios\common.py:205 cdl\widgets\fitdialog.py:133 +#: cdl\tests\scenarios\common.py:206 cdl\widgets\fitdialog.py:133 msgid "Gaussian fit" msgstr "Ajustement gaussien" #: cdl\computation\signal.py:1298 cdl\core\gui\actionhandler.py:900 -#: cdl\tests\scenarios\common.py:206 cdl\widgets\fitdialog.py:161 +#: cdl\tests\scenarios\common.py:207 cdl\widgets\fitdialog.py:161 msgid "Lorentzian fit" msgstr "Ajustement lorentzien" #: cdl\computation\signal.py:1299 cdl\core\gui\actionhandler.py:901 -#: cdl\tests\scenarios\common.py:207 cdl\widgets\fitdialog.py:189 +#: cdl\tests\scenarios\common.py:208 cdl\widgets\fitdialog.py:189 msgid "Voigt fit" msgstr "Ajustement Voigt" @@ -1077,10 +1055,10 @@ msgstr "Pleine échelle" msgid "Unit for SINAD" msgstr "Unité pour SINAD" -#: cdl\computation\signal.py:1393 cdl\core\model\image.py:401 -#: cdl\core\model\image.py:405 cdl\core\model\image.py:409 -#: cdl\core\model\signal.py:168 cdl\core\model\signal.py:172 -#: cdl\core\model\signal.py:696 +#: cdl\computation\signal.py:1393 cdl\core\model\image.py:934 +#: cdl\core\model\image.py:938 cdl\core\model\image.py:942 +#: cdl\core\model\signal.py:337 cdl\core\model\signal.py:341 +#: cdl\core\model\signal.py:766 msgid "Unit" msgstr "Unité" @@ -1173,7 +1151,7 @@ msgstr "Supprimer" msgid "Remove selected %s" msgstr "Supprimer l'objet %s sélectionné" -#: cdl\core\gui\actionhandler.py:487 cdl\core\gui\panel\base.py:563 +#: cdl\core\gui\actionhandler.py:487 cdl\core\gui\panel\base.py:570 msgid "Delete all" msgstr "Supprimer tout" @@ -1197,7 +1175,7 @@ msgstr "Coller les métadonnées" msgid "Paste metadata into selected %s" msgstr "Coller les métadonnées dans l'objet %s sélectionné" -#: cdl\core\gui\actionhandler.py:512 cdl\core\gui\panel\base.py:814 +#: cdl\core\gui\actionhandler.py:512 cdl\core\gui\panel\base.py:835 msgid "Import metadata" msgstr "Importer les métadonnées" @@ -1205,7 +1183,7 @@ msgstr "Importer les métadonnées" msgid "Import metadata into %s" msgstr "Importer les métadonnées dans l'objet %s" -#: cdl\core\gui\actionhandler.py:520 cdl\core\gui\panel\base.py:837 +#: cdl\core\gui\actionhandler.py:520 cdl\core\gui\panel\base.py:858 msgid "Export metadata" msgstr "Exporter les métadonnées" @@ -1270,33 +1248,33 @@ msgid "Refresh plot, even if auto-refresh is enabled" msgstr "" "Rafraîchit le graphique, même si le rafraîchissement automatique est activé" -#: cdl\core\gui\actionhandler.py:601 cdl\core\gui\processor\image.py:53 -#: cdl\core\gui\processor\signal.py:36 +#: cdl\core\gui\actionhandler.py:601 cdl\core\gui\processor\image.py:58 +#: cdl\core\gui\processor\signal.py:37 msgid "Sum" msgstr "Addition" -#: cdl\core\gui\actionhandler.py:607 cdl\core\gui\processor\image.py:76 -#: cdl\core\gui\processor\signal.py:61 +#: cdl\core\gui\actionhandler.py:607 cdl\core\gui\processor\image.py:81 +#: cdl\core\gui\processor\signal.py:62 msgid "Average" msgstr "Moyenne" -#: cdl\core\gui\actionhandler.py:613 cdl\core\gui\processor\image.py:407 -#: cdl\core\gui\processor\signal.py:169 +#: cdl\core\gui\actionhandler.py:613 cdl\core\gui\processor\image.py:387 +#: cdl\core\gui\processor\signal.py:153 msgid "Difference" msgstr "Soustraction" -#: cdl\core\gui\actionhandler.py:619 cdl\core\gui\processor\image.py:431 -#: cdl\core\gui\processor\signal.py:193 +#: cdl\core\gui\actionhandler.py:619 cdl\core\gui\processor\image.py:413 +#: cdl\core\gui\processor\signal.py:180 msgid "Quadratic difference" msgstr "Soustraction quadratique" -#: cdl\core\gui\actionhandler.py:625 cdl\core\gui\processor\image.py:82 -#: cdl\core\gui\processor\signal.py:67 +#: cdl\core\gui\actionhandler.py:625 cdl\core\gui\processor\image.py:87 +#: cdl\core\gui\processor\signal.py:68 msgid "Product" msgstr "Produit" -#: cdl\core\gui\actionhandler.py:631 cdl\core\gui\processor\image.py:442 -#: cdl\core\gui\processor\signal.py:204 +#: cdl\core\gui\actionhandler.py:631 cdl\core\gui\processor\image.py:424 +#: cdl\core\gui\processor\signal.py:191 msgid "Division" msgstr "Division" @@ -1308,7 +1286,7 @@ msgstr "Opération arithmétique" msgid "Constant Operations" msgstr "Opérations avec une constante" -#: cdl\core\gui\actionhandler.py:644 cdl\core\gui\processor\image.py:63 +#: cdl\core\gui\actionhandler.py:644 cdl\core\gui\processor\image.py:68 msgid "Add constant" msgstr "Additionner une constante" @@ -1324,23 +1302,23 @@ msgstr "Multiplier par une constante" msgid "Divide by constant" msgstr "Diviser par une constante" -#: cdl\core\gui\actionhandler.py:668 cdl\core\gui\processor\image.py:355 -#: cdl\core\gui\processor\signal.py:107 +#: cdl\core\gui\actionhandler.py:668 cdl\core\gui\processor\image.py:335 +#: cdl\core\gui\processor\signal.py:89 msgid "Absolute value" msgstr "Valeur absolue" -#: cdl\core\gui\actionhandler.py:674 cdl\core\gui\processor\image.py:360 -#: cdl\core\gui\processor\signal.py:112 +#: cdl\core\gui\actionhandler.py:674 cdl\core\gui\processor\image.py:340 +#: cdl\core\gui\processor\signal.py:94 msgid "Real part" msgstr "Partie réelle" -#: cdl\core\gui\actionhandler.py:679 cdl\core\gui\processor\image.py:365 -#: cdl\core\gui\processor\signal.py:117 +#: cdl\core\gui\actionhandler.py:679 cdl\core\gui\processor\image.py:345 +#: cdl\core\gui\processor\signal.py:99 msgid "Imaginary part" msgstr "Partie imaginaire" -#: cdl\core\gui\actionhandler.py:684 cdl\core\gui\processor\image.py:371 -#: cdl\core\gui\processor\signal.py:123 +#: cdl\core\gui\actionhandler.py:684 cdl\core\gui\processor\image.py:351 +#: cdl\core\gui\processor\signal.py:105 msgid "Convert data type" msgstr "Convertir le type de données" @@ -1352,8 +1330,8 @@ msgstr "Logarithme (base 10)" msgid "Axis transformation" msgstr "Transformation des axes" -#: cdl\core\gui\actionhandler.py:707 cdl\core\gui\processor\image.py:488 -#: cdl\core\gui\processor\signal.py:271 +#: cdl\core\gui\actionhandler.py:707 cdl\core\gui\processor\image.py:470 +#: cdl\core\gui\processor\signal.py:258 msgid "Linear calibration" msgstr "Étalonnage linéaire" @@ -1365,13 +1343,13 @@ msgstr "Permuter les axes X/Y" msgid "Level adjustment" msgstr "Ajustement de niveau" -#: cdl\core\gui\actionhandler.py:717 cdl\core\gui\processor\image.py:47 -#: cdl\core\gui\processor\signal.py:247 +#: cdl\core\gui\actionhandler.py:717 cdl\core\gui\processor\image.py:52 +#: cdl\core\gui\processor\signal.py:234 msgid "Normalize" msgstr "Normaliser" -#: cdl\core\gui\actionhandler.py:722 cdl\core\gui\processor\image.py:500 -#: cdl\core\gui\processor\signal.py:279 +#: cdl\core\gui\actionhandler.py:722 cdl\core\gui\processor\image.py:482 +#: cdl\core\gui\processor\signal.py:266 msgid "Clipping" msgstr "Écrêtage" @@ -1387,23 +1365,23 @@ msgstr "Évaluer et soustraire la valeur d'offset des données" msgid "Noise reduction" msgstr "Réduction de bruit" -#: cdl\core\gui\actionhandler.py:734 cdl\core\gui\processor\image.py:522 -#: cdl\core\gui\processor\signal.py:303 +#: cdl\core\gui\actionhandler.py:734 cdl\core\gui\processor\image.py:505 +#: cdl\core\gui\processor\signal.py:290 msgid "Gaussian filter" msgstr "Filtre gaussien" -#: cdl\core\gui\actionhandler.py:738 cdl\core\gui\processor\image.py:535 -#: cdl\core\gui\processor\signal.py:316 +#: cdl\core\gui\actionhandler.py:738 cdl\core\gui\processor\image.py:518 +#: cdl\core\gui\processor\signal.py:303 msgid "Moving average" msgstr "Moyenne mobile" -#: cdl\core\gui\actionhandler.py:742 cdl\core\gui\processor\image.py:546 -#: cdl\core\gui\processor\signal.py:327 +#: cdl\core\gui\actionhandler.py:742 cdl\core\gui\processor\image.py:529 +#: cdl\core\gui\processor\signal.py:314 msgid "Moving median" msgstr "Médiane mobile" -#: cdl\core\gui\actionhandler.py:746 cdl\core\gui\processor\image.py:553 -#: cdl\core\gui\processor\signal.py:334 +#: cdl\core\gui\actionhandler.py:746 cdl\core\gui\processor\image.py:536 +#: cdl\core\gui\processor\signal.py:321 msgid "Wiener filter" msgstr "Filtre de Wiener" @@ -1411,7 +1389,7 @@ msgstr "Filtre de Wiener" msgid "Fourier analysis" msgstr "Analyse de Fourier" -#: cdl\core\gui\actionhandler.py:751 cdl\core\gui\processor\signal.py:394 +#: cdl\core\gui\actionhandler.py:751 cdl\core\gui\processor\signal.py:381 msgid "FFT" msgstr "FFT" @@ -1423,12 +1401,12 @@ msgstr "Attention : seulement la partie réelles sera tracée" msgid "Inverse FFT" msgstr "FFT inverse" -#: cdl\core\gui\actionhandler.py:761 cdl\core\gui\processor\image.py:579 -#: cdl\core\gui\processor\signal.py:413 +#: cdl\core\gui\actionhandler.py:761 cdl\core\gui\processor\image.py:562 +#: cdl\core\gui\processor\signal.py:400 msgid "Magnitude spectrum" msgstr "Spectre d'amplitude" -#: cdl\core\gui\actionhandler.py:765 cdl\core\gui\processor\signal.py:420 +#: cdl\core\gui\actionhandler.py:765 cdl\core\gui\processor\signal.py:407 msgid "Phase spectrum" msgstr "Spectre de phase" @@ -1436,13 +1414,13 @@ msgstr "Spectre de phase" msgid "Power spectral density" msgstr "Densité spectrale de puissance" -#: cdl\core\gui\actionhandler.py:775 cdl\core\gui\processor\image.py:1111 -#: cdl\core\gui\processor\signal.py:584 +#: cdl\core\gui\actionhandler.py:775 cdl\core\gui\processor\image.py:1105 +#: cdl\core\gui\processor\signal.py:582 msgid "Statistics" msgstr "Statistiques" -#: cdl\core\gui\actionhandler.py:782 cdl\core\gui\processor\image.py:344 -#: cdl\core\gui\processor\signal.py:593 +#: cdl\core\gui\actionhandler.py:782 cdl\core\gui\processor\image.py:324 +#: cdl\core\gui\processor\signal.py:591 msgid "Histogram" msgstr "Histogramme" @@ -1454,27 +1432,27 @@ msgstr "Extraction de ROI" msgid "Show results" msgstr "Afficher les résultats" -#: cdl\core\gui\actionhandler.py:808 cdl\core\gui\panel\base.py:1267 +#: cdl\core\gui\actionhandler.py:808 cdl\core\gui\panel\base.py:1258 msgid "Plot results" msgstr "Tracer les résultats" -#: cdl\core\gui\actionhandler.py:814 cdl\core\gui\panel\base.py:1314 +#: cdl\core\gui\actionhandler.py:814 cdl\core\gui\panel\base.py:1305 msgid "Delete results" msgstr "Supprimer les résultats" -#: cdl\core\gui\actionhandler.py:824 cdl\core\gui\roieditor.py:236 +#: cdl\core\gui\actionhandler.py:824 cdl\core\gui\roieditor.py:423 msgid "signal" msgstr "signal" -#: cdl\core\gui\actionhandler.py:838 cdl\core\gui\processor\signal.py:139 +#: cdl\core\gui\actionhandler.py:838 cdl\core\gui\processor\signal.py:121 msgid "Square root" msgstr "Racine carrée" -#: cdl\core\gui\actionhandler.py:844 cdl\core\gui\processor\signal.py:254 +#: cdl\core\gui\actionhandler.py:844 cdl\core\gui\processor\signal.py:241 msgid "Derivative" msgstr "Dérivée" -#: cdl\core\gui\actionhandler.py:850 cdl\core\gui\processor\signal.py:259 +#: cdl\core\gui\actionhandler.py:850 cdl\core\gui\processor\signal.py:246 msgid "Integral" msgstr "Intégrale" @@ -1486,15 +1464,15 @@ msgstr "Inverser l'axe des X" msgid "Frequency filters" msgstr "Filtres fréquentiels" -#: cdl\core\gui\actionhandler.py:873 cdl\core\gui\processor\signal.py:363 +#: cdl\core\gui\actionhandler.py:873 cdl\core\gui\processor\signal.py:350 msgid "Low-pass filter" msgstr "Filtre passe-bas" -#: cdl\core\gui\actionhandler.py:883 cdl\core\gui\processor\signal.py:379 +#: cdl\core\gui\actionhandler.py:883 cdl\core\gui\processor\signal.py:366 msgid "Band-pass filter" msgstr "Filtre passe-bande" -#: cdl\core\gui\actionhandler.py:888 cdl\core\gui\processor\signal.py:387 +#: cdl\core\gui\actionhandler.py:888 cdl\core\gui\processor\signal.py:374 msgid "Band-stop filter" msgstr "Filtre coupe-bande" @@ -1502,30 +1480,30 @@ msgstr "Filtre coupe-bande" msgid "Fitting" msgstr "Ajustement" -#: cdl\core\gui\actionhandler.py:893 cdl\tests\scenarios\common.py:208 +#: cdl\core\gui\actionhandler.py:893 cdl\tests\scenarios\common.py:209 msgid "Linear fit" msgstr "Ajustement linéaire" -#: cdl\core\gui\actionhandler.py:895 cdl\core\gui\processor\signal.py:506 +#: cdl\core\gui\actionhandler.py:895 cdl\core\gui\processor\signal.py:493 msgid "Polynomial fit" msgstr "Ajustement polynomial" -#: cdl\core\gui\actionhandler.py:903 cdl\core\gui\processor\signal.py:565 +#: cdl\core\gui\actionhandler.py:903 cdl\core\gui\processor\signal.py:552 #: cdl\widgets\fitdialog.py:252 msgid "Multi-Gaussian fit" msgstr "Ajustement multi-gaussien" -#: cdl\core\gui\actionhandler.py:907 cdl\tests\scenarios\common.py:209 +#: cdl\core\gui\actionhandler.py:907 cdl\tests\scenarios\common.py:210 #: cdl\widgets\fitdialog.py:285 msgid "Exponential fit" msgstr "Ajustement exponentiel" -#: cdl\core\gui\actionhandler.py:908 cdl\tests\scenarios\common.py:211 +#: cdl\core\gui\actionhandler.py:908 cdl\tests\scenarios\common.py:212 #: cdl\widgets\fitdialog.py:321 msgid "Sinusoidal fit" msgstr "Ajustement sinusoïdal" -#: cdl\core\gui\actionhandler.py:910 cdl\tests\scenarios\common.py:210 +#: cdl\core\gui\actionhandler.py:910 cdl\tests\scenarios\common.py:211 #: cdl\widgets\fitdialog.py:361 msgid "CDF fit" msgstr "Ajustement CDF" @@ -1535,7 +1513,7 @@ msgid "Cumulative distribution function fit, related to Error function (erf)" msgstr "" "Fonction de distribution cumulative (CDF), liée à la fonction d'erreur (erf)" -#: cdl\core\gui\actionhandler.py:919 cdl\core\gui\processor\signal.py:498 +#: cdl\core\gui\actionhandler.py:919 cdl\core\gui\processor\signal.py:485 msgid "Windowing" msgstr "Fenêtrage" @@ -1545,16 +1523,16 @@ msgstr "" "Appliquer une fonction de fenêtrage (ou d'apodisation) : Hanning, " "Hamming, ..." -#: cdl\core\gui\actionhandler.py:927 cdl\core\gui\processor\signal.py:476 +#: cdl\core\gui\actionhandler.py:927 cdl\core\gui\processor\signal.py:463 msgid "Detrending" msgstr "Elimination de tendance" -#: cdl\core\gui\actionhandler.py:932 cdl\core\gui\processor\signal.py:442 +#: cdl\core\gui\actionhandler.py:932 cdl\core\gui\processor\signal.py:429 msgid "Interpolation" msgstr "Interpolation" -#: cdl\core\gui\actionhandler.py:937 cdl\core\gui\processor\signal.py:449 -#: cdl\core\gui\processor\signal.py:464 +#: cdl\core\gui\actionhandler.py:937 cdl\core\gui\processor\signal.py:436 +#: cdl\core\gui\processor\signal.py:451 msgid "Resampling" msgstr "Rééchantillonnage" @@ -1582,12 +1560,12 @@ msgstr "Valeurs de X aux min/max" msgid "Compute X values at signal minimum and maximum" msgstr "Calcule les valeurs de X aux minima et maxima du signal" -#: cdl\core\gui\actionhandler.py:962 cdl\core\gui\processor\image.py:1134 -#: cdl\core\gui\processor\image.py:1144 cdl\core\gui\processor\signal.py:226 +#: cdl\core\gui\actionhandler.py:962 cdl\core\gui\processor\image.py:1128 +#: cdl\core\gui\processor\image.py:1138 cdl\core\gui\processor\signal.py:213 msgid "Peak detection" msgstr "Détection de pics" -#: cdl\core\gui\actionhandler.py:968 cdl\core\gui\processor\signal.py:612 +#: cdl\core\gui\actionhandler.py:968 cdl\core\gui\processor\signal.py:610 msgid "Sampling rate and period" msgstr "Fréquence et période d'échantillonnage" @@ -1597,7 +1575,7 @@ msgstr "" "Calcule la fréquence et la période d'échantillonnage pour un signal à pas " "constant" -#: cdl\core\gui\actionhandler.py:976 cdl\core\gui\processor\signal.py:631 +#: cdl\core\gui\actionhandler.py:976 cdl\core\gui\processor\signal.py:629 msgid "Dynamic parameters" msgstr "Paramètres dynamiques" @@ -1616,7 +1594,7 @@ msgstr "" "Calcule la bande passante à -3dB en supposant un filtre passe-bas déjà " "exprimé en dB" -#: cdl\core\gui\actionhandler.py:991 cdl\core\gui\processor\signal.py:599 +#: cdl\core\gui\actionhandler.py:991 cdl\core\gui\processor\signal.py:597 msgid "Contrast" msgstr "Contraste" @@ -1648,12 +1626,12 @@ msgstr "" "Les styles de courbes sont parcourus dans une liste de styles prédéfinis.\n" "Cette action réinitialise la liste à son état initial." -#: cdl\core\gui\actionhandler.py:1024 cdl\core\gui\processor\signal.py:487 +#: cdl\core\gui\actionhandler.py:1024 cdl\core\gui\processor\signal.py:474 msgid "Convolution" msgstr "Convolution" -#: cdl\core\gui\actionhandler.py:1035 cdl\core\gui\main.py:1631 -#: cdl\core\gui\roieditor.py:273 +#: cdl\core\gui\actionhandler.py:1035 cdl\core\gui\main.py:1645 +#: cdl\core\gui\roieditor.py:462 msgid "image" msgstr "image" @@ -1785,7 +1763,7 @@ msgstr "Appliquer toutes les méthodes de seuillage" msgid "Exposure" msgstr "Exposition" -#: cdl\core\gui\actionhandler.py:1180 cdl\core\gui\processor\image.py:698 +#: cdl\core\gui\actionhandler.py:1180 cdl\core\gui\processor\image.py:681 msgid "Gamma correction" msgstr "Correction gamma" @@ -1793,15 +1771,15 @@ msgstr "Correction gamma" msgid "Logarithmic correction" msgstr "Correction logarithmique" -#: cdl\core\gui\actionhandler.py:1188 cdl\core\gui\processor\image.py:723 +#: cdl\core\gui\actionhandler.py:1188 cdl\core\gui\processor\image.py:706 msgid "Sigmoid correction" msgstr "Correction sigmoïde" -#: cdl\core\gui\actionhandler.py:1192 cdl\core\gui\processor\image.py:750 +#: cdl\core\gui\actionhandler.py:1192 cdl\core\gui\processor\image.py:733 msgid "Histogram equalization" msgstr "Egalisation d'histogramme" -#: cdl\core\gui\actionhandler.py:1196 cdl\core\gui\processor\image.py:764 +#: cdl\core\gui\actionhandler.py:1196 cdl\core\gui\processor\image.py:747 msgid "Adaptive histogram equalization" msgstr "Egalisation d'histogramme adaptative" @@ -1813,18 +1791,18 @@ msgstr "Ajustement des niveaux" msgid "Restoration" msgstr "Restauration" -#: cdl\core\gui\actionhandler.py:1205 cdl\core\gui\processor\image.py:775 -#: cdl\core\gui\processor\image.py:841 +#: cdl\core\gui\actionhandler.py:1205 cdl\core\gui\processor\image.py:758 +#: cdl\core\gui\processor\image.py:824 msgid "Total variation denoising" msgstr "Filtrage variationnel (débruitage)" -#: cdl\core\gui\actionhandler.py:1209 cdl\core\gui\processor\image.py:789 -#: cdl\core\gui\processor\image.py:842 +#: cdl\core\gui\actionhandler.py:1209 cdl\core\gui\processor\image.py:772 +#: cdl\core\gui\processor\image.py:825 msgid "Bilateral filter denoising" msgstr "Filtrage bilatéral (débruitage)" -#: cdl\core\gui\actionhandler.py:1213 cdl\core\gui\processor\image.py:803 -#: cdl\core\gui\processor\image.py:843 +#: cdl\core\gui\actionhandler.py:1213 cdl\core\gui\processor\image.py:786 +#: cdl\core\gui\processor\image.py:826 msgid "Wavelet denoising" msgstr "Débruitage par ondelettes" @@ -1880,59 +1858,59 @@ msgstr "Appliquer toutes les opérations morphologiques" msgid "Edges" msgstr "Contours" -#: cdl\core\gui\actionhandler.py:1259 cdl\core\gui\processor\image.py:964 +#: cdl\core\gui\actionhandler.py:1259 cdl\core\gui\processor\image.py:947 msgid "Roberts filter" msgstr "Filtre de Roberts" -#: cdl\core\gui\actionhandler.py:1262 cdl\core\gui\processor\image.py:970 +#: cdl\core\gui\actionhandler.py:1262 cdl\core\gui\processor\image.py:953 msgid "Prewitt filter" msgstr "Filtre de Prewitt" -#: cdl\core\gui\actionhandler.py:1267 cdl\core\gui\processor\image.py:978 +#: cdl\core\gui\actionhandler.py:1267 cdl\core\gui\processor\image.py:961 msgid "Prewitt filter (horizontal)" msgstr "Filtre de Prewitt (horizontal)" -#: cdl\core\gui\actionhandler.py:1271 cdl\core\gui\processor\image.py:987 +#: cdl\core\gui\actionhandler.py:1271 cdl\core\gui\processor\image.py:970 msgid "Prewitt filter (vertical)" msgstr "Filtre de Prewitt (vertical)" -#: cdl\core\gui\actionhandler.py:1275 cdl\core\gui\processor\image.py:994 +#: cdl\core\gui\actionhandler.py:1275 cdl\core\gui\processor\image.py:977 msgid "Sobel filter" msgstr "Filtre de Sobel" -#: cdl\core\gui\actionhandler.py:1280 cdl\core\gui\processor\image.py:1002 +#: cdl\core\gui\actionhandler.py:1280 cdl\core\gui\processor\image.py:985 msgid "Sobel filter (horizontal)" msgstr "Filtre de Sobel (horizontal)" -#: cdl\core\gui\actionhandler.py:1284 cdl\core\gui\processor\image.py:1011 +#: cdl\core\gui\actionhandler.py:1284 cdl\core\gui\processor\image.py:994 msgid "Sobel filter (vertical)" msgstr "Filtre de Sobel (vertical)" -#: cdl\core\gui\actionhandler.py:1288 cdl\core\gui\processor\image.py:1018 +#: cdl\core\gui\actionhandler.py:1288 cdl\core\gui\processor\image.py:1001 msgid "Scharr filter" msgstr "Filtre de Scharr" -#: cdl\core\gui\actionhandler.py:1293 cdl\core\gui\processor\image.py:1026 +#: cdl\core\gui\actionhandler.py:1293 cdl\core\gui\processor\image.py:1009 msgid "Scharr filter (horizontal)" msgstr "Filtre de Scharr (horizontal)" -#: cdl\core\gui\actionhandler.py:1297 cdl\core\gui\processor\image.py:1035 +#: cdl\core\gui\actionhandler.py:1297 cdl\core\gui\processor\image.py:1018 msgid "Scharr filter (vertical)" msgstr "Filtre de Scharr (vertical)" -#: cdl\core\gui\actionhandler.py:1301 cdl\core\gui\processor\image.py:1042 +#: cdl\core\gui\actionhandler.py:1301 cdl\core\gui\processor\image.py:1025 msgid "Farid filter" msgstr "Filtre de Farid" -#: cdl\core\gui\actionhandler.py:1306 cdl\core\gui\processor\image.py:1050 +#: cdl\core\gui\actionhandler.py:1306 cdl\core\gui\processor\image.py:1033 msgid "Farid filter (horizontal)" msgstr "Filtre de Farid (horizontal)" -#: cdl\core\gui\actionhandler.py:1310 cdl\core\gui\processor\image.py:1059 +#: cdl\core\gui\actionhandler.py:1310 cdl\core\gui\processor\image.py:1042 msgid "Farid filter (vertical)" msgstr "Filtre de Farid (vertical)" -#: cdl\core\gui\actionhandler.py:1314 cdl\core\gui\processor\image.py:1066 +#: cdl\core\gui\actionhandler.py:1314 cdl\core\gui\processor\image.py:1049 msgid "Laplace filter" msgstr "Filtre de Laplace" @@ -1944,15 +1922,15 @@ msgstr "Tous les filtres de contours" msgid "Compute all edges filters" msgstr "Calcule tous les filtres de contours" -#: cdl\core\gui\actionhandler.py:1325 cdl\core\gui\processor\image.py:957 +#: cdl\core\gui\actionhandler.py:1325 cdl\core\gui\processor\image.py:940 msgid "Canny filter" msgstr "Filtre de Canny" -#: cdl\core\gui\actionhandler.py:1328 cdl\core\gui\processor\image.py:604 +#: cdl\core\gui\actionhandler.py:1328 cdl\core\gui\processor\image.py:587 msgid "Butterworth filter" msgstr "Filtre de Butterworth" -#: cdl\core\gui\actionhandler.py:1336 cdl\core\gui\processor\image.py:1117 +#: cdl\core\gui\actionhandler.py:1336 cdl\core\gui\processor\image.py:1111 #: cdl\tests\features\images\centroid_unit_test.py:62 #: cdl\tests\features\images\enclosingcircle_unit_test.py:30 msgid "Centroid" @@ -1998,7 +1976,7 @@ msgstr "Détection de formes circulaires à partir d'une tranformée de Hough" msgid "Blob detection" msgstr "Détection de taches" -#: cdl\core\gui\actionhandler.py:1365 cdl\core\gui\processor\image.py:1213 +#: cdl\core\gui\actionhandler.py:1365 cdl\core\gui\processor\image.py:1203 msgid "Blob detection (DOG)" msgstr "Détection de taches (gaussien)" @@ -2006,7 +1984,7 @@ msgstr "Détection de taches (gaussien)" msgid "Detect blobs using Difference of Gaussian (DOG) method" msgstr "Détection de taches basée sur la méthode du discriminant gaussien" -#: cdl\core\gui\actionhandler.py:1370 cdl\core\gui\processor\image.py:1226 +#: cdl\core\gui\actionhandler.py:1370 cdl\core\gui\processor\image.py:1216 msgid "Blob detection (DOH)" msgstr "Détection de taches (hessien)" @@ -2014,7 +1992,7 @@ msgstr "Détection de taches (hessien)" msgid "Detect blobs using Determinant of Hessian (DOH) method" msgstr "Détection de taches basée sur la méthode du discriminant hessien" -#: cdl\core\gui\actionhandler.py:1375 cdl\core\gui\processor\image.py:1239 +#: cdl\core\gui\actionhandler.py:1375 cdl\core\gui\processor\image.py:1229 msgid "Blob detection (LOG)" msgstr "Détection de taches (LOG)" @@ -2023,7 +2001,7 @@ msgid "Detect blobs using Laplacian of Gaussian (LOG) method" msgstr "" "Détection de taches basée sur la méthode du laplacien de gaussien (LOG)" -#: cdl\core\gui\actionhandler.py:1380 cdl\core\gui\processor\image.py:1253 +#: cdl\core\gui\actionhandler.py:1380 cdl\core\gui\processor\image.py:1243 msgid "Blob detection (OpenCV)" msgstr "Détection de taches (OpenCV)" @@ -2031,8 +2009,8 @@ msgstr "Détection de taches (OpenCV)" msgid "Detect blobs using OpenCV SimpleBlobDetector" msgstr "Détection de taches basée sur SimpleBlobDetector d'OpenCV" -#: cdl\core\gui\actionhandler.py:1389 cdl\core\gui\processor\image.py:214 -#: cdl\core\gui\processor\image.py:226 +#: cdl\core\gui\actionhandler.py:1389 cdl\core\gui\processor\image.py:219 +#: cdl\core\gui\processor\image.py:231 msgid "Resize" msgstr "Redimensionner" @@ -2052,8 +2030,10 @@ msgstr "Chargement des données depuis %s..." msgid "Clipping uint32 data to int32." msgstr "Troncature de données uint32 en int32." -#: cdl\core\gui\h5io.py:96 cdl\core\gui\h5io.py:112 cdl\core\gui\main.py:482 -#: cdl\core\gui\main.py:1371 cdl\plugins.py:173 cdl\widgets\textimport.py:501 +#: cdl\core\gui\h5io.py:96 cdl\core\gui\h5io.py:112 cdl\core\gui\main.py:483 +#: cdl\core\gui\main.py:1372 cdl\core\gui\processor\base.py:547 +#: cdl\core\gui\processor\base.py:562 cdl\plugins.py:173 +#: cdl\widgets\textimport.py:501 msgid "Warning" msgstr "Attention" @@ -2065,8 +2045,8 @@ msgstr "Aucune donnée prise en charge dans le(s) fichier(s) HDF5." msgid "Macro simple example" msgstr "Exemple simple de macro" -#: cdl\core\gui\macroeditor.py:197 cdl\core\model\image.py:397 -#: cdl\core\model\signal.py:158 cdl\core\model\signal.py:164 +#: cdl\core\gui\macroeditor.py:197 cdl\core\model\image.py:930 +#: cdl\core\model\signal.py:327 cdl\core\model\signal.py:333 msgid "Untitled" msgstr "Sans titre" @@ -2094,13 +2074,13 @@ msgstr "Arrêt de la macro '%s'" msgid "# <== '%s' macro has finished" msgstr "# <== La macro '%s' a terminé son exécution" -#: cdl\core\gui\main.py:483 +#: cdl\core\gui\main.py:484 msgid "Available memory is below %d MB.

Do you want to continue?" msgstr "" "La mémoire disponible est inférieure à %d Mo.

Souhaitez-vous " "néanmoins continuer ?" -#: cdl\core\gui\main.py:497 +#: cdl\core\gui\main.py:498 msgid "" "This software is in the beta stage of its release cycle. The focus of " "beta testing is providing a feature complete software for users interested " @@ -2116,7 +2096,7 @@ msgstr "" "fonctionner correctement et présente plus d'anomalies résiduelles et de " "problèmes de performance que la version finale." -#: cdl\core\gui\main.py:507 +#: cdl\core\gui\main.py:508 msgid "" "This software is in the alpha stage of its release cycle. The focus " "of alpha testing is providing an incomplete software for early testing of " @@ -2130,15 +2110,15 @@ msgstr "" "Notez que la version alpha d'un logiciel ne fait pas l'objet de tests " "poussés avant sa diffusion." -#: cdl\core\gui\main.py:517 +#: cdl\core\gui\main.py:518 msgid "This is not a stable release." msgstr "Ceci n'est pas une version stable." -#: cdl\core\gui\main.py:536 cdl\core\gui\main.py:551 cdl\plugins.py:181 +#: cdl\core\gui\main.py:537 cdl\core\gui\main.py:552 cdl\plugins.py:181 msgid "Information" msgstr "Information" -#: cdl\core\gui\main.py:537 +#: cdl\core\gui\main.py:538 msgid "" "The dependency check feature is not relevant for the standalone version of " "DataLab." @@ -2146,7 +2126,7 @@ msgstr "" "La vérification des dépendances n'est pas pertinente pour la version " "autonome de DataLab." -#: cdl\core\gui\main.py:552 +#: cdl\core\gui\main.py:553 msgid "" "All critical dependencies of DataLab have been qualified on this operating " "system." @@ -2154,19 +2134,19 @@ msgstr "" "Toutes les dépendances critiques de DataLab ont été qualifiées sur ce " "système d'exploitation." -#: cdl\core\gui\main.py:561 +#: cdl\core\gui\main.py:562 msgid "Non-compliant dependency:" msgstr "Dépendance non-conforme :" -#: cdl\core\gui\main.py:563 +#: cdl\core\gui\main.py:564 msgid "Non-compliant dependencies:" msgstr "Dépendances non-conformes :" -#: cdl\core\gui\main.py:566 +#: cdl\core\gui\main.py:567 msgid "DataLab has not yet been qualified on your operating system." msgstr "DataLab n'a pas encore été qualifié sur votre système d'exploitation." -#: cdl\core\gui\main.py:572 +#: cdl\core\gui\main.py:573 msgid "" "At least one dependency does not comply with DataLab qualification standard " "reference (wrong dependency version has been installed, or dependency source " @@ -2178,7 +2158,7 @@ msgstr "" "le code source de la dépendance a été modifié, ou encore l'application n'a " "pas encore été qualifiée dans le cadre de votre système d'exploitation)." -#: cdl\core\gui\main.py:582 +#: cdl\core\gui\main.py:583 msgid "" "This means that the application has not been officially qualified in this " "context and may not behave as expected." @@ -2186,212 +2166,212 @@ msgstr "" "Cela signifie que l'application n'a pas été officiellement qualifiée dans ce " "contexte et peut donc se comporter de façon inattendue." -#: cdl\core\gui\main.py:603 +#: cdl\core\gui\main.py:604 msgid "Do you want to see available log files?" msgstr "Souhaitez-vous consulter les journaux de bord disponibles ?" -#: cdl\core\gui\main.py:745 +#: cdl\core\gui\main.py:746 msgid "Welcome to %s!" msgstr "Bienvenue dans %s !" -#: cdl\core\gui\main.py:779 +#: cdl\core\gui\main.py:780 msgid "Open HDF5 files..." msgstr "Ouvrir des fichiers HDF5..." -#: cdl\core\gui\main.py:781 +#: cdl\core\gui\main.py:782 msgid "Open one or several HDF5 files" msgstr "Ouvrir un ou plusieurs fichier(s) HDF5" -#: cdl\core\gui\main.py:786 +#: cdl\core\gui\main.py:787 msgid "Save to HDF5 file..." msgstr "Enregistrer dans un fichier HDF5..." -#: cdl\core\gui\main.py:788 +#: cdl\core\gui\main.py:789 msgid "Save to HDF5 file" msgstr "Enregistrer dans un fichier HDF5" -#: cdl\core\gui\main.py:793 +#: cdl\core\gui\main.py:794 msgid "Browse HDF5 file..." msgstr "Parcourir un fichier HDF5..." -#: cdl\core\gui\main.py:795 +#: cdl\core\gui\main.py:796 msgid "Browse an HDF5 file" msgstr "Parcourir un fichier HDF5" -#: cdl\core\gui\main.py:800 +#: cdl\core\gui\main.py:801 msgid "Settings..." msgstr "Préférences..." -#: cdl\core\gui\main.py:802 +#: cdl\core\gui\main.py:803 msgid "Open settings dialog" msgstr "Ouvrir la boîte de dialogue des préférences" -#: cdl\core\gui\main.py:806 +#: cdl\core\gui\main.py:807 msgid "Main Toolbar" msgstr "Barre d'outils principale" -#: cdl\core\gui\main.py:820 +#: cdl\core\gui\main.py:821 msgid "Hide window" msgstr "Cacher la fenêtre" -#: cdl\core\gui\main.py:821 +#: cdl\core\gui\main.py:822 msgid "Hide DataLab window" msgstr "Cacher la fenêtre de DataLab" -#: cdl\core\gui\main.py:823 cdl\core\gui\main.py:1681 +#: cdl\core\gui\main.py:824 cdl\core\gui\main.py:1695 msgid "Quit" msgstr "Quitter" -#: cdl\core\gui\main.py:824 +#: cdl\core\gui\main.py:825 msgid "Quit application" msgstr "Quitter l'application" -#: cdl\core\gui\main.py:838 +#: cdl\core\gui\main.py:839 msgid "Auto-refresh" msgstr "Rafraîchissement automatique" -#: cdl\core\gui\main.py:840 +#: cdl\core\gui\main.py:841 msgid "Auto-refresh plot when object is modified, added or removed" msgstr "" "Rafraîchir le graphique automatiquement lors de la modification, de l'ajout " "ou de la suppression d'un objet" -#: cdl\core\gui\main.py:845 +#: cdl\core\gui\main.py:846 msgid "Show first object only" msgstr "Afficher uniquement le premier objet" -#: cdl\core\gui\main.py:847 +#: cdl\core\gui\main.py:848 msgid "Show only the first selected object (signal or image)" msgstr "Afficher uniquement le premier objet sélectionné (signal ou image)" -#: cdl\core\gui\main.py:852 +#: cdl\core\gui\main.py:853 msgid "Show graphical object titles" msgstr "Afficher les titres des objets graphiques" -#: cdl\core\gui\main.py:854 +#: cdl\core\gui\main.py:855 msgid "Show or hide ROI and other graphical object titles or subtitles" msgstr "" "Afficher ou cacher les titres (et sous-titres) des régions d'intérêt et " "autres objets graphiques" -#: cdl\core\gui\main.py:861 +#: cdl\core\gui\main.py:862 msgid "Signal Panel Toolbar" msgstr "Barre d'outils du panneau Signal" -#: cdl\core\gui\main.py:877 +#: cdl\core\gui\main.py:878 msgid "Image Panel Toolbar" msgstr "Barre d'outils du panneau Image" -#: cdl\core\gui\main.py:910 +#: cdl\core\gui\main.py:911 msgid "Signal View" msgstr "Vue Signal" -#: cdl\core\gui\main.py:911 +#: cdl\core\gui\main.py:912 msgid "Image View" msgstr "Vue Image" -#: cdl\core\gui\main.py:928 cdl\core\gui\panel\signal.py:49 +#: cdl\core\gui\main.py:929 cdl\core\gui\panel\signal.py:50 #: cdl\core\gui\tour.py:685 cdl\core\gui\tour.py:692 cdl\core\gui\tour.py:703 #: cdl\core\gui\tour.py:713 cdl\core\gui\tour.py:724 cdl\core\gui\tour.py:734 #: cdl\core\gui\tour.py:744 cdl\core\gui\tour.py:751 msgid "Signal Panel" msgstr "Panneau Signal" -#: cdl\core\gui\main.py:930 cdl\core\gui\panel\image.py:49 +#: cdl\core\gui\main.py:931 cdl\core\gui\panel\image.py:50 #: cdl\core\gui\tour.py:762 cdl\core\gui\tour.py:771 cdl\core\gui\tour.py:782 #: cdl\core\gui\tour.py:792 cdl\core\gui\tour.py:803 cdl\core\gui\tour.py:813 #: cdl\core\gui\tour.py:823 cdl\core\gui\tour.py:833 msgid "Image Panel" msgstr "Panneau Image" -#: cdl\core\gui\main.py:945 cdl\widgets\textimport.py:72 +#: cdl\core\gui\main.py:946 cdl\widgets\textimport.py:72 #: cdl\widgets\textimport.py:82 cdl\widgets\wizard.py:221 msgid "File" msgstr "Fichier" -#: cdl\core\gui\main.py:947 +#: cdl\core\gui\main.py:948 msgid "&Edit" msgstr "&Édition" -#: cdl\core\gui\main.py:948 +#: cdl\core\gui\main.py:949 msgid "Operations" msgstr "Opérations" -#: cdl\core\gui\main.py:949 cdl\core\gui\settings.py:263 +#: cdl\core\gui\main.py:950 cdl\core\gui\settings.py:276 msgid "Processing" msgstr "Traitement" -#: cdl\core\gui\main.py:950 +#: cdl\core\gui\main.py:951 msgid "Analysis" msgstr "Analyse" -#: cdl\core\gui\main.py:951 cdl\core\gui\tour.py:863 +#: cdl\core\gui\main.py:952 cdl\core\gui\tour.py:863 msgid "Plugins" msgstr "Plugins" -#: cdl\core\gui\main.py:952 +#: cdl\core\gui\main.py:953 msgid "&View" msgstr "&Affichage" -#: cdl\core\gui\main.py:966 +#: cdl\core\gui\main.py:967 msgid "Online documentation" msgstr "Documentation en ligne" -#: cdl\core\gui\main.py:976 +#: cdl\core\gui\main.py:977 msgid "PDF documentation" msgstr "Documentation PDF" -#: cdl\core\gui\main.py:984 +#: cdl\core\gui\main.py:985 msgid "Tour" msgstr "Visite guidée" -#: cdl\core\gui\main.py:990 cdl\core\gui\tour.py:287 -#: cdl\tests\scenarios\demo.py:186 cdl\tests\scenarios\demo.py:201 +#: cdl\core\gui\main.py:991 cdl\core\gui\tour.py:287 +#: cdl\tests\scenarios\demo.py:193 cdl\tests\scenarios\demo.py:208 msgid "Demo" msgstr "Démo" -#: cdl\core\gui\main.py:1000 +#: cdl\core\gui\main.py:1001 msgid "Test segfault/Python error" msgstr "Tester un plantage/une erreur Python" -#: cdl\core\gui\main.py:1007 cdl\widgets\logviewer.py:26 +#: cdl\core\gui\main.py:1008 cdl\widgets\logviewer.py:26 msgid "Log files" msgstr "Journaux de bord" -#: cdl\core\gui\main.py:1013 cdl\widgets\instconfviewer.py:87 +#: cdl\core\gui\main.py:1014 cdl\widgets\instconfviewer.py:87 msgid "Installation and configuration" msgstr "Installation et configuration" -#: cdl\core\gui\main.py:1020 +#: cdl\core\gui\main.py:1021 msgid "Project home page" msgstr "Page d'accueil du projet" -#: cdl\core\gui\main.py:1026 +#: cdl\core\gui\main.py:1027 msgid "Bug report or feature request" msgstr "Rapport d'anomalie ou demande de nouvelle fonctionnalité" -#: cdl\core\gui\main.py:1032 +#: cdl\core\gui\main.py:1033 msgid "Check critical dependencies..." msgstr "Vérifier les dépendances critiques..." -#: cdl\core\gui\main.py:1037 +#: cdl\core\gui\main.py:1038 msgid "About..." msgstr "À propos..." -#: cdl\core\gui\main.py:1072 cdl\core\gui\panel\macro.py:196 -#: cdl\core\gui\settings.py:266 +#: cdl\core\gui\main.py:1073 cdl\core\gui\panel\macro.py:195 +#: cdl\core\gui\settings.py:279 msgid "Console" msgstr "Console" -#: cdl\core\gui\main.py:1081 +#: cdl\core\gui\main.py:1082 msgid "Macro Panel" msgstr "Gestionnaire de Macros" -#: cdl\core\gui\main.py:1339 +#: cdl\core\gui\main.py:1340 msgid "Save" msgstr "Enregistrer" -#: cdl\core\gui\main.py:1372 +#: cdl\core\gui\main.py:1373 msgid "" "Do you want to remove all signals and images before importing data from HDF5 " "files?" @@ -2399,60 +2379,60 @@ msgstr "" "Souhaitez-vous supprimer tous les signaux et images avant d'importer des " "données depuis un fichier HDF5 ?" -#: cdl\core\gui\main.py:1384 cdl\core\gui\panel\base.py:739 -#: cdl\core\gui\panel\macro.py:566 cdl\widgets\h5browser.py:618 +#: cdl\core\gui\main.py:1385 cdl\core\gui\panel\base.py:760 +#: cdl\core\gui\panel\macro.py:564 cdl\widgets\h5browser.py:618 msgid "Open" msgstr "Ouvrir" -#: cdl\core\gui\main.py:1384 cdl\widgets\h5browser.py:691 +#: cdl\core\gui\main.py:1385 cdl\widgets\h5browser.py:691 msgid "HDF5 files (*.h5 *.hdf5)" msgstr "Fichiers HDF5 (*.h5 *.hdf5)" -#: cdl\core\gui\main.py:1567 +#: cdl\core\gui\main.py:1568 msgid "not started" msgstr "non démarré" -#: cdl\core\gui\main.py:1569 +#: cdl\core\gui\main.py:1570 msgid "started (port %s)" msgstr "démarré (port %s)" -#: cdl\core\gui\main.py:1572 +#: cdl\core\gui\main.py:1573 msgid "enabled" msgstr "activé" -#: cdl\core\gui\main.py:1574 +#: cdl\core\gui\main.py:1575 msgid "disabled" msgstr "désactivé" -#: cdl\core\gui\main.py:1577 +#: cdl\core\gui\main.py:1578 msgid "Advanced configuration:" msgstr "Configuration avancée :" -#: cdl\core\gui\main.py:1578 +#: cdl\core\gui\main.py:1579 msgid "XML-RPC server:" msgstr "Serveur XML-RPC :" -#: cdl\core\gui\main.py:1579 +#: cdl\core\gui\main.py:1580 msgid "Process isolation:" msgstr "Isolation de processus :" -#: cdl\core\gui\main.py:1582 +#: cdl\core\gui\main.py:1583 msgid "Created by" msgstr "Créé par" -#: cdl\core\gui\main.py:1583 +#: cdl\core\gui\main.py:1584 msgid "Developed and maintained by %s open-source project team" msgstr "Développé et maintenu par l'équipe du projet libre %s" -#: cdl\core\gui\main.py:1587 +#: cdl\core\gui\main.py:1588 msgid "About" msgstr "À propos de" -#: cdl\core\gui\main.py:1626 +#: cdl\core\gui\main.py:1640 msgid "Visualization settings" msgstr "Réglages d'affichage" -#: cdl\core\gui\main.py:1627 +#: cdl\core\gui\main.py:1641 msgid "" "Default visualization settings have changed.

Do you want to update " "all active %s objects?" @@ -2460,7 +2440,7 @@ msgstr "" "Les réglages d'affichage par défaut ont changé.

Souhaitez-vous mettre " "à jour tous les objets %s actifs ?" -#: cdl\core\gui\main.py:1682 +#: cdl\core\gui\main.py:1696 msgid "" "Do you want to save all signals and images to an HDF5 file before quitting " "DataLab?" @@ -2476,35 +2456,35 @@ msgstr "Métadonnées de l'objet" msgid "(click on Metadata button for more details)" msgstr "(cliquer sur le bouton Métadonnées pour plus de détails)" -#: cdl\core\gui\objectview.py:320 +#: cdl\core\gui\objectview.py:378 msgid "Drag files here to open" msgstr "Déposer des fichiers ici pour les ouvrir" -#: cdl\core\gui\panel\base.py:83 cdl\core\model\base.py:403 +#: cdl\core\gui\panel\base.py:89 cdl\core\model\base.py:407 msgid "Properties" msgstr "Propriétés" -#: cdl\core\gui\panel\base.py:107 +#: cdl\core\gui\panel\base.py:113 msgid "Analysis parameters" msgstr "Paramètres d'analyse" -#: cdl\core\gui\panel\base.py:533 +#: cdl\core\gui\panel\base.py:540 msgid "Delete group(s)" msgstr "Supprimer le(s) groupe(s)" -#: cdl\core\gui\panel\base.py:534 +#: cdl\core\gui\panel\base.py:541 msgid "Are you sure you want to delete the selected group(s)?" msgstr "Êtes-vous sûr de vouloir supprimer le(s) groupe(s) sélectionné(s) ?" -#: cdl\core\gui\panel\base.py:564 +#: cdl\core\gui\panel\base.py:571 msgid "Do you want to delete all objects (%s)?" msgstr "Souhaitez-vous supprimer tous les objets (%s) ?" -#: cdl\core\gui\panel\base.py:588 +#: cdl\core\gui\panel\base.py:595 msgid "Delete metadata" msgstr "Supprimer les métadonnées" -#: cdl\core\gui\panel\base.py:589 +#: cdl\core\gui\panel\base.py:596 msgid "" "Some selected objects have regions of interest.
Do you want to delete " "them as well?" @@ -2512,59 +2492,59 @@ msgstr "" "Certains objets sélectionnés ont des régions d'intérêt.
Souhaitez-vous " "les supprimer également ?" -#: cdl\core\gui\panel\base.py:639 +#: cdl\core\gui\panel\base.py:648 msgid "New group" msgstr "Nouveau groupe" -#: cdl\core\gui\panel\base.py:639 cdl\core\gui\panel\base.py:648 +#: cdl\core\gui\panel\base.py:648 cdl\core\gui\panel\base.py:668 msgid "Group name:" msgstr "Nom du groupe :" -#: cdl\core\gui\panel\base.py:648 +#: cdl\core\gui\panel\base.py:667 msgid "Rename group" msgstr "Renommer le groupe" -#: cdl\core\gui\panel\base.py:703 cdl\core\gui\panel\base.py:796 +#: cdl\core\gui\panel\base.py:726 cdl\core\gui\panel\base.py:817 msgid "Adding objects to workspace" msgstr "Ajout d'objets à l'espace de travail" -#: cdl\core\gui\panel\base.py:766 cdl\core\gui\panel\macro.py:545 +#: cdl\core\gui\panel\base.py:787 cdl\core\gui\panel\macro.py:543 msgid "Save as" msgstr "Enregistrer sous" -#: cdl\core\gui\panel\base.py:883 cdl\core\gui\panel\base.py:913 -#: cdl\core\gui\panel\base.py:1141 -msgid "Annotations" -msgstr "Annotations" - -#: cdl\core\gui\panel\base.py:1018 cdl\core\gui\plothandler.py:332 +#: cdl\core\gui\panel\base.py:903 cdl\core\gui\plothandler.py:346 msgid "Creating plot items" msgstr "Création des objets graphiques" -#: cdl\core\gui\panel\base.py:1078 cdl\core\gui\roieditor.py:249 -#: cdl\core\model\base.py:1094 +#: cdl\core\gui\panel\base.py:947 cdl\core\gui\panel\base.py:964 +#: cdl\core\gui\panel\base.py:1132 +msgid "Annotations" +msgstr "Annotations" + +#: cdl\core\gui\panel\base.py:1061 cdl\core\gui\roieditor.py:440 +#: cdl\core\model\base.py:1202 msgid "Regions of interest" msgstr "Régions d'intérêt" -#: cdl\core\gui\panel\base.py:1135 cdl\core\gui\panel\base.py:1174 -#: cdl\core\gui\processor\base.py:494 +#: cdl\core\gui\panel\base.py:1126 cdl\core\gui\panel\base.py:1165 +#: cdl\core\gui\processor\base.py:503 msgid "Results" msgstr "Résultats" -#: cdl\core\gui\panel\base.py:1137 +#: cdl\core\gui\panel\base.py:1128 msgid "Show results obtained from previous analysis" msgstr "" "Afficher les résultats obtenus avec des analyses réalisées précédemment" -#: cdl\core\gui\panel\base.py:1143 +#: cdl\core\gui\panel\base.py:1134 msgid "Open a dialog to edit annotations" msgstr "Ouvrir une boîte de dialogue pour éditer les annotations" -#: cdl\core\gui\panel\base.py:1151 +#: cdl\core\gui\panel\base.py:1142 msgid "No result currently available for this object." msgstr "Aucun résultat disponible pour le moment." -#: cdl\core\gui\panel\base.py:1153 +#: cdl\core\gui\panel\base.py:1144 msgid "" "This feature leverages the results of previous analysis performed on the " "selected object(s).

To compute results, select one or more objects " @@ -2574,31 +2554,31 @@ msgstr "" "

Pour calculer des résultats, sélectionnez un ou plusieurs objets et " "choisissez une fonctionnalité dans le menu Analyse." -#: cdl\core\gui\panel\base.py:1211 -msgid "Indexes" +#: cdl\core\gui\panel\base.py:1202 +msgid "Indices" msgstr "Indices" -#: cdl\core\gui\panel\base.py:1245 +#: cdl\core\gui\panel\base.py:1236 msgid "Plot kind" msgstr "Type de graphique" -#: cdl\core\gui\panel\base.py:1249 +#: cdl\core\gui\panel\base.py:1240 msgid "One curve per object (or ROI) and per result title" msgstr "Une courbe par objet (ou ROI) et par titre de résultat" -#: cdl\core\gui\panel\base.py:1251 +#: cdl\core\gui\panel\base.py:1242 msgid "One curve per result title" msgstr "Une courbe par titre de résultat" -#: cdl\core\gui\panel\base.py:1255 +#: cdl\core\gui\panel\base.py:1246 msgid "X axis" msgstr "Axe des X" -#: cdl\core\gui\panel\base.py:1257 +#: cdl\core\gui\panel\base.py:1248 msgid "Y axis" msgstr "Axe des Y" -#: cdl\core\gui\panel\base.py:1261 +#: cdl\core\gui\panel\base.py:1252 msgid "" "Plot results obtained from previous analyses.

This plot is based on " "results associated with '%s' prefix." @@ -2606,7 +2586,7 @@ msgstr "" "Tracer les résultats obtenus avec des analyses réalisées précédemment." "

Ce graphique est basé sur les résultats associés au préfixe '%s'." -#: cdl\core\gui\panel\base.py:1315 +#: cdl\core\gui\panel\base.py:1306 msgid "Are you sure you want to delete all results of the selected object(s)?" msgstr "" "Êtes-vous sûr de vouloir supprimer tous les résultats des objets " @@ -2620,53 +2600,53 @@ msgstr "Panneau des macros" msgid "Python files" msgstr "Fichiers Python" -#: cdl\core\gui\panel\macro.py:183 +#: cdl\core\gui\panel\macro.py:182 msgid "-***- Macro Console -***-" msgstr "-***- Console des macros -***-" -#: cdl\core\gui\panel\macro.py:305 +#: cdl\core\gui\panel\macro.py:303 #: cdl\tests\features\control\embedded1_unit_test.py:128 msgid "Run macro" msgstr "Exécuter la macro" -#: cdl\core\gui\panel\macro.py:312 +#: cdl\core\gui\panel\macro.py:310 #: cdl\tests\features\control\embedded1_unit_test.py:129 msgid "Stop macro" msgstr "Arrêter la macro" -#: cdl\core\gui\panel\macro.py:320 +#: cdl\core\gui\panel\macro.py:318 msgid "New macro" msgstr "Nouvelle macro" -#: cdl\core\gui\panel\macro.py:326 +#: cdl\core\gui\panel\macro.py:324 msgid "Rename macro" msgstr "Renommer la macro" -#: cdl\core\gui\panel\macro.py:332 +#: cdl\core\gui\panel\macro.py:330 msgid "Import macro from file" msgstr "Importer la macro depuis un fichier" -#: cdl\core\gui\panel\macro.py:338 +#: cdl\core\gui\panel\macro.py:336 msgid "Export macro to file" msgstr "Exporter la macro vers un fichier" -#: cdl\core\gui\panel\macro.py:344 +#: cdl\core\gui\panel\macro.py:342 msgid "Remove macro" msgstr "Supprimer la macro" -#: cdl\core\gui\panel\macro.py:358 +#: cdl\core\gui\panel\macro.py:356 msgid "Macro editor toolbar" msgstr "Barre d'outils des macros" -#: cdl\core\gui\panel\macro.py:515 +#: cdl\core\gui\panel\macro.py:513 msgid "Rename" msgstr "Renommer" -#: cdl\core\gui\panel\macro.py:516 +#: cdl\core\gui\panel\macro.py:514 msgid "New title:" msgstr "Nouveau titre :" -#: cdl\core\gui\panel\macro.py:589 +#: cdl\core\gui\panel\macro.py:587 msgid "" "When closed, the macro is permanently destroyed, unless it has been " "exported first." @@ -2674,7 +2654,7 @@ msgstr "" "Lorsqu'elle est fermée, une macro est détruite de manière définitive, " "sauf si elle a été exportée au préalable." -#: cdl\core\gui\panel\macro.py:594 +#: cdl\core\gui\panel\macro.py:592 msgid "Do you want to continue?" msgstr "Souhaitez-vous vraiment continuer ?" @@ -2682,7 +2662,7 @@ msgstr "Souhaitez-vous vraiment continuer ?" msgid "Creating geometric shapes" msgstr "Création des formes géométriques" -#: cdl\core\gui\processor\base.py:72 +#: cdl\core\gui\processor\base.py:71 msgid "" "DataLab relies on various libraries to perform the computation. During the " "computation, errors may occur because of the data (e.g. division by zero, " @@ -2697,195 +2677,222 @@ msgstr "" "mémoire, etc.). Si vous rencontrez une erreur, avant de la signaler, assurez-" "vous que le calcul est correct, en vérifiant les données et les paramètres." -#: cdl\core\gui\processor\base.py:293 cdl\widgets\textimport.py:706 +#: cdl\core\gui\processor\base.py:302 cdl\widgets\textimport.py:706 msgid "Parameters" msgstr "Paramètres" -#: cdl\core\gui\processor\base.py:392 cdl\core\gui\processor\base.py:468 +#: cdl\core\gui\processor\base.py:401 cdl\core\gui\processor\base.py:477 msgid "Computing: %s" msgstr "Calcul en cours : %s" -#: cdl\core\gui\processor\base.py:564 cdl\core\gui\processor\base.py:644 +#: cdl\core\gui\processor\base.py:548 +msgid "In pairwise mode, you need to select objects in at least two groups." +msgstr "" +"En mode 'pairwise', vous devez sélectionner des objets dans au moins deux " +"groupes." + +#: cdl\core\gui\processor\base.py:563 +msgid "" +"In pairwise mode, you need to select the same number of objects in each " +"group." +msgstr "" +"En mode 'pairwise', vous devez sélectionner le même nombre d'objets dans " +"chaque groupe." + +#: cdl\core\gui\processor\base.py:639 cdl\core\gui\processor\base.py:687 +#: cdl\core\gui\processor\base.py:810 cdl\core\gui\processor\base.py:837 msgid "Calculating: %s" msgstr "Calcul : %s" -#: cdl\core\gui\processor\base.py:627 +#: cdl\core\gui\processor\base.py:762 msgid "Select %s" msgstr "Sélectionner %s" -#: cdl\core\gui\processor\image.py:92 cdl\core\gui\processor\signal.py:77 +#: cdl\core\gui\processor\base.py:775 +msgid "" +"Note: operation mode is pairwise: %s object(s) expected (i.e. " +"as many as in the first group)" +msgstr "" +"Note : le mode d'opération est pairwise : %s objet(s) " +"attendu(s) (c'est-à-dire autant que dans le premier groupe)" + +#: cdl\core\gui\processor\base.py:820 +msgid "Note: operation mode is single operand: 1 object expected" +msgstr "Note : le mode d'opération est single : 1 objet attendu" + +#: cdl\core\gui\processor\image.py:97 cdl\core\gui\processor\signal.py:78 msgid "Product with constant" msgstr "Produit par une constante" -#: cdl\core\gui\processor\image.py:131 +#: cdl\core\gui\processor\image.py:136 msgid "Distribute on grid" msgstr "Distribuer sur grille" -#: cdl\core\gui\processor\image.py:210 +#: cdl\core\gui\processor\image.py:215 msgid "Warning:" msgstr "Attention :" -#: cdl\core\gui\processor\image.py:212 +#: cdl\core\gui\processor\image.py:217 msgid "Selected images do not have the same size" msgstr "Les images sélectionnées n'ont pas la même taille" -#: cdl\core\gui\processor\image.py:221 +#: cdl\core\gui\processor\image.py:226 msgid "Destination size:" msgstr "Taille de destination :" -#: cdl\core\gui\processor\image.py:234 +#: cdl\core\gui\processor\image.py:239 msgid "Binning" msgstr "Binning" -#: cdl\core\gui\processor\image.py:259 cdl\core\gui\processor\signal.py:95 -msgid "Extract ROI" -msgstr "Extraire une ROI" - -#: cdl\core\gui\processor\image.py:273 cdl\core\gui\processor\image.py:293 +#: cdl\core\gui\processor\image.py:253 cdl\core\gui\processor\image.py:273 msgid "Profile" msgstr "Profil" -#: cdl\core\gui\processor\image.py:313 +#: cdl\core\gui\processor\image.py:293 msgid "Average profile" msgstr "Profil moyen" -#: cdl\core\gui\processor\image.py:333 +#: cdl\core\gui\processor\image.py:313 msgid "Radial profile" msgstr "Profil radial" -#: cdl\core\gui\processor\image.py:350 cdl\core\gui\processor\signal.py:102 +#: cdl\core\gui\processor\image.py:330 cdl\core\gui\processor\signal.py:84 msgid "Swap axes" msgstr "Permuter les axes" -#: cdl\core\gui\processor\image.py:392 +#: cdl\core\gui\processor\image.py:372 msgid "image to operate with" msgstr "image opérande" -#: cdl\core\gui\processor\image.py:396 cdl\core\gui\processor\signal.py:158 +#: cdl\core\gui\processor\image.py:376 cdl\core\gui\processor\signal.py:140 msgid "Arithmetic" msgstr "Arithmétique" -#: cdl\core\gui\processor\image.py:405 cdl\core\gui\processor\image.py:429 +#: cdl\core\gui\processor\image.py:385 cdl\core\gui\processor\image.py:411 msgid "image to subtract" msgstr "image à soustraire" -#: cdl\core\gui\processor\image.py:420 cdl\core\gui\processor\signal.py:182 +#: cdl\core\gui\processor\image.py:400 cdl\core\gui\processor\signal.py:166 msgid "Difference with constant" msgstr "Différence par une constante" -#: cdl\core\gui\processor\image.py:440 cdl\core\gui\processor\signal.py:202 +#: cdl\core\gui\processor\image.py:422 cdl\core\gui\processor\signal.py:189 msgid "divider" msgstr "le diviseur" -#: cdl\core\gui\processor\image.py:453 cdl\core\gui\processor\signal.py:215 +#: cdl\core\gui\processor\image.py:435 cdl\core\gui\processor\signal.py:202 msgid "Division by constant" msgstr "Division par une constante" -#: cdl\core\gui\processor\image.py:464 +#: cdl\core\gui\processor\image.py:446 msgid "Flat field" msgstr "Correction de champ plat" -#: cdl\core\gui\processor\image.py:470 +#: cdl\core\gui\processor\image.py:452 msgid "flat field image" msgstr "l'image de champ plat" -#: cdl\core\gui\processor\image.py:473 +#: cdl\core\gui\processor\image.py:455 msgid "Flat field correction" msgstr "Correction de champ plat" -#: cdl\core\gui\processor\image.py:592 cdl\core\gui\processor\signal.py:426 +#: cdl\core\gui\processor\image.py:575 cdl\core\gui\processor\signal.py:413 msgid "PSD" msgstr "DSP" -#: cdl\core\gui\processor\image.py:615 +#: cdl\core\gui\processor\image.py:598 msgid "Parametric threshold" msgstr "Seuillage paramétrique" -#: cdl\core\gui\processor\image.py:709 +#: cdl\core\gui\processor\image.py:692 msgid "Log correction" msgstr "Correction logarithmique" -#: cdl\core\gui\processor\image.py:737 +#: cdl\core\gui\processor\image.py:720 msgid "Rescale intensity" msgstr "Ajuster les niveaux" -#: cdl\core\gui\processor\image.py:816 cdl\core\gui\processor\image.py:844 +#: cdl\core\gui\processor\image.py:799 cdl\core\gui\processor\image.py:827 msgid "Denoise / Top-Hat" msgstr "Débruitage Top-Hat" -#: cdl\core\gui\processor\image.py:861 +#: cdl\core\gui\processor\image.py:844 msgid "White Top-Hat" msgstr "Top-Hat" -#: cdl\core\gui\processor\image.py:874 +#: cdl\core\gui\processor\image.py:857 msgid "Black Top-Hat" msgstr "Top-Hat dual" -#: cdl\core\gui\processor\image.py:885 +#: cdl\core\gui\processor\image.py:868 msgid "Erosion" msgstr "Erosion" -#: cdl\core\gui\processor\image.py:896 +#: cdl\core\gui\processor\image.py:879 msgid "Dilation" msgstr "Dilatation" -#: cdl\core\gui\processor\image.py:907 +#: cdl\core\gui\processor\image.py:890 msgid "Opening" msgstr "Ouverture" -#: cdl\core\gui\processor\image.py:918 +#: cdl\core\gui\processor\image.py:901 msgid "Closing" msgstr "Fermeture" -#: cdl\core\gui\processor\image.py:1124 +#: cdl\core\gui\processor\image.py:1092 cdl\core\gui\processor\signal.py:557 +msgid "Extract ROI" +msgstr "Extraire une ROI" + +#: cdl\core\gui\processor\image.py:1118 msgid "Enclosing circle" msgstr "Cercle minimum" -#: cdl\core\gui\processor\image.py:1182 cdl\core\gui\processor\image.py:1186 +#: cdl\core\gui\processor\image.py:1172 cdl\core\gui\processor\image.py:1176 msgid "Contour" msgstr "Contour" -#: cdl\core\gui\processor\image.py:1200 +#: cdl\core\gui\processor\image.py:1190 msgid "Hough circles" msgstr "Cercles de Hough" -#: cdl\core\gui\processor\signal.py:46 +#: cdl\core\gui\processor\signal.py:47 msgid "Sum with constant" msgstr "Addition avec une constante" -#: cdl\core\gui\processor\signal.py:154 +#: cdl\core\gui\processor\signal.py:136 msgid "signal to operate with" msgstr "signal opérande" -#: cdl\core\gui\processor\signal.py:167 cdl\core\gui\processor\signal.py:191 +#: cdl\core\gui\processor\signal.py:151 cdl\core\gui\processor\signal.py:178 msgid "signal to subtract" msgstr "signal à soustraire" -#: cdl\core\gui\processor\signal.py:240 +#: cdl\core\gui\processor\signal.py:227 msgid "Reverse X axis" msgstr "Inverser l'axe des X" -#: cdl\core\gui\processor\signal.py:401 +#: cdl\core\gui\processor\signal.py:388 msgid "iFFT" msgstr "iFFT" -#: cdl\core\gui\processor\signal.py:438 +#: cdl\core\gui\processor\signal.py:425 msgid "signal for X values" msgstr "signal pour les valeurs X" -#: cdl\core\gui\processor\signal.py:485 +#: cdl\core\gui\processor\signal.py:472 msgid "signal to convolve with" msgstr "signal à convoluer" -#: cdl\core\gui\processor\signal.py:573 +#: cdl\core\gui\processor\signal.py:571 msgid "FWHM" msgstr "LMH" -#: cdl\core\gui\processor\signal.py:578 +#: cdl\core\gui\processor\signal.py:576 msgid "FW" msgstr "LH" -#: cdl\core\gui\processor\signal.py:619 +#: cdl\core\gui\processor\signal.py:617 msgid "Bandwidth" msgstr "Bande passante" @@ -2897,30 +2904,34 @@ msgstr "Modifier les paramètres du profil" msgid "Reset selection" msgstr "Réinitialiser la sélection" -#: cdl\core\gui\roieditor.py:142 cdl\core\gui\roieditor.py:186 +#: cdl\core\gui\roieditor.py:116 +msgid "Add ROI" +msgstr "Ajouter une ROI" + +#: cdl\core\gui\roieditor.py:138 +msgid "Add rectangular ROI" +msgstr "Ajouter une ROI rectangulaire" + +#: cdl\core\gui\roieditor.py:165 +msgid "Add circular ROI" +msgstr "Ajouter une ROI circulaire" + +#: cdl\core\gui\roieditor.py:192 +msgid "Add polygonal ROI" +msgstr "Ajouter une ROI polygonale" + +#: cdl\core\gui\roieditor.py:336 cdl\core\gui\roieditor.py:364 msgid "Remove all ROIs" msgstr "Supprimer toutes les ROI" -#: cdl\core\gui\roieditor.py:161 -msgid "Extract all regions of interest into a single %s object" -msgstr "Extraire toutes les régions d'intérêt dans un seul objet %s" +#: cdl\core\gui\roieditor.py:350 +msgid "Extract all ROIs into a single %s object" +msgstr "Extraire toutes les ROI dans un seul objet %s" -#: cdl\core\gui\roieditor.py:187 +#: cdl\core\gui\roieditor.py:365 msgid "Are you sure you want to remove all ROIs?" msgstr "Êtes-vous sûr de vouloir supprimer toutes les ROI ?" -#: cdl\core\gui\roieditor.py:241 -msgid "Add ROI" -msgstr "Ajouter une ROI" - -#: cdl\core\gui\roieditor.py:280 -msgid "Rectangular ROI" -msgstr "ROI rectangulaire" - -#: cdl\core\gui\roieditor.py:287 -msgid "Circular ROI" -msgstr "ROI circulaire" - #: cdl\core\gui\settings.py:28 msgid "Settings for main window and general features" msgstr "Réglages de la fenêtre principale et des fonctionnalités générales" @@ -3028,75 +3039,93 @@ msgstr "Nom du fichier HDF5 dans le titre" msgid "Settings for computations" msgstr "Réglages des calculs" -#: cdl\core\gui\settings.py:98 +#: cdl\core\gui\settings.py:99 +msgid "Operation mode" +msgstr "Mode d'opération" + +#: cdl\core\gui\settings.py:101 +msgid "" +"Operation mode for computations taking N inputs:
Computations taking N inputs are the ones where:" +"
  • N(>=2) objects in %s 1 object out
  • N(>=1) objects + 1 object " +"in %s N objects out
" +msgstr "" +"Mode d'opération pour les calculs prenant N entrées:" +"
  • single: mode d'opération avec un seul opérande
  • pairwise: mode d'opération par paire

Les calculs " +"prenant N entrées sont ceux où :
  • N(>=2) objets en %s 1 objet " +"en sortie
  • N(>=1) objets + 1 objet en %s N objets en sortie
" + +#: cdl\core\gui\settings.py:111 msgid "FFT shift" msgstr "Décalage FFT" -#: cdl\core\gui\settings.py:99 +#: cdl\core\gui\settings.py:112 msgid "Extract ROI in single object" msgstr "Extraire les ROI dans un seul objet" -#: cdl\core\gui\settings.py:100 +#: cdl\core\gui\settings.py:113 msgid "Ignore warnings" msgstr "Ignorer les avertissements" -#: cdl\core\gui\settings.py:125 cdl\core\gui\settings.py:184 +#: cdl\core\gui\settings.py:138 cdl\core\gui\settings.py:197 msgid "Default image visualization settings" msgstr "Réglages d'affichage d'images par défaut" -#: cdl\core\gui\settings.py:138 +#: cdl\core\gui\settings.py:151 msgid "Common" msgstr "Commun" -#: cdl\core\gui\settings.py:140 +#: cdl\core\gui\settings.py:153 msgid "Plot toolbar position" msgstr "Position de la barre d'outils des graphiques" -#: cdl\core\gui\settings.py:142 +#: cdl\core\gui\settings.py:155 msgid "Top (above plot)" msgstr "Haut (au-dessus du graphique)" -#: cdl\core\gui\settings.py:143 +#: cdl\core\gui\settings.py:156 msgid "Bottom (below plot)" msgstr "Bas (en-dessous du graphique)" -#: cdl\core\gui\settings.py:144 +#: cdl\core\gui\settings.py:157 msgid "Left (of plot)" msgstr "Gauche (à gauche du graphique)" -#: cdl\core\gui\settings.py:145 +#: cdl\core\gui\settings.py:158 msgid "Right (of plot)" msgstr "Droite (à droite du graphique)" -#: cdl\core\gui\settings.py:150 cdl\core\model\signal.py:904 +#: cdl\core\gui\settings.py:163 cdl\core\model\signal.py:974 msgid "Signal" msgstr "Signal" -#: cdl\core\gui\settings.py:154 +#: cdl\core\gui\settings.py:167 msgid "Use auto downsampling" msgstr "Utiliser l'auto-sous-échantillonnage" -#: cdl\core\gui\settings.py:155 +#: cdl\core\gui\settings.py:168 msgid "Use auto downsampling for large signals" msgstr "Utiliser l'auto-sous-échantillonnage pour les signaux volumineux" -#: cdl\core\gui\settings.py:158 +#: cdl\core\gui\settings.py:171 msgid "Downsampling max points" msgstr "Nombre max. de points" -#: cdl\core\gui\settings.py:160 +#: cdl\core\gui\settings.py:173 msgid "Maximum number of points for downsampling" msgstr "Nombre maximum de points pour le sous-échantillonnage" -#: cdl\core\gui\settings.py:164 cdl\core\model\image.py:976 +#: cdl\core\gui\settings.py:177 cdl\core\model\image.py:1443 msgid "Image" msgstr "Image" -#: cdl\core\gui\settings.py:167 +#: cdl\core\gui\settings.py:180 msgid "Use reference image LUT range" msgstr "Utiliser la LUT de l'image de référence" -#: cdl\core\gui\settings.py:168 +#: cdl\core\gui\settings.py:181 msgid "" "If this setting is enabled, images are shown
with the same LUT range as " "the first selected image" @@ -3104,15 +3133,15 @@ msgstr "" "Si cette option est activée, les images sont affichées
avec la même LUT " "que la première image sélectionnée" -#: cdl\core\gui\settings.py:174 +#: cdl\core\gui\settings.py:187 msgid "Eliminate outliers" msgstr "Eliminer les bords" -#: cdl\core\gui\settings.py:175 +#: cdl\core\gui\settings.py:188 msgid "%" msgstr "%" -#: cdl\core\gui\settings.py:178 +#: cdl\core\gui\settings.py:191 msgid "" "Eliminate a percentage of the highest and lowest values
of the image " "histogram - recommanded values are below 1%" @@ -3121,47 +3150,47 @@ msgstr "" "l'histogramme de l'image - les valeurs recommandées sont inférieures à " "1%" -#: cdl\core\gui\settings.py:221 +#: cdl\core\gui\settings.py:234 msgid "Process isolation enable status" msgstr "Etat d'activation de l'isolation de processus" -#: cdl\core\gui\settings.py:222 +#: cdl\core\gui\settings.py:235 msgid "RPC server enable status" msgstr "Etat d'activation du serveur XML-RPC" -#: cdl\core\gui\settings.py:223 +#: cdl\core\gui\settings.py:236 msgid "Console enable status" msgstr "Etat d'activation de la console" -#: cdl\core\gui\settings.py:224 +#: cdl\core\gui\settings.py:237 msgid "Third-party plugins support" msgstr "Prise en charge des plugins tiers" -#: cdl\core\gui\settings.py:225 +#: cdl\core\gui\settings.py:238 msgid "Third-party plugins path" msgstr "Chemin des plugins tiers" -#: cdl\core\gui\settings.py:262 +#: cdl\core\gui\settings.py:275 msgid "General" msgstr "Général" -#: cdl\core\gui\settings.py:264 +#: cdl\core\gui\settings.py:277 msgid "Visualization" msgstr "Visualisation" -#: cdl\core\gui\settings.py:265 +#: cdl\core\gui\settings.py:278 msgid "I/O" msgstr "Entrée/sortie" -#: cdl\core\gui\settings.py:271 +#: cdl\core\gui\settings.py:284 msgid "Settings" msgstr "Préférences" -#: cdl\core\gui\settings.py:285 +#: cdl\core\gui\settings.py:298 msgid "Restart required" msgstr "Redémarrage requis" -#: cdl\core\gui\settings.py:286 +#: cdl\core\gui\settings.py:299 msgid "" "The following options have been changed:\n" "\n" @@ -3175,7 +3204,7 @@ msgstr "" "\n" "Un redémarrage est nécessaire pour que ces changements prennent effet." -#: cdl\core\gui\tour.py:281 cdl\core\model\signal.py:721 +#: cdl\core\gui\tour.py:281 cdl\core\model\signal.py:791 msgid "Start" msgstr "Démarrer" @@ -3493,313 +3522,309 @@ msgstr "Fichiers CSV" msgid "NumPy binary files" msgstr "Fichiers binaires NumPy" -#: cdl\core\model\base.py:112 cdl\tests\data.py:89 +#: cdl\core\model\base.py:115 cdl\tests\data.py:89 msgid "Seed" msgstr "Graine" -#: cdl\core\model\base.py:123 +#: cdl\core\model\base.py:126 msgid "Uniform distribution lower bound" msgstr "Borne inférieure de la distribution uniforme" -#: cdl\core\model\base.py:126 +#: cdl\core\model\base.py:129 msgid "Uniform distribution higher bound" msgstr "Borne supérieure de la distribution uniforme" -#: cdl\core\model\base.py:143 +#: cdl\core\model\base.py:146 msgid "Normal distribution mean" msgstr "Moyenne de la distribution normale" -#: cdl\core\model\base.py:148 +#: cdl\core\model\base.py:151 msgid "Normal distribution standard deviation" msgstr "Ecart-type de la distribution normale" -#: cdl\core\model\image.py:98 -msgid "Rectangle" -msgstr "Rectangle" - -#: cdl\core\model\image.py:206 +#: cdl\core\model\image.py:85 msgid "Geometry" msgstr "Géométrie" -#: cdl\core\model\image.py:211 cdl\core\model\image.py:218 +#: cdl\core\model\image.py:89 cdl\core\model\image.py:92 msgid "Top left corner" msgstr "Coin supérieur gauche" -#: cdl\core\model\image.py:219 cdl\core\model\image.py:226 -msgid "Bottom right corner" -msgstr "Coin inférieur droit" - -#: cdl\core\model\image.py:229 cdl\core\model\image.py:236 +#: cdl\core\model\image.py:97 cdl\core\model\image.py:100 msgid "Center coordinates" msgstr "Coordonnées du centre" -#: cdl\core\model\image.py:380 cdl\core\model\image.py:381 -#: cdl\core\model\image.py:383 cdl\core\model\signal.py:159 +#: cdl\core\model\image.py:104 +msgid "Coordinates" +msgstr "Coordonnées" + +#: cdl\core\model\image.py:913 cdl\core\model\image.py:914 +#: cdl\core\model\image.py:916 cdl\core\model\signal.py:328 msgid "Data" msgstr "Données" -#: cdl\core\model\image.py:382 cdl\core\model\signal.py:160 +#: cdl\core\model\image.py:915 cdl\core\model\signal.py:329 msgid "Metadata" msgstr "Métadonnées" -#: cdl\core\model\image.py:385 cdl\core\model\image.py:386 -#: cdl\core\model\image.py:389 cdl\core\model\image.py:394 +#: cdl\core\model\image.py:918 cdl\core\model\image.py:919 +#: cdl\core\model\image.py:922 cdl\core\model\image.py:927 msgid "Origin" msgstr "Origine" -#: cdl\core\model\image.py:385 cdl\core\model\image.py:390 -#: cdl\core\model\image.py:393 cdl\core\model\image.py:394 +#: cdl\core\model\image.py:918 cdl\core\model\image.py:923 +#: cdl\core\model\image.py:926 cdl\core\model\image.py:927 msgid "Pixel spacing" msgstr "Taille des pixels" -#: cdl\core\model\image.py:396 cdl\core\model\image.py:412 +#: cdl\core\model\image.py:929 cdl\core\model\image.py:945 msgid "Titles" msgstr "Titres" -#: cdl\core\model\image.py:396 cdl\core\model\image.py:412 +#: cdl\core\model\image.py:929 cdl\core\model\image.py:945 msgid "Units" msgstr "Unités" -#: cdl\core\model\image.py:397 +#: cdl\core\model\image.py:930 msgid "Image title" msgstr "Titre de l'image" -#: cdl\core\model\image.py:400 cdl\core\model\image.py:404 -#: cdl\core\model\image.py:408 cdl\core\model\image.py:837 -#: cdl\core\model\signal.py:167 cdl\core\model\signal.py:171 -#: cdl\core\model\signal.py:807 cdl\widgets\textimport.py:658 +#: cdl\core\model\image.py:933 cdl\core\model\image.py:937 +#: cdl\core\model\image.py:941 cdl\core\model\image.py:1304 +#: cdl\core\model\signal.py:336 cdl\core\model\signal.py:340 +#: cdl\core\model\signal.py:877 cdl\widgets\textimport.py:658 #: cdl\widgets\textimport.py:668 msgid "Title" msgstr "Titre" -#: cdl\core\model\image.py:407 cdl\core\model\image.py:410 +#: cdl\core\model\image.py:940 cdl\core\model\image.py:943 msgid "Z-axis" msgstr "Axe des Z" -#: cdl\core\model\image.py:414 cdl\core\model\image.py:443 -#: cdl\core\model\signal.py:177 cdl\core\model\signal.py:202 +#: cdl\core\model\image.py:947 cdl\core\model\image.py:976 +#: cdl\core\model\signal.py:346 cdl\core\model\signal.py:371 msgid "Scales" msgstr "Echelles" -#: cdl\core\model\image.py:416 cdl\core\model\signal.py:179 +#: cdl\core\model\image.py:949 cdl\core\model\signal.py:348 msgid "Auto scale" msgstr "Echelle automatique" -#: cdl\core\model\image.py:422 cdl\core\model\image.py:431 -#: cdl\core\model\image.py:439 cdl\core\model\signal.py:185 -#: cdl\core\model\signal.py:194 +#: cdl\core\model\image.py:955 cdl\core\model\image.py:964 +#: cdl\core\model\image.py:972 cdl\core\model\signal.py:354 +#: cdl\core\model\signal.py:363 msgid "Lower bound" msgstr "Borne inférieure" -#: cdl\core\model\image.py:425 cdl\core\model\image.py:434 -#: cdl\core\model\image.py:440 cdl\core\model\signal.py:188 -#: cdl\core\model\signal.py:197 +#: cdl\core\model\image.py:958 cdl\core\model\image.py:967 +#: cdl\core\model\image.py:973 cdl\core\model\signal.py:357 +#: cdl\core\model\signal.py:366 msgid "Upper bound" msgstr "Borne supérieure" -#: cdl\core\model\image.py:438 cdl\core\model\image.py:441 +#: cdl\core\model\image.py:971 cdl\core\model\image.py:974 msgid "LUT range" msgstr "Plage de LUT" -#: cdl\core\model\image.py:820 cdl\core\model\signal.py:624 +#: cdl\core\model\image.py:1287 cdl\core\model\signal.py:694 msgid "zeros" msgstr "zéros" -#: cdl\core\model\image.py:822 +#: cdl\core\model\image.py:1289 msgid "empty" msgstr "vide" -#: cdl\core\model\image.py:824 cdl\core\model\signal.py:626 +#: cdl\core\model\image.py:1291 cdl\core\model\signal.py:696 msgid "gaussian" msgstr "gaussienne" -#: cdl\core\model\image.py:826 cdl\core\model\signal.py:632 +#: cdl\core\model\image.py:1293 cdl\core\model\signal.py:702 msgid "random (uniform law)" msgstr "aléatoire (loi uniforme)" -#: cdl\core\model\image.py:828 cdl\core\model\signal.py:634 +#: cdl\core\model\image.py:1295 cdl\core\model\signal.py:704 msgid "random (normal law)" msgstr "aléatoire (loi normale)" -#: cdl\core\model\image.py:839 +#: cdl\core\model\image.py:1306 msgid "Height" msgstr "Hauteur" -#: cdl\core\model\image.py:839 +#: cdl\core\model\image.py:1306 msgid "Image height (total number of rows)" msgstr "Hauteur de l'image (nombre de lignes)" -#: cdl\core\model\image.py:842 +#: cdl\core\model\image.py:1309 msgid "Image width (total number of columns)" msgstr "Largeur de l'image (nombre de colonnes)" -#: cdl\core\model\image.py:842 cdl\tests\data.py:422 +#: cdl\core\model\image.py:1309 cdl\tests\data.py:422 msgid "Width" msgstr "Largeur" -#: cdl\core\model\image.py:847 cdl\core\model\signal.py:813 +#: cdl\core\model\image.py:1314 cdl\core\model\signal.py:883 #: cdl\widgets\h5browser.py:208 msgid "Type" msgstr "Type" -#: cdl\core\model\image.py:852 +#: cdl\core\model\image.py:1319 msgid "Untitled image" msgstr "Image sans titre" -#: cdl\core\model\image.py:948 +#: cdl\core\model\image.py:1415 msgid "2D-gaussian image" msgstr "Gaussienne 2D" -#: cdl\core\model\signal.py:109 +#: cdl\core\model\signal.py:110 msgid "First point coordinate" msgstr "Coordonnée du premier point" -#: cdl\core\model\signal.py:110 +#: cdl\core\model\signal.py:111 msgid "Last point coordinate" msgstr "Coordonnée du dernier point" -#: cdl\core\model\signal.py:157 cdl\core\model\signal.py:161 +#: cdl\core\model\signal.py:326 cdl\core\model\signal.py:330 msgid "Data and metadata" msgstr "Données et métadonnées" -#: cdl\core\model\signal.py:158 cdl\core\model\signal.py:164 +#: cdl\core\model\signal.py:327 cdl\core\model\signal.py:333 msgid "Signal title" msgstr "Titre du signal" -#: cdl\core\model\signal.py:163 cdl\core\model\signal.py:175 +#: cdl\core\model\signal.py:332 cdl\core\model\signal.py:344 msgid "Titles and units" msgstr "Titres et unités" -#: cdl\core\model\signal.py:628 +#: cdl\core\model\signal.py:698 msgid "lorentzian" msgstr "lorentzienne" -#: cdl\core\model\signal.py:636 +#: cdl\core\model\signal.py:706 msgid "sinus" msgstr "sinus" -#: cdl\core\model\signal.py:638 +#: cdl\core\model\signal.py:708 msgid "cosinus" msgstr "cosinus" -#: cdl\core\model\signal.py:640 +#: cdl\core\model\signal.py:710 msgid "sawtooth" msgstr "dent de scie" -#: cdl\core\model\signal.py:642 +#: cdl\core\model\signal.py:712 msgid "triangle" msgstr "triangle" -#: cdl\core\model\signal.py:644 +#: cdl\core\model\signal.py:714 msgid "square" msgstr "carré" -#: cdl\core\model\signal.py:646 +#: cdl\core\model\signal.py:716 msgid "cardinal sine" msgstr "sinus cardinal" -#: cdl\core\model\signal.py:648 +#: cdl\core\model\signal.py:718 msgid "step" msgstr "échelon" -#: cdl\core\model\signal.py:650 +#: cdl\core\model\signal.py:720 msgid "exponential" msgstr "exponentielle" -#: cdl\core\model\signal.py:652 +#: cdl\core\model\signal.py:722 msgid "pulse" msgstr "impulsion" -#: cdl\core\model\signal.py:654 +#: cdl\core\model\signal.py:724 msgid "polynomial" msgstr "polynomial" -#: cdl\core\model\signal.py:656 +#: cdl\core\model\signal.py:726 msgid "experimental" msgstr "expérimental" -#: cdl\core\model\signal.py:698 cdl\widgets\fitdialog.py:309 +#: cdl\core\model\signal.py:768 cdl\widgets\fitdialog.py:309 msgid "Phase" msgstr "Phase" -#: cdl\core\model\signal.py:713 cdl\core\model\signal.py:722 +#: cdl\core\model\signal.py:783 cdl\core\model\signal.py:792 msgid "Offset" -msgstr "" +msgstr "Offset" -#: cdl\core\model\signal.py:714 +#: cdl\core\model\signal.py:784 msgid "Exponent" msgstr "Exposant" -#: cdl\core\model\signal.py:723 +#: cdl\core\model\signal.py:793 msgid "End" msgstr "Fin" -#: cdl\core\model\signal.py:751 +#: cdl\core\model\signal.py:821 msgid "Select one point then press OK to accept" msgstr "Sélectionnez un point puis appuyez sur OK pour valider" -#: cdl\core\model\signal.py:756 +#: cdl\core\model\signal.py:826 msgid "Edit experimental curve" msgstr "Editer la courbe expérimentale" -#: cdl\core\model\signal.py:765 +#: cdl\core\model\signal.py:835 msgid "Insert point" msgstr "Insérer un point" -#: cdl\core\model\signal.py:811 +#: cdl\core\model\signal.py:881 msgid "Signal size (total number of points)" msgstr "Nombre de points" -#: cdl\core\model\signal.py:811 cdl\tests\data.py:279 cdl\tests\data.py:419 +#: cdl\core\model\signal.py:881 cdl\tests\data.py:279 cdl\tests\data.py:419 #: cdl\widgets\h5browser.py:208 msgid "Size" msgstr "Taille" -#: cdl\core\model\signal.py:818 +#: cdl\core\model\signal.py:888 msgid "Untitled signal" msgstr "Signal sans titre" -#: cdl\core\model\signal.py:926 +#: cdl\core\model\signal.py:996 msgid "Lorentzian" msgstr "Lorentzienne" -#: cdl\core\model\signal.py:949 cdl\core\model\signal.py:950 +#: cdl\core\model\signal.py:1019 cdl\core\model\signal.py:1020 msgid "Sinusoid" msgstr "Sinusoïde" -#: cdl\core\model\signal.py:951 +#: cdl\core\model\signal.py:1021 msgid "Sawtooth function" msgstr "Fontion dents de scie" -#: cdl\core\model\signal.py:952 +#: cdl\core\model\signal.py:1022 msgid "Triangle function" msgstr "Fonction triangle" -#: cdl\core\model\signal.py:953 +#: cdl\core\model\signal.py:1023 msgid "Square function" msgstr "Fonction carrée" -#: cdl\core\model\signal.py:954 +#: cdl\core\model\signal.py:1024 msgid "Cardinal sine" msgstr "Sinus cardinal" -#: cdl\core\model\signal.py:971 +#: cdl\core\model\signal.py:1041 msgid "Step function" msgstr "Fonction échelon" -#: cdl\core\model\signal.py:983 +#: cdl\core\model\signal.py:1053 msgid "Exponential function" msgstr "Fonction exponentielle" -#: cdl\core\model\signal.py:998 +#: cdl\core\model\signal.py:1068 msgid "Pulse function" msgstr "Fonction impulsion" -#: cdl\core\model\signal.py:1014 +#: cdl\core\model\signal.py:1084 msgid "Polynomial function" msgstr "Fonction polynomiale" -#: cdl\core\model\signal.py:1031 +#: cdl\core\model\signal.py:1101 msgid "Experimental points" msgstr "Points expérimentaux" @@ -4012,7 +4037,7 @@ msgstr "Image d'homogénéité (Distribution uniforme)" msgid "Random function" msgstr "Fonction gaussienne aléatoire" -#: cdl\tests\scenarios\demo.py:187 +#: cdl\tests\scenarios\demo.py:194 msgid "" "Click OK to start the demo.

Note:
- Demo will cover a " "selection of DataLab features (for a complete list of features, " @@ -4024,7 +4049,7 @@ msgstr "" "complète des fonctionnalités, veuillez vous référer à la documentation)." "
- Elle ne nécessite aucune interaction de l'utilisateur." -#: cdl\tests\scenarios\demo.py:201 +#: cdl\tests\scenarios\demo.py:208 msgid "Click OK to end demo." msgstr "Cliquer sur OK pour terminer la démo." @@ -4192,11 +4217,11 @@ msgstr "Afficher les valeurs" msgid "Image background selection" msgstr "Sélection de l'arrière-plan de l'image" -#: cdl\widgets\imagebackground.py:67 +#: cdl\widgets\imagebackground.py:65 msgid "Background area" msgstr "Zone d'arrière-plan" -#: cdl\widgets\imagebackground.py:72 +#: cdl\widgets\imagebackground.py:70 msgid "Background value:" msgstr "Valeur de l'arrière-plan :" @@ -4577,5 +4602,29 @@ msgstr "Merci de sélectionner le fichier à importer." msgid "Example Wizard" msgstr "Assistant exemple" +#~ msgid "ROI data" +#~ msgstr "Données de la ROI" + +#~ msgid "" +#~ "For convenience, this item accepts a 2D NumPy array, a list of list of " +#~ "numbers, or None. In the end, the data is converted to a 2D NumPy array " +#~ "of integers (if not None)." +#~ msgstr "" +#~ "Pour plus de commodité, cet élément accepte un tableau NumPy 2D, une " +#~ "liste de listes de nombres, ou None. En fin de compte, les données sont " +#~ "converties en un tableau NumPy 2D d'entiers (si non nul)." + +#~ msgid "Single object" +#~ msgstr "Objet unique" + +#~ msgid "Whether to extract the ROI as a single object or not." +#~ msgstr "Extrait la ROI comme un objet unique ou non." + +#~ msgid "Rectangle" +#~ msgstr "Rectangle" + +#~ msgid "Bottom right corner" +#~ msgstr "Coin inférieur droit" + #~ msgid "Cut-off frequency ratio (0.0 - 1.0)." #~ msgstr "Fréquence de coupure relative (0.0 - 1.0)." diff --git a/cdl/obj.py b/cdl/obj.py index a06f3e25..4fe9552a 100644 --- a/cdl/obj.py +++ b/cdl/obj.py @@ -46,6 +46,7 @@ :inherited-members: .. autofunction:: cdl.obj.read_signal .. autofunction:: cdl.obj.read_signals +.. autofunction:: cdl.obj.create_signal_roi .. autofunction:: cdl.obj.create_signal .. autofunction:: cdl.obj.create_signal_from_param .. autofunction:: cdl.obj.new_signal_param @@ -55,6 +56,7 @@ .. autodataset:: cdl.obj.StepParam .. autodataset:: cdl.obj.PeriodicParam .. autodataset:: cdl.obj.ROI1DParam +.. autoclass:: cdl.obj.SignalROI Image model ^^^^^^^^^^^ @@ -64,16 +66,16 @@ :inherited-members: .. autofunction:: cdl.obj.read_image .. autofunction:: cdl.obj.read_images +.. autofunction:: cdl.obj.create_image_roi .. autofunction:: cdl.obj.create_image .. autofunction:: cdl.obj.create_image_from_param .. autofunction:: cdl.obj.new_image_param .. autoclass:: cdl.obj.ImageTypes .. autodataset:: cdl.obj.NewImageParam .. autodataset:: cdl.obj.Gauss2DParam -.. autoclass:: cdl.obj.RoiDataGeometries .. autodataset:: cdl.obj.ROI2DParam +.. autoclass:: cdl.obj.ImageROI .. autoclass:: cdl.obj.ImageDatatypes -.. autoclass:: cdl.obj.ImageRoiDataItem """ # pylint:disable=unused-import @@ -92,11 +94,11 @@ Gauss2DParam, ImageDatatypes, ImageObj, - ImageRoiDataItem, + ImageROI, ImageTypes, NewImageParam, - RoiDataGeometries, ROI2DParam, + create_image_roi, create_image, create_image_from_param, new_image_param, @@ -108,8 +110,10 @@ SignalObj, SignalTypes, ROI1DParam, + create_signal_roi, StepParam, create_signal, create_signal_from_param, new_signal_param, + SignalROI, ) diff --git a/cdl/param.py b/cdl/param.py index 98b94c8d..16e4ea1c 100644 --- a/cdl/param.py +++ b/cdl/param.py @@ -61,8 +61,6 @@ :no-index: .. autodataset:: cdl.param.MovingMedianParam :no-index: -.. autodataset:: cdl.param.ROIDataParam - :no-index: .. autodataset:: cdl.param.ConstantParam :no-index: @@ -210,7 +208,6 @@ HistogramParam, MovingAverageParam, MovingMedianParam, - ROIDataParam, ConstantParam, NormalizeParam, ) diff --git a/cdl/tests/backbone/main_app_test.py b/cdl/tests/backbone/main_app_test.py index 7197264a..75e8a763 100644 --- a/cdl/tests/backbone/main_app_test.py +++ b/cdl/tests/backbone/main_app_test.py @@ -31,8 +31,17 @@ def test_main_app(): except ValueError: pass - # Add signals to signal panel panel = win.signalpanel + + # Create new groups + panel.add_group("Group 1") + panel.add_group("Group 2") + # Rename group + panel.objview.select_groups([2]) + panel.rename_group("Group xxx") + panel.remove_object(force=True) + + # Add signals to signal panel sig1 = create_paracetamol_signal(500) panel.add_object(sig1) panel.processor.compute_derivative() diff --git a/cdl/tests/features/common/metadata_app_test.py b/cdl/tests/features/common/metadata_app_test.py index 0daf5ecf..f14a6fff 100644 --- a/cdl/tests/features/common/metadata_app_test.py +++ b/cdl/tests/features/common/metadata_app_test.py @@ -11,10 +11,8 @@ # pylint: disable=invalid-name # Allows short reference names like x, y, ... # guitest: show -import numpy as np - -import cdl.obj -import cdl.param +import cdl.obj as dlo +import cdl.param as dlp from cdl.core.gui.panel.base import BaseDataPanel from cdl.core.gui.panel.image import ImagePanel from cdl.core.gui.panel.signal import SignalPanel @@ -27,7 +25,7 @@ def __run_signal_computations(panel: SignalPanel): """Test all signal features related to ROI""" execenv.print(" Signal features") - panel.processor.compute_fwhm(cdl.param.FWHMParam()) + panel.processor.compute_fwhm(dlp.FWHMParam()) panel.processor.compute_fw1e2() @@ -36,7 +34,7 @@ def __run_image_computations(panel: ImagePanel): execenv.print(" Image features") panel.processor.compute_centroid() panel.processor.compute_enclosing_circle() - panel.processor.compute_peak_detection(cdl.param.Peak2DDetectionParam()) + panel.processor.compute_peak_detection(dlp.Peak2DDetectionParam()) def __test_metadata_features(panel: BaseDataPanel): @@ -62,13 +60,13 @@ def test_metadata_app(): # === Signal metadata features test === panel = win.signalpanel sig = create_paracetamol_signal(size) - sig.roi = np.array([[26, 41], [125, 146]], int) + sig.roi = dlo.create_signal_roi([[26, 41], [125, 146]], indices=True) panel.add_object(sig) __run_signal_computations(panel) __test_metadata_features(panel) # === Image metadata features test === panel = win.imagepanel - param = cdl.obj.new_image_param(height=size, width=size) + param = dlo.new_image_param(height=size, width=size) ima = roi_app_test.create_test_image_with_roi(param) panel.add_object(ima) __run_image_computations(panel) diff --git a/cdl/tests/features/common/misc_app_test.py b/cdl/tests/features/common/misc_app_test.py index 09a33b81..b277e5d9 100644 --- a/cdl/tests/features/common/misc_app_test.py +++ b/cdl/tests/features/common/misc_app_test.py @@ -147,11 +147,6 @@ def __misc_unit_function(win: CDLMainWindow) -> None: ima.zlabel, ) - # Signal and Image ROI extraction test: test adding a default ROI - __print_test_result("Adding a default ROI to signal and image") - for panel in (win.signalpanel, win.imagepanel): - panel.processor.edit_regions_of_interest(add_roi=True) - # Close application __print_test_result("Close application") win.close_application() diff --git a/cdl/tests/features/common/operation_modes_app_test.py b/cdl/tests/features/common/operation_modes_app_test.py index e62acfe8..3684a878 100644 --- a/cdl/tests/features/common/operation_modes_app_test.py +++ b/cdl/tests/features/common/operation_modes_app_test.py @@ -4,6 +4,15 @@ Operation modes test -------------------- +DataLab has two operation modes: + +- **Single operand mode**: the operation is applied to the selected objects (this + is the default mode) + +- **Pairwise mode**: the operation is applied to the selected pairs of objects + +This test scenario covers the pairwise mode and the operations that can be +performed in this mode: sum, difference, product, division, ... """ # guitest: show @@ -11,17 +20,39 @@ from __future__ import annotations from cdl import app +from cdl.config import Conf +from cdl.core.gui.processor.base import is_pairwise_mode +from cdl.env import execenv from cdl.utils.qthelpers import cdl_app_context from cdl.utils.tests import get_test_fnames -def test_single_operand_mode(): - """Run single operand mode test scenario""" +def check_titles(title, titles): + """Check that the title is one of the expected titles""" + execenv.print(f"{title}:") + for actual_title, expected_title in titles: + execenv.print(f" {actual_title} == {expected_title}", end=" ") + assert actual_title == expected_title + if actual_title == expected_title: + execenv.print("✓") + else: + execenv.print("✗") + + +def test_single_operand_mode_compute_n1(): + """Run single operand mode test scenario + with compute_n1 operation (e.g. sum)""" + original_mode = Conf.proc.operation_mode.get() + Conf.proc.operation_mode.set("single") + with cdl_app_context(exec_loop=True): win = app.create(h5files=[get_test_fnames("reorder*")[0]], console=False) panel = win.signalpanel view, model = panel.objview, panel.objmodel + # Checking the operation mode: + assert not is_pairwise_mode() + # Store the number of groups before the operations n_groups = len(model.get_groups()) @@ -39,10 +70,12 @@ def test_single_operand_mode(): # - signal 2: group 2 signal 1 + group 2 signal 2 assert len(model.get_groups()) == n_groups + 1 new_group = model.get_group_from_number(n_groups + 1) - assert len(new_group.get_objects()) == 2 - for idx, obj in enumerate(new_group.get_objects()): + assert len(new_group) == 2 + titles = [] + for idx, obj in enumerate(new_group): pfx_orig = ", ".join(obj.short_id for obj in groups[idx].get_objects()) - assert obj.title == f"Σ({pfx_orig})" + titles.append((obj.title, f"Σ({pfx_orig})")) + check_titles(f"Single operand mode Σ[{new_group.title}]", titles) # Remove new group view.select_groups([new_group]) @@ -65,14 +98,276 @@ def test_single_operand_mode(): # - signal added to group 1: group 1 signal 1 + group 2 signal 1 # - signal added to group 2: group 1 signal 2 + group 2 signal 2 assert len(model.get_groups()) == n_groups # no new group + titles = [] for idx in range(1): pfx_orig = ", ".join(obj.short_id for obj in groups[idx][:2]) - assert groups[idx][-1].title == f"Σ({pfx_orig})" + titles.append((groups[idx][-1].title, f"Σ({pfx_orig})")) + check_titles(f"Single operand mode Σ[{groups[1].title}]", titles) + + Conf.proc.operation_mode.set(original_mode) + + +def test_pairwise_operations_mode_compute_n1(): + """Run pairwise operations mode test scenario + with compute_n1 operation (e.g. sum)""" + original_mode = Conf.proc.operation_mode.get() + Conf.proc.operation_mode.set("pairwise") + + with cdl_app_context(exec_loop=True): + win = app.create(h5files=[get_test_fnames("reorder*")[0]], console=False) + panel = win.signalpanel + view, model = panel.objview, panel.objmodel + + # Checking the operation mode: + assert is_pairwise_mode() + + # Store the number of groups before the operations + n_groups = len(model.get_groups()) + + # Select the two first groups + groups = [model.get_group_from_number(idx) for idx in (1, 2)] + view.select_groups(groups) + + # Checking that each group contains the same number of signals (this is + # required for pairwise operations - this part of the test is checking + # if the data file is the one we expect) + n_objects = len(groups[0]) + assert all(len(group) == n_objects for group in groups) + + # Perform a sum operation + panel.processor.compute_sum() + + # Operation mode is now pairwise, so the sum operation is applied to the + # selected groups, and we should have a new group with as many signals as + # the original groups, each signal being the sum of the corresponding signals: + # - signal 1: group 1 signal 1 + group 2 signal 1 + # - signal 2: group 1 signal 1 + group 2 signal 2 + # ... + assert len(model.get_groups()) == n_groups + 1 + new_group = model.get_group_from_number(n_groups + 1) + assert len(new_group.get_objects()) == n_objects + titles = [] + for idx in range(len(groups[0])): + obj = new_group[idx] + pfx_orig = ", ".join(obj.short_id for obj in (grp[idx] for grp in groups)) + titles.append((obj.title, f"Σ({pfx_orig})")) + check_titles(f"Pairwise operations mode Σ[{new_group.title}]", titles) + + # Remove new group + view.select_groups([new_group]) + panel.remove_object(force=True) + + # Store the number of groups before the operations + n_groups = len(model.get_groups()) + + # Select two signals of the first two groups + groups = [model.get_group_from_number(idx) for idx in (1, 2)] + objs = [groups[0][0]] + [groups[0][-1]] + groups[1][-2:] + view.select_objects(objs) + + # Perform a sum operation + panel.processor.compute_sum() + + # Operation mode is now pairwise, so the sum operation is applied to the + # selected signals, and we should have a new group with as many signals as + # the selected signals, each signal being the sum of the corresponding signals: + # - signal 1: group 1 signal 1 + group 2 signal 1 + # - signal 2: group 1 signal 1 + group 2 signal 2 + # ... + assert len(model.get_groups()) == n_groups + 1 + new_group = model.get_group_from_number(n_groups + 1) + assert len(new_group) == 2 # 2 signals were selected + titles = [] + for idx, obj in enumerate(new_group): + pfx_orig = ", ".join(obj.short_id for obj in objs[idx::2]) + titles.append((obj.title, f"Σ({pfx_orig})")) + check_titles(f"Pairwise operations mode Σ[{new_group.title}]", titles) + + Conf.proc.operation_mode.set(original_mode) + + +def test_single_operand_mode_compute_n1n(): + """Run single operand mode test scenario + with compute_n1n operation (e.g. difference)""" + original_mode = Conf.proc.operation_mode.get() + Conf.proc.operation_mode.set("single") + + with cdl_app_context(exec_loop=True): + win = app.create(h5files=[get_test_fnames("reorder*")[0]], console=False) + panel = win.signalpanel + view, model = panel.objview, panel.objmodel + + # Checking the operation mode: + assert not is_pairwise_mode() + + # Store the number of groups before the operations + n_groups = len(model.get_groups()) + + # Select the two first groups + groups = [model.get_group_from_number(idx) for idx in (1, 2)] + view.select_groups(groups) + n_objects = [len(grp) for grp in groups] + + # Perform a difference operation with the first signal of the third group + group3 = model.get_group_from_number(3) + panel.processor.compute_difference(group3[0]) + + # Default operation mode is single operand mode, so we should have new signals + # in each selected group being the difference between the original signals and + # the selected signal: + # - in group 1: + # - signal 1: group 1 signal 1 - group 3 signal 1 + # - signal 2: group 1 signal 2 - group 3 signal 1 + # - in group 2: + # - signal 1: group 2 signal 1 - group 3 signal 1 + # - signal 2: group 2 signal 2 - group 3 signal 1 + assert len(model.get_groups()) == n_groups + new_objs = [] + for i_group, group in enumerate(groups): + titles = [] + for i_obj in range(n_objects[i_group]): + obj = group[i_obj + n_objects[i_group]] + titles.append( + (obj.title, f"{group[i_obj].short_id}-{group3[0].short_id}") + ) + new_objs.append(obj) + check_titles(f"Single operand mode Δ[{group.title}]", titles) + + # Remove new signals + view.select_objects(new_objs) + panel.remove_object(force=True) + + # Store the number of groups before the operations + n_groups = len(model.get_groups()) + + # Select the two first signals of the first two groups + groups = [model.get_group_from_number(idx) for idx in (1, 2)] + objs = groups[0][:2] + groups[1][:2] + view.select_objects(objs) + n_objects = [2, 2] + + # Perform a difference operation with the first signal of the third group + panel.processor.compute_difference(group3[0]) + + # Default operation mode is single operand mode, so we should have new signals + # being the difference between the original signals and the selected signal: + # - in group 1: + # - signal 1: group 1 signal 1 - group 3 signal 1 + # - signal 2: group 1 signal 2 - group 3 signal 1 + # - in group 2: + # - signal 1: group 2 signal 1 - group 3 signal 1 + # - signal 2: group 2 signal 2 - group 3 signal 1 + assert len(model.get_groups()) == n_groups # no new group + for i_group, group in enumerate(groups): + titles = [] + for i_obj in range(n_objects[i_group]): + obj = group[len(group) - n_objects[i_group] + i_obj] + titles.append( + (obj.title, f"{group[i_obj].short_id}-{group3[0].short_id}") + ) + check_titles(f"Single operand mode Δ[{group.title}]", titles) + + Conf.proc.operation_mode.set(original_mode) + + +def test_pairwise_operations_mode_compute_n1n(): + """Run pairwise operations mode test scenario + with compute_n1n operation (e.g. difference)""" + original_mode = Conf.proc.operation_mode.get() + Conf.proc.operation_mode.set("pairwise") + + with cdl_app_context(exec_loop=True): + win = app.create(h5files=[get_test_fnames("reorder*")[0]], console=False) + panel = win.signalpanel + view, model = panel.objview, panel.objmodel + + # Checking the operation mode: + assert is_pairwise_mode() + + # Store the number of groups before the operations + n_groups = len(model.get_groups()) + + # Select the two first groups + groups = [model.get_group_from_number(idx) for idx in (1, 2)] + view.select_groups(groups) + + # Checking that each group contains the same number of signals (this is + # required for pairwise operations - this part of the test is checking + # if the data file is the one we expect) + n_objects = len(groups[0]) + assert all(len(group) == n_objects for group in groups) + + # Perform a difference operation with the third group + group3 = model.get_group_from_number(3) + assert len(group3) == n_objects + panel.processor.compute_difference(group3.get_objects()) + + # Operation mode is now pairwise, so the difference operation is applied to the + # selected groups, and we should have a new group with as many signals as the + # original groups, each signal being the difference of the corresponding + # signals: + # - signal 1: group 1 signal 1 - group 2 signal 1 + # - signal 2: group 1 signal 1 - group 2 signal 2 + # ... + assert len(model.get_groups()) == n_groups + 2 + new_groups = [ + model.get_group_from_number(idx) for idx in (n_groups + 1, n_groups + 2) + ] + execenv.print("Δ|pairwise") + for i_new_grp, new_grp in enumerate(new_groups): + assert len(new_grp.get_objects()) == n_objects + titles = [] + for idx in range(n_objects): + obj = new_grp[idx] + obj1, obj2 = groups[i_new_grp][idx], group3[idx] + titles.append((obj.title, f"{obj1.short_id}-{obj2.short_id}")) + check_titles(f"Pairwise operations mode Δ[{new_grp.title}]", titles) + + # Remove new groups + view.select_groups(new_groups) + panel.remove_object(force=True) + + # Store the number of groups before the operations + n_groups = len(model.get_groups()) + + # Select two signals of the first two groups + groups = [model.get_group_from_number(idx) for idx in (1, 2)] + objs = [groups[0][0]] + [groups[0][-1]] + groups[1][-2:] + view.select_objects(objs) + n_objects = 2 + + # Perform a difference operation with two signals from the third group + objs2 = group3[:2] + panel.processor.compute_difference(objs2) + + # Operation mode is now pairwise, so the difference operation is applied to the + # selected signals, and we should have a new group with as many signals as the + # selected signals, each signal being the difference of the corresponding + # signals: + # - signal 1: group 1 signal 1 - group 3 signal 1 + # - signal 2: group 1 signal 1 - group 3 signal 2 + # ... + assert len(model.get_groups()) == n_groups + 2 + new_groups = [ + model.get_group_from_number(idx) for idx in (n_groups + 1, n_groups + 2) + ] + i_obj1 = 0 + execenv.print("Δ|pairwise") + for i_new_grp, new_grp in enumerate(new_groups): + assert len(new_grp.get_objects()) == n_objects + titles = [] + for idx in range(n_objects): + obj = new_grp[idx] + obj1, obj2 = objs[i_obj1], objs2[idx] + i_obj1 += 1 + titles.append((obj.title, f"{obj1.short_id}-{obj2.short_id}")) + check_titles(f"Pairwise operations mode Δ[{new_grp.title}]", titles) - # Removing resulting signals - view.select_objects([groups[0][-1], groups[1][-1]]) - panel.remove_object() + Conf.proc.operation_mode.set(original_mode) if __name__ == "__main__": - test_single_operand_mode() + test_single_operand_mode_compute_n1() + test_pairwise_operations_mode_compute_n1() + test_single_operand_mode_compute_n1n() + test_pairwise_operations_mode_compute_n1n() diff --git a/cdl/tests/features/common/resultshapes_app_test.py b/cdl/tests/features/common/resultshapes_app_test.py index 82720bff..677bd2da 100644 --- a/cdl/tests/features/common/resultshapes_app_test.py +++ b/cdl/tests/features/common/resultshapes_app_test.py @@ -66,9 +66,8 @@ def __check_roi_merge( """ roi1 = obj1.roi roi2 = obj2.roi - assert np.all(roi1 == roi2[: len(roi1)]) - assert np.all(roi1 == roi2[len(roi1) : len(roi1) * 2]) - assert roi1.shape[0] * 2 == roi2.shape[0] + for single_roi2 in roi2: + assert roi1.get_single_roi(0) == single_roi2 def test_resultshapes(): @@ -76,7 +75,7 @@ def test_resultshapes(): with cdltest_app_context(console=False) as win: obj1 = test_data.create_sincos_image() obj2 = create_image_with_resultshapes() - obj2.roi = np.array([[10, 10, 60, 400]], int) + obj2.roi = cdl.obj.create_image_roi("rectangle", [10, 10, 50, 400]) panel = win.signalpanel for noised in (False, True): sig = test_data.create_noisy_signal(noised=noised) diff --git a/cdl/tests/features/common/roi_app_test.py b/cdl/tests/features/common/roi_app_test.py index 5a6ae176..b542389d 100644 --- a/cdl/tests/features/common/roi_app_test.py +++ b/cdl/tests/features/common/roi_app_test.py @@ -34,8 +34,17 @@ SROI2 = [125, 146] # Image ROIs: -IROI1 = [SIZE // 2, SIZE // 2, SIZE - 25, SIZE] # Rectangle -IROI2 = [SIZE // 4, SIZE // 2, SIZE // 2, SIZE // 2] # Circle +IROI1 = [SIZE // 2, SIZE // 2, SIZE - 25 - SIZE // 2, SIZE - SIZE // 2] # Rectangle +IROI2 = [SIZE // 3, SIZE // 2, SIZE // 4] # Circle +IROI3 = [ + SIZE // 2, + SIZE // 2, + SIZE // 2, + SIZE - SIZE // 4, + SIZE - SIZE // 4, + SIZE - SIZE // 3, +] # Polygon (triangle, that is intentionally inside the rectangle, so that this ROI +# has no impact on the mask calculations in the tests) def __run_signal_computations(panel: SignalPanel, singleobj: bool | None = None): @@ -44,17 +53,18 @@ def __run_signal_computations(panel: SignalPanel, singleobj: bool | None = None) panel.processor.compute_fw1e2() panel.processor.compute_histogram(dlp.HistogramParam()) panel.remove_object() - roiparam = dlp.ROIDataParam.create(singleobj=singleobj) obj_nb = len(panel) last_obj = panel[obj_nb] + roi = dlo.SignalROI(singleobj=singleobj) if execenv.unattended: # In unattended mode, we need to set the ROI manually. # On the contrary, in interactive mode, the ROI editor is opened and will # automatically set the ROI from the currently selected object. - roiparam.roidata = last_obj.roi + if last_obj.roi is not None: + roi.single_rois = last_obj.roi.single_rois panel.processor.compute_gaussian_filter(dlp.GaussianParam.create(sigma=10.0)) - if execenv.unattended and last_obj.roi is not None: + if execenv.unattended and last_obj.roi is not None and not last_obj.roi.is_empty(): # Check if the processed data is correct: signal should be the same as the # original data outside the ROI, and should be different inside the ROI. orig = last_obj.data @@ -76,20 +86,20 @@ def __run_signal_computations(panel: SignalPanel, singleobj: bool | None = None) ), "Signal after ROI 2 data mismatch" panel.remove_object() - panel.processor.compute_roi_extraction(roiparam) - if execenv.unattended and last_obj.roi is not None: + panel.processor.compute_roi_extraction(roi) + if execenv.unattended and last_obj.roi is not None and not last_obj.roi.is_empty(): + # Assertions texts: + ssm = "Signal %d size mismatch" + sdm = "Signal %d data mismatch" + orig = last_obj.data if singleobj is None or not singleobj: # Multiple objects mode assert len(panel) == obj_nb + 2, "Two objects expected" sig1, sig2 = panel[obj_nb + 1], panel[obj_nb + 2] - assert sig1.data.size == SROI1[1] - SROI1[0], "Signal 1 size mismatch" - assert sig2.data.size == SROI2[1] - SROI2[0], "Signal 2 size mismatch" - assert np.all( - sig1.data == orig[SROI1[0] : SROI1[1]] - ), "Signal 1 data mismatch" - assert np.all( - sig2.data == orig[SROI2[0] : SROI2[1]] - ), "Signal 2 data mismatch" + assert sig1.data.size == SROI1[1] - SROI1[0], ssm % 1 + assert sig2.data.size == SROI2[1] - SROI2[0], ssm % 2 + assert np.all(sig1.data == orig[SROI1[0] : SROI1[1]]), sdm % 1 + assert np.all(sig2.data == orig[SROI2[0] : SROI2[1]]), sdm % 2 else: assert len(panel) == obj_nb + 1, "One object expected" sig = panel[obj_nb + 1] @@ -97,10 +107,10 @@ def __run_signal_computations(panel: SignalPanel, singleobj: bool | None = None) assert sig.data.size == exp_size, "Signal size mismatch" assert np.all( sig.data[: SROI1[1] - SROI1[0]] == orig[SROI1[0] : SROI1[1]] - ), "Signal 1 data mismatch" + ), sdm % 1 assert np.all( sig.data[SROI2[0] - SROI2[1] :] == orig[SROI2[0] : SROI2[1]] - ), "Signal 2 data mismatch" + ), sdm % 2 def __run_image_computations(panel: ImagePanel, singleobj: bool | None = None): @@ -110,34 +120,37 @@ def __run_image_computations(panel: ImagePanel, singleobj: bool | None = None): panel.processor.compute_histogram(dlp.HistogramParam()) panel.processor.compute_peak_detection(dlp.Peak2DDetectionParam()) obj_nb = len(panel) - - roiparam = dlp.ROIDataParam.create(singleobj=singleobj) last_obj = panel[obj_nb] + roi = dlo.ImageROI(singleobj=singleobj) if execenv.unattended: # In unattended mode, we need to set the ROI manually. # On the contrary, in interactive mode, the ROI editor is opened and will # automatically set the ROI from the currently selected object. - roiparam.roidata = last_obj.roi + if last_obj.roi is not None: + roi.single_rois = last_obj.roi.single_rois value = 1 panel.processor.compute_addition_constant(dlp.ConstantParam.create(value=value)) - if execenv.unattended and last_obj.roi is not None: + if execenv.unattended and last_obj.roi is not None and not last_obj.roi.is_empty(): # Check if the processed data is correct: image should be the same as the # original data outside the ROI, and should be different inside the ROI. orig = last_obj.data new = panel[obj_nb + 1].data assert np.all( - new[IROI1[1] : IROI1[3], IROI1[0] : IROI1[2]] - == orig[IROI1[1] : IROI1[3], IROI1[0] : IROI1[2]] + value + new[IROI1[1] : IROI1[3] + IROI1[1], IROI1[0] : IROI1[2] + IROI1[0]] + == orig[IROI1[1] : IROI1[3] + IROI1[1], IROI1[0] : IROI1[2] + IROI1[0]] + + value ), "Image ROI 1 data mismatch" assert np.all( - new[IROI2[1] : IROI2[3], IROI2[0] : IROI2[2]] - == orig[IROI2[1] : IROI2[3], IROI2[0] : IROI2[2]] + value + new[IROI2[1] : IROI1[1] + 1, IROI2[0] : IROI2[0] + 2 * IROI2[2]] + == orig[IROI2[1] : IROI1[1] + 1, IROI2[0] : IROI2[0] + 2 * IROI2[2]] + value ), "Image ROI 2 data mismatch" - first_col, first_row = min(IROI1[0], IROI2[0]), min(IROI1[1], IROI2[1]) - last_col, last_row = max(IROI1[2], IROI2[2]), max(IROI1[3], IROI2[3]) + first_col = min(IROI1[0], IROI2[0] - IROI2[2]) + first_row = min(IROI1[1], IROI2[1] - IROI2[2]) + last_col = max(IROI1[0] + IROI1[2], IROI2[0] + 2 * IROI2[2]) + last_row = max(IROI1[1] + IROI1[3], IROI2[1] + 2 * IROI2[2]) assert np.all( - new[:first_row, :first_col] == orig[:first_row, :first_col] + new[:first_row, :first_col] == np.array(orig[:first_row, :first_col], float) ), "Image before ROIs data mismatch" assert np.all( new[:first_row, last_col:] == orig[:first_row, last_col:] @@ -150,48 +163,43 @@ def __run_image_computations(panel: ImagePanel, singleobj: bool | None = None): ), "Image after ROIs data mismatch" panel.remove_object() - panel.processor.compute_roi_extraction(roiparam) - if execenv.unattended and last_obj.roi is not None: + panel.processor.compute_roi_extraction(roi) + if execenv.unattended and last_obj.roi is not None and not last_obj.roi.is_empty(): # Assertions texts: nzroi = "Non-zero values expected in ROI" zroi = "Zero values expected outside ROI" roisham = "ROI shape mismatch" if singleobj is None or not singleobj: # Multiple objects mode - assert len(panel) == obj_nb + 2, "Two objects expected" + assert len(panel) == obj_nb + 3, "Three objects expected" im1, im2 = panel[obj_nb + 1], panel[obj_nb + 2] assert np.all(im1.data != 0), nzroi - assert im1.data.shape == (IROI1[3] - IROI1[1], IROI1[2] - IROI1[0]), roisham + assert im1.data.shape == (IROI1[3], IROI1[2]), roisham assert np.all(im2.data != 0), nzroi - assert im2.data.shape == (IROI2[2] - IROI2[0], IROI2[2] - IROI2[0]), roisham + assert im2.data.shape == (IROI2[2] * 2, IROI2[2] * 2), roisham mask2 = np.zeros(shape=im2.data.shape, dtype=bool) - xc, yc = (IROI2[0] + IROI2[2]) / 2, (IROI2[1] + IROI2[3]) / 2 - r = (IROI2[2] - IROI2[0]) / 2 - xc = yc = xc - IROI2[0] # Adjust for ROI origin + xc, yc, r = IROI2 + xc = yc = r # Adjust for ROI origin rr, cc = draw.disk((yc, xc), r) mask2[rr, cc] = 1 assert np.all(im2.maskdata == ~mask2), "Mask data mismatch" else: # Single object mode assert len(panel) == obj_nb + 1, "One object expected" - # Compute ROI image masks: - # (ROI are defined as [x1, y1, x2, y2] for rectangles - # and [x1, y1, x2, y1] for circles: - # x is the column index, y is the row index) mask1 = np.zeros(shape=(SIZE, SIZE), dtype=bool) - mask1[IROI1[1] : IROI1[3], IROI1[0] : IROI1[2]] = 1 - xc, yc = (IROI2[0] + IROI2[2]) / 2, (IROI2[1] + IROI2[3]) / 2 - r = (IROI2[2] - IROI2[0]) / 2 + mask1[IROI1[1] : IROI1[1] + IROI1[3], IROI1[0] : IROI1[0] + IROI1[2]] = 1 + xc, yc, r = IROI2 mask2 = np.zeros(shape=(SIZE, SIZE), dtype=bool) rr, cc = draw.disk((yc, xc), r) mask2[rr, cc] = 1 mask = mask1 | mask2 row_min = int(min(IROI1[1], IROI2[1] - r)) - col_min = int(min(IROI1[0], IROI2[0])) - row_max = int(max(IROI1[3], IROI2[3] + r)) - col_max = int(max(IROI1[2], IROI2[2])) + col_min = int(min(IROI1[0], IROI2[0] - r)) + row_max = int(max(IROI1[1] + IROI1[3], IROI2[1] + r)) + col_max = int(max(IROI1[0] + IROI1[2], IROI2[0] + r)) mask = mask[row_min:row_max, col_min:col_max] + # execenv.unattended = False im = panel[obj_nb + 1] assert np.all(im.data[mask] != 0), nzroi assert np.all(im.data[~mask] == 0), zroi @@ -210,17 +218,24 @@ def create_test_image_with_roi( """ ima = create_multigauss_image(newimageparam) ima.data += 1 # Ensure that the image has non-zero values (for ROI check tests) - ima.roi = np.array([IROI1, IROI2], int) + roi = dlo.create_image_roi("rectangle", IROI1) + roi.add_roi(dlo.create_image_roi("circle", IROI2)) + roi.add_roi(dlo.create_image_roi("polygon", IROI3)) + ima.roi = roi return ima def array_2d_to_str(arr: np.ndarray) -> str: """Return 2-D array characteristics as string""" + if arr.size == 0: + return "Empty array!" return f"{arr.shape[0]} x {arr.shape[1]} array (min={arr.min()}, max={arr.max()})" def array_1d_to_str(arr: np.ndarray) -> str: """Return 1-D array characteristics as string""" + if arr.size == 0: + return "Empty array!" return f"{arr.size} columns array (min={arr.min()}, max={arr.max()})" @@ -230,7 +245,7 @@ def print_obj_shapes(obj): func = array_1d_to_str if isinstance(obj, dlo.SignalObj) else array_2d_to_str execenv.print(f" data: {func(obj.data)}") if obj.roi is not None: - for idx in range(obj.roi.shape[0]): + for idx in range(len(obj.roi)): roi_data = obj.get_data(idx) if isinstance(obj, dlo.SignalObj): roi_data = roi_data[1] # y data @@ -247,7 +262,7 @@ def test_roi_app(screenshots: bool = False): panel.add_object(sig1) __run_signal_computations(panel) sig2 = create_paracetamol_signal(SIZE) - sig2.roi = np.array([SROI1, SROI2], int) + sig2.roi = dlo.create_signal_roi([SROI1, SROI2], indices=True) for singleobj in (False, True): sig2_i = sig2.copy() panel.add_object(sig2_i) diff --git a/cdl/tests/features/common/roieditor_unit_test.py b/cdl/tests/features/common/roieditor_unit_test.py new file mode 100644 index 00000000..0001d7ab --- /dev/null +++ b/cdl/tests/features/common/roieditor_unit_test.py @@ -0,0 +1,101 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +""" +ROI editor unit test +""" + +# pylint: disable=invalid-name # Allows short reference names like x, y, ... +# guitest: show + +from __future__ import annotations + +import numpy as np +from guidata.qthelpers import exec_dialog, qt_app_context +from plotpy.plot import PlotDialog + +from cdl.core.gui.panel.image import ImagePanel +from cdl.core.gui.panel.signal import SignalPanel +from cdl.core.gui.roieditor import ImageROIEditor, SignalROIEditor +from cdl.env import execenv +from cdl.obj import ImageROI, create_image_roi, create_signal_roi +from cdl.tests.data import create_multigauss_image, create_paracetamol_signal + + +def test_signal_roi_editor() -> None: + """Test signal ROI editor""" + cls = SignalROIEditor + title = f"Testing {cls.__name__}" + options = SignalPanel.ROIDIALOGOPTIONS + obj = create_paracetamol_signal() + roi = create_signal_roi([50, 100], indices=True) + obj.roi = roi + with qt_app_context(exec_loop=False): + execenv.print(title) + dlg = PlotDialog(title=title, edit=True, options=options, toolbar=True) + editor = cls(dlg, obj, extract=True) + dlg.button_layout.insertWidget(0, editor) + exec_dialog(dlg) + + +def create_image_roi_example() -> ImageROI: + """Create an example image ROI""" + roi = create_image_roi("rectangle", [500, 750, 1000, 1250]) + roi.add_roi(create_image_roi("circle", [1500, 1500, 500])) + roi.add_roi( + create_image_roi("polygon", [450, 150, 1300, 350, 1250, 950, 400, 1350]) + ) + return roi + + +def test_image_roi_editor() -> None: + """Test image ROI editor""" + cls = ImageROIEditor + title = f"Testing {cls.__name__}" + options = ImagePanel.ROIDIALOGOPTIONS + obj = create_multigauss_image() + obj.roi = create_image_roi_example() + with qt_app_context(exec_loop=False): + execenv.print(title) + for extract in (True, False): + execenv.print(f" extract={extract}") + dlg = PlotDialog(title=title, edit=True, options=options, toolbar=True) + roi_editor = cls(dlg, obj, extract=extract) + dlg.button_layout.insertWidget(0, roi_editor) + if not extract: + # Clear the ROI + roi_editor.remove_all_rois() + if exec_dialog(dlg): + results = roi_editor.get_roieditor_results() + if results is not None: + edited_roi, modified = results + if extract: + # Test that the single ROIs are equal + assert all( + [ + np.array_equal( + sroi1.get_physical_coords(obj), + sroi2.get_physical_coords(obj), + ) + for sroi1, sroi2 in zip( + obj.roi.single_rois, edited_roi.single_rois + ) + ] + ), "Single ROIs are not equal" + execenv.print(" Single ROIs indice coordinates:") + for sroi in edited_roi.single_rois: + execenv.print( + f" {sroi.title} ({sroi.__class__.__name__}):" + ) + c_i = [int(val) for val in sroi.get_indices_coords(obj)] + c_p = [float(val) for val in sroi.get_physical_coords(obj)] + execenv.print(f" Indices : {c_i}") + execenv.print(f" Physical: {c_p}") + else: + # Test the use case where the ROI is cleared + assert modified, "ROI is not modified" + assert edited_roi.is_empty(), "ROI is not cleared" + + +if __name__ == "__main__": + # test_signal_roi_editor() + test_image_roi_editor() diff --git a/cdl/tests/features/common/roiobjects_unit_test.py b/cdl/tests/features/common/roiobjects_unit_test.py new file mode 100644 index 00000000..469454e3 --- /dev/null +++ b/cdl/tests/features/common/roiobjects_unit_test.py @@ -0,0 +1,147 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +""" +ROI objects unit tests +""" + +# pylint: disable=invalid-name # Allows short reference names like x, y, ... +# guitest: show + +from __future__ import annotations + +import numpy as np +from guidata.qthelpers import qt_app_context + +from cdl.core.gui.roieditor import plot_item_to_single_roi +from cdl.env import execenv +from cdl.obj import ( + ImageObj, + ImageROI, + SignalObj, + SignalROI, + create_image_roi, + create_signal_roi, +) +from cdl.tests.data import create_multigauss_image, create_paracetamol_signal + +CLASS_NAME = "class_name" + + +def __conversion_methods(roi: SignalROI | ImageROI, obj: SignalObj | ImageObj) -> None: + """Test conversion methods for single ROI objects""" + execenv.print(" test `to_dict` and `from_dict` methods") + roi_dict = roi.to_dict() + roi_new = obj.get_roi_class().from_dict(roi_dict) + assert roi.get_single_roi(0) == roi_new.get_single_roi(0) + + execenv.print(" test `to_plot_item` and `from_plot_item` methods: ", end="") + single_roi = roi.get_single_roi(0) + with qt_app_context(exec_loop=False): + plot_item = single_roi.to_plot_item(obj) + sroi_new = plot_item_to_single_roi(plot_item) + orig_coords = [float(val) for val in single_roi.get_physical_coords(obj)] + new_coords = [float(val) for val in sroi_new.get_physical_coords(obj)] + execenv.print(f"{orig_coords} --> {new_coords}") + assert np.array_equal(orig_coords, new_coords) + + +def test_create_signal_roi() -> None: + """Test create_signal_roi""" + + # ROI coordinates: for each ROI type, the coordinates are given for indices=True + # and indices=False (physical coordinates) + coords = { + "segment": { + CLASS_NAME: "SegmentROI", + True: [50, 100], # indices [x0, dx] + False: [7.5, 10.0], # physical + }, + } + + obj = create_paracetamol_signal() + + for indices in (True, False): + execenv.print("indices:", indices) + + for geometry in coords: + execenv.print(" geometry:", geometry) + + roi = create_signal_roi(coords[geometry][indices], indices=indices) + + sroi = roi.get_single_roi(0) + assert sroi.__class__.__name__ == coords[geometry][CLASS_NAME] + + cds_ind = [int(val) for val in sroi.get_indices_coords(obj)] + assert cds_ind == coords[geometry][True] + + cds_phys = [float(val) for val in sroi.get_physical_coords(obj)] + assert cds_phys == coords[geometry][False] + + execenv.print(" get_physical_coords:", cds_phys) + execenv.print(" get_indices_coords: ", cds_ind) + + __conversion_methods(roi, obj) + + +def test_create_image_roi() -> None: + """Test create_image_roi""" + + # ROI coordinates: for each ROI type, the coordinates are given for indices=True + # and indices=False (physical coordinates) + coords = { + "rectangle": { + CLASS_NAME: "RectangularROI", + True: [500, 750, 1000, 1250], # indices [x0, y0, dx, dy] + False: [500.5, 750.5, 1000.0, 1250.0], # physical + }, + "circle": { + CLASS_NAME: "CircularROI", + True: [1500, 1500, 500], # indices [x0, y0, radius] + False: [1500.5, 1500.5, 500.0], # physical + }, + "polygon": { + CLASS_NAME: "PolygonalROI", + True: [450, 150, 1300, 350, 1250, 950, 400, 1350], # indices [x0, y0, ,...] + False: [450.5, 150.5, 1300.5, 350.5, 1250.5, 950.5, 400.5, 1350.5], # phys. + }, + } + + obj = create_multigauss_image() + + for indices in (True, False): + execenv.print("indices:", indices) + + for geometry in coords: + execenv.print(" geometry:", geometry) + + roi = create_image_roi(geometry, coords[geometry][indices], indices=indices) + + sroi = roi.get_single_roi(0) + assert sroi.__class__.__name__ == coords[geometry][CLASS_NAME] + + bbox_phys = [float(val) for val in sroi.get_bounding_box(obj)] + if geometry in ("rectangle", "circle"): + x0, y0, x1, y1 = obj.physical_to_indices(bbox_phys) + if geometry == "rectangle": + coords_from_bbox = [int(xy) for xy in [x0, y0, x1 - x0, y1 - y0]] + elif geometry == "circle": + coords_from_bbox = [ + int(xy) for xy in [(x0 + x1) / 2, (y0 + y1) / 2, (x1 - x0) / 2] + ] + assert coords_from_bbox == coords[geometry][True] + + cds_phys = [float(val) for val in sroi.get_physical_coords(obj)] + assert cds_phys == coords[geometry][False] + cds_ind = [int(val) for val in sroi.get_indices_coords(obj)] + assert cds_ind == coords[geometry][True] + + execenv.print(" get_bounding_box: ", bbox_phys) + execenv.print(" get_physical_coords:", cds_phys) + execenv.print(" get_indices_coords: ", cds_ind) + + __conversion_methods(roi, obj) + + +if __name__ == "__main__": + test_create_signal_roi() + test_create_image_roi() diff --git a/cdl/tests/features/common/stat_unit_test.py b/cdl/tests/features/common/stat_unit_test.py index 1c83a6fc..3dc923f5 100644 --- a/cdl/tests/features/common/stat_unit_test.py +++ b/cdl/tests/features/common/stat_unit_test.py @@ -57,7 +57,7 @@ def create_reference_signal() -> cdl.obj.SignalObj: snew = cdl.obj.new_signal_param("Gaussian", stype=cdl.obj.SignalTypes.GAUSS) addparam = cdl.obj.GaussLorentzVoigtParam() sig = cdl.obj.create_signal_from_param(snew, addparam=addparam, edit=False) - sig.roi = np.array([[len(sig.x) // 2, len(sig.x) - 1]], int) + sig.roi = cdl.obj.create_signal_roi([len(sig.x) // 2, len(sig.x) - 1], indices=True) return sig @@ -67,13 +67,13 @@ def create_reference_image() -> cdl.obj.ImageObj: addparam = cdl.obj.Gauss2DParam() ima = cdl.obj.create_image_from_param(inew, addparam=addparam, edit=False) dy, dx = ima.data.shape - ima.roi = np.array( + ima.roi = cdl.obj.create_image_roi( + "rectangle", [ [dx // 2, 0, dx, dy], [0, 0, dx // 3, dy // 3], [dx // 2, dy // 2, dx, dy], ], - int, ) return ima @@ -99,7 +99,7 @@ def test_signal_stats_unit() -> None: for key, val in ref.items(): colname = name_map[key] assert colname in df - assert np.isclose(df[colname][0], val) + assert np.isclose(df[colname][0], val), f"Incorrect value for {colname}" # Given the fact that signal ROI is set to [len(sig.x) // 2, len(sig.x) - 1], # we may check the relationship between the results on the whole signal and the ROI: @@ -139,7 +139,9 @@ def test_image_stats_unit() -> None: for key, val in ref.items(): colname = name_map[key] assert colname in df - assert np.isclose(df[colname][0], val, rtol=1e-4, atol=1e-5) + assert np.isclose( + df[colname][0], val, rtol=1e-4, atol=1e-5 + ), f"Incorrect value for {colname}" # Given the fact that image ROI is set to # [[dx // 2, 0, dx, dy], [0, 0, dx // 3, dy // 3], [dx // 2, dy // 2, dx, dy]], diff --git a/cdl/tests/features/control/embedded1_unit_test.py b/cdl/tests/features/control/embedded1_unit_test.py index 8ec8c249..f29aa6a8 100644 --- a/cdl/tests/features/control/embedded1_unit_test.py +++ b/cdl/tests/features/control/embedded1_unit_test.py @@ -233,11 +233,12 @@ def add_additional_buttons(self): def import_object(self, panel, title): """Import object from DataLab""" - self.host.log(f"get_object_with_dialog ({title}):") - obj = panel.get_object_with_dialog(title, parent=self.host) - if obj is not None: - self.host.log(f" -> {obj.title}:") - self.host.log(str(obj)) + self.host.log(f"get_objects_with_dialog ({title}):") + objs = panel.get_objects_with_dialog(title, parent=self.host) + if objs is not None: + for obj in objs: + self.host.log(f" -> {obj.title}:") + self.host.log(str(obj)) else: self.host.log(" -> canceled") diff --git a/cdl/tests/features/images/annotations_app_test.py b/cdl/tests/features/images/annotations_app_test.py index f4746205..5c293c75 100644 --- a/cdl/tests/features/images/annotations_app_test.py +++ b/cdl/tests/features/images/annotations_app_test.py @@ -10,9 +10,8 @@ # guitest: show -import numpy as np - from cdl.app import run +from cdl.obj import create_image_roi from cdl.tests import data as test_data @@ -20,7 +19,7 @@ def test_annotations_app(): """Annotations test""" obj1 = test_data.create_sincos_image() obj2 = test_data.create_annotated_image() - obj2.roi = np.array([[10, 10, 60, 400]], int) + obj2.roi = create_image_roi("rectangle", [10, 10, 60, 400]) run(console=False, objects=(obj1, obj2), size=(1200, 550)) diff --git a/cdl/tests/features/images/centroid_unit_test.py b/cdl/tests/features/images/centroid_unit_test.py index e25c2b38..9f9b4fa8 100644 --- a/cdl/tests/features/images/centroid_unit_test.py +++ b/cdl/tests/features/images/centroid_unit_test.py @@ -103,8 +103,8 @@ def test_image_centroid(): param = cdl.obj.NewImageParam.create(height=500, width=500) image = create_noisygauss_image(param, center=(-2.0, 3.0), add_annotations=True) df = cpi.compute_centroid(image).to_dataframe() - check_scalar_result("Centroid X", df.x[0], 199) - check_scalar_result("Centroid Y", df.y[0], 324) + check_scalar_result("Centroid X", df.x[0], 199.5, atol=0.1) + check_scalar_result("Centroid Y", df.y[0], 324.4, atol=0.1) if __name__ == "__main__": diff --git a/cdl/tests/features/images/contour_fabryperot_app_test.py b/cdl/tests/features/images/contour_fabryperot_app_test.py index 7a4e8d8c..541d39b7 100644 --- a/cdl/tests/features/images/contour_fabryperot_app_test.py +++ b/cdl/tests/features/images/contour_fabryperot_app_test.py @@ -7,8 +7,6 @@ # pylint: disable=invalid-name # Allows short reference names like x, y, ... # guitest: show -import numpy as np - import cdl.obj import cdl.param from cdl.tests import cdltest_app_context, take_plotwidget_screenshot @@ -25,7 +23,7 @@ def test_contour_app(): ima1 = get_test_image("fabry-perot1.jpg") ima1.metadata["colormap"] = "gray" xc, yc, r = 601.0, 556.0, 457.0 - roi = np.array([[xc - r, yc, xc + r, yc]], int) + roi = cdl.obj.create_image_roi("circle", [xc, yc, r]) ima1.roi = roi panel.add_object(ima1) param = cdl.param.ContourShapeParam.create(shape=shape) diff --git a/cdl/tests/features/images/offset_correction_unit_test.py b/cdl/tests/features/images/offset_correction_unit_test.py index 6f42ad20..a8e4b9ac 100644 --- a/cdl/tests/features/images/offset_correction_unit_test.py +++ b/cdl/tests/features/images/offset_correction_unit_test.py @@ -35,12 +35,13 @@ def test_image_offset_correction_interactive() -> None: # On Linux, the delay is required to ensure that the dialog is displayed # because the `QApplication.processEvents()` do not trigger the drawing # event on the dialog as expected. So, the `RangeComputation2d` is not - # drawn, the background value is not computed, and `get_index_range()` + # drawn, the background value is not computed, and `get_rect_coords()` # returns `None` which causes the test to fail. ok = exec_dialog(dlg) if ok: param = ROI2DParam() - param.xr0, param.yr0, param.xr1, param.yr1 = dlg.get_index_range() + ix0, iy0, ix1, iy1 = i1.physical_to_indices(dlg.get_rect_coords()) + param.x0, param.y0, param.dx, param.dy = ix0, iy0, ix1 - ix0, iy1 - iy0 i2 = cpi.compute_offset_correction(i1, param) i3 = cpi.compute_clip(i2, cdl.param.ClipParam.create(lower=0)) view_images_side_by_side( @@ -54,11 +55,12 @@ def test_image_offset_correction_interactive() -> None: def test_image_offset_correction() -> None: """Image offset correction validation test.""" i1 = create_noisygauss_image() - param = ROI2DParam.create(xr0=0, yr0=0, xr1=10, yr1=10) + param = ROI2DParam.create(x0=0, y0=0, dx=10, dy=10) i2 = cpi.compute_offset_correction(i1, param) # Check that the offset correction has been applied - x0, y0, x1, y1 = param.xr0, param.yr0, param.xr1, param.yr1 + x0, y0 = param.x0, param.y0 + x1, y1 = x0 + param.dx, y0 + param.dy offset = np.mean(i1.data[y0:y1, x0:x1]) assert np.allclose(i2.data, i1.data - offset), "Offset correction failed" diff --git a/cdl/tests/features/images/processing_unit_test.py b/cdl/tests/features/images/processing_unit_test.py index 2d703461..db18faea 100644 --- a/cdl/tests/features/images/processing_unit_test.py +++ b/cdl/tests/features/images/processing_unit_test.py @@ -137,9 +137,11 @@ def test_image_offset_correction() -> None: """Validation test for the image offset correction processing.""" src = get_test_image("flower.npy") # Defining the ROI that will be used to estimate the offset - p = cdl.obj.ROI2DParam.create(xr0=0, yr0=0, xr1=50, yr1=20) + p = cdl.obj.ROI2DParam.create(x0=0, y0=0, dx=50, dy=20) dst = cpi.compute_offset_correction(src, p) - exp = src.data - np.mean(src.data[p.yr0 : p.yr1, p.xr0 : p.xr1]) + ix0, iy0 = int(p.x0), int(p.y0) + ix1, iy1 = int(p.x0 + p.dx), int(p.y0 + p.dy) + exp = src.data - np.mean(src.data[iy0:iy1, ix0:ix1]) check_array_result("OffsetCorrection", dst.data, exp) diff --git a/cdl/tests/features/images/roi2dparam_unit_test.py b/cdl/tests/features/images/roi2dparam_unit_test.py index 1256d3ca..2c488af7 100644 --- a/cdl/tests/features/images/roi2dparam_unit_test.py +++ b/cdl/tests/features/images/roi2dparam_unit_test.py @@ -8,9 +8,10 @@ # guitest: show import guidata.dataset as gds +import numpy as np from guidata.qthelpers import qt_app_context -from cdl.core.model.image import ROI2DParam, RoiDataGeometries +from cdl.core.model.image import ROI2DParam from cdl.env import execenv @@ -18,12 +19,15 @@ def test_roi_param_interactive(): """ROI parameters interactive test.""" with qt_app_context(): p_circ = ROI2DParam("Circular") - p_circ.geometry = RoiDataGeometries.CIRCLE + p_circ.geometry = "circle" p_circ.xc, p_circ.yc, p_circ.r = 100, 200, 50 p_rect = ROI2DParam("Rectangular") - p_rect.geometry = RoiDataGeometries.RECTANGLE - p_rect.xr0, p_rect.yr0, p_rect.xr1, p_rect.yr1 = 50, 150, 150, 250 - params = [p_circ, p_rect] + p_rect.geometry = "rectangle" + p_rect.x0, p_rect.y0, p_rect.dx, p_rect.dy = 50, 150, 150, 250 + p_poly = ROI2DParam("Polygonal") + p_poly.geometry = "polygon" + p_poly.points = np.array([[50, 150], [150, 150], [150, 250], [50, 250]]) + params = [p_circ, p_rect, p_poly] group = gds.DataSetGroup(params, title="ROI Parameters") if group.edit(): for param in params: diff --git a/cdl/tests/features/images/roi_circ_app_test.py b/cdl/tests/features/images/roi_circ_app_test.py index f3829aab..4777ee1b 100644 --- a/cdl/tests/features/images/roi_circ_app_test.py +++ b/cdl/tests/features/images/roi_circ_app_test.py @@ -7,31 +7,39 @@ # pylint: disable=invalid-name # Allows short reference names like x, y, ... # guitest: show +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + import numpy as np from skimage import draw -import cdl.param as dlp +import cdl.obj as dlo from cdl.env import execenv -from cdl.obj import RoiDataGeometries, create_image from cdl.tests import cdltest_app_context from cdl.tests.features.common.roi_app_test import print_obj_shapes +if TYPE_CHECKING: + from cdl.obj import ImageObj + -def create_test_image_with_roi(roi_geometry: RoiDataGeometries): +def create_test_image_with_roi( + geometry: Literal["rectangle", "circle", "polygon"], +) -> ImageObj: """Create test image with ROIs""" data = np.zeros((500, 750), dtype=np.uint16) xc, yc, r = 500, 200, 100 rr, cc = draw.disk((yc, xc), r) data[rr, cc] = 10000 data[yc + r - 20 : yc + r, xc + r - 30 : xc + r - 10] = 50000 - if roi_geometry is RoiDataGeometries.RECTANGLE: - roi = [xc - r, yc - r, xc + r, yc + r] - geom = "Rectangular" + if geometry == "rectangle": + coords = [xc - r, yc - r, 2 * r, 2 * r] + elif geometry == "circle": + coords = [xc, yc, r] else: - roi = [xc - r, yc, xc + r, yc] - geom = "Circular" - ima = create_image(f"{geom} ROI test image", data) - ima.roi = np.array([roi], int) + raise NotImplementedError(f"Geometry {geometry} not implemented") + ima = dlo.create_image(f"Test image with ROI/{geometry}", data) + ima.roi = dlo.create_image_roi(geometry, coords, indices=True, singleobj=False) return ima @@ -40,17 +48,16 @@ def test_roi_circ(): with cdltest_app_context() as win: execenv.print("Circular ROI test:") panel = win.imagepanel - for geometry in RoiDataGeometries: + for geometry in ("rectangle", "circle"): # dlo.ROI2DParam.geometries: ima = create_test_image_with_roi(geometry) panel.add_object(ima) print_obj_shapes(ima) panel.processor.compute_stats() panel.processor.compute_centroid() # Extracting ROIs: - rparam = dlp.ROIDataParam.create(singleobj=False) for obj_nb in (1, 2): panel.objview.set_current_object(panel[obj_nb]) - panel.processor.compute_roi_extraction(rparam) + panel.processor.compute_roi_extraction() if __name__ == "__main__": diff --git a/cdl/tests/features/images/select_background_unit_test.py b/cdl/tests/features/images/select_background_unit_test.py index a4429c4e..cced7dec 100644 --- a/cdl/tests/features/images/select_background_unit_test.py +++ b/cdl/tests/features/images/select_background_unit_test.py @@ -26,11 +26,12 @@ def test_image_background_selection(): # For more details about the why of the delay, see the comment in # cdl\tests\features\image\offset_correction_unit_test.py exec_dialog(dlg) - execenv.print(f"background: {dlg.get_background()}") - execenv.print(f"index range: {dlg.get_index_range()}") - # Check background value: - x0, y0, x1, y1 = dlg.get_index_range() - assert np.isclose(img.data[y0:y1, x0:x1].mean(), dlg.get_background()) + execenv.print(f"background: {dlg.get_background()}") + execenv.print(f"rect coords: {dlg.get_rect_coords()}") + # Check background value: + x0, y0, x1, y1 = dlg.get_rect_coords() + ix0, iy0, ix1, iy1 = dlg.imageitem.get_closest_index_rect(x0, y0, x1, y1) + assert np.isclose(img.data[iy0:iy1, ix0:ix1].mean(), dlg.get_background()) if __name__ == "__main__": diff --git a/cdl/tests/features/signals/fitdialog_unit_test.py b/cdl/tests/features/signals/fitdialog_unit_test.py index 44d50b46..c9fb03d5 100644 --- a/cdl/tests/features/signals/fitdialog_unit_test.py +++ b/cdl/tests/features/signals/fitdialog_unit_test.py @@ -10,7 +10,7 @@ from guidata.qthelpers import qt_app_context -from cdl.algorithms.signal import peak_indexes +from cdl.algorithms.signal import peak_indices from cdl.env import execenv from cdl.tests.data import GaussianNoiseParam, create_noisy_signal, get_test_signal from cdl.utils.tests import get_default_test_name @@ -22,7 +22,7 @@ def test_fit_dialog(): with qt_app_context(): # Multi-gaussian curve fitting test s = get_test_signal("paracetamol.txt") - peakidx = peak_indexes(s.y) + peakidx = peak_indices(s.y) execenv.print( fdlg.multigaussianfit(s.x, s.y, peakidx, name=get_default_test_name("00")) ) diff --git a/cdl/tests/features/signals/select_baseline_unit_test.py b/cdl/tests/features/signals/select_baseline_unit_test.py index 0d569b75..67731abd 100644 --- a/cdl/tests/features/signals/select_baseline_unit_test.py +++ b/cdl/tests/features/signals/select_baseline_unit_test.py @@ -7,6 +7,7 @@ # pylint: disable=invalid-name # Allows short reference names like x, y, ... # guitest: show +import numpy as np from guidata.qthelpers import exec_dialog, qt_app_context from cdl.env import execenv @@ -23,9 +24,9 @@ def test_signal_baseline_selection(): dlg.setObjectName(dlg.objectName() + "_00") # to avoid timestamp suffix exec_dialog(dlg) execenv.print(f"baseline: {dlg.get_baseline()}") - execenv.print(f"index range: {dlg.get_index_range()}") + execenv.print(f"X range: {dlg.get_x_range()}") # Check baseline value: - i0, i1 = dlg.get_index_range() + i0, i1 = np.searchsorted(sig.x, dlg.get_x_range()) assert dlg.get_baseline() == sig.data[i0:i1].mean() diff --git a/cdl/tests/scenarios/beautiful_app.py b/cdl/tests/scenarios/beautiful_app.py index c954d3dc..d4fbe59f 100644 --- a/cdl/tests/scenarios/beautiful_app.py +++ b/cdl/tests/scenarios/beautiful_app.py @@ -60,9 +60,10 @@ def run_beautiful_scenario(screenshots: bool = False): panel.processor.compute_white_tophat(dlp.MorphologyParam()) panel.processor.compute_denoise_tv(dlp.DenoiseTVParam()) n = data_size // 3 - panel.processor.compute_roi_extraction( - dlp.ROIDataParam.create(roidata=[[n, n, data_size - n, data_size - n]]) + roi = dlo.create_image_roi( + "rectangle", [n, n, data_size - 2 * n, data_size - 2 * n] ) + panel.processor.compute_roi_extraction(roi) if screenshots: win.take_screenshot("i_beautiful") win.take_menu_screenshots() diff --git a/cdl/tests/scenarios/common.py b/cdl/tests/scenarios/common.py index 855d0414..e37e21ff 100644 --- a/cdl/tests/scenarios/common.py +++ b/cdl/tests/scenarios/common.py @@ -195,7 +195,8 @@ def run_signal_computations( sig = panel.objview.get_sel_objects()[0] i1 = data_size // 10 i2 = len(sig.y) - i1 - panel.processor.compute_roi_extraction(dlp.ROIDataParam.create(roidata=[[i1, i2]])) + roi = dlo.create_signal_roi([i1, i2], indices=True) + panel.processor.compute_roi_extraction(roi) sig = create_noisy_signal(GaussianNoiseParam.create(sigma=5.0)) panel.add_object(sig) @@ -401,9 +402,10 @@ def run_image_computations( panel.processor.compute_resize(param) n = data_size // 10 - panel.processor.compute_roi_extraction( - dlp.ROIDataParam.create(roidata=[[n, n, data_size - n, data_size - n]]) + roi = dlo.create_image_roi( + "rectangle", [n, n, data_size - 2 * n, data_size - 2 * n] ) + panel.processor.compute_roi_extraction(roi) panel.processor.compute_centroid() panel.processor.compute_enclosing_circle() diff --git a/cdl/tests/scenarios/demo.py b/cdl/tests/scenarios/demo.py index 8a9a69d5..af2129e1 100644 --- a/cdl/tests/scenarios/demo.py +++ b/cdl/tests/scenarios/demo.py @@ -12,7 +12,6 @@ from typing import TYPE_CHECKING -import numpy as np from guidata.qthelpers import qt_wait from qtpy import QtWidgets as QW @@ -146,9 +145,11 @@ def test_image_features(win: CDLMainWindow, data_size: int = 512) -> None: newparam.title = None ima1 = create_multigauss_image(newparam) s = data_size - ima1.roi = np.array( - [[s // 2, s // 2, s - 25, s], [s // 4, s // 2, s // 2, s // 2]], int + roi = dlo.create_image_roi( + "rectangle", [s // 2, s // 2, s - 25 - s // 2, s - s // 2] ) + roi.add_roi(dlo.create_image_roi("circle", [s // 3, s // 2, s // 4])) + ima1.roi = roi panel.add_object(ima1) qt_wait(DELAY2) @@ -175,9 +176,10 @@ def test_image_features(win: CDLMainWindow, data_size: int = 512) -> None: qt_wait(DELAY2) n = data_size // 10 - panel.processor.compute_roi_extraction( - dlp.ROIDataParam.create(roidata=[[n, n, data_size - n, data_size - n]]) + roi = dlo.create_image_roi( + "rectangle", [n, n, data_size - 2 * n, data_size - 2 * n] ) + panel.processor.compute_roi_extraction(roi) def play_demo(win: CDLMainWindow) -> None: diff --git a/cdl/tests/scenarios/example_app_test.py b/cdl/tests/scenarios/example_app_test.py index 9ea6fdb5..fb554d29 100644 --- a/cdl/tests/scenarios/example_app_test.py +++ b/cdl/tests/scenarios/example_app_test.py @@ -10,8 +10,8 @@ # pylint: disable=invalid-name # Allows short reference names like x, y, ... # guitest: show -import cdl.param -from cdl.obj import create_image +import cdl.obj as dlo +import cdl.param as dlp from cdl.proxy import proxy_context from cdl.tests.data import get_test_image @@ -22,15 +22,16 @@ def test_example_app(): dedicated instance.""" with proxy_context("local") as proxy: data = get_test_image("flower.npy").data - image = create_image("Test image with peaks", data) + image = dlo.create_image("Test image with peaks", data) proxy.add_object(image) proxy.compute_roberts() data_size = data.shape[0] n = data_size // 5 - m = int(n * 1.25) - roidata = [[n, m, data_size - n, data_size - m]] - proxy.compute_roi_extraction(cdl.param.ROIDataParam.create(roidata=roidata)) - param = cdl.param.BlobOpenCVParam.create( + roi = dlo.create_image_roi( + "rectangle", [n, n, data_size - 2 * n, data_size - 2 * n] + ) + proxy.compute_roi_extraction(roi) + param = dlp.BlobOpenCVParam.create( min_dist_between_blobs=0.1, filter_by_color=False, min_area=500, diff --git a/cdl/utils/conf.py b/cdl/utils/conf.py index 9d20b05e..498c2ad1 100644 --- a/cdl/utils/conf.py +++ b/cdl/utils/conf.py @@ -8,6 +8,8 @@ import os import os.path as osp +import warnings +from typing import Any from guidata.userconfig import NoDefault, UserConfig @@ -77,49 +79,28 @@ def get_name(cls) -> str: class Option: """Configuration option handler""" - def __init__(self): + def __init__(self) -> None: self.section = None self.option = None - def get(self, default=NoDefault): + def get(self, default=NoDefault) -> Any: """Get configuration option value""" return CONF.get(self.section, self.option, default) - def set(self, value): + def set(self, value: Any) -> None: """Set configuration option value""" CONF.set(self.section, self.option, value) - def remove(self): + def remove(self) -> None: """Remove configuration option""" # No use case for this method yet (quite dangerous!) CONF.remove_option(self.section, self.option) -class EnumOption(Option): - """Enum configuration option handler""" - - def __init__(self, choices: list[str | int | float]) -> None: - super().__init__() - self.choices = choices - - def get(self, default=NoDefault) -> str | int | float: - """Get configuration option value""" - value = super().get(default) - if value not in self.choices: - raise ValueError(f"Invalid configuration option value {value}") - return value - - def set(self, value: str | int | float) -> None: - """Set configuration option value""" - if value not in self.choices: - raise ValueError(f"Invalid configuration option value {value}") - super().set(value) - - class ConfigPathOption(Option): """Configuration file path configuration option handler""" - def get(self, default=NoDefault): + def get(self, default=NoDefault) -> str: """Get configuration file path from configuration""" if default is NoDefault: default = "" @@ -132,7 +113,7 @@ def get(self, default=NoDefault): class WorkingDirOption(Option): """Working directory configuration option handler""" - def get(self, default=NoDefault): + def get(self, default=NoDefault) -> str: """Get working directory from configuration""" if default is NoDefault: default = "" @@ -141,7 +122,7 @@ def get(self, default=NoDefault): return path return "" - def set(self, value): + def set(self, value: str) -> None: """Set working directory in configuration""" if not osp.isdir(value): value = osp.dirname(value) @@ -151,6 +132,38 @@ def set(self, value): super().set(value) +class EnumOption(Option): + """Enumeration option handler""" + + def __init__(self, values: list[Any], default: Any = NoDefault) -> None: + super().__init__() + if default is NoDefault: + default = values[0] + self.values = values + self.default = default + + def get(self, default: Any = NoDefault) -> Any: + """Get configuration option value""" + value = super().get(default) + if value not in self.values: + # Only show a warning here, as the configuration file may be edited manually + warnings.warn( + f"Invalid value {value} for option {self.option}, " + f"expected {self.values}" + ) + return self.default + return value + + def set(self, value: Any) -> None: + """Set configuration option value""" + if value not in self.values: + raise ValueError( + f"Invalid value {value} for option {self.option}, " + f"expected {self.values}" + ) + super().set(value) + + class SectionMeta(type): """Configuration metaclass""" diff --git a/cdl/utils/strings.py b/cdl/utils/strings.py index f817d206..aeabbba3 100644 --- a/cdl/utils/strings.py +++ b/cdl/utils/strings.py @@ -64,7 +64,5 @@ def shorten_docstring(docstring: str) -> str: """ shorter = docstring.split("\n")[0].strip() if docstring else "-" for suffix in (".", ":", ",", "using", "with"): - # TODO: Use string.removesuffix() when we drop Python 3.8 support - if shorter.endswith(suffix): - shorter = shorter[: -len(suffix)] + shorter.removesuffix(suffix) return shorter diff --git a/cdl/widgets/fitdialog.py b/cdl/widgets/fitdialog.py index c4125c6f..9ea386e6 100644 --- a/cdl/widgets/fitdialog.py +++ b/cdl/widgets/fitdialog.py @@ -205,19 +205,19 @@ def multigaussian(x, *values, **kwargs): return y -def multigaussianfit(x, y, peak_indexes, parent=None, name=None): +def multigaussianfit(x, y, peak_indices, parent=None, name=None): """Compute Multi-Gaussian fit Returns (yfit, params), where yfit is the fitted curve and params are the fitting parameters""" params = [] - for index, i0 in enumerate(peak_indexes): + for index, i0 in enumerate(peak_indices): istart = 0 iend = len(x) - 1 if index > 0: - istart = (peak_indexes[index - 1] + i0) // 2 - if index < len(peak_indexes) - 1: - iend = (peak_indexes[index + 1] + i0) // 2 + istart = (peak_indices[index - 1] + i0) // 2 + if index < len(peak_indices) - 1: + iend = (peak_indices[index + 1] + i0) // 2 dx = 0.5 * (x[iend] - x[istart]) dy = np.max(y[istart:iend]) - np.min(y[istart:iend]) stri = f"{index + 1:02d}" @@ -232,7 +232,7 @@ def multigaussianfit(x, y, peak_indexes, parent=None, name=None): ) ) - kwargs = {"a_x0": x[peak_indexes]} + kwargs = {"a_x0": x[peak_indices]} def fitfunc(xi, params): return multigaussian(xi, *params, **kwargs) diff --git a/cdl/widgets/imagebackground.py b/cdl/widgets/imagebackground.py index e4456b57..7c79de0e 100644 --- a/cdl/widgets/imagebackground.py +++ b/cdl/widgets/imagebackground.py @@ -32,7 +32,7 @@ class ImageBackgroundDialog(PlotDialog): def __init__(self, image: ImageObj, parent: QWidget | None = None) -> None: self.__background: float | None = None - self.__indexrange: tuple[int, int, int, int] | None = None + self.__rect_coords: tuple[float, float, float, float] | None = None self.imageitem: ImageItem | None = None self.rectarea: RectangleShape | None = None self.comput2d: RangeComputation2d | None = None @@ -52,9 +52,7 @@ def __compute_background( z: np.ndarray, ) -> float: """Compute background value""" - x0, y0, x1, y1 = self.rectarea.get_rect() - ix0, iy0, ix1, iy1 = self.imageitem.get_closest_index_rect(x0, y0, x1, y1) - self.__indexrange = ix0, iy0, ix1, iy1 + self.__rect_coords = self.rectarea.get_rect() self.__background = z.mean() return self.__background @@ -64,7 +62,7 @@ def __setup_dialog(self) -> None: self.imageitem = obj.make_item() plot = self.get_plot() self.rectarea = make.rectangle( - obj.x0, obj.y0, obj.xc, obj.yc, _("Background area") + obj.x0, obj.y0, obj.xc + obj.dx, obj.yc + obj.dy, _("Background area") ) self.comput2d = make.computation2d( self.rectarea, @@ -82,10 +80,6 @@ def get_background(self) -> float: """Get background value""" return self.__background - def get_index_range(self) -> tuple[int, int, int, int]: - """Get index range - - Returns: - Index range (x0, y0, x1, y1) - """ - return self.__indexrange + def get_rect_coords(self) -> tuple[float, float, float, float]: + """Get rectangle coordinates""" + return self.__rect_coords diff --git a/cdl/widgets/signalbaseline.py b/cdl/widgets/signalbaseline.py index fc487022..f152958b 100644 --- a/cdl/widgets/signalbaseline.py +++ b/cdl/widgets/signalbaseline.py @@ -34,7 +34,7 @@ class SignalBaselineDialog(PlotDialog): def __init__(self, signal: SignalObj, parent: QWidget | None = None) -> None: self.__curve_styles = CURVESTYLES.style_generator() self.__baseline: float | None = None - self.__indexrange: tuple[int, int] | None = None + self.__x_range: tuple[float, float] = [np.nan, np.nan] self.curve: CurveItem | None = None self.cursor: Marker | None = None self.xrange: XRangeSelection | None = None @@ -67,10 +67,10 @@ def __setup_dialog(self) -> None: # pylint: disable=unused-argument def xrange_changed(self, item: XRangeSelection, xmin: float, xmax: float) -> None: """X range changed""" - imin, imax = np.searchsorted(self.__signal.x, sorted([xmin, xmax])) + self.__x_range = sorted([xmin, xmax]) + imin, imax = np.searchsorted(self.__signal.x, self.__x_range) if imin == imax: return - self.__indexrange = imin, imax self.cursor.set_pos(0, np.mean(self.__signal.y[imin:imax])) plot = self.get_plot() plot.replot() @@ -83,11 +83,6 @@ def get_baseline(self) -> float: """Get baseline""" return self.__baseline - def get_index_range(self) -> tuple[int, int]: - """Get index range""" - return self.__indexrange - def get_x_range(self) -> tuple[float, float]: """Get x range""" - x = self.__signal.x - return x[self.__indexrange[0]], x[self.__indexrange[1]] + return self.__x_range diff --git a/cdl/widgets/signalpeak.py b/cdl/widgets/signalpeak.py index 95d0bd7d..71217493 100644 --- a/cdl/widgets/signalpeak.py +++ b/cdl/widgets/signalpeak.py @@ -15,7 +15,7 @@ from qtpy import QtCore as QC from qtpy import QtWidgets as QW -from cdl.algorithms.signal import peak_indexes +from cdl.algorithms.signal import peak_indices from cdl.config import _ from cdl.core.model.signal import CURVESTYLES @@ -81,7 +81,7 @@ class SignalPeakDetectionDialog(PlotDialog): def __init__(self, signal: SignalObj, parent: QWidget | None = None) -> None: self.__curve_styles = CURVESTYLES.style_generator() self.peaks = None - self.peak_indexes = None + self.peak_indices = None self.in_curve = None self.in_threshold = None self.in_threshold_cursor = None @@ -132,9 +132,9 @@ def get_peaks(self) -> list[tuple[float, float]]: """Return peaks coordinates""" return self.peaks - def get_peak_indexes(self) -> list[int]: - """Return peak indexes""" - return self.peak_indexes + def get_peak_indices(self) -> list[int]: + """Return peak indices""" + return self.peak_indices def get_threshold(self) -> float: """Return relative threshold""" @@ -149,13 +149,13 @@ def compute_peaks(self) -> None: """Compute peak detection""" x, y = self.__signal.xydata plot = self.get_plot() - self.peak_indexes = peak_indexes( + self.peak_indices = peak_indices( y, thres=self.in_threshold, min_dist=self.min_distance, thres_abs=True, ) - self.peaks = [(x[index], y[index]) for index in self.peak_indexes] + self.peaks = [(x[index], y[index]) for index in self.peak_indices] markers = [ make.marker( pos, diff --git a/doc/conf.py b/doc/conf.py index 93e684b1..d1ab3dc4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -20,11 +20,7 @@ rst_prolog = f""" .. |download_link1| raw:: html - DataLab {release} | Windows 8, 10, 11 - -.. |download_link2| raw:: html - - DataLab {release} | Windows 7 SP1 + DataLab {release} | Windows 7 SP1, 8, 10, 11 """ # noqa: E501 # -- General configuration --------------------------------------------------- diff --git a/doc/contributing/environment.md b/doc/contributing/environment.md index cb305693..bafdd332 100644 --- a/doc/contributing/environment.md +++ b/doc/contributing/environment.md @@ -64,7 +64,6 @@ The following table lists the currently officially used Python distributions: | Python version | Status | WinPython version | | -------------- | ------------ | ----------------- | -| 3.8 | OK | 3.8.10.0 | | 3.9 | OK | 3.9.10.0 | | 3.10 | OK | 3.10.11.1 | | 3.11 | OK | 3.11.5.0 | @@ -103,7 +102,7 @@ Visual Studio Code configuration used in `launch.json` and `tasks.json` (examples) : @REM Development environment - set CDL_PYTHONEXE=C:\C2OIQ-DevCDL\python-3.8.10.amd64\python.exe + set CDL_PYTHONEXE=C:\python-3.9.10.amd64\python.exe @REM Folder containing additional working test data set CDL_DATA=C:\Dev\Projets\CDL_data diff --git a/doc/images/shots/i_roi_image.png b/doc/images/shots/i_roi_image.png index 96b7bc20..4fdd1422 100644 Binary files a/doc/images/shots/i_roi_image.png and b/doc/images/shots/i_roi_image.png differ diff --git a/doc/images/shots/s_roi_signal.png b/doc/images/shots/s_roi_signal.png index 6cae5a92..1aab6571 100644 Binary files a/doc/images/shots/s_roi_signal.png and b/doc/images/shots/s_roi_signal.png differ diff --git a/doc/intro/installation.rst b/doc/intro/installation.rst index 9aea470c..dc94f3b9 100644 --- a/doc/intro/installation.rst +++ b/doc/intro/installation.rst @@ -13,10 +13,9 @@ Installation :animate: fade-in :icon: zap - Direct download links for the latest version of DataLab: + Direct download link for the latest version of DataLab: - |download_link1| - - |download_link2| This section provides information on how to install DataLab on your system. Once installed, you can start DataLab by running the ``cdl`` command in a terminal, @@ -120,9 +119,7 @@ application). .. warning:: - DataLab Windows installer is available for Windows 8, 10 and 11 (main release, - based on Python 3.11) and also for Windows 7 SP1 (Python 3.8 based release, see - file ending with ``-Win7.msi``). + DataLab Windows installer is available for Windows 7 SP1, 8, 10 and 11. :octicon:`alert;1em;sd-text-warning` On Windows 7 SP1, before running DataLab (or any other Python 3 application), you must install Microsoft Update `KB2533623` diff --git a/doc/intro/tutorials/laser_beam.rst b/doc/intro/tutorials/laser_beam.rst index d713130d..44c2859f 100644 --- a/doc/intro/tutorials/laser_beam.rst +++ b/doc/intro/tutorials/laser_beam.rst @@ -194,7 +194,7 @@ the whole group "g001"). propagation axis. To do that, we use the "Plot results" feature |plot_results| in the "Analysis" menu. This feature allows to plot result data sets by choosing the x and y axes among the result columns. Here, we choose the - to plot the FWHM values (`L`) as a function of the image index (`Indexes`). + to plot the FWHM values (`L`) as a function of the image index (`Indices`). .. |plot_results| image:: ../../../cdl/data/icons/analysis/plot_results.svg :width: 24px diff --git a/doc/locale/fr/LC_MESSAGES/api/algorithms.po b/doc/locale/fr/LC_MESSAGES/api/algorithms.po index 44ee8269..7bf0dd73 100644 --- a/doc/locale/fr/LC_MESSAGES/api/algorithms.po +++ b/doc/locale/fr/LC_MESSAGES/api/algorithms.po @@ -114,7 +114,7 @@ msgstr "Normaliser le tableau d'entrée à un paramètre donné." #: cdl.algorithms.signal.find_x_at_value cdl.algorithms.signal.fw1e2 #: cdl.algorithms.signal.fwhm cdl.algorithms.signal.ifft1d #: cdl.algorithms.signal.interpolate cdl.algorithms.signal.magnitude_spectrum -#: cdl.algorithms.signal.normalize cdl.algorithms.signal.peak_indexes +#: cdl.algorithms.signal.normalize cdl.algorithms.signal.peak_indices #: cdl.algorithms.signal.phase_spectrum cdl.algorithms.signal.psd #: cdl.algorithms.signal.sampling_period cdl.algorithms.signal.sampling_rate #: cdl.algorithms.signal.sfdr cdl.algorithms.signal.sinad @@ -163,7 +163,7 @@ msgstr "Paramètre de normalisation. Par défaut : \"maximum\"" #: cdl.algorithms.signal.find_x_at_value cdl.algorithms.signal.fw1e2 #: cdl.algorithms.signal.fwhm cdl.algorithms.signal.ifft1d #: cdl.algorithms.signal.interpolate cdl.algorithms.signal.magnitude_spectrum -#: cdl.algorithms.signal.normalize cdl.algorithms.signal.peak_indexes +#: cdl.algorithms.signal.normalize cdl.algorithms.signal.peak_indices #: cdl.algorithms.signal.phase_spectrum cdl.algorithms.signal.psd #: cdl.algorithms.signal.sampling_period cdl.algorithms.signal.sampling_rate #: cdl.algorithms.signal.sfdr cdl.algorithms.signal.sinad @@ -246,7 +246,7 @@ msgstr "" #: cdl.algorithms.coordinates.circle_to_center_radius #: cdl.algorithms.coordinates.circle_to_diameter -#: cdl.algorithms.signal.bandwidth cdl.algorithms.signal.peak_indexes +#: cdl.algorithms.signal.bandwidth cdl.algorithms.signal.peak_indices #: cdl.algorithms.signal.psd of msgid "Return type" msgstr "Type de retour" @@ -259,11 +259,11 @@ msgstr "Trier à partir des données X,Y en calculant FFT(y)." msgid "Sorted frequencies in ascending order" msgstr "Fréquences triées par ordre croissant" -#: cdl.algorithms.signal.peak_indexes:1 of +#: cdl.algorithms.signal.peak_indices:1 of msgid "Peak detection routine." msgstr "Routine de détection de pics." -#: cdl.algorithms.signal.peak_indexes:3 of +#: cdl.algorithms.signal.peak_indices:3 of msgid "" "Finds the numeric index of the peaks in *y* by taking its first order " "difference. By using *thres* and *min_dist* parameters, it is possible to" @@ -273,11 +273,11 @@ msgstr "" "différence d'ordre. En utilisant les paramètres *thres* et *min_dist*, il" " est possible de réduire le nombre de pics détectés. *y* doit être signé." -#: cdl.algorithms.signal.peak_indexes:7 of +#: cdl.algorithms.signal.peak_indices:7 of msgid "1D amplitude data to search for peaks." msgstr "Données d'amplitude 1D à rechercher pour les pics." -#: cdl.algorithms.signal.peak_indexes:9 of +#: cdl.algorithms.signal.peak_indices:9 of msgid "" "Normalized threshold. Only the peaks with amplitude higher than the " "threshold will be detected." @@ -285,7 +285,7 @@ msgstr "" "Seuil normalisé. Seuls les pics dont l'amplitude est supérieure au seuil " "seront détectés." -#: cdl.algorithms.signal.peak_indexes:12 of +#: cdl.algorithms.signal.peak_indices:12 of msgid "" "Minimum distance between each detected peak. The peak with the highest " "amplitude is preferred to satisfy this constraint." @@ -293,7 +293,7 @@ msgstr "" "Distance minimale entre chaque pic détecté. Le pic avec l'amplitude la " "plus élevée est préféré pour satisfaire cette contrainte." -#: cdl.algorithms.signal.peak_indexes:15 of +#: cdl.algorithms.signal.peak_indices:15 of msgid "" "If True, the thres value will be interpreted as an absolute value, " "instead of a normalized threshold." @@ -301,8 +301,8 @@ msgstr "" "Si True, la valeur thres sera interprétée comme une valeur absolue, au " "lieu d'un seuil normalisé." -#: cdl.algorithms.signal.peak_indexes:19 of -msgid "Array containing the numeric indexes of the peaks that were detected" +#: cdl.algorithms.signal.peak_indices:19 of +msgid "Array containing the numeric indices of the peaks that were detected" msgstr "Tableau contenant les index numériques des pics qui ont été détectés" #: cdl.algorithms.signal.xpeak:1 of @@ -411,11 +411,11 @@ msgid "1-dimensional Voigt fit model" msgstr "Modèle de régression Voigt à 1 dimension" #: cdl.algorithms.signal.find_nearest_zero_point_idx:1 of -msgid "Find the x indexes where the corresponding y is the closest to zero" +msgid "Find the x indices where the corresponding y is the closest to zero" msgstr "Rechercher les indices x où le y correspondant est le plus proche de zéro" #: cdl.algorithms.signal.find_nearest_zero_point_idx:5 of -msgid "Indexes of the points right before or at zero crossing" +msgid "Indices of the points right before or at zero crossing" msgstr "Indices des points juste avant ou au croisement à zéro" #: cdl.algorithms.signal.find_x_at_value:1 of diff --git a/doc/locale/fr/LC_MESSAGES/api/obj.po b/doc/locale/fr/LC_MESSAGES/api/obj.po index 11d53e17..9346242d 100644 --- a/doc/locale/fr/LC_MESSAGES/api/obj.po +++ b/doc/locale/fr/LC_MESSAGES/api/obj.po @@ -115,7 +115,7 @@ msgstr "" #: cdl.core.model.image.ImageObj.make_item #: cdl.core.model.image.ImageObj.new_roi_item #: cdl.core.model.image.ImageObj.params_to_roidata -#: cdl.core.model.image.ImageObj.roi_coords_to_indexes +#: cdl.core.model.image.ImageObj.roi_coords_to_indices #: cdl.core.model.image.ImageObj.set_data_type #: cdl.core.model.image.ImageObj.set_metadata_from #: cdl.core.model.image.ImageObj.update_item @@ -138,7 +138,7 @@ msgstr "" #: cdl.core.model.signal.SignalObj.make_item #: cdl.core.model.signal.SignalObj.new_roi_item #: cdl.core.model.signal.SignalObj.params_to_roidata -#: cdl.core.model.signal.SignalObj.roi_coords_to_indexes +#: cdl.core.model.signal.SignalObj.roi_coords_to_indices #: cdl.core.model.signal.SignalObj.set_data_type #: cdl.core.model.signal.SignalObj.set_xydata #: cdl.core.model.signal.SignalObj.update_item @@ -245,7 +245,7 @@ msgstr "" #: cdl.core.model.image.ImageObj.get_masked_view #: cdl.core.model.image.ImageObj.make_item #: cdl.core.model.image.ImageObj.params_to_roidata -#: cdl.core.model.image.ImageObj.roi_coords_to_indexes +#: cdl.core.model.image.ImageObj.roi_coords_to_indices #: cdl.core.model.image.ROI2DParam.get_data cdl.core.model.image.create_image #: cdl.core.model.image.create_image_from_param #: cdl.core.model.image.new_image_param @@ -255,7 +255,7 @@ msgstr "" #: cdl.core.model.signal.SignalObj.get_roi_param #: cdl.core.model.signal.SignalObj.make_item #: cdl.core.model.signal.SignalObj.params_to_roidata -#: cdl.core.model.signal.SignalObj.roi_coords_to_indexes +#: cdl.core.model.signal.SignalObj.roi_coords_to_indices #: cdl.core.model.signal.create_signal #: cdl.core.model.signal.create_signal_from_param #: cdl.core.model.signal.new_signal_param cdl.obj.ImageObj.maskdata @@ -855,29 +855,29 @@ msgstr "Met à jour l'élément de tracé à partir des données." msgid "if True, data has changed" msgstr "si True, les données ont changé" -#: cdl.core.model.image.ImageObj.roi_coords_to_indexes:1 -#: cdl.core.model.signal.SignalObj.roi_coords_to_indexes:1 of -msgid "Convert ROI coordinates to indexes." +#: cdl.core.model.image.ImageObj.roi_coords_to_indices:1 +#: cdl.core.model.signal.SignalObj.roi_coords_to_indices:1 of +msgid "Convert ROI coordinates to indices." msgstr "Convertit les coordonnées de la ROI en index." -#: cdl.core.model.image.ImageObj.roi_coords_to_indexes:3 -#: cdl.core.model.signal.SignalObj.roi_coords_to_indexes:3 of +#: cdl.core.model.image.ImageObj.roi_coords_to_indices:3 +#: cdl.core.model.signal.SignalObj.roi_coords_to_indices:3 of msgid "coordinates" msgstr "coordonnées" -#: cdl.core.model.image.ImageObj.roi_coords_to_indexes:5 -#: cdl.core.model.signal.SignalObj.roi_coords_to_indexes:5 of -msgid "Indexes" +#: cdl.core.model.image.ImageObj.roi_coords_to_indices:5 +#: cdl.core.model.signal.SignalObj.roi_coords_to_indices:5 of +msgid "Indices" msgstr "Indices" #: cdl.core.model.signal.SignalObj.get_roi_param:1 of msgid "" -"Return ROI parameters dataset (converting ROI point indexes to " +"Return ROI parameters dataset (converting ROI point indices to " "coordinates)" msgstr "" #: cdl.core.model.signal.SignalObj.get_roi_param:4 of -msgid "default values (first, last point indexes)" +msgid "default values (first, last point indices)" msgstr "" #: cdl.core.model.signal.SignalObj.get_roi_param:6 of @@ -1143,8 +1143,8 @@ msgstr "Itérer sur les formes de résultat de l'objet." msgid "Result shape" msgstr "Forme résultat" -#: cdl.core.model.base.BaseObj.iterate_roi_indexes:1 of -msgid "Iterate over object ROI indexes ([0] if there is no ROI)" +#: cdl.core.model.base.BaseObj.iterate_roi_indices:1 of +msgid "Iterate over object ROI indices ([0] if there is no ROI)" msgstr "Itérer sur les index de ROI de l'objet ([0] s'il n'y a pas de ROI)" #: cdl.core.model.base.BaseObj.iterate_shape_items:1 of @@ -2116,9 +2116,9 @@ msgstr "coordonnées" msgid "Get single ROI, i.e. after extracting ROI from image" msgstr "" -#: cdl.core.model.image.ROI2DParam.get_rect_indexes:1 of +#: cdl.core.model.image.ROI2DParam.get_rect_indices:1 of #, fuzzy -msgid "Get rectangle indexes" +msgid "Get rectangle indices" msgstr "Forme rectangulaire" #: cdl.core.model.image.ROI2DParam.get_data:1 of diff --git a/doc/locale/fr/LC_MESSAGES/contributing/changelog.po b/doc/locale/fr/LC_MESSAGES/contributing/changelog.po index 33020a42..48b0eadf 100644 --- a/doc/locale/fr/LC_MESSAGES/contributing/changelog.po +++ b/doc/locale/fr/LC_MESSAGES/contributing/changelog.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: DataLab \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-02 17:00+0200\n" +"POT-Creation-Date: 2024-11-14 15:42+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: fr\n" @@ -33,18 +33,230 @@ msgstr "" "futurs et passés." #: ../../CHANGELOG.md:5 -msgid "DataLab Version 0.17.0" -msgstr "DataLab Version 0.17.0" +msgid "DataLab Version 0.18.0" +msgstr "DataLab Version 0.18.0" #: ../../CHANGELOG.md:7 -msgid "ℹ️ PlotPy v2.6 is required for this release." -msgstr "ℹ️ PlotPy v2.6 est requis pour cette version." +msgid "ℹ️ General information:" +msgstr "ℹ️ Informations générales :" -#: ../../CHANGELOG.md:9 ../../CHANGELOG.md:172 ../../CHANGELOG.md:265 +#: ../../CHANGELOG.md:9 +msgid "PlotPy v2.7 is required for this release." +msgstr "PlotPy v2.7 est requis pour cette version." + +#: ../../CHANGELOG.md:10 +msgid "Dropped support for Python 3.8." +msgstr "Prise en charge de Python 3.8 abandonnée." + +#: ../../CHANGELOG.md:11 +msgid "" +"Python 3.13 is not supported yet, due to the fact that some dependencies " +"are not compatible with this version." +msgstr "Python 3.13 n'est pas encore pris en charge, car certaines dépendances ne sont pas compatibles avec cette version." + +#: ../../CHANGELOG.md:13 ../../CHANGELOG.md:44 ../../CHANGELOG.md:68 +#: ../../CHANGELOG.md:231 ../../CHANGELOG.md:324 msgid "💥 New features and enhancements:" msgstr "💥 Nouvelles fonctionnalités et améliorations :" -#: ../../CHANGELOG.md:11 +#: ../../CHANGELOG.md:15 +msgid "New operation mode feature:" +msgstr "Nouvelle fonctionnalité de mode d'opération :" + +#: ../../CHANGELOG.md:16 +msgid "" +"Added \"Operation mode\" feature to the \"Processing\" tab in the " +"\"Settings\" dialog box" +msgstr "Ajout de la fonctionnalité \"Mode d'opération\" à l'onglet \"Traitement\" de la boîte de dialogue \"Paramètres\"" + +#: ../../CHANGELOG.md:17 +msgid "" +"This feature allows to choose between \"single\" and \"pairwise\" " +"operation modes for all basic operations (addition, subtraction, " +"multiplication, division, etc.):" +msgstr "Cette fonctionnalité permet de choisir entre les modes d'opération \"single\" et \"pairwise\" pour toutes les opérations de base (addition, soustraction, multiplication, division, etc.) :" + +#: ../../CHANGELOG.md:18 +msgid "" +"\"Single\" mode: single operand mode (default mode: the operation is done" +" on each object independently)" +msgstr "Mode \"single\" : mode d'opérande unique (mode par défaut : l'opération est effectuée sur chaque objet indépendamment)" + +#: ../../CHANGELOG.md:19 +msgid "" +"\"Pairwise\" mode: pairwise operand mode (the operation is done on each " +"pair of objects)" +msgstr "Mode \"pairwise\" : mode d'opérande par paire (l'opération est effectuée sur chaque paire d'objets)" + +#: ../../CHANGELOG.md:20 +msgid "" +"This applies to both signals and images, and to computations taking *N* " +"inputs" +msgstr "Cela s'applique à la fois aux signaux et aux images, et aux calculs prenant *N* entrées" + +#: ../../CHANGELOG.md:21 +msgid "Computations taking *N* inputs are the ones where:" +msgstr "Les calculs prenant *N* entrées sont ceux où :" + +#: ../../CHANGELOG.md:22 +msgid "*N(>=2)* objects in give *N* objects out" +msgstr "*N(>=2)* objets en entrée donnent *N* objets en sortie" + +#: ../../CHANGELOG.md:23 +msgid "*N(>=1)* object(s) + 1 object in give N objects out" +msgstr "*N(>=1)* objet(s) + 1 objet en entrée donnent N objets en sortie" + +#: ../../CHANGELOG.md:25 +msgid "New ROI (Region Of Interest) features:" +msgstr "Nouvelles fonctionnalités de ROI (Région d'intérêt) :" + +#: ../../CHANGELOG.md:26 +msgid "New polygonal ROI feature" +msgstr "Nouvelle fonctionnalité de ROI polygonale" + +#: ../../CHANGELOG.md:27 +msgid "" +"Complete redesign of the ROI editor user interfaces, improving ergonomics" +" and consistency with the rest of the application" +msgstr "Refonte complète des interfaces utilisateur de l'éditeur de ROI, améliorant l'ergonomie et la cohérence avec le reste de l'application" + +#: ../../CHANGELOG.md:28 +msgid "" +"Major internal refactoring of the ROI system to make it more robust (more" +" tests) and easier to maintain" +msgstr "Refactorisation interne majeure du système de ROI pour le rendre plus robuste (plus de tests) et plus facile à maintenir" + +#: ../../CHANGELOG.md:30 +msgid "" +"Implemented [Issue #102](https://github.com/DataLab-" +"Platform/DataLab/issues/102) - Launch DataLab using `datalab` instead of " +"`cdl`. Note that the `cdl` command is still available for backward " +"compatibility." +msgstr "Implémentation de l'[Issue #102](https://github.com/DataLab-Platform/DataLab/issues/102) - Lancer DataLab en utilisant `datalab` au lieu de `cdl`. Notez que la commande `cdl` est toujours disponible pour la compatibilité ascendante." + +#: ../../CHANGELOG.md:32 +msgid "" +"Implemented [Issue #101](https://github.com/DataLab-" +"Platform/DataLab/issues/101) - Configuration: set default image " +"interpolation to anti-aliasing (`5` instead of `0` for nearest). This " +"change is motivated by the fact that a performance improvement was made " +"in PlotPy v2.7 on Windows, which allows to use anti-aliasing " +"interpolation by default without a significant performance impact." +msgstr "Implémentation de l'[Issue #101](https://github.com/DataLab-Platform/DataLab/issues/101) - Configuration : définir l'interpolation d'image par défaut sur l'anti-crénelage (`5` au lieu de `0` pour le plus proche). Ce changement est motivé par le fait qu'une amélioration des performances a été apportée dans PlotPy v2.7 sur Windows, ce qui permet d'utiliser l'interpolation par anti-crénelage par défaut sans impact significatif sur les performances." + +#: ../../CHANGELOG.md:34 +msgid "" +"Implemented [Issue #100](https://github.com/DataLab-" +"Platform/DataLab/issues/100) - Use the same installer and executable on " +"Windows 7 SP1, 8, 10, 11. Before this change, a specific installer was " +"required for Windows 7 SP1, due to the fact that Python 3.9 and later " +"versions are not supported on this platform. A workaround was implemented" +" to make DataLab work on Windows 7 SP1 with Python 3.9." +msgstr "Implémentation de l'[Issue #100](https://github.com/DataLab-Platform/DataLab/issues/100) - Utiliser le même programme d'installation et exécutable sur Windows 7 SP1, 8, 10, 11. Avant ce changement, un programme d'installation spécifique était requis pour Windows 7 SP1, en raison du fait que Python 3.9 et les versions ultérieures ne sont pas prises en charge sur cette plateforme. Une solution de contournement a été mise en œuvre pour faire fonctionner DataLab sur Windows 7 SP1 avec Python 3.9." + +#: ../../CHANGELOG.md:36 ../../CHANGELOG.md:54 ../../CHANGELOG.md:91 +#: ../../CHANGELOG.md:125 ../../CHANGELOG.md:138 ../../CHANGELOG.md:170 +#: ../../CHANGELOG.md:202 ../../CHANGELOG.md:287 ../../CHANGELOG.md:298 +#: ../../CHANGELOG.md:347 ../../CHANGELOG.md:363 ../../CHANGELOG.md:388 +#: ../../CHANGELOG.md:421 ../../CHANGELOG.md:479 ../../CHANGELOG.md:578 +#: ../../CHANGELOG.md:650 ../../CHANGELOG.md:675 ../../CHANGELOG.md:687 +msgid "🛠️ Bug fixes:" +msgstr "🛠️ Corrections de bugs :" + +#: ../../CHANGELOG.md:38 +msgid "" +"Fixed [Issue #103](https://github.com/DataLab-" +"Platform/DataLab/issues/103) - `proxy.add_annotations_from_items`: circle" +" shape color seems to be ignored." +msgstr "Correction de l'[Issue #103](https://github.com/DataLab-Platform/DataLab/issues/103) - `proxy.add_annotations_from_items` : la couleur de la forme de cercle semble être ignorée." + +#: ../../CHANGELOG.md:40 +msgid "DataLab Version 0.17.1" +msgstr "DataLab Version 0.17.1" + +#: ../../CHANGELOG.md:42 +msgid "ℹ️ PlotPy v2.6.2 is required for this release." +msgstr "ℹ️ PlotPy v2.6.2 est requis pour cette version." + +#: ../../CHANGELOG.md:46 +msgid "Image View:" +msgstr "Vue Image :" + +#: ../../CHANGELOG.md:47 +msgid "" +"Before this release, when selecting a high number of images (e.g. when " +"selecting a group of images), the application was very slow because all " +"the images were displayed in the image view, even if they were all " +"superimposed on the same image" +msgstr "Avant cette version, lors de la sélection d'un grand nombre d'images (par exemple lors de la sélection d'un groupe d'images), l'application était très lente car toutes les images étaient affichées dans la vue image, même si elles étaient toutes superposées sur la même image" + +#: ../../CHANGELOG.md:48 +msgid "The workaround was to enable the \"Show first only\" option" +msgstr "La solution de contournement était d'activer l'option \"Afficher uniquement le premier\"" + +#: ../../CHANGELOG.md:49 +msgid "" +"Now, to improve performance, if multiple images are selected, only the " +"last image of the selection is displayed in the image view if this last " +"image has no transparency and if the other images are completely covered " +"by this last image" +msgstr "Maintenant, pour améliorer les performances, si plusieurs images sont sélectionnées, seule la dernière image de la sélection est affichée dans la vue image si cette dernière image n'a pas de transparence et si les autres images sont complètement recouvertes par cette dernière image" + +#: ../../CHANGELOG.md:50 +msgid "" +"Clarification: action \"Show first only\" was renamed to \"Show first " +"object only\", and a new icon was added to the action" +msgstr "Clarification : l'action \"Afficher uniquement le premier\" a été renommée en \"Afficher uniquement le premier objet\", et une nouvelle icône a été ajoutée à l'action" + +#: ../../CHANGELOG.md:51 +msgid "" +"API: added `width` and `height` properties to `ImageObj` class (returns " +"the width and height of the image in physical units)" +msgstr "API : ajout des propriétés `width` et `height` à la classe `ImageObj` (retourne la largeur et la hauteur de l'image en unités physiques)" + +#: ../../CHANGELOG.md:52 +msgid "" +"Windows launcher \"start.pyw\": writing a log file \"datalab_error.log\" " +"when an exception occurs at startup" +msgstr "Lanceur Windows \"start.pyw\" : écriture d'un fichier journal \"datalab_error.log\" lorsqu'une exception se produit au démarrage" + +#: ../../CHANGELOG.md:56 +msgid "" +"Changing the color theme now correctly updates all DataLab's user " +"interface components without the need to restart the application" +msgstr "Changer le thème de couleur met maintenant à jour correctement tous les composants de l'interface utilisateur de DataLab sans avoir besoin de redémarrer l'application" + +#: ../../CHANGELOG.md:58 +msgid "ℹ️ Other changes:" +msgstr "ℹ️ Autres changements :" + +#: ../../CHANGELOG.md:60 +msgid "OpenCV is now an optional dependency:" +msgstr "OpenCV est désormais une dépendance facultative :" + +#: ../../CHANGELOG.md:61 +msgid "" +"This change is motivated by the fact that the OpenCV conda package is not" +" maintained on Windows (at least), which leads to an error when " +"installing DataLab with conda" +msgstr "Ce changement est motivé par le fait que le paquet conda OpenCV n'est pas maintenu sur Windows (au moins), ce qui entraîne une erreur lors de l'installation de DataLab avec conda" + +#: ../../CHANGELOG.md:62 +msgid "" +"When OpenCV is not installed, only the \"OpenCV blob detection\" feature " +"won't work, and a warning message will be displayed when trying to use " +"this feature" +msgstr "Lorsque OpenCV n'est pas installé, seule la fonctionnalité \"Détection de blob OpenCV\" ne fonctionnera pas, et un message d'avertissement sera affiché lors de la tentative d'utilisation de cette fonctionnalité" + +#: ../../CHANGELOG.md:64 +msgid "DataLab Version 0.17.0" +msgstr "DataLab Version 0.17.0" + +#: ../../CHANGELOG.md:66 +msgid "ℹ️ PlotPy v2.6 is required for this release." +msgstr "ℹ️ PlotPy v2.6 est requis pour cette version." + +#: ../../CHANGELOG.md:70 msgid "" "Menu \"Computing\" was renamed to \"Analysis\" for both Signal and Image " "panels, to better reflect the nature of the features in this menu" @@ -52,7 +264,7 @@ msgstr "" "Le menu \"Calcul\" a été renommé en \"Analyse\" pour les panneaux Signal " "et Image, pour mieux refléter la nature des fonctionnalités de ce menu" -#: ../../CHANGELOG.md:12 +#: ../../CHANGELOG.md:71 msgid "" "Regions Of Interest (ROIs) are now taken into account everywhere in the " "application where it makes sense, and not only for the old \"Computing\" " @@ -66,7 +278,7 @@ msgstr "" " l'[Issue #93](https://github.com/DataLab-Platform/DataLab/issues/93). Si" " un signal ou une image a une ROI définie :" -#: ../../CHANGELOG.md:13 +#: ../../CHANGELOG.md:72 msgid "" "Operations are done on the ROI only (except if the operation changes the " "data shape, or the pixel size for images)" @@ -74,7 +286,7 @@ msgstr "" "Les opérations sont effectuées uniquement sur la ROI (sauf si l'opération" " modifie la forme des données, ou la taille des pixels pour les images)" -#: ../../CHANGELOG.md:14 +#: ../../CHANGELOG.md:73 msgid "" "Processing features are done on the ROI only (if the destination object " "data type is compatible with the source object data type, which excludes " @@ -85,17 +297,17 @@ msgstr "" "type de données de l'objet source, ce qui exclut le seuillage, par " "exemple)" -#: ../../CHANGELOG.md:15 +#: ../../CHANGELOG.md:74 msgid "Analysis features are done on the ROI only, like before" msgstr "" "Les fonctionnalités d'analyse sont effectuées uniquement sur la ROI, " "comme avant" -#: ../../CHANGELOG.md:16 +#: ../../CHANGELOG.md:75 msgid "As a consequence of previous point, and for clarity:" msgstr "En conséquence du point précédent, et pour plus de clarté :" -#: ../../CHANGELOG.md:17 +#: ../../CHANGELOG.md:76 msgid "" "The \"Edit Regions of interest\" and \"Remove all Regions of interest\" " "features have been moved from the old \"Computing\" (now \"Analysis\") " @@ -106,7 +318,7 @@ msgstr "" " (maintenant \"Analyse\") vers le menu \"Édition\" où se trouvent toutes " "les fonctionnalités liées aux métadonnées" -#: ../../CHANGELOG.md:18 +#: ../../CHANGELOG.md:77 msgid "" "The \"Edit Regions of interest\" action has been added to both Signal and" " Image View vertical toolbars (in second position, after the \"View in a " @@ -116,7 +328,7 @@ msgstr "" "d'outils verticales de la Vue Signal et Image (en deuxième position, " "après l'action \"Voir dans une nouvelle fenêtre\")" -#: ../../CHANGELOG.md:19 +#: ../../CHANGELOG.md:78 msgid "" "Following the bug fix on image data type conversion issues with basic " "operations, a new \"Arithmetic operation\" feature has been added to the " @@ -131,117 +343,137 @@ msgstr "" "opérations linéaires sur les signaux et les images, avec les opérations " "suivantes :" -#: ../../CHANGELOG.md:20 +#: ../../CHANGELOG.md:79 msgid "Addition: ``obj3 = (obj1 + obj2) * a + b``" msgstr "Addition : ``obj3 = (obj1 + obj2) * a + b``" -#: ../../CHANGELOG.md:21 +#: ../../CHANGELOG.md:80 msgid "Subtraction: ``obj3 = (obj1 - obj2) * a + b``" msgstr "Soustraction : ``obj3 = (obj1 - obj2) * a + b``" -#: ../../CHANGELOG.md:22 +#: ../../CHANGELOG.md:81 msgid "Multiplication: ``obj3 = (obj1 * obj2) * a + b``" msgstr "Multiplication : ``obj3 = (obj1 * obj2) * a + b``" -#: ../../CHANGELOG.md:23 +#: ../../CHANGELOG.md:82 msgid "Division: ``obj3 = (obj1 / obj2) * a + b``" msgstr "Division : ``obj3 = (obj1 / obj2) * a + b``" -#: ../../CHANGELOG.md:24 +#: ../../CHANGELOG.md:83 msgid "" "Improved \"View in a new window\" and \"ROI editor\" dialog boxes size " "management: default size won't be larger than DataLab's main window size" -msgstr "Amélioration de la gestion de la taille des boîtes de dialogue \"Voir dans une nouvelle fenêtre\" et \"Éditeur de ROI\" : la taille par défaut ne sera pas plus grande que la taille de la fenêtre principale de DataLab" +msgstr "" +"Amélioration de la gestion de la taille des boîtes de dialogue \"Voir " +"dans une nouvelle fenêtre\" et \"Éditeur de ROI\" : la taille par défaut " +"ne sera pas plus grande que la taille de la fenêtre principale de DataLab" -#: ../../CHANGELOG.md:25 +#: ../../CHANGELOG.md:84 msgid "ROI editor:" msgstr "Éditeur de ROI :" -#: ../../CHANGELOG.md:26 +#: ../../CHANGELOG.md:85 msgid "" "Added toolbars for both Signal and Image ROI editors, to allow to zoom in" " and out, and to reset the zoom level easily" -msgstr "Ajout de barres d'outils pour les éditeurs de ROI Signal et Image, pour permettre de zoomer et dézoomer, et de réinitialiser facilement le niveau de zoom" +msgstr "" +"Ajout de barres d'outils pour les éditeurs de ROI Signal et Image, pour " +"permettre de zoomer et dézoomer, et de réinitialiser facilement le niveau" +" de zoom" -#: ../../CHANGELOG.md:27 +#: ../../CHANGELOG.md:86 msgid "" "Rearranged the buttons in the ROI editor dialog box for better ergonomics" " and consistency with the Annotations editor (\"View in a new window\" " "dialog box)" -msgstr "Réorganisation des boutons dans la boîte de dialogue de l'éditeur de ROI pour une meilleure ergonomie et une cohérence avec l'éditeur d'annotations (boîte de dialogue \"Voir dans une nouvelle fenêtre\")" +msgstr "" +"Réorganisation des boutons dans la boîte de dialogue de l'éditeur de ROI " +"pour une meilleure ergonomie et une cohérence avec l'éditeur " +"d'annotations (boîte de dialogue \"Voir dans une nouvelle fenêtre\")" -#: ../../CHANGELOG.md:28 +#: ../../CHANGELOG.md:87 msgid "Application color theme:" msgstr "Application de thème de couleur :" -#: ../../CHANGELOG.md:29 +#: ../../CHANGELOG.md:88 msgid "" "Added support for color theme (auto, light, dark) in the \"Settings\" " "dialog box" -msgstr "Ajout de la prise en charge du thème de couleur (auto, clair, foncé) dans la boîte de dialogue \"Paramètres\"" +msgstr "" +"Ajout de la prise en charge du thème de couleur (auto, clair, foncé) dans" +" la boîte de dialogue \"Paramètres\"" -#: ../../CHANGELOG.md:30 +#: ../../CHANGELOG.md:89 msgid "The color theme is applied without restarting the application" msgstr "Le thème de couleur est appliqué sans redémarrer l'application" -#: ../../CHANGELOG.md:32 ../../CHANGELOG.md:66 ../../CHANGELOG.md:79 -#: ../../CHANGELOG.md:111 ../../CHANGELOG.md:143 ../../CHANGELOG.md:228 -#: ../../CHANGELOG.md:239 ../../CHANGELOG.md:288 ../../CHANGELOG.md:304 -#: ../../CHANGELOG.md:329 ../../CHANGELOG.md:362 ../../CHANGELOG.md:420 -#: ../../CHANGELOG.md:519 ../../CHANGELOG.md:591 ../../CHANGELOG.md:616 -#: ../../CHANGELOG.md:628 -msgid "🛠️ Bug fixes:" -msgstr "🛠️ Corrections de bugs :" - -#: ../../CHANGELOG.md:34 +#: ../../CHANGELOG.md:93 msgid "Intensity profile / Segment profile extraction:" msgstr "Extraction de profil d'intensité / Profil de segment :" -#: ../../CHANGELOG.md:35 +#: ../../CHANGELOG.md:94 msgid "" "When extracting a profile on an image with a ROI defined, the associated " "PlotPy feature show a warning message ('UserWarning: Warning: converting " "a masked element to nan.') but the profile is correctly extracted and " "displayed, with NaN values where the ROI is not defined." -msgstr "Lors de l'extraction d'un profil sur une image avec une ROI définie, la fonction PlotPy associée affiche un message d'avertissement ('UserWarning: Attention : conversion d'un élément masqué en nan.') mais le profil est correctement extrait et affiché, avec des valeurs NaN là où la ROI n'est pas définie." +msgstr "" +"Lors de l'extraction d'un profil sur une image avec une ROI définie, la " +"fonction PlotPy associée affiche un message d'avertissement " +"('UserWarning: Attention : conversion d'un élément masqué en nan.') mais " +"le profil est correctement extrait et affiché, avec des valeurs NaN là où" +" la ROI n'est pas définie." -#: ../../CHANGELOG.md:36 +#: ../../CHANGELOG.md:95 msgid "NaN values are now removed from the profile before plotting it" msgstr "Les valeurs NaN sont maintenant supprimées du profil avant de le tracer" -#: ../../CHANGELOG.md:37 +#: ../../CHANGELOG.md:96 msgid "" "Simple processing features with a one-to-on mapping with a Python " "function (e.g. `numpy.absolute`, `numpy.log10`, etc.) and without " "parameters: fix result object title which was systematically ending with " "\"|\" (the character that usually precedes the list of parameters)" -msgstr "Les fonctionnalités de traitement simples avec une correspondance un-à-un avec une fonction Python (par exemple `numpy.absolute`, `numpy.log10`, etc.) et sans paramètres : correction du titre de l'objet résultat qui se terminait systématiquement par \"|\" (le caractère qui précède généralement la liste des paramètres)" +msgstr "" +"Les fonctionnalités de traitement simples avec une correspondance un-à-un" +" avec une fonction Python (par exemple `numpy.absolute`, `numpy.log10`, " +"etc.) et sans paramètres : correction du titre de l'objet résultat qui se" +" terminait systématiquement par \"|\" (le caractère qui précède " +"généralement la liste des paramètres)" -#: ../../CHANGELOG.md:38 +#: ../../CHANGELOG.md:97 msgid "" "Butterworth filter: fix cutoff frequency ratio default value and valid " "range" -msgstr "Filtrage de Butterworth : correction de la valeur par défaut et de la plage de valeurs valides de la fréquence de coupure" +msgstr "" +"Filtrage de Butterworth : correction de la valeur par défaut et de la " +"plage de valeurs valides de la fréquence de coupure" -#: ../../CHANGELOG.md:39 +#: ../../CHANGELOG.md:98 msgid "Fix actions refresh issue in Image View vertical toolbar:" msgstr "" "Toutes les actions liées à la visualisation sont désormais regroupées " "dans la barre d'outils verticale du panneau de visualisation" -#: ../../CHANGELOG.md:40 +#: ../../CHANGELOG.md:99 msgid "" "When starting DataLab with the Signal Panel active, switching to the " "Image View was showing \"View in a new window\" or \"Edit Regions of " "interest\" actions enabled in the vertical toolbar, even if no image was " "displayed in the Image View" -msgstr "Lors du démarrage de DataLab avec le panneau Signal actif, le passage à la Vue Image affichait les actions \"Voir dans une nouvelle fenêtre\" ou \"Modifier les régions d'intérêt\" activées dans la barre d'outils verticale, même si aucune image n'était affichée dans la Vue Image" +msgstr "" +"Lors du démarrage de DataLab avec le panneau Signal actif, le passage à " +"la Vue Image affichait les actions \"Voir dans une nouvelle fenêtre\" ou " +"\"Modifier les régions d'intérêt\" activées dans la barre d'outils " +"verticale, même si aucune image n'était affichée dans la Vue Image" -#: ../../CHANGELOG.md:41 +#: ../../CHANGELOG.md:100 msgid "The Image View vertical toolbar is now correctly updated at startup" -msgstr "La barre d'outils verticale de la Vue Image est maintenant correctement mise à jour au démarrage" +msgstr "" +"La barre d'outils verticale de la Vue Image est maintenant correctement " +"mise à jour au démarrage" -#: ../../CHANGELOG.md:42 +#: ../../CHANGELOG.md:101 msgid "" "View in a new window: cross section tools (intensity profiles) stayed " "disabled unless the user selected an image through the item list - this " @@ -252,7 +484,7 @@ msgstr "" "sélectionne une image via la liste des éléments - c'est maintenant " "corrigé" -#: ../../CHANGELOG.md:43 +#: ../../CHANGELOG.md:102 msgid "" "Image View: \"Show contrast panel\" toolbar button was not enabled at " "startup, and was only enabled when at least one image was displayed in " @@ -263,11 +495,11 @@ msgstr "" "lorsqu'au moins une image était affichée dans la Vue Image - il est " "maintenant toujours activé, comme prévu" -#: ../../CHANGELOG.md:44 +#: ../../CHANGELOG.md:103 msgid "Image data type conversion:" msgstr "Conversion du type de données des images :" -#: ../../CHANGELOG.md:45 +#: ../../CHANGELOG.md:104 msgid "" "Previously, the data type conversion feature was common to signal and " "image processing features, i.e. a simple conversion of the data type " @@ -278,7 +510,7 @@ msgstr "" "simple conversion du type de données en utilisant la méthode `astype` de " "NumPy" -#: ../../CHANGELOG.md:46 +#: ../../CHANGELOG.md:105 msgid "" "This was not sufficient for image processing features, in particular for " "integer images, because even if the result was correct from a numerical " @@ -291,7 +523,7 @@ msgstr "" "débordement ou un dépassement pouvait légitimement être considéré comme " "un bug d'un point de vue mathématique" -#: ../../CHANGELOG.md:47 +#: ../../CHANGELOG.md:106 msgid "" "The image data type conversion feature now relies on the internal " "`clip_astype` function, which clips the data to the valid range of the " @@ -302,15 +534,15 @@ msgstr "" "plage valide du type de données cible avant de les convertir (dans le cas" " des images de type entier)" -#: ../../CHANGELOG.md:48 +#: ../../CHANGELOG.md:107 msgid "Image ROI extraction issues:" msgstr "Problèmes d'extraction de ROI d'image :" -#: ../../CHANGELOG.md:49 +#: ../../CHANGELOG.md:108 msgid "Multiple regressions were introduced in version 0.16.0:" msgstr "Plusieurs régressions ont été introduites dans la version 0.16.0 :" -#: ../../CHANGELOG.md:50 +#: ../../CHANGELOG.md:109 msgid "" "Single circular ROI extraction was not working as expected (a rectangular" " ROI was extracted, with unexpected coordinates)" @@ -318,13 +550,13 @@ msgstr "" "L'extraction d'une seule ROI circulaire ne fonctionnait pas comme prévu " "(une ROI rectangulaire était extraite, avec des coordonnées inattendues)" -#: ../../CHANGELOG.md:51 +#: ../../CHANGELOG.md:110 msgid "Multiple circular ROI extraction lead to a rectangular ROI extraction" msgstr "" "L'extraction de plusieurs ROI circulaires conduisait à une extraction de " "ROI rectangulaire" -#: ../../CHANGELOG.md:52 +#: ../../CHANGELOG.md:111 msgid "" "Multiple ROI extraction was no longer cropping the image to the overall " "bounding box of the ROIs" @@ -332,7 +564,7 @@ msgstr "" "L'extraction de plusieurs ROI ne recadrait plus l'image à la boîte " "englobante globale des ROIs" -#: ../../CHANGELOG.md:53 +#: ../../CHANGELOG.md:112 msgid "" "These issues are now fixed, and unit tests have been added to prevent " "regressions:" @@ -340,7 +572,7 @@ msgstr "" "Ces problèmes sont maintenant corrigés, et des tests unitaires ont été " "ajoutés pour éviter les régressions :" -#: ../../CHANGELOG.md:54 +#: ../../CHANGELOG.md:113 msgid "" "An independent test algorithm has been implemented to check the " "correctness of the ROI extraction in all cases mentioned above" @@ -348,7 +580,7 @@ msgstr "" "Un algorithme de test indépendant a été implémenté pour vérifier la " "correction de l'extraction de ROI dans tous les cas mentionnés ci-dessus" -#: ../../CHANGELOG.md:55 +#: ../../CHANGELOG.md:114 msgid "" "Tests cover both single and multiple ROI extraction, with circular and " "rectangular ROIs" @@ -356,13 +588,13 @@ msgstr "" "Les tests couvrent à la fois l'extraction de ROI unique et multiple, avec" " des ROIs circulaires et rectangulaires" -#: ../../CHANGELOG.md:56 +#: ../../CHANGELOG.md:115 msgid "Overflow and underflow issues in some operations on integer images:" msgstr "" "Problèmes de débordement et de dépassement dans certaines opérations sur " "des images de type entier :" -#: ../../CHANGELOG.md:57 +#: ../../CHANGELOG.md:116 msgid "" "When processing integer images, some features were causing overflow or " "underflow issues, leading to unexpected results (correct results from a " @@ -373,7 +605,7 @@ msgstr "" "des résultats inattendus (résultats corrects d'un point de vue numérique," " mais pas d'un point de vue mathématique)" -#: ../../CHANGELOG.md:58 +#: ../../CHANGELOG.md:117 msgid "" "This issue only concerned basic operations (addition, subtraction, " "multiplication, division, and constant operations) - all the other " @@ -383,25 +615,25 @@ msgstr "" "soustraction, multiplication, division et opérations constantes) - toutes" " les autres fonctionnalités fonctionnaient déjà comme prévu" -#: ../../CHANGELOG.md:59 +#: ../../CHANGELOG.md:118 msgid "This is now fixed as result output are now floating point images" msgstr "" -#: ../../CHANGELOG.md:60 +#: ../../CHANGELOG.md:119 msgid "Unit tests have been added to prevent regressions for all these operations" msgstr "" "Des tests unitaires ont été ajoutés pour éviter les régressions pour " "toutes ces opérations" -#: ../../CHANGELOG.md:62 +#: ../../CHANGELOG.md:121 msgid "DataLab Version 0.16.4" msgstr "DataLab Version 0.16.4" -#: ../../CHANGELOG.md:64 +#: ../../CHANGELOG.md:123 msgid "This is a minor maintenance release." msgstr "Cette version concerne une maintenance mineure." -#: ../../CHANGELOG.md:68 +#: ../../CHANGELOG.md:127 msgid "" "Requires PlotPy v2.4.1 or later to fix the following issues related to " "the contrast adjustment feature:" @@ -409,7 +641,7 @@ msgstr "" "PlotPy v2.4.1 ou ultérieur est requis pour corriger les problèmes " "suivants liés à la fonction d'ajustement de contraste :" -#: ../../CHANGELOG.md:69 +#: ../../CHANGELOG.md:128 msgid "" "A regression was introduced in an earlier version of PlotPy: levels " "histogram was no longer removed from contrast adjustment panel when the " @@ -419,7 +651,7 @@ msgstr "" "l'histogramme des niveaux n'était plus supprimé du panneau d'ajustement " "de contraste lorsque l'image associée était supprimée du graphique" -#: ../../CHANGELOG.md:70 +#: ../../CHANGELOG.md:129 msgid "" "This is now fixed: when an image is removed, the histogram is removed as " "well and the contrast panel is refreshed (which was not the case even " @@ -429,7 +661,7 @@ msgstr "" "l'histogramme est également supprimé et le panneau de contraste est " "rafraîchi (ce qui n'était pas le cas même avant la régression)" -#: ../../CHANGELOG.md:71 +#: ../../CHANGELOG.md:130 msgid "" "Ignore `AssertionError` in *config_unit_test.py* when executing test " "suite on WSL" @@ -437,19 +669,19 @@ msgstr "" "Ignorer `AssertionError` dans *config_unit_test.py* lors de l'exécution " "de la suite de tests sur WSL" -#: ../../CHANGELOG.md:73 ../../CHANGELOG.md:391 ../../CHANGELOG.md:498 +#: ../../CHANGELOG.md:132 ../../CHANGELOG.md:450 ../../CHANGELOG.md:557 msgid "📚 Documentation:" msgstr "📚 Documentation :" -#: ../../CHANGELOG.md:75 +#: ../../CHANGELOG.md:134 msgid "Fix class reference in `Wrap11Func` documentation" msgstr "Correction de la référence de classe dans la documentation de `Wrap11Func`" -#: ../../CHANGELOG.md:77 +#: ../../CHANGELOG.md:136 msgid "DataLab Version 0.16.3" msgstr "DataLab Version 0.16.3" -#: ../../CHANGELOG.md:81 +#: ../../CHANGELOG.md:140 msgid "" "Fixed [Issue #84](https://github.com/DataLab-Platform/DataLab/issues/84) " "- Build issues with V0.16.1: `signal` name conflict, ..." @@ -458,7 +690,7 @@ msgstr "" "Platform/DataLab/issues/84) - Problèmes de construction avec V0.16.1 : " "conflit de nom `signal`, ..." -#: ../../CHANGELOG.md:82 +#: ../../CHANGELOG.md:141 msgid "" "This issue was intended to be fixed in version 0.16.2, but the fix was " "not complete" @@ -466,7 +698,7 @@ msgstr "" "Ce problème devait être corrigé dans la version 0.16.2, mais la " "correction n'était pas suffisante" -#: ../../CHANGELOG.md:83 +#: ../../CHANGELOG.md:142 msgid "" "Thanks to [@rolandmas](https://github.com/rolandmas) for reporting the " "issue and for the help in investigating the problem and testing the fix" @@ -475,7 +707,7 @@ msgstr "" "problème et pour l'aide apportée dans l'investigation du problème et le " "test de la correction" -#: ../../CHANGELOG.md:84 +#: ../../CHANGELOG.md:143 msgid "" "Fixed [Issue #85](https://github.com/DataLab-Platform/DataLab/issues/85) " "- Test data paths may be added multiple times to " @@ -485,7 +717,7 @@ msgstr "" "Platform/DataLab/issues/85) - Les chemins des données de test peuvent " "être ajoutés plusieurs fois à `cdl.utils.tests.TST_PATH`" -#: ../../CHANGELOG.md:85 +#: ../../CHANGELOG.md:144 msgid "" "This issue is related to [Issue #84](https://github.com/DataLab-" "Platform/DataLab/issues/84)" @@ -493,7 +725,7 @@ msgstr "" "Ce problème est lié à l'[Issue #84](https://github.com/DataLab-" "Platform/DataLab/issues/84)" -#: ../../CHANGELOG.md:86 +#: ../../CHANGELOG.md:145 msgid "" "Adding the test data paths multiple times to `cdl.utils.tests.TST_PATH` " "was causing the test data to be loaded multiple times, which lead to some" @@ -506,7 +738,7 @@ msgstr "" "contournement simple a été ajoutée à V0.16.2 : ce problème est maintenant" " corrigé)" -#: ../../CHANGELOG.md:87 +#: ../../CHANGELOG.md:146 msgid "" "Thanks again to [@rolandmas](https://github.com/rolandmas) for reporting " "the issue in the context of the Debian packaging" @@ -514,7 +746,7 @@ msgstr "" "Merci encore à [@rolandmas](https://github.com/rolandmas) pour avoir " "signalé le problème dans le contexte de l'emballage Debian" -#: ../../CHANGELOG.md:88 +#: ../../CHANGELOG.md:147 msgid "" "Fixed [Issue #86](https://github.com/DataLab-Platform/DataLab/issues/86) " "- Average of N integer images overflows data type" @@ -523,7 +755,7 @@ msgstr "" "Platform/DataLab/issues/86) - La moyenne de N images entières dépasse le " "type de données" -#: ../../CHANGELOG.md:89 +#: ../../CHANGELOG.md:148 msgid "" "Fixed [Issue #87](https://github.com/DataLab-Platform/DataLab/issues/87) " "- Image average profile extraction: `AttributeError` when trying to edit " @@ -534,7 +766,7 @@ msgstr "" "`AttributeError` lors de la tentative de modification des paramètres du " "profil" -#: ../../CHANGELOG.md:90 +#: ../../CHANGELOG.md:149 msgid "" "Fixed [Issue #88](https://github.com/DataLab-Platform/DataLab/issues/88) " "- Image segment profile: point coordinates inversion" @@ -543,11 +775,11 @@ msgstr "" "Platform/DataLab/issues/88) - Profil de segment d'image : inversion des " "coordonnées des points" -#: ../../CHANGELOG.md:92 +#: ../../CHANGELOG.md:151 msgid "DataLab Version 0.16.2" msgstr "DataLab Version 0.16.2" -#: ../../CHANGELOG.md:94 +#: ../../CHANGELOG.md:153 msgid "" "This release requires PlotPy v2.4.0 or later, which brings the following " "bug fixes and new features:" @@ -555,13 +787,13 @@ msgstr "" "Cette version nécessite PlotPy v2.4.0 ou ultérieure, qui apporte les " "corrections de bugs et les nouvelles fonctionnalités suivantes :" -#: ../../CHANGELOG.md:96 +#: ../../CHANGELOG.md:155 msgid "New constrast adjustment features and bug fixes:" msgstr "" "Nouvelles fonctionnalités d'ajustement de contraste et corrections de " "bugs :" -#: ../../CHANGELOG.md:97 +#: ../../CHANGELOG.md:156 msgid "" "New layout: the vertical toolbar (which was constrained in a small area " "on the right side of the panel) is now a horizontal toolbar at the top of" @@ -571,7 +803,7 @@ msgstr "" "dans une petite zone sur le côté droit du panneau) est désormais une " "barre d'outils horizontale en haut du panneau, à côté du titre" -#: ../../CHANGELOG.md:98 +#: ../../CHANGELOG.md:157 msgid "" "New \"Set range\" button: allows the user to set manually the minimum and" " maximum values of the histogram range" @@ -580,7 +812,7 @@ msgstr "" "manuellement les valeurs minimale et maximale de la plage de " "l'histogramme" -#: ../../CHANGELOG.md:99 +#: ../../CHANGELOG.md:158 msgid "" "Fixed histogram update issues when no image was currently selected (even " "if the an image was displayed and was selected before)" @@ -589,7 +821,7 @@ msgstr "" "image n'était actuellement sélectionnée (même si une image était affichée" " et sélectionnée auparavant)" -#: ../../CHANGELOG.md:100 +#: ../../CHANGELOG.md:159 msgid "" "Histogram range was not updated when either the minimum or maximum value " "was set using the \"Minimum value\" or \"Maximum value\" buttons (which " @@ -600,7 +832,7 @@ msgstr "" "minimale\" ou \"Valeur maximale\" (qui ont été renommés en \"Min.\" et " "\"Max.\" dans cette version)" -#: ../../CHANGELOG.md:101 +#: ../../CHANGELOG.md:160 msgid "" "Histogram range was not updated when the \"Set full range\" button was " "clicked, or when the LUT range was modified using the \"Scales / LUT " @@ -611,27 +843,27 @@ msgstr "" "était modifiée à l'aide du formulaire \"Échelles / Plage de LUT\" dans le" " groupe \"Propriétés\"" -#: ../../CHANGELOG.md:103 +#: ../../CHANGELOG.md:162 msgid "Image view context menu: new \"Reverse X axis\" feature" msgstr "" "Menu contextuel de la Vue Image : nouvelle fonctionnalité \"Inverser " "l'axe X\"" -#: ../../CHANGELOG.md:105 ../../CHANGELOG.md:136 +#: ../../CHANGELOG.md:164 ../../CHANGELOG.md:195 msgid "ℹ️ Minor new features and enhancements:" msgstr "ℹ️ Nouvelles fonctionnalités mineures et améliorations :" -#: ../../CHANGELOG.md:107 +#: ../../CHANGELOG.md:166 msgid "Image file types:" msgstr "Types de fichiers image :" -#: ../../CHANGELOG.md:108 +#: ../../CHANGELOG.md:167 msgid "Added native support for reading .SPE, .GEL, .NDPI and .REC image files" msgstr "" "Ajout de la prise en charge native de la lecture des fichiers image .SPE," " .GEL, .NDPI et .REC" -#: ../../CHANGELOG.md:109 +#: ../../CHANGELOG.md:168 msgid "" "Added support for any `imageio`-supported file format through " "configuration file (entry `imageio_formats` may be customized to " @@ -647,11 +879,11 @@ msgstr "" "platform.com/fr/features/image/menu_file.html#open-image) pour plus de " "détails)" -#: ../../CHANGELOG.md:113 +#: ../../CHANGELOG.md:172 msgid "Image Fourier analysis:" msgstr "Analyse de Fourier des images :" -#: ../../CHANGELOG.md:114 +#: ../../CHANGELOG.md:173 msgid "" "Fixed logarithmic scale for the magnitude spectrum (computing dB instead " "of natural logarithm)" @@ -659,7 +891,7 @@ msgstr "" "Correction de l'échelle logarithmique pour le spectre de magnitude " "(calcul en dB au lieu du logarithme naturel)" -#: ../../CHANGELOG.md:115 +#: ../../CHANGELOG.md:174 msgid "" "Fixed PSD computation with logarithmic scale (computing dB instead of " "natural logarithm)" @@ -667,7 +899,7 @@ msgstr "" "Correction du calcul de la DSP avec une échelle logarithmique (calcul en " "dB au lieu du logarithme naturel)" -#: ../../CHANGELOG.md:116 +#: ../../CHANGELOG.md:175 msgid "" "Updated the documentation to explicitly mention that the logarithmic " "scale is in dB" @@ -675,7 +907,7 @@ msgstr "" "Mise à jour de la documentation pour mentionner explicitement que " "l'échelle logarithmique est en dB" -#: ../../CHANGELOG.md:118 +#: ../../CHANGELOG.md:177 msgid "" "Fixed [Issue #82](https://github.com/DataLab-Platform/DataLab/issues/82) " "- Macros are not renamed in DataLab after exporting them to Python " @@ -685,7 +917,7 @@ msgstr "" "Platform/DataLab/issues/82) - Les macros ne sont pas renommées dans " "DataLab après les avoir exportées vers des scripts Python" -#: ../../CHANGELOG.md:120 +#: ../../CHANGELOG.md:179 msgid "" "`ResultProperties` object can now be added to `SignalObj` or `ImageObj` " "metadata even outside a Qt event loop (because the label item is no " @@ -695,7 +927,7 @@ msgstr "" "`SignalObj` ou `ImageObj` même en dehors d'une boucle d'événements Qt " "(car l'élément d'étiquette n'est plus créé immédiatement)" -#: ../../CHANGELOG.md:122 +#: ../../CHANGELOG.md:181 msgid "" "Progress bar is now automatically closed as expected when an error " "occurrs during a long operation (e.g. when opening a file)" @@ -704,7 +936,7 @@ msgstr "" "d'erreur lors d'une opération longue (par exemple, lors de l'ouverture " "d'un fichier)" -#: ../../CHANGELOG.md:124 +#: ../../CHANGELOG.md:183 msgid "" "Difference, division, ...: dialog box for the second operand selection " "was allowing to select a group (only a signal or an image should be " @@ -714,7 +946,7 @@ msgstr "" "second opérande permettait de sélectionner un groupe (seul un signal ou " "une image devrait être sélectionné)" -#: ../../CHANGELOG.md:126 +#: ../../CHANGELOG.md:185 msgid "" "When doing an operation which involves an object (signal or image) with " "higher order number than the current object (e.g. when subtracting an " @@ -733,7 +965,7 @@ msgstr "" " l'objet résultant faisait précédemment référence au numéro d'ordre avant" " l'insertion de l'image résultante)" -#: ../../CHANGELOG.md:128 +#: ../../CHANGELOG.md:187 msgid "" "Added support for additional test data folder thanks to the `CDL_DATA` " "environment variable (useful for testing purposes, and especially in the " @@ -744,11 +976,11 @@ msgstr "" "des fins de test, et notamment dans le contexte de la préparation du " "paquet Debian de DataLab)" -#: ../../CHANGELOG.md:130 +#: ../../CHANGELOG.md:189 msgid "DataLab Version 0.16.1" msgstr "DataLab Version 0.16.1" -#: ../../CHANGELOG.md:132 +#: ../../CHANGELOG.md:191 #, python-format msgid "" "Since version 0.16.0, many validation functions have been added to the " @@ -759,15 +991,15 @@ msgstr "" "ajoutées à la suite de tests. Le pourcentage de fonctions de calcul " "validées est passé de 37 % à 84 % dans cette version." -#: ../../CHANGELOG.md:134 +#: ../../CHANGELOG.md:193 msgid "NumPy 2.0 support has been added with this release." msgstr "" -#: ../../CHANGELOG.md:138 +#: ../../CHANGELOG.md:197 msgid "Signal and image moving average and median filters:" msgstr "Filtres moyenne mobile et médiane pour les signaux et les images :" -#: ../../CHANGELOG.md:139 +#: ../../CHANGELOG.md:198 msgid "" "Added \"Mode\" parameter to choose the mode of the filter (e.g. " "\"reflect\", \"constant\", \"nearest\", \"mirror\", \"wrap\")" @@ -775,7 +1007,7 @@ msgstr "" "Ajout du paramètre \"Mode\" pour choisir le mode du filtre (par exemple " "\"reflect\", \"constant\", \"nearest\", \"mirror\", \"wrap\")" -#: ../../CHANGELOG.md:140 +#: ../../CHANGELOG.md:199 msgid "" "The default mode is \"reflect\" for moving average and \"nearest\" for " "moving median" @@ -783,13 +1015,13 @@ msgstr "" "Le mode par défaut est \"reflect\" pour la moyenne mobile et \"nearest\" " "pour la médiane mobile" -#: ../../CHANGELOG.md:141 +#: ../../CHANGELOG.md:200 msgid "This allows to handle edge effects when filtering signals and images" msgstr "" "Cela permet de gérer les effets de bord lors du filtrage des signaux et " "des images" -#: ../../CHANGELOG.md:145 +#: ../../CHANGELOG.md:204 msgid "" "Fixed Canny edge detection to return binary image as `uint8` instead of " "`bool` (for consistency with other image processing features)" @@ -798,7 +1030,7 @@ msgstr "" "binaire en `uint8` au lieu de `bool` (par soucis de cohérence avec les " "autres fonctionnalités de traitement d'image)" -#: ../../CHANGELOG.md:147 +#: ../../CHANGELOG.md:206 msgid "" "Fixed Image normalization: lower bound was wrongly set for `maximum` " "method" @@ -806,13 +1038,13 @@ msgstr "" "Correction de la normalisation d'image : la borne inférieure était mal " "définie pour la méthode `maximum`" -#: ../../CHANGELOG.md:149 +#: ../../CHANGELOG.md:208 msgid "Fixed `ValueError` when computing PSD with logarithmic scale" msgstr "" "Correction de l'erreur `ValueError` lors du calcul de la DSP avec une " "échelle logarithmique" -#: ../../CHANGELOG.md:151 +#: ../../CHANGELOG.md:210 msgid "" "Fixed Signal derivative algorithm: now using `numpy.gradient` instead of " "a custom implementation" @@ -820,13 +1052,13 @@ msgstr "" "Correction de l'algorithme de dérivation d'un signal : utilisation de " "`numpy.gradient` au lieu d'une implémentation personnalisée" -#: ../../CHANGELOG.md:153 +#: ../../CHANGELOG.md:212 msgid "Fixed SciPy's `cumtrapz` deprecation: use `cumulative_trapezoid` instead" msgstr "" "Correction de l'obsolescence de `cumtrapz` de SciPy : utilisation de " "`cumulative_trapezoid` à la place" -#: ../../CHANGELOG.md:155 +#: ../../CHANGELOG.md:214 msgid "" "Curve selection now shows the individual points of the curve (before, " "only the curve line width was broadened)" @@ -835,7 +1067,7 @@ msgstr "" "courbe (auparavant, seule la largeur de la ligne de la courbe était " "élargie)" -#: ../../CHANGELOG.md:157 +#: ../../CHANGELOG.md:216 msgid "" "Windows installer: add support for unstable releases (e.g., 0.16.1.dev0)," " thus allowing to easily install the latest development version of " @@ -845,7 +1077,7 @@ msgstr "" "(par exemple, 0.16.1.dev0), permettant ainsi d'installer facilement la " "dernière version de développement de DataLab sur Windows" -#: ../../CHANGELOG.md:159 +#: ../../CHANGELOG.md:218 msgid "" "Fixed [Issue #81](https://github.com/DataLab-Platform/DataLab/issues/81) " "- When opening files, show progress dialog only if necessary" @@ -854,7 +1086,7 @@ msgstr "" "Platform/DataLab/issues/81) - Lors de l'ouverture de fichiers, afficher " "la boîte de dialogue de progression uniquement si nécessaire" -#: ../../CHANGELOG.md:161 +#: ../../CHANGELOG.md:220 msgid "" "Fixed [Issue #80](https://github.com/DataLab-Platform/DataLab/issues/80) " "- Plotting results: support for two use cases" @@ -863,7 +1095,7 @@ msgstr "" "Platform/DataLab/issues/80) - Tracé des résultats : prise en charge de " "deux cas d'utilisation" -#: ../../CHANGELOG.md:162 +#: ../../CHANGELOG.md:221 msgid "" "The features of the \"Analysis\" menu produce *results* (scalars): blob " "detection (circle coordinates), 2D peak detection (point coordinates), " @@ -887,7 +1119,7 @@ msgstr "" "d'autres peuvent consister en plusieurs résultats individuels (par " "exemple, détection de taches, détection de contours, etc.)." -#: ../../CHANGELOG.md:163 +#: ../../CHANGELOG.md:222 msgid "" "Before this change, the \"Plot results\" feature only supported plotting " "the first individual result of a result table, as a function of the index" @@ -902,13 +1134,13 @@ msgstr "" "suffisant pour certains cas d'utilisation, où l'utilisateur souhaitait " "tracer plusieurs résultats individuels d'une table de résultats." -#: ../../CHANGELOG.md:164 +#: ../../CHANGELOG.md:223 msgid "Now, the \"Plot results\" feature supports two use cases:" msgstr "" "A présent, la fonctionnalité \"Tracer les résultats\" prend en charge " "deux cas d'utilisation :" -#: ../../CHANGELOG.md:165 +#: ../../CHANGELOG.md:224 msgid "" "\"One curve per result title\": Plotting the first individual result of a" " result table, as before" @@ -916,7 +1148,7 @@ msgstr "" "\"Une courbe par titre de résultat\" : Tracer le premier résultat " "individuel d'une table de résultats, comme auparavant" -#: ../../CHANGELOG.md:166 +#: ../../CHANGELOG.md:225 msgid "" "\"One curve per object (or ROI) and per result title\": Plotting all " "individual results of a result table, as a function of the index (of the " @@ -927,13 +1159,13 @@ msgstr "" "l'index (des objets de signal ou d'image) ou de l'une des colonnes de la " "table de résultats" -#: ../../CHANGELOG.md:167 +#: ../../CHANGELOG.md:226 msgid "The selection of the use case is done in the \"Plot results\" dialog box" msgstr "" "La sélection du cas d'utilisation se fait dans la boîte de dialogue " "\"Tracer les résultats\"" -#: ../../CHANGELOG.md:168 +#: ../../CHANGELOG.md:227 msgid "" "The default use case is \"One curve per result title\" if the result " "table has only one line, and \"One curve per object (or ROI) and per " @@ -943,15 +1175,15 @@ msgstr "" "si la table de résultats ne comporte qu'une seule ligne, et \"Une courbe " "par objet (ou ROI) et par titre de résultat\" dans les autres cas" -#: ../../CHANGELOG.md:170 +#: ../../CHANGELOG.md:229 msgid "DataLab Version 0.16.0" msgstr "DataLab Version 0.16.0" -#: ../../CHANGELOG.md:174 +#: ../../CHANGELOG.md:233 msgid "Major user interface overhaul:" msgstr "Refonte majeure de l'interface utilisateur :" -#: ../../CHANGELOG.md:175 +#: ../../CHANGELOG.md:234 msgid "" "The menu bar and toolbars have been reorganized to make the application " "more intuitive and easier to use" @@ -959,13 +1191,13 @@ msgstr "" "La barre de menu et les barres d'outils ont été réorganisées pour rendre " "l'application plus intuitive et plus facile à utiliser" -#: ../../CHANGELOG.md:176 +#: ../../CHANGELOG.md:235 msgid "Operations and processing features have been regrouped in submenus" msgstr "" "Les fonctionnalités d'opérations et de traitement ont été regroupées dans" " des sous-menus" -#: ../../CHANGELOG.md:177 +#: ../../CHANGELOG.md:236 msgid "" "All visualization-related actions are now grouped in the plot view " "vertical toolbar" @@ -973,7 +1205,7 @@ msgstr "" "Toutes les actions liées à la visualisation sont désormais regroupées " "dans la barre d'outils verticale du panneau de visualisation" -#: ../../CHANGELOG.md:178 +#: ../../CHANGELOG.md:237 msgid "" "Clarified the \"Annotations\" management (new buttons, toolbar action, " "...)" @@ -981,13 +1213,13 @@ msgstr "" "Clarification de la gestion des \"Annotations\" (nouveaux boutons, action" " de la barre d'outils, ...)" -#: ../../CHANGELOG.md:180 +#: ../../CHANGELOG.md:239 msgid "New validation process for signal and image features:" msgstr "" "Nouveau processus de validation pour les fonctionnalités de traitement du" " signal et de l'image :" -#: ../../CHANGELOG.md:181 +#: ../../CHANGELOG.md:240 msgid "" "Before this release, DataLab's validation process was exclusively done " "from the programmer's point of view, by writing unit tests and " @@ -1000,7 +1232,7 @@ msgstr "" "code fonctionnait comme prévu (c'est-à-dire qu'aucune exception n'était " "levée et que le comportement était correct)" -#: ../../CHANGELOG.md:182 +#: ../../CHANGELOG.md:241 msgid "" "With this release, a new validation process has been introduced, from the" " user's point of view, by adding new validation functions (marked with " @@ -1011,7 +1243,7 @@ msgstr "" "validation (marquées avec le décorateur `@pytest.mark.validation`) dans " "la suite de tests" -#: ../../CHANGELOG.md:183 +#: ../../CHANGELOG.md:242 msgid "" "A new \"Validation\" section in the documentation explains how validation" " is done and contains a list of all validation functions with the " @@ -1022,7 +1254,7 @@ msgstr "" "fonctions de validation avec les statistiques du processus de validation " "(générées à partir de la suite de tests)" -#: ../../CHANGELOG.md:184 +#: ../../CHANGELOG.md:243 msgid "" "The validation process is a work in progress and will be improved in " "future versions" @@ -1030,29 +1262,29 @@ msgstr "" "Le processus de validation est en cours et sera amélioré dans les " "versions futures" -#: ../../CHANGELOG.md:186 +#: ../../CHANGELOG.md:245 msgid "\"Properties\" group box:" msgstr "Groupe \"Propriétés\" :" -#: ../../CHANGELOG.md:187 +#: ../../CHANGELOG.md:246 msgid "Added \"Scales\" tab, to show and set the plot scales:" msgstr "" "Ajout de l'onglet \"Échelles\", pour afficher et définir les échelles du " "graphique :" -#: ../../CHANGELOG.md:188 +#: ../../CHANGELOG.md:247 msgid "X, Y for signals" msgstr "X, Y pour les signaux" -#: ../../CHANGELOG.md:189 +#: ../../CHANGELOG.md:248 msgid "X, Y, Z (LUT range) for images" msgstr "X, Y, Z (plage de LUT) pour les images" -#: ../../CHANGELOG.md:191 +#: ../../CHANGELOG.md:250 msgid "View options:" msgstr "Options d'affichage :" -#: ../../CHANGELOG.md:192 +#: ../../CHANGELOG.md:251 msgid "" "New \"Show first only\" option in the \"View\" menu, to show only the " "first curve (or image) when multiple curves (or images) are displayed in " @@ -1063,7 +1295,7 @@ msgstr "" "lorsque plusieurs courbes (ou images) sont affichées dans la vue du " "graphique" -#: ../../CHANGELOG.md:193 +#: ../../CHANGELOG.md:252 msgid "" "New (movable) label for FWHM computations, additional to the existing " "segment annotation" @@ -1071,17 +1303,17 @@ msgstr "" "Nouvelle étiquette (déplaçable) pour les calculs de FWHM, en plus de " "l'annotation de segment existante" -#: ../../CHANGELOG.md:195 +#: ../../CHANGELOG.md:254 msgid "I/O features:" msgstr "Fonctionnalités d'E/S :" -#: ../../CHANGELOG.md:196 +#: ../../CHANGELOG.md:255 msgid "Added support for reading and writing .MAT files (MATLAB format)" msgstr "" "Ajout de la prise en charge de la lecture et de l'écriture des fichiers " ".MAT (format MATLAB)" -#: ../../CHANGELOG.md:197 +#: ../../CHANGELOG.md:256 msgid "" "Create a new group when opening a file containing multiple signals or " "images (e.g. CSV file with multiple curves)" @@ -1090,11 +1322,11 @@ msgstr "" "plusieurs signaux ou images (par exemple, un fichier CSV avec plusieurs " "courbes)" -#: ../../CHANGELOG.md:199 +#: ../../CHANGELOG.md:258 msgid "Add support for binary images" msgstr "Ajout de la prise en charge des images binaires" -#: ../../CHANGELOG.md:200 +#: ../../CHANGELOG.md:259 msgid "" "Signal ROI extraction: added new dialog box to manually edit the ROI " "lower and upper bounds after defining the ROI graphically" @@ -1103,7 +1335,7 @@ msgstr "" " éditer manuellement les bornes inférieure et supérieure du ROI après " "avoir défini le ROI graphiquement" -#: ../../CHANGELOG.md:202 +#: ../../CHANGELOG.md:261 msgid "ℹ️ New **Signal** operations, processing and analysis features:" msgstr "ℹ️ Nouvelles fonctionnalités de traitement des **Signaux** :" @@ -1217,7 +1449,7 @@ msgstr "" "d'échantillonnage, Paramètres dynamiques (ENOB, SNR, SINAD, THD, SFDR), " "Largeur de bande -3dB, Contraste" -#: ../../CHANGELOG.md:217 +#: ../../CHANGELOG.md:276 msgid "ℹ️ New **Image** operations, processing and analysis features:" msgstr "ℹ️ Nouvelles fonctionnalités de traitement des **Images** :" @@ -1245,7 +1477,7 @@ msgstr "Seuillage" msgid "Parametric, ISODATA, Li, Mean, Minimum, Otsu, Triangle, Yen" msgstr "Paramétrique, ISODATA, Li, Moyenne, Minimum, Otsu, Triangle, Yen" -#: ../../CHANGELOG.md:230 +#: ../../CHANGELOG.md:289 msgid "" "Fixed a performance issue due to an unnecessary refresh of the plot view " "when adding a new signal or image" @@ -1254,7 +1486,7 @@ msgstr "" "de la vue du graphique lors de l'ajout d'un nouveau signal ou d'une " "nouvelle image" -#: ../../CHANGELOG.md:231 +#: ../../CHANGELOG.md:290 msgid "" "Fixed [Issue #77](https://github.com/DataLab-Platform/DataLab/issues/77) " "- Intensity profiles: unable to accept dialog the second time" @@ -1263,7 +1495,7 @@ msgstr "" "Platform/DataLab/issues/77) - Profils d'intensité : impossible d'accepter" " la boîte de dialogue la deuxième fois" -#: ../../CHANGELOG.md:232 +#: ../../CHANGELOG.md:291 msgid "" "Fixed [Issue #75](https://github.com/DataLab-Platform/DataLab/issues/75) " "- View in a new window: curve anti-aliasing is not enabled by default" @@ -1272,13 +1504,13 @@ msgstr "" "Platform/DataLab/issues/75) - Afficher dans une nouvelle fenêtre : l" "'anti-crénelage de la courbe n'est pas activé par défaut" -#: ../../CHANGELOG.md:233 +#: ../../CHANGELOG.md:292 msgid "Annotations visibility is now correctly saved and restored:" msgstr "" "La visibilité des annotations est désormais correctement sauvegardée et " "restaurée :" -#: ../../CHANGELOG.md:234 +#: ../../CHANGELOG.md:293 msgid "" "Before this release, when modifying the annotations visibility in the " "separate plot view, the visibility was not saved and restored when " @@ -1288,7 +1520,7 @@ msgstr "" " dans la vue du graphique séparée, elle n'était pas sauvegardée et " "restaurée lors de la réouverture de la vue du graphique" -#: ../../CHANGELOG.md:235 +#: ../../CHANGELOG.md:294 msgid "" "This has been [fixed " "upstream](https://github.com/PlotPyStack/PlotPy/commit/03faaa42e5d6d4016ea8c99334c29d46a5963467)" @@ -1298,11 +1530,11 @@ msgstr "" "amont](https://github.com/PlotPyStack/PlotPy/commit/03faaa42e5d6d4016ea8c99334c29d46a5963467)" " dans PlotPy (v2.3.3)" -#: ../../CHANGELOG.md:237 +#: ../../CHANGELOG.md:296 msgid "DataLab Version 0.15.1" msgstr "DataLab Version 0.15.1" -#: ../../CHANGELOG.md:241 +#: ../../CHANGELOG.md:300 msgid "" "Fixed [Issue #68](https://github.com/DataLab-Platform/DataLab/issues/68) " "- Slow loading of even simple plots:" @@ -1311,13 +1543,13 @@ msgstr "" "Platform/DataLab/issues/68) - Chargement lent même pour des graphiques " "simples :" -#: ../../CHANGELOG.md:242 +#: ../../CHANGELOG.md:301 msgid "On macOS, the user experience was degraded when handling even simple plots" msgstr "" "Sur macOS, l'expérience utilisateur était dégradée lors de la " "manipulation de graphiques simples" -#: ../../CHANGELOG.md:243 +#: ../../CHANGELOG.md:302 msgid "" "This was due to the way macOS handles the pop-up windows, e.g. when " "refreshing the plot view (\"Creating plot items\" progress bar), hence " @@ -1330,7 +1562,7 @@ msgstr "" "effet de scintillement très gênant et un ralentissement global de " "l'application" -#: ../../CHANGELOG.md:244 +#: ../../CHANGELOG.md:303 msgid "" "This is now fixed by showing the progress bar only after a short delay " "(1s), that is when it is really needed (i.e. for long operations)" @@ -1339,7 +1571,7 @@ msgstr "" "uniquement après un court délai (1s), c'est-à-dire lorsqu'elle est " "vraiment nécessaire (c'est-à-dire pour les opérations longues)" -#: ../../CHANGELOG.md:245 +#: ../../CHANGELOG.md:304 msgid "" "Thanks to [@marcel-goldschen-ohm](https://github.com/marcel-goldschen-" "ohm) for the very thorough feedback and the help in testing the fix" @@ -1348,7 +1580,7 @@ msgstr "" "pour la description très détaillée du problème et l'aide apportée pour " "tester la correction" -#: ../../CHANGELOG.md:246 +#: ../../CHANGELOG.md:305 msgid "" "Fixed [Issue #69](https://github.com/DataLab-Platform/DataLab/issues/69) " "- Annotations should be read-only in Signal/Image View" @@ -1357,13 +1589,13 @@ msgstr "" "Platform/DataLab/issues/69) - Les annotations devraient être en lecture " "seule dans la vue Signal/Image" -#: ../../CHANGELOG.md:247 +#: ../../CHANGELOG.md:306 msgid "Regarding the annotations, DataLab's current behavior is the following:" msgstr "" "En ce qui concerne les annotations, le comportement actuel de DataLab est" " le suivant :" -#: ../../CHANGELOG.md:248 +#: ../../CHANGELOG.md:307 msgid "" "Annotations are created only when showing the signal/image in a separate " "window (double-click on the object, or \"View\" > \"View in a new " @@ -1373,7 +1605,7 @@ msgstr "" "dans une fenêtre séparée (double-clic sur l'objet, ou \"Affichage\" > " "\"Afficher dans une nouvelle fenêtre\")" -#: ../../CHANGELOG.md:249 +#: ../../CHANGELOG.md:308 msgid "" "When displaying the objects in either the \"Signal View\" or the \"Image " "View\", the annotations should be read-only (i.e. not movable, nor " @@ -1383,7 +1615,7 @@ msgstr "" "Image\", les annotations devraient être en lecture seule (c'est-à-dire " "non déplaçables, ni redimensionnables ou supprimables)" -#: ../../CHANGELOG.md:250 +#: ../../CHANGELOG.md:309 msgid "" "However, some annotations were still deletable in the \"Signal View\" and" " the \"Image View\": this is now fixed" @@ -1391,7 +1623,7 @@ msgstr "" "Toutefois, certaines annotations étaient toujours supprimables dans la " "\"Vue Signal\" et la \"Vue Image\" : c'est maintenant corrigé" -#: ../../CHANGELOG.md:251 +#: ../../CHANGELOG.md:310 msgid "" "Note that the fact that annotations can't be created in the \"Signal " "View\" or the \"Image View\" is a limitation of the current " @@ -1402,15 +1634,15 @@ msgstr "" "l'implémentation actuelle, et peut être amélioré dans les versions " "futures" -#: ../../CHANGELOG.md:253 +#: ../../CHANGELOG.md:312 msgid "DataLab Version 0.15.0" msgstr "DataLab Version 0.15.0" -#: ../../CHANGELOG.md:255 +#: ../../CHANGELOG.md:314 msgid "🎁 New installer for the stand-alone version on Windows:" msgstr "🎁 Nouvel installeur pour la version autonome sur Windows :" -#: ../../CHANGELOG.md:257 +#: ../../CHANGELOG.md:316 msgid "" "The stand-alone version on Windows is now distributed as an MSI installer" " (instead of an EXE installer)" @@ -1418,7 +1650,7 @@ msgstr "" "La version autonome sur Windows est désormais distribuée en tant " "qu'installeur MSI (au lieu d'un installeur EXE)" -#: ../../CHANGELOG.md:258 +#: ../../CHANGELOG.md:317 msgid "" "This avoids the false positive detection of the stand-alone version as a " "potential threat by some antivirus software" @@ -1426,27 +1658,27 @@ msgstr "" "Cela évite la détection de faux positifs de la version autonome comme une" " menace potentielle par certains logiciels antivirus" -#: ../../CHANGELOG.md:259 +#: ../../CHANGELOG.md:318 msgid "The program will install files and shortcuts:" msgstr "Le programme installera des fichiers et des raccourcis :" -#: ../../CHANGELOG.md:260 +#: ../../CHANGELOG.md:319 msgid "For current user, if the user has no administrator privileges" msgstr "" "Pour l'utilisateur actuel, si l'utilisateur n'a pas de privilèges " "d'administrateur" -#: ../../CHANGELOG.md:261 +#: ../../CHANGELOG.md:320 msgid "For all users, if the user has administrator privileges" msgstr "" "Pour tous les utilisateurs, si l'utilisateur a des privilèges " "d'administrateur" -#: ../../CHANGELOG.md:262 +#: ../../CHANGELOG.md:321 msgid "Installation directory may be customized" msgstr "Le répertoire d'installation peut être personnalisé" -#: ../../CHANGELOG.md:263 +#: ../../CHANGELOG.md:322 msgid "" "MSI installer allows to integrate DataLab's installation seemlessly in an" " organization's deployment system" @@ -1454,11 +1686,11 @@ msgstr "" "L'installeur MSI permet d'intégrer l'installation de DataLab de manière " "transparente dans le système de déploiement d'une organisation" -#: ../../CHANGELOG.md:267 +#: ../../CHANGELOG.md:326 msgid "Added support for large text/CSV files:" msgstr "Ajout de la prise en charge des fichiers texte/CSV volumineux :" -#: ../../CHANGELOG.md:268 +#: ../../CHANGELOG.md:327 msgid "" "Files over 1 GB (and with reasonable number of lines) can now be imported" " as signals or images without crashing the application or even slowing it" @@ -1468,7 +1700,7 @@ msgstr "" "peuvent désormais être importés en tant que signaux ou images sans " "quitter inopinément l'application ou même la ralentir" -#: ../../CHANGELOG.md:269 +#: ../../CHANGELOG.md:328 msgid "" "The file is read by chunks and, for signals, the data is downsampled to a" " reasonable number of points for visualization" @@ -1476,7 +1708,7 @@ msgstr "" "Le fichier est lu par morceaux et, pour les signaux, les données sont " "rééchantillonnées à un nombre raisonnable de points pour la visualisation" -#: ../../CHANGELOG.md:270 +#: ../../CHANGELOG.md:329 msgid "" "Large files are supported when opening a file (or dragging and dropping a" " file in the Signal Panel) and when importing a file in the Text Import " @@ -1487,11 +1719,11 @@ msgstr "" "lors de l'importation d'un fichier dans l'Assistant d'importation de " "texte" -#: ../../CHANGELOG.md:271 +#: ../../CHANGELOG.md:330 msgid "Auto downsampling feature:" msgstr "Nouvelle fonctionnalité de sous-échantillonnage automatique :" -#: ../../CHANGELOG.md:272 +#: ../../CHANGELOG.md:331 msgid "" "Added \"Auto downsampling\" feature to signal visualization settings (see" " \"Settings\" dialog box)" @@ -1500,7 +1732,7 @@ msgstr "" "paramètres de visualisation des signaux (voir la boîte de dialogue " "\"Paramètres\")" -#: ../../CHANGELOG.md:273 +#: ../../CHANGELOG.md:332 msgid "" "This feature allows to automatically downsample the signal data for " "visualization when the number of points is too high and would lead to a " @@ -1510,7 +1742,7 @@ msgstr "" "données du signal pour la visualisation lorsque le nombre de points est " "trop élevé et entraînerait un rendu lent" -#: ../../CHANGELOG.md:274 +#: ../../CHANGELOG.md:333 msgid "" "The downsampling factor is automatically computed based on the configured" " maximum number of points to display" @@ -1518,7 +1750,7 @@ msgstr "" "Le facteur de sous-échantillonnage est automatiquement calculé en " "fonction du nombre maximal de points à afficher configuré" -#: ../../CHANGELOG.md:275 +#: ../../CHANGELOG.md:334 msgid "" "This feature is enabled by default and may be disabled in the signal " "visualization settings" @@ -1526,37 +1758,37 @@ msgstr "" "Cette fonctionnalité est activée par défaut et peut être désactivée dans " "les paramètres de visualisation du signal" -#: ../../CHANGELOG.md:276 +#: ../../CHANGELOG.md:335 msgid "CSV format handling:" msgstr "Gestion du format CSV :" -#: ../../CHANGELOG.md:277 +#: ../../CHANGELOG.md:336 msgid "Improved support for CSV files with a header row (column names)" msgstr "" "Amélioration de la prise en charge des fichiers CSV avec une ligne d'en-" "tête (noms de colonnes)" -#: ../../CHANGELOG.md:278 +#: ../../CHANGELOG.md:337 msgid "Added support for CSV files with empty columns" msgstr "Ajout de la prise en charge des fichiers CSV avec des colonnes vides" -#: ../../CHANGELOG.md:279 +#: ../../CHANGELOG.md:338 msgid "Open/save file error handling:" msgstr "Gestion des erreurs d'ouverture/enregistrement de fichiers :" -#: ../../CHANGELOG.md:280 +#: ../../CHANGELOG.md:339 msgid "Error messages are now more explicit when opening or saving a file fails" msgstr "" "Les messages d'erreur sont désormais plus explicites lorsque l'ouverture " "ou l'enregistrement d'un fichier échoue" -#: ../../CHANGELOG.md:281 +#: ../../CHANGELOG.md:340 msgid "Added a link to the folder containing the file in the error message" msgstr "" "Ajout d'un lien vers le dossier contenant le fichier dans le message " "d'erreur" -#: ../../CHANGELOG.md:282 +#: ../../CHANGELOG.md:341 msgid "" "Added \"Plugins and I/O formats\" page to the Installation and " "Configuration Viewer (see \"Help\" menu)" @@ -1564,11 +1796,11 @@ msgstr "" "Ajout de la page \"Plugins et fonctionnalités d'entrée/sortie\" au " "Visualiseur d'installation et de configuration (voir le menu \"Aide\")" -#: ../../CHANGELOG.md:283 +#: ../../CHANGELOG.md:342 msgid "Reset DataLab configuration:" msgstr "Réinitialisation de la configuration de DataLab :" -#: ../../CHANGELOG.md:284 +#: ../../CHANGELOG.md:343 msgid "" "In some cases, it may be useful to reset the DataLab configuration file " "to its default values (e.g. when the configuration file is corrupted)" @@ -1577,19 +1809,19 @@ msgstr "" "configuration de DataLab à ses valeurs par défaut (par exemple, lorsque " "le fichier de configuration est corrompu)" -#: ../../CHANGELOG.md:285 +#: ../../CHANGELOG.md:344 msgid "Added new `--reset` command line option to remove the configuration folder" msgstr "" "Ajout de la nouvelle option de ligne de commande `--reset` pour supprimer" " le dossier de configuration" -#: ../../CHANGELOG.md:286 +#: ../../CHANGELOG.md:345 msgid "Added new \"Reset DataLab\" Start Menu shortcut to the Windows installer" msgstr "" "Ajout du nouveau raccourci du Menu Démarer \"Reset DataLab\" à " "l'installeur Windows" -#: ../../CHANGELOG.md:290 +#: ../../CHANGELOG.md:349 msgid "" "Fixed [Issue #64](https://github.com/DataLab-Platform/DataLab/issues/64) " "- HDF5 browser does not show datasets with 1x1 size:" @@ -1598,13 +1830,13 @@ msgstr "" "Platform/DataLab/issues/64) - L'explorateur HDF5 ne montre pas les " "ensembles de données de taille 1x1 :" -#: ../../CHANGELOG.md:291 +#: ../../CHANGELOG.md:350 msgid "HDF5 datasets with a size of 1x1 were not shown in the HDF5 browser" msgstr "" "Les datasets HDF5 de taille 1x1 n'étaient pas affichés dans l'explorateur" " HDF5" -#: ../../CHANGELOG.md:292 +#: ../../CHANGELOG.md:351 msgid "" "Even if those datasets should not be considered as signals or images, " "they are now shown in the HDF5 browser (but not checkable, i.e. not " @@ -1615,11 +1847,11 @@ msgstr "" "(mais non sélectionnables, c'est-à-dire non importables en tant que " "signaux ou images)" -#: ../../CHANGELOG.md:294 +#: ../../CHANGELOG.md:353 msgid "DataLab Version 0.14.2" msgstr "DataLab Version 0.14.2" -#: ../../CHANGELOG.md:296 +#: ../../CHANGELOG.md:355 msgid "" "⚠️ API changes required for fixing support for multiple signals loading " "feature:" @@ -1627,7 +1859,7 @@ msgstr "" "⚠️ Modifications de l'API nécessaires pour corriger la fonctionnalité de " "chargement de plusieurs signaux :" -#: ../../CHANGELOG.md:298 +#: ../../CHANGELOG.md:357 msgid "" "Merged `open_object` and `open_objects` methods to `load_from_files` in " "proxy classes, main window and data panels" @@ -1635,7 +1867,7 @@ msgstr "" "Fusion des méthodes `open_object` et `open_objects` en `load_from_files` " "dans les classes proxy, la fenêtre principale et les panneaux de données" -#: ../../CHANGELOG.md:299 +#: ../../CHANGELOG.md:358 msgid "" "For consistency's sake: merged `save_object` and `save_objects` into " "`save_to_files`" @@ -1643,17 +1875,17 @@ msgstr "" "Pour des raisons de cohérence : fusion de `save_object` et `save_objects`" " en `save_to_files`" -#: ../../CHANGELOG.md:300 +#: ../../CHANGELOG.md:359 msgid "To sum up, those changes lead to the following situation:" msgstr "En résumé, ces changements mènent à la situation suivante :" -#: ../../CHANGELOG.md:301 +#: ../../CHANGELOG.md:360 msgid "`load_from_files`: load a sequence of objects from multiple files" msgstr "" "`load_from_files` : charge une séquence d'objets à partir de plusieurs " "fichiers" -#: ../../CHANGELOG.md:302 +#: ../../CHANGELOG.md:361 msgid "" "`save_to_files`: save a sequence of objects to multiple files (at the " "moment, it only supports saving a single object to a single file, but it " @@ -1666,7 +1898,7 @@ msgstr "" "pour prendre en charge la sauvegarde de plusieurs objets dans un seul " "fichier)" -#: ../../CHANGELOG.md:306 +#: ../../CHANGELOG.md:365 msgid "" "Fixed [Issue #61](https://github.com/DataLab-Platform/DataLab/issues/61) " "- Text file import wizard: application crash when importing a multiple " @@ -1677,7 +1909,7 @@ msgstr "" " plantage de l'application lors de l'importation d'un fichier texte à " "courbes multiples :" -#: ../../CHANGELOG.md:307 +#: ../../CHANGELOG.md:366 msgid "" "This issue concerns a use case where the text file contains multiple " "curves" @@ -1685,7 +1917,7 @@ msgstr "" "Ce problème concerne un cas d'utilisation où le fichier texte contient " "plusieurs courbes" -#: ../../CHANGELOG.md:308 +#: ../../CHANGELOG.md:367 msgid "" "This is now fixed and an automatic test has been added to prevent " "regressions" @@ -1693,28 +1925,28 @@ msgstr "" "C'est maintenant corrigé et un test automatique a été ajouté pour éviter " "les régressions" -#: ../../CHANGELOG.md:310 +#: ../../CHANGELOG.md:369 msgid "DataLab Version 0.14.1" msgstr "DataLab Version 0.14.1" -#: ../../CHANGELOG.md:312 +#: ../../CHANGELOG.md:371 msgid "🎉 New domain name: [datalab-platform.com](https://datalab-platform.com)" msgstr "" "🎉 Nouveau nom de domaine : [datalab-platform.com](https://datalab-" "platform.com)" -#: ../../CHANGELOG.md:314 ../../CHANGELOG.md:342 ../../CHANGELOG.md:401 -#: ../../CHANGELOG.md:432 ../../CHANGELOG.md:551 +#: ../../CHANGELOG.md:373 ../../CHANGELOG.md:401 ../../CHANGELOG.md:460 +#: ../../CHANGELOG.md:491 ../../CHANGELOG.md:610 msgid "💥 New features:" msgstr "💥 Nouvelles fonctionnalités :" -#: ../../CHANGELOG.md:316 +#: ../../CHANGELOG.md:375 msgid "Added support for colormap inversion in Image View:" msgstr "" "Ajout de la prise en charge de l'inversion de la palette de couleurs dans" " la Vue Image :" -#: ../../CHANGELOG.md:317 +#: ../../CHANGELOG.md:376 msgid "" "New \"Invert colormap\" entry in plot context menu, image parameters, and" " in the default image view settings" @@ -1723,15 +1955,15 @@ msgstr "" "menu contextuel du graphique, les paramètres de l'image et dans les " "paramètres par défaut de la vue image" -#: ../../CHANGELOG.md:318 +#: ../../CHANGELOG.md:377 msgid "This requires `PlotPy` v2.3 or later" msgstr "Cette fonctionnalité nécessite `PlotPy` v2.3 ou ultérieur" -#: ../../CHANGELOG.md:319 +#: ../../CHANGELOG.md:378 msgid "HDF5 Browser:" msgstr "Explorateur HDF5 :" -#: ../../CHANGELOG.md:320 +#: ../../CHANGELOG.md:379 msgid "" "Added \"Show array\" button at the corner of the \"Group\" and " "\"Attributes\" tabs, to show the array in a separate window (useful for " @@ -1742,17 +1974,17 @@ msgstr "" "séparée (utile pour copier/coller des données dans d'autres applications," " par exemple)" -#: ../../CHANGELOG.md:321 +#: ../../CHANGELOG.md:380 msgid "Attributes: added support for more scalar data types" msgstr "" "Attributs : ajout de la prise en charge de plus de types de données " "scalaires" -#: ../../CHANGELOG.md:322 +#: ../../CHANGELOG.md:381 msgid "Testability and maintainability:" msgstr "Testabilité et maintenabilité :" -#: ../../CHANGELOG.md:323 +#: ../../CHANGELOG.md:382 msgid "" "DataLab's unit tests are now using [pytest](https://pytest.org). This has" " required a lot of work for the transition, especially to readapt the " @@ -1767,19 +1999,19 @@ msgstr "" " particulière a été portée à l'isolement des tests, afin qu'ils " "n'interfèrent pas les uns avec les autres." -#: ../../CHANGELOG.md:324 +#: ../../CHANGELOG.md:383 msgid "Added continuous integration (CI) with GitHub Actions" msgstr "Ajout de l'intégration continue (CI) avec GitHub Actions" -#: ../../CHANGELOG.md:325 +#: ../../CHANGELOG.md:384 msgid "For this release, test coverage is 87%" msgstr "Pour cette version, la couverture des tests est de 87%" -#: ../../CHANGELOG.md:326 +#: ../../CHANGELOG.md:385 msgid "Text file import assistant:" msgstr "Assistant d'importation de fichiers texte :" -#: ../../CHANGELOG.md:327 +#: ../../CHANGELOG.md:386 msgid "" "Drastically improved the performance of the array preview when importing " "large text files (no more progress bar, and the preview is now displayed " @@ -1789,13 +2021,13 @@ msgstr "" "l'importation de fichiers texte volumineux (plus de barre de progression," " et l'aperçu est désormais affiché presque instantanément)" -#: ../../CHANGELOG.md:331 +#: ../../CHANGELOG.md:390 msgid "XML-RPC server was not shut down properly when closing DataLab" msgstr "" "Le serveur XML-RPC n'était pas arrêté correctement lors de la fermeture " "de DataLab" -#: ../../CHANGELOG.md:332 +#: ../../CHANGELOG.md:391 msgid "" "Fixed test-related issues: some edge cases were hidden by the old test " "suite, and have been revealed by the transition to `pytest`. This has led" @@ -1806,7 +2038,7 @@ msgstr "" "transition vers `pytest`. Cela a conduit à des corrections de bugs et à " "des améliorations du code." -#: ../../CHANGELOG.md:333 +#: ../../CHANGELOG.md:392 msgid "" "On Linux, when running a computation on a signal or an image, and on rare" " occasions, the computation was stuck as if it was running indefinitely. " @@ -1828,7 +2060,7 @@ msgstr "" " qui est la méthode recommandée pour les dernières versions de Python sur" " Linux lorsque le multithreading est employé." -#: ../../CHANGELOG.md:334 +#: ../../CHANGELOG.md:393 msgid "" "Fixed [Issue #60](https://github.com/DataLab-Platform/DataLab/issues/60) " "- `OSError: Invalid HDF5 file [...]` when trying to open an HDF5 file " @@ -1839,7 +2071,7 @@ msgstr "" "la tentative d'ouverture d'un fichier HDF5 avec une extension autre que " "\".h5\"" -#: ../../CHANGELOG.md:335 +#: ../../CHANGELOG.md:394 msgid "" "Image Region of Interest (ROI) extraction: when modifying the image " "bounds in the confirmation dialog box, the ROI was not updated " @@ -1849,33 +2081,33 @@ msgstr "" "l'image dans la boîte de dialogue de confirmation, la ROI n'était pas " "mise à jour en conséquence jusqu'à ce que l'opération soit relancée" -#: ../../CHANGELOG.md:336 +#: ../../CHANGELOG.md:395 msgid "Deprecation issues:" msgstr "Problèmes d'obsolescence :" -#: ../../CHANGELOG.md:337 +#: ../../CHANGELOG.md:396 msgid "Fixed `scipy.ndimage.filters` deprecation warning" msgstr "Correction de l'avertissement d'obsolescence `scipy.ndimage.filters`" -#: ../../CHANGELOG.md:338 +#: ../../CHANGELOG.md:397 msgid "Fixed `numpy.fromstring` deprecation warning" msgstr "Correction de l'avertissement d'obsolescence `numpy.fromstring`" -#: ../../CHANGELOG.md:340 +#: ../../CHANGELOG.md:399 msgid "DataLab Version 0.14.0" msgstr "DataLab Version 0.14.0" -#: ../../CHANGELOG.md:344 +#: ../../CHANGELOG.md:403 msgid "New \"Histogram\" feature in \"Analysis\" menu:" msgstr "Nouvelle fonctionnalité \"Histogramme\" dans le menu Analyse\" :" -#: ../../CHANGELOG.md:345 +#: ../../CHANGELOG.md:404 msgid "Added histogram computation feature for both signals and images" msgstr "" "Ajout de la fonctionnalité de calcul d'histogramme pour les signaux et " "les images" -#: ../../CHANGELOG.md:346 +#: ../../CHANGELOG.md:405 msgid "" "The histogram is computed on the regions of interest (ROI) if any, or on " "the whole signal/image if no ROI is defined" @@ -1883,23 +2115,23 @@ msgstr "" "L'histogramme est calculé sur les régions d'intérêt (ROI) le cas échéant," " ou sur l'ensemble du signal/image si aucune ROI n'est définie" -#: ../../CHANGELOG.md:347 +#: ../../CHANGELOG.md:406 msgid "Editable parameters: number of bins, lower and upper bounds" msgstr "" "Paramètres modifiables : nombre de classes, limites inférieure et " "supérieure" -#: ../../CHANGELOG.md:348 +#: ../../CHANGELOG.md:407 msgid "HDF5 browser:" msgstr "Explorateur HDF5 :" -#: ../../CHANGELOG.md:349 +#: ../../CHANGELOG.md:408 msgid "Improved tree view layout (more compact and readable)" msgstr "" "Amélioration de la disposition de la vue arborescente (plus compacte et " "lisible)" -#: ../../CHANGELOG.md:350 +#: ../../CHANGELOG.md:409 msgid "" "Multiple files can now be opened at once, using the file selection dialog" " box" @@ -1907,19 +2139,19 @@ msgstr "" "Plusieurs fichiers peuvent désormais être ouverts en même temps, en " "utilisant la boîte de dialogue de sélection de fichiers" -#: ../../CHANGELOG.md:351 +#: ../../CHANGELOG.md:410 msgid "Added tabs with information below the graphical preview:" msgstr "Ajout d'onglets avec des informations sous l'aperçu graphique :" -#: ../../CHANGELOG.md:352 +#: ../../CHANGELOG.md:411 msgid "Group info: path, textual preview, etc." msgstr "Informations sur le groupe : chemin, aperçu textuel, etc." -#: ../../CHANGELOG.md:353 +#: ../../CHANGELOG.md:412 msgid "Attributes info: name, value" msgstr "Informations sur les attributs : nom, valeur" -#: ../../CHANGELOG.md:354 +#: ../../CHANGELOG.md:413 msgid "" "Added \"Show only supported data\" check box: when checked, only " "supported data (signals and images) are shown in the tree view" @@ -1928,27 +2160,27 @@ msgstr "" "charge\" : lorsqu'elle est cochée, seules les données prises en charge " "(signaux et images) sont affichées dans la vue arborescente" -#: ../../CHANGELOG.md:355 +#: ../../CHANGELOG.md:414 msgid "Added \"Show values\" check box, to show/hide the values in the tree view" msgstr "" "Ajout de la case à cocher \"Afficher les valeurs\", pour afficher/masquer" " les valeurs dans la vue arborescente" -#: ../../CHANGELOG.md:356 +#: ../../CHANGELOG.md:415 msgid "Macro Panel:" msgstr "Panneau de macro-commandes :" -#: ../../CHANGELOG.md:357 +#: ../../CHANGELOG.md:416 msgid "Macro commands are now numbered, starting from 1, like signals and images" msgstr "" "Les macro-commandes sont désormais numérotées, à partir de 1, comme les " "signaux et les images" -#: ../../CHANGELOG.md:358 +#: ../../CHANGELOG.md:417 msgid "Remote control API (`RemoteProxy` and `LocalProxy`):" msgstr "API de contrôle à distance (`RemoteProxy` et `LocalProxy`) :" -#: ../../CHANGELOG.md:359 +#: ../../CHANGELOG.md:418 msgid "" "`get_object_titles` method now accepts \"macro\" as panel name and " "returns the list of macro titles" @@ -1956,27 +2188,27 @@ msgstr "" "`get_object_titles` accepte désormais \"macro\" comme nom de panneau et " "retourne la liste des titres de macro" -#: ../../CHANGELOG.md:360 +#: ../../CHANGELOG.md:419 msgid "New `run_macro`, `stop_macro` and `import_macro_from_file` methods" msgstr "Nouvelles méthodes `run_macro`, `stop_macro` et `import_macro_from_file`" -#: ../../CHANGELOG.md:364 +#: ../../CHANGELOG.md:423 msgid "Stand-alone version - Integration in Windows start menu:" msgstr "Version autonome - Intégration dans le menu de démarrage de Windows :" -#: ../../CHANGELOG.md:365 +#: ../../CHANGELOG.md:424 msgid "Fixed \"Uninstall\" shortcut (unclickable due to a generic name)" msgstr "" "Correction du raccourci \"Désinstaller\" (non cliquable en raison d'un " "nom générique)" -#: ../../CHANGELOG.md:366 +#: ../../CHANGELOG.md:425 msgid "Translated \"Browse installation directory\" and \"Uninstall\" shortcuts" msgstr "" "Traduction des raccourcis \"Parcourir le répertoire d'installation\" et " "\"Désinstaller\"" -#: ../../CHANGELOG.md:367 +#: ../../CHANGELOG.md:426 msgid "" "Fixed [Issue #55](https://github.com/DataLab-Platform/DataLab/issues/55) " "- Changing image bounds in Image View has no effect on the associated " @@ -1986,7 +2218,7 @@ msgstr "" "Platform/DataLab/issues/55) - Changer les limites de l'image dans la vue " "d'image n'a aucun effet sur les propriétés de l'objet image associé" -#: ../../CHANGELOG.md:368 +#: ../../CHANGELOG.md:427 msgid "" "Fixed [Issue #56](https://github.com/DataLab-Platform/DataLab/issues/56) " "- \"Test data\" plugin: `AttributeError: 'NoneType' object has no " @@ -1997,7 +2229,7 @@ msgstr "" "`AttributeError: 'NoneType' object has no attribute 'data'` lors de " "l'annulation de \"Créer une image avec des pics\"" -#: ../../CHANGELOG.md:369 +#: ../../CHANGELOG.md:428 msgid "" "Fixed [Issue #57](https://github.com/DataLab-Platform/DataLab/issues/57) " "- Circle and ellipse result shapes are not transformed properly" @@ -2006,11 +2238,11 @@ msgstr "" "Platform/DataLab/issues/57) - Les formes résultat cercle et ellipse ne " "sont pas transformées correctement" -#: ../../CHANGELOG.md:370 +#: ../../CHANGELOG.md:429 msgid "Curve color and style cycle:" msgstr "Cycle de couleur et de style de courbe :" -#: ../../CHANGELOG.md:371 +#: ../../CHANGELOG.md:430 msgid "" "Before this release, this cycle was handled by the same mechanism either " "for the Signal Panel or the HDF5 Browser, which was not the expected " @@ -2020,7 +2252,7 @@ msgstr "" "Panneau Signal ou l'Explorateur HDF5, ce qui n'était pas le comportement " "attendu" -#: ../../CHANGELOG.md:372 +#: ../../CHANGELOG.md:431 msgid "" "Now, the cycle is handled separately: the HDF5 Browser or the Text Import" " Wizard use always the same color and style for curves, and they don't " @@ -2031,15 +2263,15 @@ msgstr "" "le même style pour les courbes, et ils n'interfèrent pas avec le cycle du" " Panneau Signal" -#: ../../CHANGELOG.md:374 +#: ../../CHANGELOG.md:433 msgid "DataLab Version 0.12.0" msgstr "DataLab Version 0.12.0" -#: ../../CHANGELOG.md:376 +#: ../../CHANGELOG.md:435 msgid "🧹 Clarity-Enhanced Interface Update:" msgstr "🧹 Clarification de certains libellés des interfaces graphiques :" -#: ../../CHANGELOG.md:378 +#: ../../CHANGELOG.md:437 msgid "" "The tabs used to switch between the data panels (signals and images) and " "the visualization components (\"Curve panel\" and \"Image panel\") have " @@ -2051,7 +2283,7 @@ msgstr "" " \"Panneau d'images\") ont été renommés en \"Panneau Signal\" et " "\"Panneau Image\" (au lieu de \"Signaux\" et \"Images\")" -#: ../../CHANGELOG.md:379 +#: ../../CHANGELOG.md:438 msgid "" "The visualization components have been renamed to \"Signal View\" and " "\"Image View\" (instead of \"Curve panel\" and \"Image panel\")" @@ -2059,7 +2291,7 @@ msgstr "" "Les composants de visualisation ont été renommés en \"Vue Signal\" et " "\"Vue Image\" (au lieu de \"Panneau de courbes\" et \"Panneau d'images\")" -#: ../../CHANGELOG.md:380 +#: ../../CHANGELOG.md:439 msgid "" "The data panel toolbar has been renamed to \"Signal Toolbar\" and \"Image" " Toolbar\" (instead of \"Signal Processing Toolbar\" and \"Image " @@ -2069,7 +2301,7 @@ msgstr "" "d'outils Signal\" et \"Barre d'outils Image\" (au lieu de \"Barre " "d'outils Traitement du Signal\" et \"Barre d'outils Traitement d'Image\")" -#: ../../CHANGELOG.md:381 +#: ../../CHANGELOG.md:440 msgid "" "Ergonomics improvements: the \"Signal Panel\" and \"Image Panel\" are now" " displayed on the left side of the main window, and the \"Signal View\" " @@ -2086,11 +2318,11 @@ msgstr "" "d'outils et menus), et rend l'interface plus intuitive et plus facile à " "utiliser" -#: ../../CHANGELOG.md:383 +#: ../../CHANGELOG.md:442 msgid "✨ New tour and demo feature:" msgstr "✨ Nouvelle fonctionnalité de visite guidée et de démonstration :" -#: ../../CHANGELOG.md:385 +#: ../../CHANGELOG.md:444 msgid "" "When starting DataLab for the first time, an optional tour is now shown " "to the user to introduce the main features of the application" @@ -2099,15 +2331,15 @@ msgstr "" "affichée à l'utilisateur pour présenter les principales fonctionnalités " "de l'application" -#: ../../CHANGELOG.md:386 +#: ../../CHANGELOG.md:445 msgid "The tour can be started again at any time from the \"?\" menu" msgstr "La visite guidée peut être relancée à tout moment depuis le menu \"?\"" -#: ../../CHANGELOG.md:387 +#: ../../CHANGELOG.md:446 msgid "Also added a new \"Demo\" feature to the \"?\" menu" msgstr "Ajout d'une nouvelle fonctionnalité \"Démonstration\" dans le menu \"?\"" -#: ../../CHANGELOG.md:389 +#: ../../CHANGELOG.md:448 msgid "" "🚀 New Binder environment to test DataLab online without installing " "anything" @@ -2115,19 +2347,19 @@ msgstr "" "🚀 Nouvel environnement Binder pour tester DataLab en ligne sans rien " "installer" -#: ../../CHANGELOG.md:393 +#: ../../CHANGELOG.md:452 msgid "New text tutorials are available:" msgstr "De nouveaux tutoriels textuels sont disponibles :" -#: ../../CHANGELOG.md:394 +#: ../../CHANGELOG.md:453 msgid "Measuring Laser Beam Size" msgstr "Mesure de la taille d'un faisceau laser" -#: ../../CHANGELOG.md:395 +#: ../../CHANGELOG.md:454 msgid "DataLab and Spyder: a perfect match" msgstr "DataLab et Spyder : un mariage parfait" -#: ../../CHANGELOG.md:396 +#: ../../CHANGELOG.md:455 msgid "" "\"Getting started\" section: added more explanations and links to the " "tutorials" @@ -2135,7 +2367,7 @@ msgstr "" "Section \"Premiers pas\" : ajout de plus d'explications et de liens vers " "les tutoriels" -#: ../../CHANGELOG.md:397 +#: ../../CHANGELOG.md:456 msgid "" "New \"Contributing\" section explaining how to contribute to DataLab, " "whether you are a developer or not" @@ -2143,31 +2375,31 @@ msgstr "" "Nouvelle section \"Contribuer\" expliquant comment contribuer à DataLab, " "que vous soyez développeur ou non" -#: ../../CHANGELOG.md:398 +#: ../../CHANGELOG.md:457 msgid "New \"Macros\" section explaining how to use the macro commands feature" msgstr "" "Nouvelle section \"Macros\" expliquant comment utiliser la fonctionnalité" " de macro-commandes" -#: ../../CHANGELOG.md:399 +#: ../../CHANGELOG.md:458 msgid "Added \"Copy\" button to code blocks in the documentation" msgstr "Ajout du bouton \"Copier\" aux blocs de code dans la documentation" -#: ../../CHANGELOG.md:403 +#: ../../CHANGELOG.md:462 msgid "New \"Text file import assistant\" feature:" msgstr "Nouvelle fonctionnalité d'assistant d'importation de fichiers texte :" -#: ../../CHANGELOG.md:404 +#: ../../CHANGELOG.md:463 msgid "This feature allows to import text files as signals or images" msgstr "" "Cette fonctionnalité permet d'importer des fichiers texte en tant que " "signaux ou images" -#: ../../CHANGELOG.md:405 +#: ../../CHANGELOG.md:464 msgid "The user can define the source (clipboard or texte file)" msgstr "L'utilisateur peut définir la source (presse-papiers ou fichier texte)" -#: ../../CHANGELOG.md:406 +#: ../../CHANGELOG.md:465 msgid "" "Then, it is possible to define the delimiter, the number of rows to skip," " the destination data type, etc." @@ -2175,7 +2407,7 @@ msgstr "" "Ensuite, il est possible de définir le délimiteur, le nombre de lignes à " "sauter, le type de données de destination, etc." -#: ../../CHANGELOG.md:407 +#: ../../CHANGELOG.md:466 msgid "" "Added menu on the \"Signal Panel\" and \"Image Panel\" tabs corner to " "quickly access the most used features (e.g. \"Add\", \"Remove\", " @@ -2185,11 +2417,11 @@ msgstr "" "Image\" pour accéder rapidement aux fonctionnalités les plus utilisées " "(par exemple \"Ajouter\", \"Supprimer\", \"Dupliquer\", etc.)" -#: ../../CHANGELOG.md:408 +#: ../../CHANGELOG.md:467 msgid "Intensity profile extraction feature:" msgstr "Fonctionnalité d'extraction de profil d'intensité :" -#: ../../CHANGELOG.md:409 +#: ../../CHANGELOG.md:468 msgid "" "Added graphical user interface to extract intensity profiles from images," " for both line and averaged profiles" @@ -2197,7 +2429,7 @@ msgstr "" "Ajout d'une interface graphique pour extraire des profils d'intensité des" " images, pour les profils linéaires et moyennés" -#: ../../CHANGELOG.md:410 +#: ../../CHANGELOG.md:469 msgid "" "Parameters are still directly editable by the user (\"Edit profile " "parameters\" button)" @@ -2205,15 +2437,15 @@ msgstr "" "Les paramètres sont toujours directement modifiables par l'utilisateur " "(bouton \"Modifier les paramètres du profil\")" -#: ../../CHANGELOG.md:411 +#: ../../CHANGELOG.md:470 msgid "Parameters are now stored from one profile extraction to another" msgstr "Les paramètres sont désormais stockés d'une extraction de profil à l'autre" -#: ../../CHANGELOG.md:412 +#: ../../CHANGELOG.md:471 msgid "Statistics feature:" msgstr "Fonctionnalité de statistiques :" -#: ../../CHANGELOG.md:413 +#: ../../CHANGELOG.md:472 msgid "" "Added `/σ(y)` to the signal \"Statistics\" result table (in addition " "to the mean, median, standard deviation, etc.)" @@ -2221,13 +2453,13 @@ msgstr "" "Ajout de `/σ(y)` au tableau de résultats \"Statistiques\" du signal " "(en plus de la moyenne, de la médiane, de l'écart-type, etc.)" -#: ../../CHANGELOG.md:414 +#: ../../CHANGELOG.md:473 msgid "Added `peak-to-peak` to the signal and image \"Statistics\" result table" msgstr "" "Ajout de `peak-to-peak` au tableau de résultats \"Statistiques\" du " "signal et de l'image" -#: ../../CHANGELOG.md:415 +#: ../../CHANGELOG.md:474 msgid "" "Curve fitting feature: fit results are now stored in a dictionary in the " "signal metadata (instead of being stored individually in the signal " @@ -2238,11 +2470,11 @@ msgstr "" "signal (au lieu d'être stockés individuellement dans les métadonnées du " "signal)" -#: ../../CHANGELOG.md:416 +#: ../../CHANGELOG.md:475 msgid "Window state:" msgstr "État de la fenêtre :" -#: ../../CHANGELOG.md:417 +#: ../../CHANGELOG.md:476 msgid "" "The toolbars and dock widgets state (visibility, position, etc.) are now " "stored in the configuration file and restored at startup (size and " @@ -2253,7 +2485,7 @@ msgstr "" "au démarrage (la taille et la position étaient déjà stockées et " "restaurées)" -#: ../../CHANGELOG.md:418 +#: ../../CHANGELOG.md:477 msgid "" "This implements part of [Issue #30](https://github.com/DataLab-" "Platform/DataLab/issues/30) - Save/restore main window layout" @@ -2262,7 +2494,7 @@ msgstr "" "Platform/DataLab/issues/30) - Sauvegarder/restaurer la disposition de la " "fenêtre principale" -#: ../../CHANGELOG.md:422 +#: ../../CHANGELOG.md:481 msgid "" "Fixed [Issue #41](https://github.com/DataLab-Platform/DataLab/issues/41) " "- Radial profile extraction: unable to enter user-defined center " @@ -2272,7 +2504,7 @@ msgstr "" "Platform/DataLab/issues/41) - Extraction du profil radial : impossible " "d'entrer les coordonnées du centre définies par l'utilisateur" -#: ../../CHANGELOG.md:423 +#: ../../CHANGELOG.md:482 msgid "" "Fixed [Issue #49](https://github.com/DataLab-Platform/DataLab/issues/49) " "- Error when trying to open a (UTF-8 BOM) text file as an image" @@ -2281,7 +2513,7 @@ msgstr "" "Platform/DataLab/issues/49) - Erreur lors de l'ouverture d'un fichier " "texte (UTF-8 BOM) en tant qu'image" -#: ../../CHANGELOG.md:424 +#: ../../CHANGELOG.md:483 msgid "" "Fixed [Issue #51](https://github.com/DataLab-Platform/DataLab/issues/51) " "- Unexpected dimensions when adding new ROI on an image with X/Y " @@ -2292,13 +2524,13 @@ msgstr "" "d'une nouvelle ROI sur une image avec des unités X/Y arbitraires (pas des" " pixels)" -#: ../../CHANGELOG.md:425 +#: ../../CHANGELOG.md:484 msgid "Improved plot item style serialization management:" msgstr "" "Amélioration de la gestion de la sérialisation du style des items " "graphiques :" -#: ../../CHANGELOG.md:426 +#: ../../CHANGELOG.md:485 msgid "" "Before this release, the plot item style was stored in the signal/image " "metadata only when saving the workspace to an HDF5 file. So, when " @@ -2313,7 +2545,7 @@ msgstr "" " vue), le style n'était pas conservé dans certains cas (par exemple lors " "de la duplication du signal/image)." -#: ../../CHANGELOG.md:427 +#: ../../CHANGELOG.md:486 msgid "" "Now, the plot item style is stored in the signal/image metadata whenever " "the style is modified, and is restored when reloading the workspace" @@ -2322,7 +2554,7 @@ msgstr "" "du signal/image chaque fois que le style est modifié, et est restauré " "lors du rechargement de l'espace de travail" -#: ../../CHANGELOG.md:428 +#: ../../CHANGELOG.md:487 msgid "" "Handled `ComplexWarning` cast warning when adding regions of interest " "(ROI) to a signal with complex data" @@ -2330,17 +2562,17 @@ msgstr "" "Traitement de l'avertissement de conversion `ComplexWarning` lors de " "l'ajout de régions d'intérêt (ROI) à un signal avec des données complexes" -#: ../../CHANGELOG.md:430 +#: ../../CHANGELOG.md:489 msgid "DataLab Version 0.11.0" msgstr "DataLab Version 0.11.0" -#: ../../CHANGELOG.md:434 +#: ../../CHANGELOG.md:493 msgid "Signals and images may now be reordered in the tree view:" msgstr "" "Les signaux et les images peuvent maintenant être réorganisés dans la vue" " arborescente :" -#: ../../CHANGELOG.md:435 +#: ../../CHANGELOG.md:494 msgid "" "Using the new \"Move up\" and \"Move down\" actions in the \"Edit\" menu " "(or using the corresponding toolbar buttons):" @@ -2349,7 +2581,7 @@ msgstr "" "menu \"Édition\" (ou en utilisant les boutons de la barre d'outils " "correspondants) :" -#: ../../CHANGELOG.md:436 +#: ../../CHANGELOG.md:495 msgid "" "This fixes [Issue #22](https://github.com/DataLab-" "Platform/DataLab/issues/22) - Add \"move up/down\" actions in \"Edit\" " @@ -2359,13 +2591,13 @@ msgstr "" "Platform/DataLab/issues/22) - Ajout des actions \"monter/descendre\" dans" " le menu \"Édition\", pour les signaux/images et les groupes" -#: ../../CHANGELOG.md:437 +#: ../../CHANGELOG.md:496 msgid "Signals and images may also be reordered using drag and drop:" msgstr "" "Les signaux et les images peuvent également être réorganisés par glisser-" "déposer :" -#: ../../CHANGELOG.md:438 +#: ../../CHANGELOG.md:497 msgid "" "Signals and images can be dragged and dropped inside their own panel to " "change their order" @@ -2373,11 +2605,11 @@ msgstr "" "Les signaux et les images peuvent être déplacés et déposés dans leur " "propre panneau pour changer leur ordre" -#: ../../CHANGELOG.md:439 +#: ../../CHANGELOG.md:498 msgid "Groups can also be dragged and dropped inside their panel" msgstr "Les groupes peuvent également être déplacés et déposés dans leur panneau" -#: ../../CHANGELOG.md:440 +#: ../../CHANGELOG.md:499 msgid "" "The feature also supports multi-selection (using the standard Ctrl and " "Shift modifiers), so that multiple signals/images/groups can be moved at " @@ -2388,7 +2620,7 @@ msgstr "" "plusieurs signaux/images/groupes peuvent être déplacés à la fois, pas " "nécessairement avec des positions contiguës" -#: ../../CHANGELOG.md:441 +#: ../../CHANGELOG.md:500 msgid "" "This fixes [Issue #17](https://github.com/DataLab-" "Platform/DataLab/issues/17) - Add Drag and Drop feature to Signals/Images" @@ -2398,23 +2630,23 @@ msgstr "" "Platform/DataLab/issues/17) - Ajout de la fonctionnalité de glisser-" "déposer aux vues arborescentes des signaux/images" -#: ../../CHANGELOG.md:442 +#: ../../CHANGELOG.md:501 msgid "New 1D interpolation features:" msgstr "Nouvelles fonctionnalités d'interpolation 1D :" -#: ../../CHANGELOG.md:443 +#: ../../CHANGELOG.md:502 msgid "Added \"Interpolation\" feature to signal panel's \"Processing\" menu" msgstr "" "Ajout de la fonctionnalité \"Interpolation\" au menu \"Traitement\" du " "panneau de signal" -#: ../../CHANGELOG.md:444 +#: ../../CHANGELOG.md:503 msgid "Methods available: linear, spline, quadratic, cubic, barycentric and PCHIP" msgstr "" "Méthodes disponibles : linéaire, spline, quadratique, cubique, " "barycentrique et PCHIP" -#: ../../CHANGELOG.md:445 +#: ../../CHANGELOG.md:504 msgid "" "Thanks to [@marcel-goldschen-ohm](https://github.com/marcel-goldschen-" "ohm) for the contribution to spline interpolation" @@ -2422,7 +2654,7 @@ msgstr "" "Merci à [@marcel-goldschen-ohm](https://github.com/marcel-goldschen-ohm) " "pour sa contribution à l'interpolation spline" -#: ../../CHANGELOG.md:446 +#: ../../CHANGELOG.md:505 msgid "" "This fixes [Issue #20](https://github.com/DataLab-" "Platform/DataLab/issues/20) - Add 1D interpolation features" @@ -2431,29 +2663,29 @@ msgstr "" "Platform/DataLab/issues/20) - Ajout des fonctionnalités d'interpolation " "1D" -#: ../../CHANGELOG.md:447 +#: ../../CHANGELOG.md:506 msgid "New 1D resampling feature:" msgstr "Nouvelle fonctionnalité de rééchantillonnage 1D :" -#: ../../CHANGELOG.md:448 +#: ../../CHANGELOG.md:507 msgid "Added \"Resampling\" feature to signal panel's \"Processing\" menu" msgstr "" "Ajout de la fonctionnalité \"Rééchantillonnage\" au menu \"Traitement\" " "du panneau de signal" -#: ../../CHANGELOG.md:449 +#: ../../CHANGELOG.md:508 msgid "Same interpolation methods as for the \"Interpolation\" feature" msgstr "" "Mêmes méthodes d'interpolation que pour la fonctionnalité " "\"Interpolation\"" -#: ../../CHANGELOG.md:450 +#: ../../CHANGELOG.md:509 msgid "Possibility to specify the resampling step or the number of points" msgstr "" "Possibilité de spécifier le pas de rééchantillonnage ou le nombre de " "points" -#: ../../CHANGELOG.md:451 +#: ../../CHANGELOG.md:510 msgid "" "This fixes [Issue #21](https://github.com/DataLab-" "Platform/DataLab/issues/21) - Add 1D resampling feature" @@ -2462,17 +2694,17 @@ msgstr "" "Platform/DataLab/issues/21) - Ajout de la fonctionnalité de " "rééchantillonnage 1D" -#: ../../CHANGELOG.md:452 +#: ../../CHANGELOG.md:511 msgid "New 1D convolution feature:" msgstr "Nouvelle fonctionnalité de convolution 1D :" -#: ../../CHANGELOG.md:453 +#: ../../CHANGELOG.md:512 msgid "Added \"Convolution\" feature to signal panel's \"Operation\" menu" msgstr "" "Ajout de la fonctionnalité \"Convolution\" au menu \"Opération\" du " "panneau de signal" -#: ../../CHANGELOG.md:454 +#: ../../CHANGELOG.md:513 msgid "" "This fixes [Issue #23](https://github.com/DataLab-" "Platform/DataLab/issues/23) - Add 1D convolution feature" @@ -2481,21 +2713,21 @@ msgstr "" "Platform/DataLab/issues/23) - Ajout de la fonctionnalité de convolution " "1D" -#: ../../CHANGELOG.md:455 +#: ../../CHANGELOG.md:514 msgid "New 1D detrending feature:" msgstr "Nouvelle fonctionnalité de d'élimination de tendance 1D :" -#: ../../CHANGELOG.md:456 +#: ../../CHANGELOG.md:515 msgid "Added \"Detrending\" feature to signal panel's \"Processing\" menu" msgstr "" "Ajout de la fonctionnalité \"Élimination de tendance\" au menu " "\"Traitement\" du panneau de signal" -#: ../../CHANGELOG.md:457 +#: ../../CHANGELOG.md:516 msgid "Methods available: linear or constant" msgstr "Méthodes disponibles : linéaire ou constante" -#: ../../CHANGELOG.md:458 +#: ../../CHANGELOG.md:517 msgid "" "This fixes [Issue #24](https://github.com/DataLab-" "Platform/DataLab/issues/24) - Add 1D detrending feature" @@ -2504,11 +2736,11 @@ msgstr "" "Platform/DataLab/issues/24) - Ajout de la fonctionnalité d'élimination de" " tendance 1D" -#: ../../CHANGELOG.md:459 +#: ../../CHANGELOG.md:518 msgid "2D analysis results:" msgstr "Résultats d'analyse 2D :" -#: ../../CHANGELOG.md:460 +#: ../../CHANGELOG.md:519 msgid "" "Before this release, 2D analysis results such as contours, blobs, etc. " "were stored in image metadata dictionary as coordinates (x0, y0, x1, y1, " @@ -2521,7 +2753,7 @@ msgstr "" "cercles et les ellipses (c'est-à-dire les coordonnées des rectangles " "englobants)." -#: ../../CHANGELOG.md:461 +#: ../../CHANGELOG.md:520 msgid "" "For convenience, the circle and ellipse coordinates are now stored in " "image metadata dictionary as (x0, y0, radius) and (x0, y0, a, b, theta) " @@ -2531,7 +2763,7 @@ msgstr "" "désormais stockées dans le dictionnaire de métadonnées de l'image sous la" " forme (x0, y0, rayon) et (x0, y0, a, b, theta) respectivement." -#: ../../CHANGELOG.md:462 +#: ../../CHANGELOG.md:521 msgid "" "These results are also shown as such in the \"Results\" dialog box " "(either at the end of the computing process or when clicking on the " @@ -2541,7 +2773,7 @@ msgstr "" "dialogue \"Résultats\" (à la fin du processus de calcul ou en cliquant " "sur le bouton \"Afficher les résultats\")." -#: ../../CHANGELOG.md:463 +#: ../../CHANGELOG.md:522 msgid "" "This fixes [Issue #32](https://github.com/DataLab-" "Platform/DataLab/issues/32) - Contour detection: show circle `(x, y, r)` " @@ -2552,11 +2784,11 @@ msgstr "" " `(x, y, r)` et l'ellipse `(x, y, a, b, theta)` au lieu de `(x0, y0, x1, " "x1, ...)`" -#: ../../CHANGELOG.md:464 +#: ../../CHANGELOG.md:523 msgid "1D and 2D analysis results:" msgstr "Résultats d'analyse 1D et 2D :" -#: ../../CHANGELOG.md:465 +#: ../../CHANGELOG.md:524 msgid "" "Additionnaly to the previous enhancement, more analysis results are now " "shown in the \"Results\" dialog box" @@ -2564,7 +2796,7 @@ msgstr "" "En complément de l'amélioration précédente, plus de résultats d'analyse " "sont désormais affichés dans la boîte de dialogue \"Résultats\"" -#: ../../CHANGELOG.md:466 +#: ../../CHANGELOG.md:525 msgid "" "This concerns both 1D (FHWM, ...) and 2D analysis results (contours, " "blobs, ...):" @@ -2572,42 +2804,42 @@ msgstr "" "Cela concerne à la fois les résultats d'analyse 1D (FHWM, ...) et 2D " "(contours, blobs, ...) :" -#: ../../CHANGELOG.md:467 +#: ../../CHANGELOG.md:526 msgid "Segment results now also show length (L) and center coordinates (Xc, Yc)" msgstr "" "Les résultats de type segment affichent désormais également la longueur " "(L) et les coordonnées du centre (Xc, Yc)" -#: ../../CHANGELOG.md:468 +#: ../../CHANGELOG.md:527 msgid "Circle and ellipse results now also show area (A)" msgstr "" "Les résultats de type cercle et ellipse affichent désormais également " "l'aire (A)" -#: ../../CHANGELOG.md:469 +#: ../../CHANGELOG.md:528 msgid "Added \"Plot results\" entry in \"Analysis\" menu:" msgstr "Ajout de l'entrée \"Tracer les résultats\" dans le menu Analyse\" :" -#: ../../CHANGELOG.md:470 +#: ../../CHANGELOG.md:529 msgid "This feature allows to plot analysis results (1D or 2D)" msgstr "Cette fonctionnalité permet de tracer les résultats d'analyse (1D ou 2D)" -#: ../../CHANGELOG.md:471 +#: ../../CHANGELOG.md:530 msgid "" "It creates a new signal with X and Y axes corresponding to user-defined " -"parameters (e.g. X = indexes and Y = radius for circle results)" +"parameters (e.g. X = indices and Y = radius for circle results)" msgstr "" "Cela crée un nouveau signal avec des axes X et Y correspondant à des " "paramètres définis par l'utilisateur (par exemple X = index et Y = rayon " "pour les résultats de cercle)" -#: ../../CHANGELOG.md:472 +#: ../../CHANGELOG.md:531 msgid "Increased default width of the object selection dialog box:" msgstr "" "Augmentation de la largeur par défaut de la boîte de dialogue de " "sélection d'objet :" -#: ../../CHANGELOG.md:473 +#: ../../CHANGELOG.md:532 msgid "" "The object selection dialog box is now wider by default, so that the full" " signal/image/group titles may be more easily readable" @@ -2616,11 +2848,11 @@ msgstr "" "défaut, de sorte que les titres complets des signaux/images/groupes " "soient plus facilement lisibles" -#: ../../CHANGELOG.md:474 +#: ../../CHANGELOG.md:533 msgid "Delete metadata feature:" msgstr "Fonctionnalité de suppression de métadonnées :" -#: ../../CHANGELOG.md:475 +#: ../../CHANGELOG.md:534 msgid "" "Before this release, the feature was deleting all metadata, including the" " Regions Of Interest (ROI) metadata, if any." @@ -2628,7 +2860,7 @@ msgstr "" "Avant cette version, la fonctionnalité supprimait toutes les métadonnées," " y compris les métadonnées des régions d'intérêt (ROI), le cas échéant." -#: ../../CHANGELOG.md:476 +#: ../../CHANGELOG.md:535 msgid "" "Now a confirmation dialog box is shown to the user before deleting all " "metadata if the signal/image has ROI metadata: this allows to keep the " @@ -2639,7 +2871,7 @@ msgstr "" "signal/l'image a des métadonnées ROI : cela permet de conserver les " "métadonnées ROI si nécessaire." -#: ../../CHANGELOG.md:477 +#: ../../CHANGELOG.md:536 msgid "" "Image profile extraction feature: added support for masked images (when " "defining regions of interest, the areas outside the ROIs are masked, and " @@ -2652,7 +2884,7 @@ msgstr "" " zones non masquées, ou moyenné sur les zones non masquées dans le cas de" " l'extraction du profil moyen)" -#: ../../CHANGELOG.md:478 +#: ../../CHANGELOG.md:537 msgid "" "Curve style: added \"Reset curve styles\" in \"View\" menu. This feature " "allows to reset the curve style cycle to its initial state." @@ -2661,11 +2893,11 @@ msgstr "" " menu \"Affichage\". Cette fonctionnalité permet de réinitialiser le " "cycle de style de courbe à son état initial." -#: ../../CHANGELOG.md:479 +#: ../../CHANGELOG.md:538 msgid "Plugin base classe `PluginBase`:" msgstr "Classe de base des plugins `PluginBase` :" -#: ../../CHANGELOG.md:480 +#: ../../CHANGELOG.md:539 msgid "" "Added `edit_new_signal_parameters` method for showing a dialog box to " "edit parameters for a new signal" @@ -2673,7 +2905,7 @@ msgstr "" "Ajout de la méthode `edit_new_signal_parameters` pour afficher une boîte " "de dialogue pour éditer les paramètres d'un nouveau signal" -#: ../../CHANGELOG.md:481 +#: ../../CHANGELOG.md:540 msgid "" "Added `edit_new_image_parameters` method for showing a dialog box to edit" " parameters for a new image (updated the *cdl_testdata.py* plugin " @@ -2683,15 +2915,15 @@ msgstr "" "de dialogue pour éditer les paramètres d'une nouvelle image (mise à jour " "du plugin *cdl_testdata.py* en conséquence)" -#: ../../CHANGELOG.md:482 +#: ../../CHANGELOG.md:541 msgid "Signal and image computations API (`cdl.computations`):" msgstr "API de calculs sur les signaux et les images (`cdl.computations`) :" -#: ../../CHANGELOG.md:483 +#: ../../CHANGELOG.md:542 msgid "Added wrappers for signal and image 1 -> 1 computations" msgstr "Ajout de wrappers pour les calculs 1 -> 1 sur les signaux et les images" -#: ../../CHANGELOG.md:484 +#: ../../CHANGELOG.md:543 msgid "" "These wrappers aim at simplifying the creation of a basic computation " "function operating on DataLab's native objects (`SignalObj` and " @@ -2701,7 +2933,7 @@ msgstr "" "base opérant sur les objets natifs de DataLab (`SignalObj` et `ImageObj`)" " à partir d'une fonction opérant sur des tableaux NumPy" -#: ../../CHANGELOG.md:485 +#: ../../CHANGELOG.md:544 msgid "" "This simplifies DataLab's internals and makes it easier to create new " "computing features inside plugins" @@ -2709,13 +2941,13 @@ msgstr "" "Cela simplifie les fonctionnalités internes de DataLab et facilite la " "création de nouvelles fonctionnalités de calcul dans les plugins" -#: ../../CHANGELOG.md:486 +#: ../../CHANGELOG.md:545 msgid "See the *cdl_custom_func.py* example plugin for a practical use case" msgstr "" "Voir le plugin d'exemple *cdl_custom_func.py* pour un cas d'utilisation " "pratique" -#: ../../CHANGELOG.md:487 +#: ../../CHANGELOG.md:546 msgid "" "Added \"Radial profile extraction\" feature to image panel's " "\"Operation\" menu:" @@ -2723,27 +2955,27 @@ msgstr "" "Ajout de la fonctionnalité \"Extraction du profil radial\" au menu " "\"Opération\" du panneau d'image :" -#: ../../CHANGELOG.md:488 +#: ../../CHANGELOG.md:547 msgid "This feature allows to extract a radially averaged profile from an image" msgstr "" "Cette fonctionnalité permet d'extraire un profil moyenné radialement à " "partir d'une image" -#: ../../CHANGELOG.md:489 +#: ../../CHANGELOG.md:548 msgid "The profile is extracted around a user-defined center (x0, y0)" msgstr "Le profil est extrait autour d'un centre défini par l'utilisateur (x0, y0)" -#: ../../CHANGELOG.md:490 +#: ../../CHANGELOG.md:549 msgid "The center may also be computed (centroid or image center)" msgstr "" "Le centre peut également être calculé (centre de gravité ou centre de " "l'image)" -#: ../../CHANGELOG.md:491 +#: ../../CHANGELOG.md:550 msgid "Automated test suite:" msgstr "Suite de tests automatisés :" -#: ../../CHANGELOG.md:492 +#: ../../CHANGELOG.md:551 msgid "" "Since version 0.10, DataLab's proxy object has a `toggle_auto_refresh` " "method to toggle the \"Auto-refresh\" feature. This feature may be useful" @@ -2754,7 +2986,7 @@ msgstr "" "refresh\". Cette fonctionnalité peut être utile pour améliorer les " "performances lors de l'exécution de scripts de test" -#: ../../CHANGELOG.md:493 +#: ../../CHANGELOG.md:552 msgid "" "Test scenarios on signals and images are now using this feature to " "improve performance" @@ -2762,11 +2994,11 @@ msgstr "" "Les scénarios de test sur les signaux et les images utilisent désormais " "cette fonctionnalité pour améliorer les performances" -#: ../../CHANGELOG.md:494 +#: ../../CHANGELOG.md:553 msgid "Signal and image metadata:" msgstr "Métadonnées des signaux et des images :" -#: ../../CHANGELOG.md:495 +#: ../../CHANGELOG.md:554 msgid "" "Added \"source\" entry to the metadata dictionary, to store the source " "file path when importing a signal or an image from a file" @@ -2775,7 +3007,7 @@ msgstr "" "stocker le chemin du fichier source lors de l'importation d'un signal ou " "d'une image à partir d'un fichier" -#: ../../CHANGELOG.md:496 +#: ../../CHANGELOG.md:555 msgid "" "This field is kept while processing the signal/image, in order to keep " "track of the source file path" @@ -2783,7 +3015,7 @@ msgstr "" "Ce champ est conservé lors du traitement du signal/image, afin de garder " "une trace du chemin du fichier source" -#: ../../CHANGELOG.md:500 +#: ../../CHANGELOG.md:559 msgid "" "New [Tutorial section](https://datalab-" "platform.com/en/intro/tutorials/index.html) in the documentation:" @@ -2791,45 +3023,45 @@ msgstr "" "Nouvelle [section Tutoriels](https://datalab-" "platform.com/fr/intro/tutorials/index.html) dans la documentation :" -#: ../../CHANGELOG.md:501 +#: ../../CHANGELOG.md:560 msgid "This section provides a set of tutorials to learn how to use DataLab" msgstr "" "Cette section fournit un ensemble de tutoriels pour apprendre à utiliser " "DataLab" -#: ../../CHANGELOG.md:502 +#: ../../CHANGELOG.md:561 msgid "The following video tutorials are available:" msgstr "Les tutoriels vidéo suivants sont disponibles :" -#: ../../CHANGELOG.md:503 +#: ../../CHANGELOG.md:562 msgid "Quick demo" msgstr "Démo rapide" -#: ../../CHANGELOG.md:504 +#: ../../CHANGELOG.md:563 msgid "Adding your own features" msgstr "Ajout de vos propres fonctionnalités" -#: ../../CHANGELOG.md:505 +#: ../../CHANGELOG.md:564 msgid "The following text tutorials are available:" msgstr "Les tutoriels textuels suivants sont disponibles :" -#: ../../CHANGELOG.md:506 +#: ../../CHANGELOG.md:565 msgid "Processing a spectrum" msgstr "Traitement d'un spectre" -#: ../../CHANGELOG.md:507 +#: ../../CHANGELOG.md:566 msgid "Detecting blobs on an image" msgstr "Détection de taches sur une image" -#: ../../CHANGELOG.md:508 +#: ../../CHANGELOG.md:567 msgid "Measuring Fabry-Perot fringes" msgstr "Mesure de franges Fabry-Perot" -#: ../../CHANGELOG.md:509 +#: ../../CHANGELOG.md:568 msgid "Prototyping a custom processing pipeline" msgstr "Prototypage d'un pipeline de traitement personnalisé" -#: ../../CHANGELOG.md:510 +#: ../../CHANGELOG.md:569 msgid "" "New [API section](https://datalab-platform.com/en/api/index.html) in the " "documentation:" @@ -2837,7 +3069,7 @@ msgstr "" "Nouvelle [section API](https://datalab-platform.com/fr/api/index.html) " "dans la documentation :" -#: ../../CHANGELOG.md:511 +#: ../../CHANGELOG.md:570 msgid "" "This section explains how to use DataLab as a Python library, by covering" " the following topics:" @@ -2845,11 +3077,11 @@ msgstr "" "Cette section explique comment utiliser DataLab comme une bibliothèque " "Python, en abordant les sujets suivants :" -#: ../../CHANGELOG.md:512 +#: ../../CHANGELOG.md:571 msgid "How to use DataLab algorithms on NumPy arrays" msgstr "Comment utiliser les algorithmes DataLab sur les tableaux NumPy" -#: ../../CHANGELOG.md:513 +#: ../../CHANGELOG.md:572 msgid "" "How to use DataLab computation features on DataLab objects (signals and " "images)" @@ -2857,15 +3089,15 @@ msgstr "" "Comment utiliser les fonctionnalités de calcul de DataLab sur les objets " "DataLab (signaux et images)" -#: ../../CHANGELOG.md:514 +#: ../../CHANGELOG.md:573 msgid "How to use DataLab I/O features" msgstr "Comment utiliser les fonctionnalités d'entrée/sortie de DataLab" -#: ../../CHANGELOG.md:515 +#: ../../CHANGELOG.md:574 msgid "How to use proxy objects to control DataLab remotely" msgstr "Comment utiliser les objets proxy pour contrôler DataLab à distance" -#: ../../CHANGELOG.md:516 +#: ../../CHANGELOG.md:575 msgid "" "This section also provides a complete API reference for DataLab objects " "and features" @@ -2873,7 +3105,7 @@ msgstr "" "Cette section fournit également une référence complète de l'API pour les " "objets et les fonctionnalités de DataLab" -#: ../../CHANGELOG.md:517 +#: ../../CHANGELOG.md:576 msgid "" "This fixes [Issue #19](https://github.com/DataLab-" "Platform/DataLab/issues/19) - Add API documentation (data model, " @@ -2884,7 +3116,7 @@ msgstr "" "de données, fonctions sur les tableaux ou les objets de signaux/images, " "...)" -#: ../../CHANGELOG.md:521 +#: ../../CHANGELOG.md:580 msgid "" "Fixed [Issue #29](https://github.com/DataLab-Platform/DataLab/issues/29) " "- Polynomial fit error: `QDialog [...] argument 1 has an unexpected type " @@ -2894,24 +3126,24 @@ msgstr "" "Platform/DataLab/issues/29) - Erreur d'ajustement polynomial : `QDialog " "[...] argument 1 has an unexpected type 'SignalProcessor'`" -#: ../../CHANGELOG.md:522 +#: ../../CHANGELOG.md:581 msgid "Image ROI extraction feature:" msgstr "Fonctionnalité d'extraction de ROI d'image :" -#: ../../CHANGELOG.md:523 +#: ../../CHANGELOG.md:582 msgid "" "Before this release, when extracting a single circular ROI from an image " -"with the \"Extract all regions of interest into a single image object\" " -"option enabled, the result was a single image without the ROI mask (the " -"ROI mask was only available when extracting ROI with the option disabled)" +"with the \"Extract all ROIs into a single image object\" option enabled, " +"the result was a single image without the ROI mask (the ROI mask was only" +" available when extracting ROI with the option disabled)" msgstr "" "Avant cette version, lors de l'extraction d'une seule ROI circulaire " -"d'une image avec l'option \"Extraire toutes les régions d'intérêt dans un" -" seul objet d'image\" activée, le résultat était une seule image sans le " -"masque de ROI (le masque de ROI n'était disponible que lors de " -"l'extraction de ROI avec l'option désactivée)" +"d'une image avec l'option \"Extraire toutes les ROI dans un seul objet " +"d'image\" activée, le résultat était une seule image sans le masque de " +"ROI (le masque de ROI n'était disponible que lors de l'extraction de ROI " +"avec l'option désactivée)" -#: ../../CHANGELOG.md:524 +#: ../../CHANGELOG.md:583 msgid "" "This was leading to an unexpected behavior, because one could interpret " "the result (a square image without the ROI mask) as the result of a " @@ -2921,19 +3153,19 @@ msgstr "" "le résultat (une image carrée sans le masque de ROI) comme le résultat " "d'une seule ROI rectangulaire" -#: ../../CHANGELOG.md:525 +#: ../../CHANGELOG.md:584 msgid "" "Now, when extracting a single circular ROI from an image with the " -"\"Extract all regions of interest into a single image object\" option " -"enabled, the result is a single image with the ROI mask (as if the option" -" was disabled)" +"\"Extract all ROIs into a single image object\" option enabled, the " +"result is a single image with the ROI mask (as if the option was " +"disabled)" msgstr "" "A présent, lors de l'extraction d'une seule ROI circulaire d'une image " -"avec l'option \"Extraire toutes les régions d'intérêt dans un seul objet " -"d'image\" activée, le résultat est une seule image avec le masque de ROI " -"(comme si l'option était désactivée)" +"avec l'option \"Extraire toutes les ROI dans un seul objet d'image\" " +"activée, le résultat est une seule image avec le masque de ROI (comme si " +"l'option était désactivée)" -#: ../../CHANGELOG.md:526 +#: ../../CHANGELOG.md:585 msgid "" "This fixes [Issue #31](https://github.com/DataLab-" "Platform/DataLab/issues/31) - Single circular ROI extraction: " @@ -2943,11 +3175,11 @@ msgstr "" "Platform/DataLab/issues/31) - Extraction d'une seule ROI circulaire : " "bascule automatique vers la fonction `extract_single_roi`" -#: ../../CHANGELOG.md:527 +#: ../../CHANGELOG.md:586 msgid "Analysis on circular ROI:" msgstr "Analyse sur ROI circulaire :" -#: ../../CHANGELOG.md:528 +#: ../../CHANGELOG.md:587 msgid "" "Before this release, when running computations on a circular ROI, the " "results were unexpected in terms of coordinates (results seemed to be " @@ -2958,11 +3190,11 @@ msgstr "" "(les résultats semblaient être calculés dans une région située au-dessus " "de la ROI réelle)." -#: ../../CHANGELOG.md:529 +#: ../../CHANGELOG.md:588 msgid "This was due to a regression introduced in an earlier release." msgstr "Cela était dû à une régression introduite dans une version antérieure." -#: ../../CHANGELOG.md:530 +#: ../../CHANGELOG.md:589 msgid "" "Now, when defining a circular ROI and running computations on it, the " "results are computed on the actual ROI" @@ -2970,7 +3202,7 @@ msgstr "" "A présent, lors de la définition d'une ROI circulaire et de l'exécution " "de calculs sur celle-ci, les résultats sont calculés sur la ROI réelle" -#: ../../CHANGELOG.md:531 +#: ../../CHANGELOG.md:590 msgid "" "This fixes [Issue #33](https://github.com/DataLab-" "Platform/DataLab/issues/33) - Analysis on circular ROI: unexpected " @@ -2980,11 +3212,11 @@ msgstr "" "Platform/DataLab/issues/33) - Analyse sur ROI circulaire : résultats " "inattendus" -#: ../../CHANGELOG.md:532 +#: ../../CHANGELOG.md:591 msgid "Contour detection on ROI:" msgstr "Détection des contours sur ROI :" -#: ../../CHANGELOG.md:533 +#: ../../CHANGELOG.md:592 msgid "" "Before this release, when running contour detection on a ROI, some " "contours were detected outside the ROI (it may be due to a limitation of " @@ -2995,13 +3227,13 @@ msgstr "" "peut être dû à une limitation de la fonction `find_contours` de scikit-" "image)." -#: ../../CHANGELOG.md:534 +#: ../../CHANGELOG.md:593 msgid "Now, thanks a workaround, the erroneous contours are filtered out." msgstr "" "A présent, grâce à une solution de contournement, les contours erronés " "sont filtrés." -#: ../../CHANGELOG.md:535 +#: ../../CHANGELOG.md:594 msgid "" "A new test module `cdl.tests.features.images.contour_fabryperot_app` has " "been added to test the contour detection feature on a Fabry-Perot image " @@ -3014,7 +3246,7 @@ msgstr "" "Perot (merci à [@emarin2642](https://github.com/emarin2642) pour sa " "contribution)" -#: ../../CHANGELOG.md:536 +#: ../../CHANGELOG.md:595 msgid "" "This fixes [Issue #34](https://github.com/DataLab-" "Platform/DataLab/issues/34) - Contour detection: unexpected results " @@ -3024,11 +3256,11 @@ msgstr "" "Platform/DataLab/issues/34) - Détection des contours : résultats " "inattendus en dehors de la ROI" -#: ../../CHANGELOG.md:537 +#: ../../CHANGELOG.md:596 msgid "Analysis result merging:" msgstr "Fusion des résultats d'analyse :" -#: ../../CHANGELOG.md:538 +#: ../../CHANGELOG.md:597 msgid "" "Before this release, when doing a `1->N` computation (sum, average, " "product) on a group of signals/images, the analysis results associated to" @@ -3040,7 +3272,7 @@ msgstr "" "chaque signal/image étaient fusionnés en un seul résultat, mais seul le " "type de résultat présent dans le premier signal/image était conservé." -#: ../../CHANGELOG.md:539 +#: ../../CHANGELOG.md:598 msgid "" "Now, the analysis results associated to each signal/image are merged into" " a single result, whatever the type of result is." @@ -3048,7 +3280,7 @@ msgstr "" "A présent, les résultats d'analyse associés à chaque signal/image sont " "fusionnés en un seul résultat, quel que soit le type de résultat." -#: ../../CHANGELOG.md:540 +#: ../../CHANGELOG.md:599 msgid "" "Fixed [Issue #36](https://github.com/DataLab-Platform/DataLab/issues/36) " "- \"Delete all\" action enable state is sometimes not refreshed" @@ -3057,7 +3289,7 @@ msgstr "" "Platform/DataLab/issues/36) - L'état d'activation de l'action \"Tout " "supprimer\" n'est parfois pas rafraîchi" -#: ../../CHANGELOG.md:541 +#: ../../CHANGELOG.md:600 msgid "" "Image X/Y swap: when swapping X and Y axes, the regions of interest (ROI)" " were not removed and not swapped either (ROI are now removed, until we " @@ -3068,7 +3300,7 @@ msgstr "" "sont désormais supprimées, jusqu'à ce que nous implémentions la fonction " "d'inversion, si demandé)" -#: ../../CHANGELOG.md:542 +#: ../../CHANGELOG.md:601 msgid "" "\"Properties\" group box: the \"Apply\" button was enabled by default, " "even when no property was modified, which was confusing for the user (the" @@ -3080,7 +3312,7 @@ msgstr "" " pour l'utilisateur (le bouton \"Appliquer\" est désormais désactivé par " "défaut, et n'est activé que lorsqu'une propriété est modifiée)" -#: ../../CHANGELOG.md:543 +#: ../../CHANGELOG.md:602 msgid "" "Fixed proxy `get_object` method when there is no object to return (`None`" " is returned instead of an exception)" @@ -3088,7 +3320,7 @@ msgstr "" "Correction de la méthode `get_object` du proxy lorsqu'il n'y a pas " "d'objet à retourner (`None` est retourné au lieu d'une exception)" -#: ../../CHANGELOG.md:544 +#: ../../CHANGELOG.md:603 msgid "" "Fixed `IndexError: list index out of range` when performing some " "operations or computations on groups of signals/images (e.g. \"ROI " @@ -3099,17 +3331,17 @@ msgstr "" "\"Extraction de ROI\", \"Détection de pics\", \"Redimensionnement\", " "etc.)" -#: ../../CHANGELOG.md:545 +#: ../../CHANGELOG.md:604 msgid "Drag and drop from a file manager: filenames are now sorted alphabetically" msgstr "" "Glisser-déposer depuis un gestionnaire de fichiers : les noms de fichiers" " sont désormais triés par ordre alphabétique" -#: ../../CHANGELOG.md:547 +#: ../../CHANGELOG.md:606 msgid "DataLab Version 0.10.1" msgstr "DataLab Version 0.10.1" -#: ../../CHANGELOG.md:549 +#: ../../CHANGELOG.md:608 msgid "" "*Note*: V0.10.0 was almost immediately replaced by V0.10.1 due to a last " "minute bug fix" @@ -3117,35 +3349,35 @@ msgstr "" "*Note* : la version 0.10.0 a été presque immédiatement remplacée par la " "version 0.10.1 en raison d'une correction de bogue de dernière minute" -#: ../../CHANGELOG.md:553 +#: ../../CHANGELOG.md:612 msgid "Features common to signals and images:" msgstr "Fonctionnalités communes aux signaux et aux images :" -#: ../../CHANGELOG.md:554 +#: ../../CHANGELOG.md:613 msgid "Added \"Real part\" and \"Imaginary part\" features to \"Operation\" menu" msgstr "" "Ajout des fonctionnalités \"Partie réelle\" et \"Partie imaginaire\" au " "menu \"Opération\"" -#: ../../CHANGELOG.md:555 +#: ../../CHANGELOG.md:614 msgid "Added \"Convert data type\" feature to \"Operation\" menu" msgstr "" "Ajout de la fonctionnalité \"Convertir le type de données\" au menu " "\"Opération\"" -#: ../../CHANGELOG.md:556 +#: ../../CHANGELOG.md:615 msgid "Features added following user requests (12/18/2023 meetup @ CEA):" msgstr "" "Fonctionnalités ajoutées suite aux demandes des utilisateurs (rencontre " "du 18/12/2023 au CEA) :" -#: ../../CHANGELOG.md:557 +#: ../../CHANGELOG.md:616 msgid "Curve and image styles are now saved in the HDF5 file:" msgstr "" "Les styles de courbe et d'image sont désormais enregistrés dans le " "fichier HDF5 :" -#: ../../CHANGELOG.md:558 +#: ../../CHANGELOG.md:617 msgid "" "Curve style covers the following properties: color, line style, line " "width, marker style, marker size, marker edge color, marker face color, " @@ -3155,13 +3387,13 @@ msgstr "" "ligne, largeur de ligne, style de marqueur, taille de marqueur, couleur " "de bordure de marqueur, couleur de remplissage de marqueur, etc." -#: ../../CHANGELOG.md:559 +#: ../../CHANGELOG.md:618 msgid "Image style covers the following properties: colormap, interpolation, etc." msgstr "" "Le style d'image couvre les propriétés suivantes : colormap, " "interpolation, etc." -#: ../../CHANGELOG.md:560 +#: ../../CHANGELOG.md:619 msgid "" "Those properties were already persistent during the working session, but " "were lost when saving and reloading the HDF5 file" @@ -3170,7 +3402,7 @@ msgstr "" "mais étaient perdues lors de l'enregistrement et du rechargement du " "fichier HDF5" -#: ../../CHANGELOG.md:561 +#: ../../CHANGELOG.md:620 msgid "" "Now, those properties are saved in the HDF5 file and are restored when " "reloading the HDF5 file" @@ -3178,11 +3410,11 @@ msgstr "" "Désormais, ces propriétés sont enregistrées dans le fichier HDF5 et sont " "restaurées lors du rechargement du fichier HDF5" -#: ../../CHANGELOG.md:562 +#: ../../CHANGELOG.md:621 msgid "New profile extraction features for images:" msgstr "Nouvelles fonctionnalités d'extraction de profil pour les images :" -#: ../../CHANGELOG.md:563 +#: ../../CHANGELOG.md:622 msgid "" "Added \"Line profile\" to \"Operations\" menu, to extract a profile from " "an image along a row or a column" @@ -3190,7 +3422,7 @@ msgstr "" "Ajout de la fonctionnalité \"Profil rectiligne\" au menu \"Opérations\", " "pour extraire un profil d'une image le long d'une ligne ou d'une colonne" -#: ../../CHANGELOG.md:564 +#: ../../CHANGELOG.md:623 msgid "" "Added \"Average profile\" to \"Operations\" menu, to extract the average " "profile on a rectangular area of an image, along a row or a column" @@ -3199,7 +3431,7 @@ msgstr "" "extraire le profil moyen sur une zone rectangulaire d'une image, le long " "d'une ligne ou d'une colonne" -#: ../../CHANGELOG.md:565 +#: ../../CHANGELOG.md:624 msgid "" "Image LUT range (contrast/brightness settings) is now saved in the HDF5 " "file:" @@ -3207,7 +3439,7 @@ msgstr "" "La plage LUT de l'image (paramètres de contraste/luminosité) est " "désormais enregistrée dans le fichier HDF5 :" -#: ../../CHANGELOG.md:566 +#: ../../CHANGELOG.md:625 msgid "" "As for curve and image styles, the LUT range was already persistent " "during the working session, but was lost when saving and reloading the " @@ -3217,7 +3449,7 @@ msgstr "" "persistante pendant la session de travail, mais était perdue lors de " "l'enregistrement et du rechargement du fichier HDF5" -#: ../../CHANGELOG.md:567 +#: ../../CHANGELOG.md:626 msgid "" "Now, the LUT range is saved in the HDF5 file and is restored when " "reloading it" @@ -3225,7 +3457,7 @@ msgstr "" "Désormais, la plage LUT est enregistrée dans le fichier HDF5 et est " "restaurée lors du rechargement" -#: ../../CHANGELOG.md:568 +#: ../../CHANGELOG.md:627 msgid "" "Added \"Auto-refresh\" and \"Refresh manually\" actions in \"View\" menu " "(and main toolbar):" @@ -3233,7 +3465,7 @@ msgstr "" "Ajout des actions \"Rafraîchissement automatique\" et \"Rafraîchissement " "manuel\" dans le menu \"Affichage\" (et la barre d'outils principale) :" -#: ../../CHANGELOG.md:569 +#: ../../CHANGELOG.md:628 msgid "" "When \"Auto-refresh\" is enabled (default), the plot view is " "automatically refreshed when a signal/image is modified, added or " @@ -3246,7 +3478,7 @@ msgstr "" "cela peut entraîner des problèmes de performances lors de l'utilisation " "de grands ensembles de données." -#: ../../CHANGELOG.md:570 +#: ../../CHANGELOG.md:629 msgid "" "When disabled, the plot view is not automatically refreshed. The user " "must manually refresh the plot view by clicking on the \"Refresh " @@ -3259,11 +3491,11 @@ msgstr "" "barre d'outils principale ou en appuyant sur la touche de " "rafraîchissement standard (par exemple \"F5\")." -#: ../../CHANGELOG.md:571 +#: ../../CHANGELOG.md:630 msgid "Added `toggle_auto_refresh` method to DataLab proxy object:" msgstr "Ajout de la méthode `toggle_auto_refresh` à l'objet proxy de DataLab :" -#: ../../CHANGELOG.md:572 +#: ../../CHANGELOG.md:631 msgid "" "This method allows to toggle the \"Auto-refresh\" feature from a macro-" "command, a plugin or a remote control client." @@ -3272,7 +3504,7 @@ msgstr "" "\"Rafraîchissement automatique\" à partir d'une macro-commande, d'un " "plugin ou d'un client de contrôle à distance." -#: ../../CHANGELOG.md:573 +#: ../../CHANGELOG.md:632 msgid "" "A context manager `context_no_refresh` is also available to temporarily " "disable the \"Auto-refresh\" feature from a macro-command, a plugin or a " @@ -3283,11 +3515,11 @@ msgstr "" "automatique\" à partir d'une macro-commande, d'un plugin ou d'un client " "de contrôle à distance. Utilisation typique :" -#: ../../CHANGELOG.md:580 +#: ../../CHANGELOG.md:639 msgid "Improved curve readability:" msgstr "Amélioration de la lisibilité des courbes :" -#: ../../CHANGELOG.md:581 +#: ../../CHANGELOG.md:640 msgid "" "Until this release, the curve style was automatically set by cycling " "through **PlotPy** predefined styles" @@ -3295,7 +3527,7 @@ msgstr "" "Jusqu'à cette version, le style de la courbe était automatiquement défini" " en faisant défiler les styles prédéfinis de **PlotPy**" -#: ../../CHANGELOG.md:583 +#: ../../CHANGELOG.md:642 msgid "" "However, some styles are not suitable for curve readability (e.g. " "\"cyan\" and \"yellow\" colors are not readable on a white background, " @@ -3306,7 +3538,7 @@ msgstr "" "lisibles sur un fond blanc, en particulier lorsqu'elles sont combinées " "avec un style de ligne \"pointillée\")" -#: ../../CHANGELOG.md:584 +#: ../../CHANGELOG.md:643 msgid "" "This release introduces a new curve style management with colors which " "are distinguishable and accessible, even to color vision deficiency " @@ -3316,13 +3548,13 @@ msgstr "" "couleurs qui sont distinguables et accessibles, même pour les personnes " "atteintes de déficience de la vision des couleurs" -#: ../../CHANGELOG.md:585 +#: ../../CHANGELOG.md:644 msgid "Added \"Curve anti-aliasing\" feature to \"View\" menu (and toolbar):" msgstr "" "Ajout de la fonctionnalité \"Anti-aliasing de la courbe\" au menu " "\"Affichage\" (et à la barre d'outils) :" -#: ../../CHANGELOG.md:586 +#: ../../CHANGELOG.md:645 msgid "" "This feature allows to enable/disable curve anti-aliasing (default: " "enabled)" @@ -3330,7 +3562,7 @@ msgstr "" "Cette fonctionnalité permet d'activer/désactiver l'anti-aliasing de la " "courbe (par défaut : activé)" -#: ../../CHANGELOG.md:587 +#: ../../CHANGELOG.md:646 msgid "" "When enabled, the curve rendering is smoother but may lead to performance" " issues when working with large datasets (that's why it can be disabled)" @@ -3339,7 +3571,7 @@ msgstr "" "entraîner des problèmes de performances lors de l'utilisation de grands " "ensembles de données (c'est pourquoi il peut être désactivé)" -#: ../../CHANGELOG.md:588 +#: ../../CHANGELOG.md:647 msgid "" "Added `toggle_show_titles` method to DataLab proxy object. This method " "allows to toggle the \"Show graphical object titles\" feature from a " @@ -3350,7 +3582,7 @@ msgstr "" "les titres des objets graphiques\" à partir d'une macro-commande, d'un " "plugin ou d'un client de contrôle à distance." -#: ../../CHANGELOG.md:589 +#: ../../CHANGELOG.md:648 msgid "" "Remote client is now checking the server version and shows a warning " "message if the server version may not be fully compatible with the client" @@ -3360,11 +3592,11 @@ msgstr "" "message d'avertissement si la version du serveur n'est peut-être pas " "entièrement compatible avec la version du client." -#: ../../CHANGELOG.md:593 +#: ../../CHANGELOG.md:652 msgid "Image contour detection feature (\"Analysis\" menu):" msgstr "Fonctionnalité de détection des contours de l'image (menu Analyseer\") :" -#: ../../CHANGELOG.md:594 +#: ../../CHANGELOG.md:653 msgid "" "The contour detection feature was not taking into account the \"shape\" " "parameter (circle, ellipse, polygon) when computing the contours. The " @@ -3376,7 +3608,7 @@ msgstr "" "contours. Le paramètre était stocké mais vraiment utilisé uniquement lors" " de l'appel de la fonctionnalité une deuxième fois." -#: ../../CHANGELOG.md:595 +#: ../../CHANGELOG.md:654 msgid "" "This unintentional behavior led to an `AssertionError` when choosing " "\"polygon\" as the contour shape and trying to compute the contours for " @@ -3386,7 +3618,7 @@ msgstr "" "choix de \"polygone\" comme forme de contour et de la tentative de calcul" " des contours pour la première fois." -#: ../../CHANGELOG.md:596 +#: ../../CHANGELOG.md:655 msgid "" "This is now fixed (see [Issue #9](https://github.com/DataLab-" "Platform/DataLab/issues/9) - Image contour detection: `AssertionError` " @@ -3396,11 +3628,11 @@ msgstr "" "Platform/DataLab/issues/9) - Détection des contours de l'image : " "`AssertionError` lors du choix de \"polygone\" comme forme de contour)" -#: ../../CHANGELOG.md:597 +#: ../../CHANGELOG.md:656 msgid "Keyboard shortcuts:" msgstr "Raccourcis clavier :" -#: ../../CHANGELOG.md:598 +#: ../../CHANGELOG.md:657 msgid "" "The keyboard shortcuts for \"New\", \"Open\", \"Save\", \"Duplicate\", " "\"Remove\", \"Delete all\" and \"Refresh manually\" actions were not " @@ -3410,7 +3642,7 @@ msgstr "" "\"Enregistrer\", \"Dupliquer\", \"Supprimer\", \"Tout supprimer\" et " "\"Rafraîchir manuellement\" ne fonctionnaient pas correctement." -#: ../../CHANGELOG.md:599 +#: ../../CHANGELOG.md:658 msgid "" "Those shortcuts were specific to each signal/image panel, and were " "working only when the panel on which the shortcut was pressed for the " @@ -3425,7 +3657,7 @@ msgstr "" "d'avertissement était affiché dans la console, par exemple " "`QAction::event: Ambiguous shortcut overload: Ctrl+C`)" -#: ../../CHANGELOG.md:600 +#: ../../CHANGELOG.md:659 msgid "" "Besides, the shortcuts were not working at startup (when no panel had " "focus)." @@ -3433,7 +3665,7 @@ msgstr "" "De plus, les raccourcis ne fonctionnaient pas au démarrage (lorsqu'aucun " "panneau n'avait le focus)." -#: ../../CHANGELOG.md:601 +#: ../../CHANGELOG.md:660 msgid "" "This is now fixed: the shortcuts are now working whatever the active " "panel is, and even at startup (see [Issue #10](https://github.com" @@ -3446,7 +3678,7 @@ msgstr "" "clavier ne fonctionnant pas correctement : `QAction::event: Ambiguous " "shortcut overload: Ctrl+C`)" -#: ../../CHANGELOG.md:602 +#: ../../CHANGELOG.md:661 msgid "" "\"Show graphical object titles\" and \"Auto-refresh\" actions were not " "working properly:" @@ -3454,7 +3686,7 @@ msgstr "" "Les actions \"Afficher les titres des objets graphiques\" et " "\"Rafraîchissement automatique\" ne fonctionnaient pas correctement :" -#: ../../CHANGELOG.md:603 +#: ../../CHANGELOG.md:662 msgid "" "The \"Show graphical object titles\" and \"Auto-refresh\" actions were " "only working on the active signal/image panel, and not on all panels." @@ -3463,7 +3695,7 @@ msgstr "" "\"Rafraîchissement automatique\" ne fonctionnaient que sur le panneau de " "signal/image actif, et non sur tous les panneaux." -#: ../../CHANGELOG.md:604 +#: ../../CHANGELOG.md:663 msgid "" "This is now fixed (see [Issue #11](https://github.com/DataLab-" "Platform/DataLab/issues/11) - \"Show graphical object titles\" and " @@ -3474,7 +3706,7 @@ msgstr "" "objets graphiques\" et \"Rafraîchissement automatique\" ne fonctionnaient" " que sur le panneau de signal/image actuel)" -#: ../../CHANGELOG.md:605 +#: ../../CHANGELOG.md:664 msgid "" "Fixed [Issue #14](https://github.com/DataLab-Platform/DataLab/issues/14) " "- Saving/Reopening HDF5 project without cleaning-up leads to `ValueError`" @@ -3483,7 +3715,7 @@ msgstr "" "Platform/DataLab/issues/14) - Enregistrer/Réouvrir le projet HDF5 sans " "nettoyage entraîne une `ValueError`" -#: ../../CHANGELOG.md:606 +#: ../../CHANGELOG.md:665 msgid "" "Fixed [Issue #15](https://github.com/DataLab-Platform/DataLab/issues/15) " "- MacOS: 1. `pip install cdl` error - 2. Missing menus:" @@ -3492,7 +3724,7 @@ msgstr "" "Platform/DataLab/issues/15) - MacOS : 1. Erreur `pip install cdl` - 2. " "Menus manquants :" -#: ../../CHANGELOG.md:607 +#: ../../CHANGELOG.md:666 msgid "" "Part 1: `pip install cdl` error on MacOS was actually an issue from " "**PlotPy** (see [this " @@ -3503,7 +3735,7 @@ msgstr "" "Partie 1 : l'erreur `pip install cdl` sur MacOS était en fait un problème" " de **PlotPy** (voir [ce problème]" -#: ../../CHANGELOG.md:608 +#: ../../CHANGELOG.md:667 msgid "" "Part 2: Missing menus on MacOS was due to a PyQt/MacOS bug regarding " "dynamic menus" @@ -3511,7 +3743,7 @@ msgstr "" "Partie 2 : les menus manquants sur MacOS étaient dus à un bogue " "PyQt/MacOS concernant les menus dynamiques" -#: ../../CHANGELOG.md:609 +#: ../../CHANGELOG.md:668 msgid "" "HDF5 file format: when importing an HDF5 dataset as a signal or an image," " the dataset attributes were systematically copied to signal/image " @@ -3527,7 +3759,7 @@ msgstr "" "éviter les erreurs lors de la sérialisation/désérialisation de l'objet " "signal/image" -#: ../../CHANGELOG.md:610 +#: ../../CHANGELOG.md:669 msgid "" "Installation/configuration viewer: improved readability (removed syntax " "highlighting)" @@ -3535,7 +3767,7 @@ msgstr "" "Visualiseur d'installation/configuration : amélioration de la lisibilité " "(suppression de la coloration syntaxique)" -#: ../../CHANGELOG.md:611 +#: ../../CHANGELOG.md:670 msgid "" "PyInstaller specification file: added missing `skimage` data files " "manually in order to continue supporting Python 3.8 (see [Issue " @@ -3548,7 +3780,7 @@ msgstr "" "Platform/DataLab/issues/12) - Version autonome sur Windows 7 : `api-ms-" "win-core-path-l1-1-0.dll` manquant)" -#: ../../CHANGELOG.md:612 +#: ../../CHANGELOG.md:671 msgid "" "Fixed [Issue #13](https://github.com/DataLab-Platform/DataLab/issues/13) " "- ArchLinux: `qt.qpa.plugin: Could not load the Qt platform plugin " @@ -3558,34 +3790,34 @@ msgstr "" "Platform/DataLab/issues/13) - ArchLinux : `qt.qpa.plugin: Could not load " "the Qt platform plugin \"xcb\" in \"\" even though it was found`" -#: ../../CHANGELOG.md:614 +#: ../../CHANGELOG.md:673 msgid "DataLab Version 0.9.2" msgstr "DataLab Version 0.9.2" -#: ../../CHANGELOG.md:618 +#: ../../CHANGELOG.md:677 msgid "Region of interest (ROI) extraction feature for images:" msgstr "Fonctionnalité d'extraction de la région d'intérêt (ROI) pour les images :" -#: ../../CHANGELOG.md:619 +#: ../../CHANGELOG.md:678 msgid "" -"ROI extraction was not working properly when the \"Extract all regions of" -" interest into a single image object\" option was enabled if there was " -"only one defined ROI. The result was an image positioned at the origin " -"(0, 0) instead of the expected position (x0, y0) and the ROI rectangle " -"itself was not removed as expected. This is now fixed (see [Issue " -"#6](https://github.com/DataLab-Platform/DataLab/issues/6) - 'Extract " -"multiple ROI' feature: unexpected result for a single ROI)" +"ROI extraction was not working properly when the \"Extract all ROIs into " +"a single image object\" option was enabled if there was only one defined " +"ROI. The result was an image positioned at the origin (0, 0) instead of " +"the expected position (x0, y0) and the ROI rectangle itself was not " +"removed as expected. This is now fixed (see [Issue #6](https://github.com" +"/DataLab-Platform/DataLab/issues/6) - 'Extract multiple ROI' feature: " +"unexpected result for a single ROI)" msgstr "" "L'extraction de la ROI ne fonctionnait pas correctement lorsque l'option " -"\"Extraire toutes les régions d'intérêt dans un seul objet image\" était " -"activée s'il n'y avait qu'une seule ROI définie. Le résultat était une " -"image positionnée à l'origine (0, 0) au lieu de la position attendue (x0," -" y0) et le rectangle de la ROI lui-même n'était pas supprimé comme prévu." -" Ceci est maintenant corrigé (voir [Issue #6](https://github.com/DataLab-" +"\"Extraire toutes les ROI dans un seul objet image\" était activée s'il " +"n'y avait qu'une seule ROI définie. Le résultat était une image " +"positionnée à l'origine (0, 0) au lieu de la position attendue (x0, y0) " +"et le rectangle de la ROI lui-même n'était pas supprimé comme prévu. Ceci" +" est maintenant corrigé (voir [Issue #6](https://github.com/DataLab-" "Platform/DataLab/issues/6) - Fonctionnalité 'Extraire plusieurs ROI' : " "résultat inattendu pour une seule ROI)" -#: ../../CHANGELOG.md:620 +#: ../../CHANGELOG.md:679 msgid "" "ROI rectangles with negative coordinates were not properly handled: ROI " "extraction was raising a `ValueError` exception, and the image mask was " @@ -3602,7 +3834,7 @@ msgstr "" "`ValueError: zero-size array to reduction operation minimum which has no " "identity`)" -#: ../../CHANGELOG.md:621 +#: ../../CHANGELOG.md:680 msgid "" "ROI extraction was not taking into account the pixel size (dx, dy) and " "the origin (x0, y0) of the image. This is now fixed (see [Issue " @@ -3614,11 +3846,11 @@ msgstr "" "[Issue #8](https://github.com/DataLab-Platform/DataLab/issues/8) - " "Extraction de la ROI de l'image : prendre en compte la taille des pixels)" -#: ../../CHANGELOG.md:622 +#: ../../CHANGELOG.md:681 msgid "Macro-command console is now read-only:" msgstr "La console de macro-commande est désormais en lecture seule :" -#: ../../CHANGELOG.md:623 +#: ../../CHANGELOG.md:682 msgid "" "The macro-command panel Python console is currently not supporting " "standard input stream (`stdin`) and this is intended (at least for now)" @@ -3627,21 +3859,21 @@ msgstr "" "en charge le flux d'entrée standard (`stdin`) et c'est voulu (du moins " "pour l'instant)" -#: ../../CHANGELOG.md:624 +#: ../../CHANGELOG.md:683 msgid "Set Python console read-only to avoid confusion" msgstr "Définir la console Python en lecture seule pour éviter toute confusion" -#: ../../CHANGELOG.md:626 +#: ../../CHANGELOG.md:685 msgid "DataLab Version 0.9.1" msgstr "DataLab Version 0.9.1" -#: ../../CHANGELOG.md:630 +#: ../../CHANGELOG.md:689 msgid "French translation is not available on Windows/Stand alone version:" msgstr "" "La traduction française n'est pas disponible sur Windows/Version autonome" " :" -#: ../../CHANGELOG.md:631 +#: ../../CHANGELOG.md:690 msgid "" "Locale was not properly detected on Windows for stand-alone version " "(frozen with `pyinstaller`) due to an issue with `locale.getlocale()` " @@ -3653,7 +3885,7 @@ msgstr "" "`locale.getlocale()` (fonction renvoyant `None` au lieu de la locale " "attendue sur les applications gelées)" -#: ../../CHANGELOG.md:632 +#: ../../CHANGELOG.md:691 msgid "" "This is ultimately a `pyinstaller` issue, but a workaround has been " "implemented in `guidata` V3.2.2 (see [guidata issue " @@ -3665,7 +3897,7 @@ msgstr "" "guidata #68](https://github.com/PlotPyStack/guidata/issues/68) - Windows " ": la traduction gettext ne fonctionne pas sur les applications gelées)" -#: ../../CHANGELOG.md:633 +#: ../../CHANGELOG.md:692 msgid "" "[Issue #2](https://github.com/DataLab-Platform/DataLab/issues/2) - French" " translation is not available on Windows Stand alone version" @@ -3674,19 +3906,19 @@ msgstr "" "traduction française n'est pas disponible sur la version autonome de " "Windows" -#: ../../CHANGELOG.md:634 +#: ../../CHANGELOG.md:693 msgid "Saving image to JPEG2000 fails for non integer data:" msgstr "" "L'enregistrement d'une image au format JPEG2000 échoue pour les données " "non entières :" -#: ../../CHANGELOG.md:635 +#: ../../CHANGELOG.md:694 msgid "JPEG2000 encoder does not support non integer data or signed integer data" msgstr "" "L'encodeur JPEG2000 ne prend pas en charge les données non entières ou " "les données entières signées" -#: ../../CHANGELOG.md:636 +#: ../../CHANGELOG.md:695 msgid "" "Before, DataLab was showing an error message when trying to save " "incompatible data to JPEG2000: this was not a consistent behavior with " @@ -3701,7 +3933,7 @@ msgstr "" "automatiquement les données au format approprié (entier non signé sur 8 " "bits)" -#: ../../CHANGELOG.md:637 +#: ../../CHANGELOG.md:696 msgid "" "Current behavior is now consistent with other standard image formats: " "when saving to JPEG2000, DataLab automatically converts data to 8-bit " @@ -3713,7 +3945,7 @@ msgstr "" "convertit automatiquement les données en entier non signé sur 8 bits ou " "en entier non signé sur 16 bits (en fonction du type de données original)" -#: ../../CHANGELOG.md:638 +#: ../../CHANGELOG.md:697 msgid "" "[Issue #3](https://github.com/DataLab-Platform/DataLab/issues/3) - Save " "image to JPEG2000: 'OSError: encoder error -2 when writing image file'" @@ -3722,7 +3954,7 @@ msgstr "" "Sauvegarde d'image au format JPEG2000 : 'OSError: erreur d'encodeur -2 " "lors de l'écriture du fichier image'" -#: ../../CHANGELOG.md:639 +#: ../../CHANGELOG.md:698 msgid "" "Windows stand-alone version shortcuts not showing in current user start " "menu:" @@ -3730,7 +3962,7 @@ msgstr "" "Les raccourcis de la version autonome de Windows ne s'affichent pas dans " "le menu de démarrage de l'utilisateur actuel :" -#: ../../CHANGELOG.md:640 +#: ../../CHANGELOG.md:699 msgid "" "When installing DataLab on Windows from a non-administrator account, the " "shortcuts were not showing in the current user start menu but in the " @@ -3745,7 +3977,7 @@ msgstr "" "fait que l'installeur ne prend pas en charge l'installation de raccourcis" " pour tous les utilisateurs)" -#: ../../CHANGELOG.md:641 +#: ../../CHANGELOG.md:700 msgid "" "Now, the installer *does not* ask for elevated privileges anymore, and " "shortcuts are installed in the current user start menu (this also means " @@ -3757,7 +3989,7 @@ msgstr "" "actuel (cela signifie également que l'utilisateur actuel doit disposer " "d'un accès en écriture au répertoire d'installation)" -#: ../../CHANGELOG.md:642 +#: ../../CHANGELOG.md:701 msgid "" "In future releases, the installer will support installing shortcuts for " "all users if there is a demand for it (see [Issue #5](https://github.com" @@ -3767,7 +3999,7 @@ msgstr "" "de raccourcis pour tous les utilisateurs s'il y a une demande à cet effet" " (voir [Issue #5](https://github.com/DataLab-Platform/DataLab/issues/5))" -#: ../../CHANGELOG.md:643 +#: ../../CHANGELOG.md:702 msgid "" "[Issue #4](https://github.com/DataLab-Platform/DataLab/issues/4) - " "Windows: stand-alone version shortcuts not showing in current user start " @@ -3777,15 +4009,15 @@ msgstr "" "Windows : les raccourcis de la version autonome ne s'affichent pas dans " "le menu de démarrage de l'utilisateur actuel" -#: ../../CHANGELOG.md:644 +#: ../../CHANGELOG.md:703 msgid "Installation and configuration window for stand-alone version:" msgstr "Fenêtre d'installation et de configuration pour la version autonome :" -#: ../../CHANGELOG.md:645 +#: ../../CHANGELOG.md:704 msgid "Do not show ambiguous error message 'Invalid dependencies' anymore" msgstr "Ne plus afficher le message d'erreur ambigu 'Dépendances invalides'" -#: ../../CHANGELOG.md:646 +#: ../../CHANGELOG.md:705 msgid "" "Dependencies are supposed to be checked when building the stand-alone " "version" @@ -3793,15 +4025,15 @@ msgstr "" "Les dépendances sont censées être vérifiées lors de la construction de la" " version autonome" -#: ../../CHANGELOG.md:647 +#: ../../CHANGELOG.md:706 msgid "Added PDF documentation to stand-alone version:" msgstr "Ajout de la documentation PDF à la version autonome :" -#: ../../CHANGELOG.md:648 +#: ../../CHANGELOG.md:707 msgid "The PDF documentation was missing in previous release" msgstr "La documentation PDF était manquante dans la version précédente" -#: ../../CHANGELOG.md:649 +#: ../../CHANGELOG.md:708 msgid "" "Now, the PDF documentation (in English and French) is included in the " "stand-alone version" @@ -3809,33 +4041,33 @@ msgstr "" "Désormais, la documentation PDF (en anglais et en français) est incluse " "dans la version autonome" -#: ../../CHANGELOG.md:651 +#: ../../CHANGELOG.md:710 msgid "DataLab Version 0.9.0" msgstr "DataLab Version 0.9.0" -#: ../../CHANGELOG.md:653 +#: ../../CHANGELOG.md:712 msgid "New dependencies:" msgstr "Nouvelles dépendances :" -#: ../../CHANGELOG.md:655 +#: ../../CHANGELOG.md:714 msgid "DataLab is now powered by [PlotPyStack](https://github.com/PlotPyStack):" msgstr "" "DataLab est désormais alimenté par " "[PlotPyStack](https://github.com/PlotPyStack) :" -#: ../../CHANGELOG.md:656 +#: ../../CHANGELOG.md:715 msgid "[PythonQwt](https://github.com/PlotPyStack/PythonQwt)" msgstr "[PythonQwt](https://github.com/PlotPyStack/PythonQwt)" -#: ../../CHANGELOG.md:657 +#: ../../CHANGELOG.md:716 msgid "[guidata](https://github.com/PlotPyStack/guidata)" msgstr "[guidata](https://github.com/PlotPyStack/guidata)" -#: ../../CHANGELOG.md:658 +#: ../../CHANGELOG.md:717 msgid "[PlotPy](https://github.com/PlotPyStack/PlotPy)" msgstr "[PlotPy](https://github.com/PlotPyStack/PlotPy)" -#: ../../CHANGELOG.md:659 +#: ../../CHANGELOG.md:718 msgid "" "[opencv-python](https://pypi.org/project/opencv-python/) (algorithms for " "image processing)" @@ -3843,15 +4075,15 @@ msgstr "" "[opencv-python](https://pypi.org/project/opencv-python/) (algorithmes de " "traitement d'image)" -#: ../../CHANGELOG.md:661 +#: ../../CHANGELOG.md:720 msgid "New reference platform:" msgstr "Nouvelle plateforme de référence :" -#: ../../CHANGELOG.md:663 +#: ../../CHANGELOG.md:722 msgid "DataLab is validated on Windows 11 with Python 3.11 and PyQt 5.15" msgstr "DataLab est validé sur Windows 11 avec Python 3.11 et PyQt 5.15" -#: ../../CHANGELOG.md:664 +#: ../../CHANGELOG.md:723 msgid "" "DataLab is also compatible with other OS (Linux, MacOS) and other Python-" "Qt bindings and versions (Python 3.8-3.12, PyQt6, PySide6)" @@ -3860,25 +4092,25 @@ msgstr "" "(Linux, MacOS) et d'autres liaisons Python-Qt et versions (Python " "3.8-3.12, PyQt6, PySide6)" -#: ../../CHANGELOG.md:666 +#: ../../CHANGELOG.md:725 msgid "New features:" msgstr "Nouvelles fonctionnalités :" -#: ../../CHANGELOG.md:668 +#: ../../CHANGELOG.md:727 msgid "DataLab is a platform:" msgstr "DataLab est une plateforme :" -#: ../../CHANGELOG.md:669 +#: ../../CHANGELOG.md:728 msgid "Added support for plugins" msgstr "Ajout de la prise en charge des plugins" -#: ../../CHANGELOG.md:670 +#: ../../CHANGELOG.md:729 msgid "Custom processing features available in the \"Plugins\" menu" msgstr "" "Fonctionnalités de traitement personnalisées disponibles dans le menu " "\"Plugins\"" -#: ../../CHANGELOG.md:671 +#: ../../CHANGELOG.md:730 msgid "" "Custom I/O features: new file formats can be added to the standard I/O " "features for signals and images" @@ -3887,7 +4119,7 @@ msgstr "" "peuvent être ajoutés aux fonctionnalités d'E/S standard pour les signaux " "et les images" -#: ../../CHANGELOG.md:672 +#: ../../CHANGELOG.md:731 msgid "" "Custom HDF5 features: new HDF5 file formats can be added to the standard " "HDF5 import feature" @@ -3895,11 +4127,11 @@ msgstr "" "Fonctionnalités HDF5 personnalisées : de nouveaux formats de fichier HDF5" " peuvent être ajoutés à la fonctionnalité d'importation HDF5 standard" -#: ../../CHANGELOG.md:673 +#: ../../CHANGELOG.md:732 msgid "More features to come..." msgstr "D'autres fonctionnalités à venir..." -#: ../../CHANGELOG.md:674 +#: ../../CHANGELOG.md:733 msgid "" "Added remote control feature: DataLab can be controlled remotely via a " "TCP/IP connection (see [Remote control](https://datalab-" @@ -3909,7 +4141,7 @@ msgstr "" "contrôlé à distance via une connexion TCP/IP (voir [Contrôle à " "distance](https://datalab-platform.com/fr/remote_control.html))" -#: ../../CHANGELOG.md:675 +#: ../../CHANGELOG.md:734 msgid "" "Added macro commands: DataLab can be controlled via a macro file (see " "[Macro commands](https://datalab-platform.com/en/macro_commands.html))" @@ -3918,33 +4150,33 @@ msgstr "" "macro (voir [Commandes de macro](https://datalab-" "platform.com/fr/macro_commands.html))" -#: ../../CHANGELOG.md:676 +#: ../../CHANGELOG.md:735 msgid "General features:" msgstr "Fonctionnalités générales :" -#: ../../CHANGELOG.md:677 +#: ../../CHANGELOG.md:736 msgid "Added settings dialog box (see \"Settings\" entry in \"File\" menu):" msgstr "" "Ajout de la boîte de dialogue des paramètres (voir l'entrée " "\"Paramètres\" dans le menu \"Fichier\") :" -#: ../../CHANGELOG.md:678 +#: ../../CHANGELOG.md:737 msgid "General settings" msgstr "Paramètres généraux" -#: ../../CHANGELOG.md:679 +#: ../../CHANGELOG.md:738 msgid "Visualization settings" msgstr "Paramètres de visualisation" -#: ../../CHANGELOG.md:680 +#: ../../CHANGELOG.md:739 msgid "Processing settings" msgstr "Paramètres de traitement" -#: ../../CHANGELOG.md:681 +#: ../../CHANGELOG.md:740 msgid "Etc." msgstr "Etc." -#: ../../CHANGELOG.md:682 +#: ../../CHANGELOG.md:741 msgid "" "New default layout: signal/image panels are on the right side of the main" " window, visualization panels are on the left side with a vertical " @@ -3954,11 +4186,11 @@ msgstr "" " le côté droit de la fenêtre principale, les panneaux de visualisation " "sont sur le côté gauche avec une barre d'outils verticale" -#: ../../CHANGELOG.md:683 +#: ../../CHANGELOG.md:742 msgid "Signal/Image features:" msgstr "Fonctionnalités de signal/image :" -#: ../../CHANGELOG.md:684 +#: ../../CHANGELOG.md:743 msgid "" "Added process isolation: each signal/image is processed in a separate " "process, so that DataLab does not freeze anymore when processing large " @@ -3968,7 +4200,7 @@ msgstr "" "un processus séparé, de sorte que DataLab ne se fige plus lors du " "traitement de signaux/images volumineux" -#: ../../CHANGELOG.md:685 +#: ../../CHANGELOG.md:744 msgid "" "Added support for groups: signals and images can be grouped together, and" " operations can be applied to all objects in a group, or between groups" @@ -3977,7 +4209,7 @@ msgstr "" "peuvent être regroupés, et des opérations peuvent être appliquées à tous " "les objets d'un groupe ou entre les groupes" -#: ../../CHANGELOG.md:686 +#: ../../CHANGELOG.md:745 msgid "" "Added warning and error dialogs with detailed traceback links to the " "source code (warnings may be optionally ignored)" @@ -3986,21 +4218,21 @@ msgstr "" " poursuite détaillés vers le code source (les avertissements peuvent être" " ignorés en option)" -#: ../../CHANGELOG.md:687 +#: ../../CHANGELOG.md:746 msgid "Drastically improved performance when selecting objects" msgstr "Amélioration significative des performances lors de la sélection d'objets" -#: ../../CHANGELOG.md:688 +#: ../../CHANGELOG.md:747 msgid "Optimized performance when showing large images" msgstr "Optimisation des performances lors de l'affichage d'images volumineuses" -#: ../../CHANGELOG.md:689 +#: ../../CHANGELOG.md:748 msgid "Added support for dropping files on signal/image panel" msgstr "" "Ajout de la prise en charge du dépôt de fichiers sur le panneau de " "signal/image" -#: ../../CHANGELOG.md:690 +#: ../../CHANGELOG.md:749 msgid "" "Added \"Analysis parameters\" group box to show last result input " "parameters" @@ -4008,13 +4240,13 @@ msgstr "" "Ajout de la boîte de groupe \"Paramètres de calcul\" pour afficher les " "paramètres d'entrée du dernier résultat" -#: ../../CHANGELOG.md:691 +#: ../../CHANGELOG.md:750 msgid "Added \"Copy titles to clipboard\" feature in \"Edit\" menu" msgstr "" "Ajout de la fonctionnalité \"Copier les titres dans le presse-papiers\" " "dans le menu \"Édition\"" -#: ../../CHANGELOG.md:692 +#: ../../CHANGELOG.md:751 msgid "" "For every single processing feature (operation, processing and analysis " "menus), the entered parameters (dialog boxes) are stored in cache to be " @@ -4025,21 +4257,21 @@ msgstr "" "stockés en cache pour être utilisés par défaut la prochaine fois que la " "fonctionnalité est utilisée" -#: ../../CHANGELOG.md:693 +#: ../../CHANGELOG.md:752 msgid "Signal processing:" msgstr "Traitement de signal :" -#: ../../CHANGELOG.md:694 ../../CHANGELOG.md:736 +#: ../../CHANGELOG.md:753 ../../CHANGELOG.md:795 msgid "Added support for optional FFT shift (see Settings dialog box)" msgstr "" "Ajout de la prise en charge du décalage FFT facultatif (voir la boîte de " "dialogue des paramètres)" -#: ../../CHANGELOG.md:695 +#: ../../CHANGELOG.md:754 msgid "Image processing:" msgstr "Traitement d'image :" -#: ../../CHANGELOG.md:696 +#: ../../CHANGELOG.md:755 msgid "" "Added pixel binning operation (X/Y binning factors, operation: sum, mean," " ...)" @@ -4047,7 +4279,7 @@ msgstr "" "Ajout de l'opération de binning de pixels (facteurs de binning X/Y, " "opération : somme, moyenne, ...)" -#: ../../CHANGELOG.md:697 +#: ../../CHANGELOG.md:756 msgid "" "Added \"Distribute on a grid\" and \"Reset image positions\" in operation" " menu" @@ -4055,107 +4287,107 @@ msgstr "" "Ajout des options \"Distribuer sur une grille\" et \"Réinitialiser les " "positions des images\" dans le menu des opérations" -#: ../../CHANGELOG.md:698 +#: ../../CHANGELOG.md:757 msgid "Added Butterworth filter" msgstr "Ajout du filtre de Butterworth" -#: ../../CHANGELOG.md:699 +#: ../../CHANGELOG.md:758 msgid "Added exposure processing features:" msgstr "Ajout des fonctionnalités de traitement de l'exposition :" -#: ../../CHANGELOG.md:700 +#: ../../CHANGELOG.md:759 msgid "Gamma correction" msgstr "Correction gamma" -#: ../../CHANGELOG.md:701 +#: ../../CHANGELOG.md:760 msgid "Logarithmic correction" msgstr "Correction logarithmique" -#: ../../CHANGELOG.md:702 +#: ../../CHANGELOG.md:761 msgid "Sigmoïd correction" msgstr "Correction sigmoïde" -#: ../../CHANGELOG.md:703 +#: ../../CHANGELOG.md:762 msgid "Added restoration processing features:" msgstr "Ajout des fonctionnalités de traitement de la restauration :" -#: ../../CHANGELOG.md:704 +#: ../../CHANGELOG.md:763 msgid "Total variation denoising filter (TV Chambolle)" msgstr "Filtre de débruitage par variation totale (TV Chambolle)" -#: ../../CHANGELOG.md:705 +#: ../../CHANGELOG.md:764 msgid "Bilateral filter (denoising)" msgstr "Filtre bilatéral (débruitage)" -#: ../../CHANGELOG.md:706 +#: ../../CHANGELOG.md:765 msgid "Wavelet denoising filter" msgstr "Filtre de débruitage par ondelettes" -#: ../../CHANGELOG.md:707 +#: ../../CHANGELOG.md:766 msgid "White Top-Hat denoising filter" msgstr "Filtre de débruitage White Top-Hat" -#: ../../CHANGELOG.md:708 +#: ../../CHANGELOG.md:767 msgid "Added morphological transforms (disk footprint):" msgstr "Ajout des transformations morphologiques (empreinte de disque) :" -#: ../../CHANGELOG.md:709 +#: ../../CHANGELOG.md:768 msgid "White Top-Hat" msgstr "White Top-Hat" -#: ../../CHANGELOG.md:710 +#: ../../CHANGELOG.md:769 msgid "Black Top-Hat" msgstr "Black Top-Hat" -#: ../../CHANGELOG.md:711 +#: ../../CHANGELOG.md:770 msgid "Erosion" msgstr "Érosion" -#: ../../CHANGELOG.md:712 +#: ../../CHANGELOG.md:771 msgid "Dilation" msgstr "Dilatation" -#: ../../CHANGELOG.md:713 +#: ../../CHANGELOG.md:772 msgid "Opening" msgstr "Ouverture" -#: ../../CHANGELOG.md:714 +#: ../../CHANGELOG.md:773 msgid "Closing" msgstr "Fermeture" -#: ../../CHANGELOG.md:715 +#: ../../CHANGELOG.md:774 msgid "Added edge detection features:" msgstr "Ajout des fonctionnalités de détection des contours :" -#: ../../CHANGELOG.md:716 +#: ../../CHANGELOG.md:775 msgid "Roberts filter" msgstr "Filtre de Roberts" -#: ../../CHANGELOG.md:717 +#: ../../CHANGELOG.md:776 msgid "Prewitt filter (vertical, horizontal, both)" msgstr "Filtre de Prewitt (vertical, horizontal, les deux)" -#: ../../CHANGELOG.md:718 +#: ../../CHANGELOG.md:777 msgid "Sobel filter (vertical, horizontal, both)" msgstr "Filtre de Sobel (vertical, horizontal, les deux)" -#: ../../CHANGELOG.md:719 +#: ../../CHANGELOG.md:778 msgid "Scharr filter (vertical, horizontal, both)" msgstr "Filtre de Scharr (vertical, horizontal, les deux)" -#: ../../CHANGELOG.md:720 +#: ../../CHANGELOG.md:779 msgid "Farid filter (vertical, horizontal, both)" msgstr "Filtre de Farid (vertical, horizontal, les deux)" -#: ../../CHANGELOG.md:721 +#: ../../CHANGELOG.md:780 msgid "Laplace filter" msgstr "Filtre de Laplace" -#: ../../CHANGELOG.md:722 +#: ../../CHANGELOG.md:781 msgid "Canny filter" msgstr "Filtre de Canny" -#: ../../CHANGELOG.md:723 +#: ../../CHANGELOG.md:782 msgid "" "Contour detection: added support for polygonal contours (in addition to " "circle and ellipse contours)" @@ -4163,45 +4395,45 @@ msgstr "" "Détection des contours : ajout de la prise en charge des contours " "polygonaux (en plus des contours de cercle et d'ellipse)" -#: ../../CHANGELOG.md:724 +#: ../../CHANGELOG.md:783 msgid "Added circle Hough transform (circle detection)" msgstr "" "Ajout de la transformation de Hough pour les cercles (détection de " "cercles)" -#: ../../CHANGELOG.md:725 +#: ../../CHANGELOG.md:784 msgid "Added image intensity levels rescaling" msgstr "Ajout du rééchelonnement des niveaux d'intensité de l'image" -#: ../../CHANGELOG.md:726 +#: ../../CHANGELOG.md:785 msgid "Added histogram equalization" msgstr "Ajout de l'égalisation d'histogramme" -#: ../../CHANGELOG.md:727 +#: ../../CHANGELOG.md:786 msgid "Added adaptative histogram equalization" msgstr "Ajout de l'égalisation d'histogramme adaptative" -#: ../../CHANGELOG.md:728 +#: ../../CHANGELOG.md:787 msgid "Added blob detection methods:" msgstr "Ajout des méthodes de détection de blobs :" -#: ../../CHANGELOG.md:729 +#: ../../CHANGELOG.md:788 msgid "Difference of Gaussian" msgstr "Différence de Gaussienne" -#: ../../CHANGELOG.md:730 +#: ../../CHANGELOG.md:789 msgid "Determinant of Hessian method" msgstr "Méthode du déterminant de Hessian" -#: ../../CHANGELOG.md:731 +#: ../../CHANGELOG.md:790 msgid "Laplacian of Gaussian" msgstr "Laplacien de Gaussienne" -#: ../../CHANGELOG.md:732 +#: ../../CHANGELOG.md:791 msgid "Blob detection using OpenCV" msgstr "Détection de blobs à l'aide d'OpenCV" -#: ../../CHANGELOG.md:733 +#: ../../CHANGELOG.md:792 msgid "" "Result shapes and annotations are now transformed (instead of removed) " "when executing one of the following operations:" @@ -4210,15 +4442,15 @@ msgstr "" "(au lieu d'être supprimées) lors de l'exécution de l'une des opérations " "suivantes :" -#: ../../CHANGELOG.md:734 +#: ../../CHANGELOG.md:793 msgid "Rotation (arbitrary angle, +90°, -90°)" msgstr "Rotation (angle arbitraire, +90°, -90°)" -#: ../../CHANGELOG.md:735 +#: ../../CHANGELOG.md:794 msgid "Symetry (vertical/horizontal)" msgstr "Symétrie (verticale/horizontale)" -#: ../../CHANGELOG.md:737 +#: ../../CHANGELOG.md:796 msgid "" "Console: added configurable external editor (default: VSCode) to follow " "the traceback links to the source code" @@ -4268,3 +4500,12 @@ msgstr "" #~ msgid "DataLab Version 0.16.5" #~ msgstr "DataLab Version 0.16.5" + +#~ msgid "" +#~ "ℹ️ PlotPy v2.7 is required for " +#~ "this release. ℹ️ Dropped support for " +#~ "Python 3.8. ℹ️ Python 3.13 is not" +#~ " supported yet, due to the fact " +#~ "that some dependencies are not " +#~ "compatible with this version." +#~ msgstr "" diff --git a/doc/locale/fr/LC_MESSAGES/contributing/environment.po b/doc/locale/fr/LC_MESSAGES/contributing/environment.po index 4f647084..ebb4bca5 100644 --- a/doc/locale/fr/LC_MESSAGES/contributing/environment.po +++ b/doc/locale/fr/LC_MESSAGES/contributing/environment.po @@ -253,7 +253,7 @@ msgid "WinPython version" msgstr "Version de WinPython" #: ../../doc/contributing/environment.md -msgid "3.8" +msgid "3.9" msgstr "" #: ../../doc/contributing/environment.md diff --git a/doc/locale/fr/LC_MESSAGES/contributing/index.po b/doc/locale/fr/LC_MESSAGES/contributing/index.po index ae37bc96..31042938 100644 --- a/doc/locale/fr/LC_MESSAGES/contributing/index.po +++ b/doc/locale/fr/LC_MESSAGES/contributing/index.po @@ -8,32 +8,36 @@ msgid "" msgstr "" "Project-Id-Version: DataLab \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-16 14:01+0100\n" +"POT-Creation-Date: 2024-11-13 18:41+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.13.0\n" +"Generated-By: Babel 2.14.0\n" -#: ../../contributing/index.rst:-1 +#: ../../doc/contributing/index.rst:-1 msgid "" "Contribute to DataLab project, the open-source scientific data analysis " "platform" -msgstr "Contribuer au projet DataLab, la plateforme open-source d'analyse de données scientifiques" +msgstr "" +"Contribuer au projet DataLab, la plateforme open-source d'analyse de " +"données scientifiques" -#: ../../contributing/index.rst:-1 +#: ../../doc/contributing/index.rst:-1 msgid "" "DataLab, contribute, open-source, scientific, data, analysis, platform, " "signal processing, image processing" -msgstr "DataLab, contribuer, open-source, scientifique, données, analyse, plateforme, traitement du signal, traitement d'images" +msgstr "" +"DataLab, contribuer, open-source, scientifique, données, analyse, " +"plateforme, traitement du signal, traitement d'images" -#: ../../contributing/index.rst:2 +#: ../../doc/contributing/index.rst:2 msgid "Contributing" msgstr "Contribuer" -#: ../../contributing/index.rst:8 +#: ../../doc/contributing/index.rst:8 msgid "" "DataLab is **your** platform. If you want it to be improved, you **can** " "contribute to the project, whether you are a developer or not." @@ -41,7 +45,7 @@ msgstr "" "DataLab est **votre** plateforme. Si vous souhaitez qu'elle s'améliore, " "vous **pouvez** contribuer au projet, que vous soyez développeur ou non." -#: ../../contributing/index.rst:11 +#: ../../doc/contributing/index.rst:11 msgid "" "There are many ways to contribute to DataLab project, depending on how " "much time you have, your experience with open source projects, and your " @@ -51,15 +55,15 @@ msgstr "" "fonction du temps dont vous disposez, de votre expérience avec les " "projets open source et de vos compétences." -#: ../../contributing/index.rst:15 +#: ../../doc/contributing/index.rst:15 msgid "Share your ideas and experiences" msgstr "Partagez vos idées et vos expériences" -#: ../../contributing/index.rst:19 ../../contributing/index.rst:66 +#: ../../doc/contributing/index.rst:19 ../../doc/contributing/index.rst:66 msgid ":octicon:`info;1em;sd-text-info` :bdg-success-line:`No coding required`" msgstr ":octicon:`info;1em;sd-text-info` :bdg-success-line:`Aucun codage requis`" -#: ../../contributing/index.rst:21 +#: ../../doc/contributing/index.rst:21 msgid "" "Besides the classic bug reports and feature requests, you can share your " "ideas and experiences for improving DataLab. In particular, we are very " @@ -74,97 +78,96 @@ msgstr "" " un cas d'utilisation que vous souhaitez partager avec la communauté, " "faites-le nous savoir." -#: ../../contributing/index.rst:31 +#: ../../doc/contributing/index.rst:31 msgid "" " Bugs" +"hidden=\"true\"> Bugs" msgstr "Anomalies" -#: ../../contributing/index.rst:34 +#: ../../doc/contributing/index.rst:34 msgid "Reporting a bug" msgstr "Signaler une anomalie" -#: ../../contributing/index.rst:36 +#: ../../doc/contributing/index.rst:36 msgid "" " Enhancements" +"hidden=\"true\"> Enhancements" msgstr "Améliorations" -#: ../../contributing/index.rst:39 +#: ../../doc/contributing/index.rst:39 msgid "Suggesting an enhancement" msgstr "Suggérer une amélioration" -#: ../../contributing/index.rst:41 +#: ../../doc/contributing/index.rst:41 msgid "" " Documentation" +"hidden=\"true\"> Documentation" msgstr "Documentation" -#: ../../contributing/index.rst:44 +#: ../../doc/contributing/index.rst:44 msgid "Suggesting a documentation topic" msgstr "Suggérer un sujet de documentation" -#: ../../contributing/index.rst:46 +#: ../../doc/contributing/index.rst:46 msgid "" " " -"Tutorial" +"5.684a.75.75 0 0 1 0-1.368ZM2.583 5 8 7.428 13.416 5 8 2.572ZM2.5 " +"11.25v2.25H4v-2.25c0-.388-.125-.611-.25-.735a.697.697 0 0 " +"0-.5-.203.707.707 0 0 0-.5.203c-.125.124-.25.347-.25.735Z\">" +" Tutorial" msgstr "Tutoriel" -#: ../../contributing/index.rst:49 +#: ../../doc/contributing/index.rst:49 msgid "Suggesting a tutorial topic" msgstr "Suggérer un sujet de tutoriel" -#: ../../contributing/index.rst:54 +#: ../../doc/contributing/index.rst:54 msgid "Without coding, you can contribute to DataLab project by:" msgstr "Sans coder une seule ligne, vous pouvez contribuer au projet DataLab en :" -#: ../../contributing/index.rst:56 +#: ../../doc/contributing/index.rst:56 msgid "" "`Reporting a bug `_" @@ -172,7 +175,7 @@ msgstr "" "`Signaler une anomalie `_" -#: ../../contributing/index.rst:57 +#: ../../doc/contributing/index.rst:57 msgid "" "`Suggesting an enhancement `_" @@ -180,7 +183,7 @@ msgstr "" "`Suggérer une amélioration `_" -#: ../../contributing/index.rst:58 +#: ../../doc/contributing/index.rst:58 msgid "" "`Suggesting a documentation topic `_" @@ -188,7 +191,7 @@ msgstr "" "`Suggérer un sujet de documentation `_" -#: ../../contributing/index.rst:59 +#: ../../doc/contributing/index.rst:59 msgid "" "`Suggesting a tutorial topic `_" @@ -196,11 +199,11 @@ msgstr "" "`Suggérer un sujet de tutoriel `_" -#: ../../contributing/index.rst:62 +#: ../../doc/contributing/index.rst:62 msgid "Share your scientific/technical knowledge" msgstr "Partagez vos connaissances scientifiques/techniques" -#: ../../contributing/index.rst:68 +#: ../../doc/contributing/index.rst:68 msgid "" "Your technical or scientific knowledge is also very valuable to us, as " "you may directly contribute to the documentation or tutorials. Or, " @@ -211,23 +214,23 @@ msgstr "" "documentation ou aux tutoriels. Ou, mieux encore, si vous souhaitez " "rédiger un tutoriel, nous serons heureux de vous aider." -#: ../../contributing/index.rst:72 +#: ../../doc/contributing/index.rst:72 msgid "Again without coding, you can contribute to DataLab project by:" msgstr "Encore une fois sans coder, vous pouvez contribuer au projet DataLab en :" -#: ../../contributing/index.rst:74 +#: ../../doc/contributing/index.rst:74 msgid "Writing documentation" msgstr "Ecrire de la documentation" -#: ../../contributing/index.rst:75 +#: ../../doc/contributing/index.rst:75 msgid "Writing a tutorial" msgstr "Ecrire un tutoriel" -#: ../../contributing/index.rst:78 +#: ../../doc/contributing/index.rst:78 msgid "Contribute to new features" msgstr "Contribuer aux nouvelles fonctionnalités" -#: ../../contributing/index.rst:82 +#: ../../doc/contributing/index.rst:82 msgid "" ":octicon:`info;1em;sd-text-info` :bdg-info-line:`Coding (beginner)` :bdg-" "warning-line:`Coding (advanced)`" @@ -235,31 +238,31 @@ msgstr "" ":octicon:`info;1em;sd-text-info` :bdg-info-line:`Codage (débutant)` :bdg-" "warning-line:`Codage (avancé)`" -#: ../../contributing/index.rst:84 +#: ../../doc/contributing/index.rst:84 msgid "Even if you are not a developer, you can contribute to the project by:" msgstr "Même si vous n'êtes pas développeur, vous pouvez contribuer au projet en :" -#: ../../contributing/index.rst:86 +#: ../../doc/contributing/index.rst:86 msgid "Testing new features" msgstr "Testant de nouvelles fonctionnalités" -#: ../../contributing/index.rst:87 +#: ../../doc/contributing/index.rst:87 msgid "Writing and submitting new Plugins" msgstr "Ecrivant et soumettant de nouveaux plugins" -#: ../../contributing/index.rst:88 +#: ../../doc/contributing/index.rst:88 msgid "Writing and submitting macro-commands" msgstr "Ecivant et soumettant des macro-commandes" -#: ../../contributing/index.rst:91 +#: ../../doc/contributing/index.rst:91 msgid "Develop new features" msgstr "Développer de nouvelles fonctionnalités" -#: ../../contributing/index.rst:95 +#: ../../doc/contributing/index.rst:95 msgid ":octicon:`info;1em;sd-text-info` :bdg-warning-line:`Coding (advanced)`" msgstr ":octicon:`info;1em;sd-text-info` :bdg-warning-line:`Codage (avancé)`" -#: ../../contributing/index.rst:97 +#: ../../doc/contributing/index.rst:97 msgid "" "If you are a developer, you can contribute to the core of the project by " "fixing bugs or implementing new features." @@ -267,25 +270,25 @@ msgstr "" "Si vous êtes développeur, vous pouvez contribuer au cœur du projet en " "corrigeant des anomalies ou en implémentant de nouvelles fonctionnalités." -#: ../../contributing/index.rst:105 +#: ../../doc/contributing/index.rst:105 msgid "" " Code" +"hidden=\"true\"> Code" msgstr "Code" -#: ../../contributing/index.rst:109 +#: ../../doc/contributing/index.rst:109 msgid "Contributing to the code" msgstr "Contribuer au code" -#: ../../contributing/index.rst:113 +#: ../../doc/contributing/index.rst:113 msgid "See :ref:`contribute_code` section for more information." msgstr "Voir la section :ref:`contribute_code` pour plus d'informations." diff --git a/doc/locale/fr/LC_MESSAGES/installation.po b/doc/locale/fr/LC_MESSAGES/installation.po index cd3a4752..d5d97c32 100644 --- a/doc/locale/fr/LC_MESSAGES/installation.po +++ b/doc/locale/fr/LC_MESSAGES/installation.po @@ -50,7 +50,7 @@ msgid "Python" msgstr "" #: ../../install_requires.txt:7 -msgid "3.8" +msgid "3.9" msgstr "" #: ../../install_requires.txt:8 @@ -130,8 +130,8 @@ msgid "1.9" msgstr "" #: ../../installation.rst:15 -msgid "Python 3.8 is the reference for production release" -msgstr "Python 3.8 est la référence de la version de production" +msgid "Python 3.10 is the reference for production release" +msgstr "Python 3.10 est la référence de la version de production" #: ../../installation.rst:17 msgid "Optional modules for development and testing:" diff --git a/doc/locale/fr/LC_MESSAGES/intro/index.po b/doc/locale/fr/LC_MESSAGES/intro/index.po index d8874c58..f743c378 100644 --- a/doc/locale/fr/LC_MESSAGES/intro/index.po +++ b/doc/locale/fr/LC_MESSAGES/intro/index.po @@ -8,14 +8,14 @@ msgid "" msgstr "" "Project-Id-Version: DataLab \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-24 16:49+0200\n" +"POT-Creation-Date: 2024-11-13 18:41+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.13.0\n" +"Generated-By: Babel 2.14.0\n" #: ../../doc/intro/index.rst:81 msgid "Contents:" @@ -49,13 +49,21 @@ msgid "" " is a versatile software that can be used for a wide range of " "applications, from simple data analysis to complex signal processing and " "image analysis tasks." -msgstr "DataLab est une plateforme ouverte de traitement de signaux et d'images, conçue pour être utilisée par les scientifiques, ingénieurs et chercheurs du monde académique et industriel, tout en offrant la fiabilité d'un logiciel industriel. C'est un logiciel polyvalent qui peut être utilisé pour un large éventail d'applications, de l'analyse de données simple aux tâches complexes de traitement de signaux et d'analyse d'images." +msgstr "" +"DataLab est une plateforme ouverte de traitement de signaux et d'images, " +"conçue pour être utilisée par les scientifiques, ingénieurs et chercheurs" +" du monde académique et industriel, tout en offrant la fiabilité d'un " +"logiciel industriel. C'est un logiciel polyvalent qui peut être utilisé " +"pour un large éventail d'applications, de l'analyse de données simple aux" +" tâches complexes de traitement de signaux et d'analyse d'images." #: ../../doc/intro/index.rst:14 msgid "" "DataLab integrates seemlessly into your workflow thanks to three main " "operating modes:" -msgstr "DataLab s'intègre parfaitement dans votre flux de travail grâce à trois modes de fonctionnement principaux :" +msgstr "" +"DataLab s'intègre parfaitement dans votre flux de travail grâce à trois " +"modes de fonctionnement principaux :" #: ../../doc/intro/index.rst:19 msgid "|appmode|" @@ -70,7 +78,10 @@ msgid "" "**Stand-alone application**, with a graphical user interface that allows " "you to interact with your data and visualize the results of your analysis" " in real time." -msgstr "**Application autonome**, avec une interface graphique qui vous permet d'interagir avec vos données et de visualiser les résultats de votre analyse en temps réel." +msgstr "" +"**Application autonome**, avec une interface graphique qui vous permet " +"d'interagir avec vos données et de visualiser les résultats de votre " +"analyse en temps réel." #: ../../doc/intro/index.rst:22 msgid "|libmode|" @@ -85,7 +96,10 @@ msgid "" "**Python library**, allowing you to integrate DataLab functions (or " "graphical user interfaces) into your own Python scripts and programs or " "Jupyter notebooks." -msgstr "**Bibliothèque Python**, vous permettant d'intégrer les fonctions de DataLab (ou les interfaces graphiques) dans vos propres scripts et programmes Python ou des notebooks Jupyter." +msgstr "" +"**Bibliothèque Python**, vous permettant d'intégrer les fonctions de " +"DataLab (ou les interfaces graphiques) dans vos propres scripts et " +"programmes Python ou des notebooks Jupyter." #: ../../doc/intro/index.rst:25 msgid "|remotemode|" @@ -99,7 +113,9 @@ msgstr "" msgid "" "**Remotely controlled** from your own software, or from an IDE (e.g., " "Spyder) or a Jupyter notebook, using the DataLab API." -msgstr "**Piloté à distance** depuis votre propre logiciel, ou depuis un IDE (par exemple Spyder) ou un notebook Jupyter, en utilisant l'API de DataLab." +msgstr "" +"**Piloté à distance** depuis votre propre logiciel, ou depuis un IDE (par" +" exemple Spyder) ou un notebook Jupyter, en utilisant l'API de DataLab." #: ../../doc/intro/index.rst:43 msgid "" @@ -153,14 +169,14 @@ msgstr "" msgid "" " Installation" +"hidden=\"true\"> Installation" msgstr "Installation" #: ../../doc/intro/index.rst:61 @@ -171,21 +187,20 @@ msgstr "Comment installer DataLab sur votre ordinateur" msgid "" " Introduction" -msgstr "Introduction" +"hidden=\"true\"> Introduction" +msgstr "" #: ../../doc/intro/index.rst:67 msgid "Use cases and key strengths of DataLab" @@ -195,15 +210,14 @@ msgstr "Cas d'utilisation et points forts de DataLab" msgid "" " Key features" +"hidden=\"true\"> Key features" msgstr "Fonctionnalités clés" #: ../../doc/intro/index.rst:73 @@ -214,18 +228,18 @@ msgstr "Matrice des fonctionnalités de DataLab" msgid "" " " -"Tutorials" +"5.684a.75.75 0 0 1 0-1.368ZM2.583 5 8 7.428 13.416 5 8 2.572ZM2.5 " +"11.25v2.25H4v-2.25c0-.388-.125-.611-.25-.735a.697.697 0 0 " +"0-.5-.203.707.707 0 0 0-.5.203c-.125.124-.25.347-.25.735Z\">" +" Tutorials" msgstr "Tutoriels" #: ../../doc/intro/index.rst:79 @@ -533,3 +547,31 @@ msgstr "Tutoriels pour apprendre à utiliser DataLab" #~ " utiliser, simple à adapter et " #~ "offrant la fiabilité d'un logiciel " #~ "industriel." + +#~ msgid "" +#~ "" +#~ " Introduction" +#~ msgstr "Introduction" diff --git a/doc/locale/fr/LC_MESSAGES/intro/installation.po b/doc/locale/fr/LC_MESSAGES/intro/installation.po index cbeb8ea1..0ccd604b 100644 --- a/doc/locale/fr/LC_MESSAGES/intro/installation.po +++ b/doc/locale/fr/LC_MESSAGES/intro/installation.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: DataLab \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-09-24 11:01+0200\n" +"POT-Creation-Date: 2024-11-14 15:37+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -40,18 +40,14 @@ msgid "Quick install on Windows" msgstr "Installation rapide sur Windows" #: ../../doc/intro/installation.rst:16 -msgid "Direct download links for the latest version of DataLab:" -msgstr "Liens de téléchargement directs pour la dernière version de DataLab :" +msgid "Direct download link for the latest version of DataLab:" +msgstr "Lien de téléchargement direct pour la dernière version de DataLab :" #: ../../doc/intro/installation.rst:18 msgid "|download_link1|" msgstr "|download_link1|" -#: ../../doc/intro/installation.rst:19 -msgid "|download_link2|" -msgstr "|download_link2|" - -#: ../../doc/intro/installation.rst:21 +#: ../../doc/intro/installation.rst:20 msgid "" "This section provides information on how to install DataLab on your " "system. Once installed, you can start DataLab by running the ``cdl`` " @@ -63,7 +59,7 @@ msgstr "" "exécutant la commande ``cdl`` dans un terminal, ou en cliquant sur le " "raccourci DataLab dans le menu Démarrer (sous Windows)." -#: ../../doc/intro/installation.rst:27 +#: ../../doc/intro/installation.rst:26 msgid "" "For more details on how to execute DataLab and its command-line options, " "see :ref:`ref-to-command-line-features`." @@ -71,23 +67,23 @@ msgstr "" "Pour plus de détails sur l'exécution de DataLab et ses options en ligne " "de commande, voir :ref:`ref-to-command-line-features`." -#: ../../doc/intro/installation.rst:31 +#: ../../doc/intro/installation.rst:30 msgid "How to install" msgstr "Modes d'installation" -#: ../../doc/intro/installation.rst:33 +#: ../../doc/intro/installation.rst:32 msgid "DataLab is available in several forms:" msgstr "DataLab est disponible sous plusieurs formes :" -#: ../../doc/intro/installation.rst:35 +#: ../../doc/intro/installation.rst:34 msgid "As a :ref:`install_conda`." msgstr "En tant que :ref:`install_conda`." -#: ../../doc/intro/installation.rst:37 +#: ../../doc/intro/installation.rst:36 msgid "As a Python package, which can be installed using the :ref:`install_pip`." msgstr "Un paquet Python qui peut être installé à l'aide du :ref:`install_pip`." -#: ../../doc/intro/installation.rst:39 +#: ../../doc/intro/installation.rst:38 msgid "" ":bdg-info-line:`Windows` As a stand-alone application, which does not " "require any Python distribution to be installed. Just run the " @@ -97,7 +93,7 @@ msgstr "" "nécessite pas d'installation de Python. Il suffit d'exécuter " "l':ref:`install_aioinstaller` et le tour est joué !" -#: ../../doc/intro/installation.rst:43 +#: ../../doc/intro/installation.rst:42 msgid "" ":bdg-info-line:`Windows` Within a ready-to-use :ref:`install_winpython`, " "based on `WinPython `_." @@ -106,7 +102,7 @@ msgstr "" ":ref:`install_winpython`, basée sur `WinPython " "`_." -#: ../../doc/intro/installation.rst:46 +#: ../../doc/intro/installation.rst:45 msgid "" "As a precompiled :ref:`install_wheel`, which can be installed using " "``pip``." @@ -114,7 +110,7 @@ msgstr "" "En tant que :ref:`install_wheel` précompilé, qui peut être installé à " "l'aide de ``pip``." -#: ../../doc/intro/installation.rst:48 +#: ../../doc/intro/installation.rst:47 msgid "" "As a :ref:`install_source`, which can be installed using ``pip`` or " "manually." @@ -122,7 +118,7 @@ msgstr "" "En tant que :ref:`install_source`, qui peut être installé à l'aide de " "``pip`` ou manuellement." -#: ../../doc/intro/installation.rst:52 +#: ../../doc/intro/installation.rst:51 msgid "" "Impatient to try the next version of DataLab? You can also install the " "latest development version of DataLab from the master branch of the Git " @@ -133,29 +129,32 @@ msgstr "" "partir de la branche principale du dépôt Git. Voir " ":ref:`install_development` pour plus d'informations." -#: ../../doc/intro/installation.rst:59 +#: ../../doc/intro/installation.rst:58 msgid "Conda package" msgstr "Paquet Conda" -#: ../../doc/intro/installation.rst:61 ../../doc/intro/installation.rst:74 -#: ../../doc/intro/installation.rst:173 ../../doc/intro/installation.rst:187 -#: ../../doc/intro/installation.rst:217 +#: ../../doc/intro/installation.rst:60 ../../doc/intro/installation.rst:73 +#: ../../doc/intro/installation.rst:170 ../../doc/intro/installation.rst:184 +#: ../../doc/intro/installation.rst:214 msgid "" ":octicon:`info;1em;sd-text-info` :bdg-info-line:`GNU/Linux` :bdg-info-" "line:`Windows` :bdg-info-line:`macOS`" msgstr "" -#: ../../doc/intro/installation.rst:63 +#: ../../doc/intro/installation.rst:62 msgid "" "To install ``datalab`` package from the `conda-forge` channel " "(https://anaconda.org/conda-forge/datalab), run the following command:" -msgstr "Pour installer le paquet ``datalab`` depuis le canal ``conda-forge`` (https://anaconda.org/conda-forge/datalab), exécutez la commande suivante :" +msgstr "" +"Pour installer le paquet ``datalab`` depuis le canal ``conda-forge`` " +"(https://anaconda.org/conda-forge/datalab), exécutez la commande suivante" +" :" -#: ../../doc/intro/installation.rst:72 +#: ../../doc/intro/installation.rst:71 msgid "Package manager ``pip``" msgstr "Gestionnaire de paquets ``pip``" -#: ../../doc/intro/installation.rst:76 +#: ../../doc/intro/installation.rst:75 msgid "" "DataLab's package ``cdl`` is available on the Python Package Index (PyPI)" " on the following URL: https://pypi.python.org/pypi/cdl." @@ -163,7 +162,7 @@ msgstr "" "Le paquet ``cdl`` de DataLab est disponible sur l'index des paquets " "Python (PyPI) à l'adresse suivante : https://pypi.python.org/pypi/cdl." -#: ../../doc/intro/installation.rst:79 +#: ../../doc/intro/installation.rst:78 msgid "" "Installing DataLab from PyPI with Qt is as simple as running this command" " (you may need to use ``pip3`` instead of ``pip`` on some systems):" @@ -172,7 +171,7 @@ msgstr "" "d'exécuter cette commande (vous devrez peut-être utiliser ``pip3`` au " "lieu de ``pip`` sur certains systèmes) :" -#: ../../doc/intro/installation.rst:86 +#: ../../doc/intro/installation.rst:85 msgid "" "Or, if you prefer, you can install DataLab without the Qt library (not " "recommended):" @@ -180,7 +179,7 @@ msgstr "" "Ou, si vous préférez, vous pouvez installer DataLab sans la bibliothèque " "Qt (non recommandé) :" -#: ../../doc/intro/installation.rst:94 +#: ../../doc/intro/installation.rst:93 msgid "" "If you already have a previous version of DataLab installed, you can " "upgrade it by running the same command with the ``--upgrade`` option:" @@ -189,15 +188,15 @@ msgstr "" "pouvez la mettre à jour en exécutant la même commande avec l'option " "``--upgrade`` :" -#: ../../doc/intro/installation.rst:104 +#: ../../doc/intro/installation.rst:103 msgid "All-in-one installer" msgstr "Installeur tout-en-un" -#: ../../doc/intro/installation.rst:106 ../../doc/intro/installation.rst:139 +#: ../../doc/intro/installation.rst:105 ../../doc/intro/installation.rst:136 msgid ":octicon:`info;1em;sd-text-info` :bdg-info-line:`Windows`" msgstr "" -#: ../../doc/intro/installation.rst:108 +#: ../../doc/intro/installation.rst:107 msgid "" "DataLab is available as a stand-alone application for Windows, which does" " not require any Python distribution to be installed. Just run the " @@ -207,11 +206,11 @@ msgstr "" "Windows qui ne nécessite pas d'installation de Python. Il suffit " "d'exécuter l'installeur et vous êtes prêt à partir !" -#: ../../doc/intro/installation.rst:114 +#: ../../doc/intro/installation.rst:113 msgid "DataLab all-in-one installer for Windows" msgstr "Installeur tout-en-un de DataLab pour Windows" -#: ../../doc/intro/installation.rst:116 +#: ../../doc/intro/installation.rst:115 msgid "" "The installer package is available in the `Releases`_ section. It " "supports automatic uninstall and upgrade feature (no need to uninstall " @@ -223,18 +222,13 @@ msgstr "" "besoin de désinstaller DataLab avant d'exécuter l'installeur d'une autre " "version de l'application)." -#: ../../doc/intro/installation.rst:123 -msgid "" -"DataLab Windows installer is available for Windows 8, 10 and 11 (main " -"release, based on Python 3.11) and also for Windows 7 SP1 (Python 3.8 " -"based release, see file ending with ``-Win7.msi``)." +#: ../../doc/intro/installation.rst:122 +msgid "DataLab Windows installer is available for Windows 7 SP1, 8, 10 and 11." msgstr "" -"L'installeur Windows de DataLab est disponible pour Windows 8, 10 et 11 " -"(version principale, basée sur Python 3.11) et également pour Windows 7 " -"SP1 (version basée sur Python 3.8, voir le fichier se terminant par " -"``-Win7.msi``)." +"L'installeur Windows de DataLab est disponible pour Windows 7 SP1, 8, 10 " +"et 11." -#: ../../doc/intro/installation.rst:127 +#: ../../doc/intro/installation.rst:124 msgid "" ":octicon:`alert;1em;sd-text-warning` On Windows 7 SP1, before running " "DataLab (or any other Python 3 application), you must install Microsoft " @@ -250,11 +244,11 @@ msgstr "" "Microsoft Visual C++ 2015-2022 `_." -#: ../../doc/intro/installation.rst:137 +#: ../../doc/intro/installation.rst:134 msgid "Python distribution" msgstr "Distribution Python" -#: ../../doc/intro/installation.rst:141 +#: ../../doc/intro/installation.rst:138 msgid "" "DataLab is also available within a ready-to-use Python distribution, " "based on `WinPython `_. This distribution " @@ -270,7 +264,7 @@ msgstr "" " disponible dans la section `DataLab-WinPython Releases " "`_." -#: ../../doc/intro/installation.rst:149 +#: ../../doc/intro/installation.rst:146 msgid "" "DataLab-WinPython is a ready-to-use Python distribution including the " "DataLab platform." @@ -278,7 +272,7 @@ msgstr "" "DataLab-WinPython est une distribution Python prête à l'emploi incluant " "la plateforme DataLab." -#: ../../doc/intro/installation.rst:151 +#: ../../doc/intro/installation.rst:148 msgid "" "The main difference with the all-in-one installer is that you can use the" " Python distribution for other purposes than running DataLab, and you may" @@ -293,11 +287,11 @@ msgstr "" "volumineuse* que l'installeur tout-en-un car elle inclut une distribution" " Python complète." -#: ../../doc/intro/installation.rst:158 +#: ../../doc/intro/installation.rst:155 msgid "DataLab-WinPython Control Panel" msgstr "Panneau de contrôle DataLab-WinPython" -#: ../../doc/intro/installation.rst:162 +#: ../../doc/intro/installation.rst:159 msgid "" "Whereas the all-in-one installer provides a monolithic package that " "guarantees the compatibility of all its components because it cannot be " @@ -313,11 +307,11 @@ msgstr "" "distribution Python par l'utilisateur. Cela doit être pris en compte lors" " du choix de la méthode d'installation." -#: ../../doc/intro/installation.rst:171 +#: ../../doc/intro/installation.rst:168 msgid "Wheel package" msgstr "Paquet Wheel" -#: ../../doc/intro/installation.rst:175 +#: ../../doc/intro/installation.rst:172 msgid "" "On any operating system, using pip and the Wheel package is the easiest " "way to install DataLab on an existing Python distribution:" @@ -326,11 +320,11 @@ msgstr "" "paquet Wheel est le moyen le plus simple d'installer DataLab sur une " "distribution Python existante :" -#: ../../doc/intro/installation.rst:185 +#: ../../doc/intro/installation.rst:182 msgid "Source package" msgstr "Paquet source" -#: ../../doc/intro/installation.rst:189 +#: ../../doc/intro/installation.rst:186 msgid "" "Installing DataLab directly from the source package may be done using " "``pip``:" @@ -338,7 +332,7 @@ msgstr "" "L'installation de DataLab directement depuis le paquet source peut être " "effectuée à l'aide de ``pip`` :" -#: ../../doc/intro/installation.rst:195 +#: ../../doc/intro/installation.rst:192 msgid "" "Or, if you prefer, you can install it manually by running the following " "command from the root directory of the source package:" @@ -346,7 +340,7 @@ msgstr "" "Ou, si vous préférez, vous pouvez l'installer manuellement en exécutant " "la commande suivante depuis le répertoire racine du paquet source :" -#: ../../doc/intro/installation.rst:202 +#: ../../doc/intro/installation.rst:199 msgid "" "Finally, you can also build your own Wheel package and install it using " "``pip``, by running the following command from the root directory of the " @@ -358,11 +352,11 @@ msgstr "" " le répertoire racine du paquet source (cela nécessite que les paquets " "``build`` et ``wheel`` soient installés) :" -#: ../../doc/intro/installation.rst:215 +#: ../../doc/intro/installation.rst:212 msgid "Development version" msgstr "Version de développement" -#: ../../doc/intro/installation.rst:219 +#: ../../doc/intro/installation.rst:216 msgid "" "If you want to try the latest development version of DataLab, you can " "install it directly from the master branch of the Git repository." @@ -371,7 +365,7 @@ msgstr "" "DataLab, vous pouvez l'installer directement depuis la branche principale" " du dépôt Git." -#: ../../doc/intro/installation.rst:222 +#: ../../doc/intro/installation.rst:219 msgid "" "The first time you install DataLab from the Git repository, enter the " "following command:" @@ -379,7 +373,7 @@ msgstr "" "La première fois que vous installez DataLab depuis le dépôt Git, entrez " "la commande suivante :" -#: ../../doc/intro/installation.rst:229 +#: ../../doc/intro/installation.rst:226 msgid "" "Then, if at some point you want to upgrade to the latest version of " "DataLab, just run the same command with options to force the reinstall of" @@ -391,7 +385,7 @@ msgstr "" "forcer la réinstallation du paquet sans gérer les dépendances (car cela " "réinstallerait toutes les dépendances) :" -#: ../../doc/intro/installation.rst:239 +#: ../../doc/intro/installation.rst:236 msgid "" "If dependencies have changed, you may need to execute the same command as" " above, but without the ``--no-deps`` option." @@ -399,11 +393,11 @@ msgstr "" "Si les dépendances ont changé, vous devrez peut-être exécuter la même " "commande que ci-dessus, mais sans l'option ``--no-deps``." -#: ../../doc/intro/installation.rst:243 +#: ../../doc/intro/installation.rst:240 msgid "Dependencies" msgstr "Dépendances" -#: ../../doc/intro/installation.rst:247 +#: ../../doc/intro/installation.rst:244 msgid "" "The DataLab all-in-one installer already include all those required " "libraries as well as Python itself." @@ -415,18 +409,18 @@ msgstr "" msgid "The :mod:`cdl` package requires the following Python modules:" msgstr "Le paquet :mod:`cdl` requiert les modules Python suivants :" -#: ../../doc/requirements.rst:7 ../../doc/requirements.rst:56 -#: ../../doc/requirements.rst:78 ../../doc/requirements.rst:112 +#: ../../doc/requirements.rst:7 ../../doc/requirements.rst:44 +#: ../../doc/requirements.rst:66 ../../doc/requirements.rst:100 msgid "Name" msgstr "Nom" -#: ../../doc/requirements.rst:8 ../../doc/requirements.rst:57 -#: ../../doc/requirements.rst:79 ../../doc/requirements.rst:113 +#: ../../doc/requirements.rst:8 ../../doc/requirements.rst:45 +#: ../../doc/requirements.rst:67 ../../doc/requirements.rst:101 msgid "Version" msgstr "Version" -#: ../../doc/requirements.rst:9 ../../doc/requirements.rst:58 -#: ../../doc/requirements.rst:80 ../../doc/requirements.rst:114 +#: ../../doc/requirements.rst:9 ../../doc/requirements.rst:46 +#: ../../doc/requirements.rst:68 ../../doc/requirements.rst:102 msgid "Summary" msgstr "Description" @@ -435,7 +429,7 @@ msgid "Python" msgstr "Python" #: ../../doc/requirements.rst:11 -msgid ">=3.8, <4" +msgid ">=3.9, <4" msgstr "" #: ../../doc/requirements.rst:12 @@ -443,35 +437,37 @@ msgid "Python programming language" msgstr "Langage de programmation Python" #: ../../doc/requirements.rst:13 -msgid "h5py" +msgid "guidata" msgstr "" #: ../../doc/requirements.rst:14 -msgid ">= 3.0" +msgid ">= 3.7" msgstr "" #: ../../doc/requirements.rst:15 -msgid "Read and write HDF5 files from Python" -msgstr "Ecriture et lecture de fichiers HDF5 depuis Python" +msgid "Automatic GUI generation for easy dataset editing and display" +msgstr "" +"Génération automatique d'interfaces graphiques pour l'édition et " +"l'affichage de jeux de données" #: ../../doc/requirements.rst:16 -msgid "NumPy" +msgid "PlotPy" msgstr "" #: ../../doc/requirements.rst:17 -msgid ">= 1.21" +msgid ">= 2.7" msgstr "" #: ../../doc/requirements.rst:18 -msgid "Fundamental package for array computing in Python" -msgstr "Calcul matriciel et algèbre linéaire en Python" +msgid "Curve and image plotting tools for Python/Qt applications" +msgstr "Outils de tracé de courbes et d'images pour les applications Python/Qt" #: ../../doc/requirements.rst:19 msgid "SciPy" msgstr "" #: ../../doc/requirements.rst:20 -msgid ">= 1.7" +msgid ">=1.5" msgstr "" #: ../../doc/requirements.rst:21 @@ -491,146 +487,92 @@ msgid "Image processing in Python" msgstr "Traitement d'images en Python" #: ../../doc/requirements.rst:25 -msgid "opencv-python-headless" +msgid "pandas" msgstr "" #: ../../doc/requirements.rst:26 -msgid ">= 4.5" +msgid ">= 1.2" msgstr "" #: ../../doc/requirements.rst:27 -msgid "Wrapper package for OpenCV python bindings." -msgstr "Interface Python pour OpenCV" - -#: ../../doc/requirements.rst:28 -msgid "pandas" -msgstr "" - -#: ../../doc/requirements.rst:29 -msgid ">= 1.3" -msgstr "" - -#: ../../doc/requirements.rst:30 msgid "Powerful data structures for data analysis, time series, and statistics" msgstr "Analyse de données, séries temporelles et statistiques" -#: ../../doc/requirements.rst:31 +#: ../../doc/requirements.rst:28 msgid "PyWavelets" msgstr "" -#: ../../doc/requirements.rst:32 +#: ../../doc/requirements.rst:29 msgid ">= 1.1" msgstr "" -#: ../../doc/requirements.rst:33 +#: ../../doc/requirements.rst:30 msgid "PyWavelets, wavelet transform module" msgstr "PyWavelets, module de transformation en ondelettes" -#: ../../doc/requirements.rst:34 +#: ../../doc/requirements.rst:31 msgid "psutil" msgstr "" -#: ../../doc/requirements.rst:35 -msgid ">= 5.5" +#: ../../doc/requirements.rst:32 +msgid ">= 5.7" msgstr "" -#: ../../doc/requirements.rst:36 +#: ../../doc/requirements.rst:33 msgid "Cross-platform lib for process and system monitoring in Python." msgstr "" "Bibliothèque multiplateforme pour la surveillance des processus et du " "système en Python." -#: ../../doc/requirements.rst:37 -msgid "guidata" -msgstr "" - -#: ../../doc/requirements.rst:38 -msgid ">= 3.5" -msgstr "" - -#: ../../doc/requirements.rst:39 -msgid "Automatic GUI generation for easy dataset editing and display" -msgstr "" -"Génération automatique d'interfaces graphiques pour l'édition et " -"l'affichage de jeux de données" - -#: ../../doc/requirements.rst:40 -msgid "PlotPy" -msgstr "" - -#: ../../doc/requirements.rst:41 -msgid ">= 2.5" -msgstr "" - -#: ../../doc/requirements.rst:42 -msgid "Curve and image plotting tools for Python/Qt applications" -msgstr "Outils de tracé de courbes et d'images pour les applications Python/Qt" - -#: ../../doc/requirements.rst:43 -msgid "QtPy" -msgstr "" - -#: ../../doc/requirements.rst:44 -msgid ">= 1.9" -msgstr "" - -#: ../../doc/requirements.rst:45 -msgid "" -"Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 " -"and PySide2/6)." -msgstr "" -"Couche d'abstraction pour les différentes bibliothèques Qt (PyQt5/6 et " -"PySide2/6)." - -#: ../../doc/requirements.rst:46 ../../doc/requirements.rst:81 +#: ../../doc/requirements.rst:34 ../../doc/requirements.rst:69 msgid "PyQt5" msgstr "" -#: ../../doc/requirements.rst:47 +#: ../../doc/requirements.rst:35 msgid ">=5.11" msgstr "" -#: ../../doc/requirements.rst:48 ../../doc/requirements.rst:83 +#: ../../doc/requirements.rst:36 ../../doc/requirements.rst:71 msgid "Python bindings for the Qt cross platform application toolkit" msgstr "Bibliothèque d'interfaces graphiques Qt pour Python" -#: ../../doc/requirements.rst:50 +#: ../../doc/requirements.rst:38 msgid "Optional modules for development:" msgstr "Modules optionnels pour le développement :" -#: ../../doc/requirements.rst:59 +#: ../../doc/requirements.rst:47 msgid "ruff" msgstr "" -#: ../../doc/requirements.rst:61 +#: ../../doc/requirements.rst:49 msgid "An extremely fast Python linter and code formatter, written in Rust." msgstr "Analyseur de code et formateur Python extrêmement rapide, écrit en Rust." -#: ../../doc/requirements.rst:62 +#: ../../doc/requirements.rst:50 msgid "pylint" msgstr "" -#: ../../doc/requirements.rst:64 +#: ../../doc/requirements.rst:52 msgid "python code static checker" msgstr "Analyseur statique de code Python" -#: ../../doc/requirements.rst:65 +#: ../../doc/requirements.rst:53 msgid "Coverage" msgstr "" -#: ../../doc/requirements.rst:67 +#: ../../doc/requirements.rst:55 msgid "Code coverage measurement for Python" msgstr "Mesure de la couverture de code pour Python" -#: ../../doc/requirements.rst:68 +#: ../../doc/requirements.rst:56 msgid "pyinstaller" msgstr "" -#: ../../doc/requirements.rst:69 +#: ../../doc/requirements.rst:57 msgid ">=6.0" msgstr "" -#: ../../doc/requirements.rst:70 +#: ../../doc/requirements.rst:58 msgid "" "PyInstaller bundles a Python application and all its dependencies into a " "single package." @@ -638,91 +580,91 @@ msgstr "" "PyInstaller permet de créer un exécutable unique à partir d'une " "application Python et de ses dépendances." -#: ../../doc/requirements.rst:72 +#: ../../doc/requirements.rst:60 msgid "Optional modules for building the documentation:" msgstr "Modules optionnels pour la génération de la documentation :" -#: ../../doc/requirements.rst:84 +#: ../../doc/requirements.rst:72 msgid "sphinx" msgstr "" -#: ../../doc/requirements.rst:86 +#: ../../doc/requirements.rst:74 msgid "Python documentation generator" msgstr "Générateur de documentation Python" -#: ../../doc/requirements.rst:87 +#: ../../doc/requirements.rst:75 msgid "sphinx_intl" msgstr "" -#: ../../doc/requirements.rst:89 +#: ../../doc/requirements.rst:77 msgid "Sphinx utility that make it easy to translate and to apply translation." msgstr "Utilitaire pour la traduction de la documentation générée par Sphinx." -#: ../../doc/requirements.rst:90 +#: ../../doc/requirements.rst:78 msgid "sphinx-sitemap" msgstr "" -#: ../../doc/requirements.rst:92 +#: ../../doc/requirements.rst:80 msgid "Sitemap generator for Sphinx" msgstr "" -#: ../../doc/requirements.rst:93 +#: ../../doc/requirements.rst:81 msgid "myst_parser" msgstr "" -#: ../../doc/requirements.rst:95 +#: ../../doc/requirements.rst:83 msgid "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," msgstr "" "Un parseur étendu compatible avec " "[CommonMark](https://spec.commonmark.org/)" -#: ../../doc/requirements.rst:96 +#: ../../doc/requirements.rst:84 msgid "sphinx_design" msgstr "" -#: ../../doc/requirements.rst:98 +#: ../../doc/requirements.rst:86 msgid "" "A sphinx extension for designing beautiful, view size responsive web " "components." msgstr "Extension sphinx pour la conception de composants web réactifs." -#: ../../doc/requirements.rst:99 +#: ../../doc/requirements.rst:87 msgid "sphinx-copybutton" msgstr "" -#: ../../doc/requirements.rst:101 +#: ../../doc/requirements.rst:89 msgid "Add a copy button to each of your code cells." msgstr "Extension sphinx ajoutant un bouton de copie à chaque cellule de code." -#: ../../doc/requirements.rst:102 +#: ../../doc/requirements.rst:90 msgid "pydata-sphinx-theme" msgstr "" -#: ../../doc/requirements.rst:104 +#: ../../doc/requirements.rst:92 msgid "Bootstrap-based Sphinx theme from the PyData community" msgstr "Thème Bootstrap pour Sphinx de la communauté PyData" -#: ../../doc/requirements.rst:106 +#: ../../doc/requirements.rst:94 msgid "Optional modules for running test suite:" msgstr "Modules optionnels pour l'exécution de la suite de tests :" -#: ../../doc/requirements.rst:115 +#: ../../doc/requirements.rst:103 msgid "pytest" msgstr "" -#: ../../doc/requirements.rst:117 +#: ../../doc/requirements.rst:105 msgid "pytest: simple powerful testing with Python" msgstr "pytest : tests simples et puissants avec Python" -#: ../../doc/requirements.rst:118 +#: ../../doc/requirements.rst:106 msgid "pytest-xvfb" msgstr "" -#: ../../doc/requirements.rst:120 +#: ../../doc/requirements.rst:108 msgid "A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests." msgstr "Plugin pytest pour exécuter Xvfb (ou Xephyr/Xvnc) pour les tests." -#: ../../doc/intro/installation.rst:254 +#: ../../doc/intro/installation.rst:251 msgid "Python 3.11 and PyQt5 are the reference for production release" msgstr "Python 3.11 et PyQt5 sont les références pour la version de production" @@ -740,3 +682,60 @@ msgstr "Python 3.11 et PyQt5 sont les références pour la version de production #~ msgid ">= 2.4" #~ msgstr "" + +#~ msgid "|download_link2|" +#~ msgstr "|download_link2|" + +#~ msgid "h5py" +#~ msgstr "" + +#~ msgid ">= 3.0" +#~ msgstr "" + +#~ msgid "Read and write HDF5 files from Python" +#~ msgstr "Ecriture et lecture de fichiers HDF5 depuis Python" + +#~ msgid "NumPy" +#~ msgstr "" + +#~ msgid "Fundamental package for array computing in Python" +#~ msgstr "Calcul matriciel et algèbre linéaire en Python" + +#~ msgid ">= 1.7" +#~ msgstr "" + +#~ msgid "opencv-python-headless" +#~ msgstr "" + +#~ msgid ">= 4.5" +#~ msgstr "" + +#~ msgid "Wrapper package for OpenCV python bindings." +#~ msgstr "Interface Python pour OpenCV" + +#~ msgid ">= 1.3" +#~ msgstr "" + +#~ msgid ">= 5.5" +#~ msgstr "" + +#~ msgid ">= 3.5" +#~ msgstr "" + +#~ msgid ">= 2.5" +#~ msgstr "" + +#~ msgid "QtPy" +#~ msgstr "" + +#~ msgid ">= 1.9" +#~ msgstr "" + +#~ msgid "" +#~ "Provides an abstraction layer on top " +#~ "of the various Qt bindings (PyQt5/6 " +#~ "and PySide2/6)." +#~ msgstr "" +#~ "Couche d'abstraction pour les différentes " +#~ "bibliothèques Qt (PyQt5/6 et PySide2/6)." + diff --git a/doc/locale/fr/LC_MESSAGES/intro/tutorials/laser_beam.po b/doc/locale/fr/LC_MESSAGES/intro/tutorials/laser_beam.po index 3810a32b..4fed85c7 100644 --- a/doc/locale/fr/LC_MESSAGES/intro/tutorials/laser_beam.po +++ b/doc/locale/fr/LC_MESSAGES/intro/tutorials/laser_beam.po @@ -400,7 +400,7 @@ msgid "" "|plot_results| in the \"Analysis\" menu. This feature allows to plot " "result data sets by choosing the x and y axes among the result columns. " "Here, we choose the to plot the FWHM values (`L`) as a function of the " -"image index (`Indexes`)." +"image index (`Indices`)." msgstr "" "Enfin, nous pouvons tracer la taille du faisceau en fonction de la " "position le long de l'axe de propagation. Pour ce faire, nous utilisons " @@ -408,7 +408,7 @@ msgstr "" "Analyse\". Cette fonction permet de tracer les ensembles de données de " "résultats en choisissant les axes x et y parmi les colonnes de résultats." " Ici, nous choisissons de tracer les valeurs FWHM (`L`) en fonction de " -"l'index de l'image (`Indexes`)." +"l'index de l'image (`Indices`)." #: ../../intro/tutorials/laser_beam.rst:193 msgid "plot_results" diff --git a/doc/locale/fr/LC_MESSAGES/requirements.po b/doc/locale/fr/LC_MESSAGES/requirements.po index c14473c6..52cb37ac 100644 --- a/doc/locale/fr/LC_MESSAGES/requirements.po +++ b/doc/locale/fr/LC_MESSAGES/requirements.po @@ -43,7 +43,7 @@ msgid "Python" msgstr "Python" #: ../../doc/requirements.rst:11 -msgid ">=3.8, <4" +msgid ">=3.9, <4" msgstr "" #: ../../doc/requirements.rst:12 diff --git a/doc/requirements.rst b/doc/requirements.rst index 28e894dd..6090dbb1 100644 --- a/doc/requirements.rst +++ b/doc/requirements.rst @@ -8,38 +8,29 @@ The :mod:`cdl` package requires the following Python modules: - Version - Summary * - Python - - >=3.8, <4 + - >=3.9, <4 - Python programming language - * - h5py - - >= 3.0 - - Read and write HDF5 files from Python - * - NumPy - - >= 1.21 - - Fundamental package for array computing in Python + * - guidata + - >= 3.7 + - Automatic GUI generation for easy dataset editing and display + * - PlotPy + - >= 2.7 + - Curve and image plotting tools for Python/Qt applications * - SciPy - - >= 1.7 + - >=1.5 - Fundamental algorithms for scientific computing in Python * - scikit-image - >= 0.18 - Image processing in Python * - pandas - - >= 1.3 + - >= 1.2 - Powerful data structures for data analysis, time series, and statistics * - PyWavelets - >= 1.1 - PyWavelets, wavelet transform module * - psutil - - >= 5.5 + - >= 5.7 - Cross-platform lib for process and system monitoring in Python. - * - guidata - - >= 3.6.2 - - Automatic GUI generation for easy dataset editing and display - * - PlotPy - - >= 2.6.2 - - Curve and image plotting tools for Python/Qt applications - * - QtPy - - >= 1.9 - - Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6). * - PyQt5 - >=5.11 - Python bindings for the Qt cross platform application toolkit diff --git a/doc/update_requirements.py b/doc/update_requirements.py index a4dd1d82..98dde779 100644 --- a/doc/update_requirements.py +++ b/doc/update_requirements.py @@ -15,5 +15,5 @@ if __name__ == "__main__": print("Updating requirements.rst file...", end=" ") - gen_module_req_rst(cdl, ["Python>=3.8", "PyQt5>=5.11"]) + gen_module_req_rst(cdl, ["Python>=3.9", "PyQt5>=5.11"]) print("done.") diff --git a/doc/validation_status_signal.csv b/doc/validation_status_signal.csv index 188e92b3..7973b278 100644 --- a/doc/validation_status_signal.csv +++ b/doc/validation_status_signal.csv @@ -32,7 +32,7 @@ :py:func:`compute_moving_median `,Compute moving median with :py:func:`scipy.ndimage.median_filter`,`test_signal_moving_median `_ :py:func:`compute_normalize `,Normalize data with :py:func:`cdl.algorithms.signal.normalize`,`test_signal_normalize `_ :py:func:`compute_offset_correction `,Correct offset: subtract the mean value of the signal in the specified range,`test_signal_offset_correction `_ -:py:func:`compute_peak_detection `,Peak detection with :py:func:`cdl.algorithms.signal.peak_indexes`,N/A +:py:func:`compute_peak_detection `,Peak detection with :py:func:`cdl.algorithms.signal.peak_indices`,N/A :py:func:`compute_phase_spectrum `,Compute phase spectrum,`test_signal_phase_spectrum `_ :py:func:`compute_power `,Compute power with :py:data:`numpy.power`,`test_signal_power `_ :py:func:`compute_product `,Multiply **dst** and **src** signals and return **dst** signal modified in place,`test_signal_product `_ diff --git a/pyproject.toml b/pyproject.toml index 66fa6426..88eb8a22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,11 +26,11 @@ classifiers = [ "Operating System :: Microsoft :: Windows :: Windows 10", "Operating System :: Microsoft :: Windows :: Windows 11", "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Image Processing", "Topic :: Scientific/Engineering :: Human Machine Interfaces", @@ -38,18 +38,15 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Widget Sets", ] -requires-python = ">=3.8, <4" +requires-python = ">=3.9, <4" dependencies = [ - "h5py >= 3.0", - "NumPy >= 1.21", - "SciPy >= 1.7", + "guidata >= 3.7", + "PlotPy >= 2.7", + "SciPy>=1.5", "scikit-image >= 0.18", - "pandas >= 1.3", + "pandas >= 1.2", "PyWavelets >= 1.1", - "psutil >= 5.5", - "guidata >= 3.6.2", - "PlotPy >= 2.6.2", - "QtPy >= 1.9", + "psutil >= 5.7", ] dynamic = ["version"] @@ -116,7 +113,7 @@ addopts = "cdl" exclude = [".git", ".vscode", "build", "dist"] line-length = 88 # Same as Black. indent-width = 4 # Same as Black. -target-version = "py38" # Assume Python 3.8 +target-version = "py39" # Assume Python 3.9. [tool.ruff.lint] # all rules can be found here: https://beta.ruff.rs/docs/rules/ diff --git a/resources/api-ms-win-core-path-l1-1-0.dll b/resources/api-ms-win-core-path-l1-1-0.dll new file mode 100644 index 00000000..d2a143b5 Binary files /dev/null and b/resources/api-ms-win-core-path-l1-1-0.dll differ diff --git a/resources/api-ms-win-core-path-l1-1-0.txt b/resources/api-ms-win-core-path-l1-1-0.txt new file mode 100644 index 00000000..2c32d0c7 --- /dev/null +++ b/resources/api-ms-win-core-path-l1-1-0.txt @@ -0,0 +1,12 @@ +api-ms-win-core-path-l1-1-0.dll +=============================== + +This is a Windows Core API library that provides functions for working with file paths. +The provided DLL was built using Wine's implementation of the Windows API. +See https://github.com/nalexandru/api-ms-win-core-path-HACK for more information. + +Information about the build: + +- Version of the DLL: 0.3.1 +- Date of the build: 2021-03-07 +- Release page: https://github.com/nalexandru/api-ms-win-core-path-HACK/releases/tag/0.3.1 \ No newline at end of file diff --git a/scripts/build_exe.bat b/scripts/build_exe.bat index b80cf771..d973accb 100644 --- a/scripts/build_exe.bat +++ b/scripts/build_exe.bat @@ -34,6 +34,11 @@ del "%RESPATH%\tmp-*.png" @REM Building executable pyinstaller %LIBNAME%.spec --noconfirm --clean + +@REM Windows 7 SP1 compatibility fix +copy "%RESPATH%\api-ms-win-core-path-l1-1-0.dll" "dist\%LIBNAME%\_internal" /Y + +@REM Zipping executable cd dist set ZIPNAME=%LIBNAME%-v%VERSION%_exe.zip "C:\Program Files\7-Zip\7z.exe" a -mx1 "%ZIPNAME%" %LIBNAME% diff --git a/scripts/release_win7.bat b/scripts/release_win7.bat deleted file mode 100644 index 91ca205c..00000000 --- a/scripts/release_win7.bat +++ /dev/null @@ -1,22 +0,0 @@ -@echo off - -call %~dp0utils GetScriptPath SCRIPTPATH -call %FUNC% GetLibName LIBNAME -call %FUNC% GetModName MODNAME -call %FUNC% SetPythonPath -call %FUNC% UsePython -call %FUNC% GetVersion VERSION - -echo =========================================================================== -echo Making %LIBNAME% v%VERSION% release with %WINPYDIRBASE% for Windows 7 -echo =========================================================================== - -set destdir=releases\%LIBNAME%-v%VERSION%-release -if not exist %destdir% ( mkdir %destdir% ) -pushd "wix" -ren %LIBNAME%-%VERSION%.msi %LIBNAME%-%VERSION%-Win7.msi -popd -move wix\%LIBNAME%-%VERSION%-Win7.msi %destdir% -explorer %destdir% - -call %FUNC% EndOfScript \ No newline at end of file