pax_global_header 0000666 0000000 0000000 00000000064 14435624103 0014514 g ustar 00root root 0000000 0000000 52 comment=e38185943ef5c7dfd4eb4a70e5d49a485285378c
CodraFT-2.2.1/ 0000775 0000000 0000000 00000000000 14435624103 0013000 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/.coveragerc 0000664 0000000 0000000 00000000156 14435624103 0015123 0 ustar 00root root 0000000 0000000 [run]
parallel = True
omit =
*/codraft/utils/tests.py
*/codraft/tests/*
*/guidata/*
*/guiqwt/* CodraFT-2.2.1/.env 0000664 0000000 0000000 00000000014 14435624103 0013564 0 ustar 00root root 0000000 0000000 PYTHONPATH=. CodraFT-2.2.1/.github/ 0000775 0000000 0000000 00000000000 14435624103 0014340 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14435624103 0016523 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001333 14435624103 0021215 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Installation information**
- CodraFT installation type ["Python package" or "stand-alone Windows version"]
- Copy/paste here the contents of "About CodraFT installation..." window (Menu "?")
**Additional context**
Add any other context about the problem here.
CodraFT-2.2.1/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000001134 14435624103 0022247 0 ustar 00root root 0000000 0000000 ---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
CodraFT-2.2.1/.gitignore 0000664 0000000 0000000 00000001642 14435624103 0014773 0 ustar 00root root 0000000 0000000 winpython.env
.spyderproject
doc.zip
Thumbs.db
doctmp/
.vs/
*.pyproj
*.sln
releases/
*.chm
.doctrees/
doc/install_requires.txt
# Created by https://www.gitignore.io/api/python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
_build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
# *.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
/.spyproject
CodraFT-2.2.1/.isort.cfg 0000664 0000000 0000000 00000000044 14435624103 0014675 0 ustar 00root root 0000000 0000000 [settings]
known_first_party=codraft CodraFT-2.2.1/.pylintrc 0000664 0000000 0000000 00000000722 14435624103 0014646 0 ustar 00root root 0000000 0000000 [FORMAT]
# Essential to be able to compare code side-by-side (`black` default setting)
# and best compromise to minimize file size
max-line-length=88
[TYPECHECK]
ignored-modules=qtpy.QtWidgets,qtpy.QtCore,qtpy.QtGui,cv2
[MESSAGES CONTROL]
disable=wrong-import-order
[DESIGN]
max-args=8 # default: 5
max-attributes=12 # default: 7
max-branches=17 # default: 12
max-locals=20 # default: 15
min-public-methods=0 # default: 2
max-public-methods=25 # default: 20 CodraFT-2.2.1/.vscode/ 0000775 0000000 0000000 00000000000 14435624103 0014341 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/.vscode/launch.json 0000664 0000000 0000000 00000006540 14435624103 0016513 0 ustar 00root root 0000000 0000000 {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Run CodraFT",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/codraft/app.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/dev.env",
"python": "${config:python.defaultInterpreterPath}",
"justMyCode": true,
"env": {
// "DEBUG": "1",
"LANG": "en",
"QT_COLOR_MODE": "light",
}
},
{
"name": "Run Test Launcher",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/codraft/tests/__init__.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/dev.env",
"python": "${config:python.defaultInterpreterPath}",
"justMyCode": true,
"env": {
// "DEBUG": "1",
"QT_COLOR_MODE": "light",
}
},
{
"name": "Run current file",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/dev.env",
"python": "${config:python.defaultInterpreterPath}",
"justMyCode": false,
"args": [
// "--h5browser",
// "${workspaceFolder}/codraft/data/tests/format_v1.7.h5",
// "C:/Dev/Projets/CodraFT_Test_CEA/codraft_test_cea/data/lmj_cbfx.h5,/A/valeur"
// "${workspaceFolder}/codraft/data/tests/format_v1.7.h5,/CodraFT_Ima/i002: i002+i004",
// "--mode",
// "unattended",
// "--verbose",
// "quiet",
// "screenshot",
// "--delay",
// "1"
],
"env": {
// "DEBUG": "1",
// "TEST_SEGFAULT_ERROR": "1",
"LANG": "en",
"QT_COLOR_MODE": "light",
}
},
{
"name": "Profile current file",
"type": "python",
"request": "launch",
"module": "cProfile",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/dev.env",
"python": "${config:python.defaultInterpreterPath}",
"args": [
"-o",
"${file}.prof",
"${file}"
],
},
{
"name": "Run H5browser",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/codraft_test_cea/tests/h5browser1_test.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/dev.env",
"python": "${config:python.defaultInterpreterPath}",
"justMyCode": true,
"env": {
"DEBUG": "1",
// "QT_COLOR_MODE": "light",
},
"args": [
// "--mode",
// "unattended",
],
},
]
} CodraFT-2.2.1/.vscode/settings.json 0000664 0000000 0000000 00000001672 14435624103 0017102 0 ustar 00root root 0000000 0000000 {
"[bat]": {
"files.encoding": "cp850",
},
"editor.rulers": [
88
],
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true,
"**/*.pyo": true
},
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"python.defaultInterpreterPath": "${env:PYTHON_CODRAFT_DEV}",
"editor.formatOnSave": true,
"python.sortImports.args": [
"--profile",
"black"
],
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"python.linting.pycodestyleArgs": [
"--max-line-length=88",
"--ignore=E203,W503"
],
"python.linting.pycodestyleEnabled": true,
"python.formatting.provider": "black",
"esbonio.server.enabled": true,
"restructuredtext.linter.doc8.extraArgs": [
"--ignore=D004"
],
"esbonio.sphinx.confDir": "${workspaceFolder}\\doc"
} CodraFT-2.2.1/.vscode/tasks.json 0000664 0000000 0000000 00000023062 14435624103 0016364 0 ustar 00root root 0000000 0000000 {
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "gettext - Scan",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"gettext_scan.bat",
],
"options": {
"cwd": "scripts",
"env": {
"UNATTENDED": "1",
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
}
},
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
},
{
"label": "gettext - Compile",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"gettext.bat",
"compile",
],
"options": {
"cwd": "scripts",
"env": {
"UNATTENDED": "1",
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
}
},
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
},
{
"label": "Run Pylint",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"run_pylint.bat",
"--disable=fixme",
"codraft",
],
"options": {
"cwd": "scripts",
"env": {
"UNATTENDED": "1",
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
}
},
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true
}
},
{
"label": "Run Coverage",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"run_coverage.bat",
// "--contains",
// "scenario",
],
"options": {
"cwd": "scripts",
"env": {
"UNATTENDED": "1",
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
}
},
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true
}
},
{
"label": "Upgrade environment",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"upgrade_env.bat"
],
"options": {
"cwd": "scripts",
"env": {
"UNATTENDED": "1",
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
}
},
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
},
{
"label": "Clean Up",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"clean_up.bat"
],
"options": {
"cwd": "scripts",
},
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
},
{
"label": "Create executable",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
"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": "Create installer",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
"UNATTENDED": "1",
"NSIS_COPYRIGHT_INFO": "Copyright (c) CEA-CODRA 2019-2022",
"NSIS_HELP_LINK": "https://codraft.readthedocs.io/en/latest/",
"NSIS_URLUPDATEINFO": "https://github.com/CODRA-Ingenierie-Informatique/CodraFT/releases",
"NSIS_URLINFOABOUT": "https://github.com/CODRA-Ingenierie-Informatique/CodraFT",
}
},
"args": [
"/c",
"build_installer.bat"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
}
},
{
"label": "Build documentation",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
"PYTHONPATH": "${env:PYTHONPATH_CODRAFT}",
"QT_COLOR_MODE": "light",
"UNATTENDED": "1",
}
},
"args": [
"/c",
"build_doc.bat"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
}
},
{
"label": "Build Python packages",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
"UNATTENDED": "1",
}
},
"args": [
"/c",
"build_dist.bat"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
},
"dependsOrder": "sequence",
"dependsOn": [
"Build documentation",
]
},
{
"label": "New release",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"release.bat"
],
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${env:PYTHON_CODRAFT_DEV}",
"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",
"Build Python packages",
"Create executable",
"Create installer",
]
},
]
} CodraFT-2.2.1/CHANGELOG.md 0000664 0000000 0000000 00000053435 14435624103 0014623 0 ustar 00root root 0000000 0000000 # CodraFT Releases #
See CodraFT [roadmap page](https://codraft.readthedocs.io/en/latest/roadmap.html)
for future and past milestones.
## Version 2.2.1 ##
Bug fixes:
* Fixed 1D FFT (added optionnal frequency shift)
* Fixed ROI/pixel alignment issue
## Version 2.2.0 ##
New features:
* Images: added support for XYZ image files
* All shapes: removed shape drag symbols, so that background image is no longer
masked by small-sized shapes
* At startup, restoring last current panel (image or signal panel)
* Plot cleanup and shape management: greatly optimized performance
* After removing object(s) (signal/image), the previous object in the list is selected
* Added default image visualization settings in .INI configuration file
* Using guiqwt v4.3.2: fixed pixel position (first pixel is centered at (0,0) coords)
## Version 2.1.4 ##
Bug fixes:
* HDF5 import/browser features: added support for non-ASCII dataset names
* ANDOR SIF files:
* Fixed compatibility issues for various SIF files
* Fixed unicode error
* Image Contour detection:
* Fixed level default value for 8-bit data
* Added missing "level" parameter
* Dev/VSCode: simplified `launch.json` and fixed environment variable substitution issue
Other changes:
* Alpha/beta release: fixed installer, added warning
## Version 2.1.3 ##
Bug fixes:
* Panel's object list `select_rows` method: fixed plot refresh behavior in case of
multiple selection (refresh widget only once)
* LMJ-formatted HDF5 file: now reading invalid compound datasets
* [Issue #16](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/issues/16) - Embedding CodraFT: "add_object" method call with invalid data should lead to app crash
* Panel's `add_object` method (public API): check data type before adding object to
panel - this prevents CodraFT from crashing when trying to plot invalid data type
afterwards
* Now handling exceptions in `add_object` and `insert_object` methods
* Multigaussian curve fitting: fixed default fit parameters
* Improved I/O application test with respect to unsupported filetypes
Other changes:
* Images: added support for `numpy.int32` datatype
* Added unit tests for all curve fitting dialogs
## Version 2.1.2 ##
Bug fixes:
* [Pull Request #2](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/pull/2) - Load / Save conventional CSVs, by [@aanastasiou](https://github.com/aanastasiou)
* [Issue #3](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/issues/3) - Wrong units/titles are displayed
* [Issue #6](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/issues/6) - 2D peak detection: GUI freezes when creating ROIs
* [Issue #4](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/issues/4) - Processing multiple images/signals: avoid unnecessary time-consuming plot updates
* [Issue #7](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/issues/7) - Image/Circular ROI: IndexError when circle exceeds the image size
* [Issue #5](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/issues/5) - ROI dialog box: unable to remove all ROIs and validate
* [Issue #8](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/issues/8) - HDF5 import: unable to easily distinguish datasets with the same name but different path
* Average operation now merges ROI data (i.e. same behavior as sum)
* Fixed multiple regressions with ROI management (adding, removing ROI, ...)
Other changes:
* Optimized load time (especially for images): avoid unnecessary refresh when adding objects
* Added "Remove regions of interest" entry to "Computing" menu (and context menu)
* Signal/image list: added tooltip showing a summary of metadata values (e.g. when
importing data from HDF5, this shows HDF5 filename and HDF5 dataset path) - Issue #8
* Dependencies hash check: feature is now OS-dependent (+ more explicit messages)
* Slightly improved test coverage
## Version 2.1.1 ##
Changes:
* Image Regions Of Interest (ROI):
* ROIs are now shown as masks (areas outside ROIs are shaded)
* Added support for circular ROIs
* ROIs now take into account pixel size (dx, dy) as well as origin (x0, y0)
* Signal and Image ROIs:
* New default extract mode: creating as many signals/images as ROIs (each ROI is
extracted into a single signal/image)
* The old extract mode (single signal/image output) is still available and may be
enabled using the new checkbox added in ROI extraction dialog box
* Image visualization:
* Added "Show contrast panel" option in toolbar and view menu
* By default, contrast panel is now visible
* When multiple images are selected, the first image LUT range is applied to all
* "View in a new window": now opens non-modal dialogs, thus allowing to visualize
multiple signals or images in separate windows
* Added demo mode (from command line, simply run: codraft-demo)
* Command line option --h5 is now a positionnal argument (h5)
* Added command line option -b (or --h5browser) to browse a HDF5 file at startup
* Added command line option --version to show CodraFT version
Bug fixes:
* Image computations now takes into account origin (x0, y0), pixel size (dx, dy) as
well as regions of interest (related features: centroid, enclosing circle, 2D peak
detection and contour detection)
* Image ROI definition dialog: maximum rows and columns were erroneously truncated
* Centralized argument parsing in CodraFT exec env object, thus avoiding conflicts
## Version 2.0.3 ##
Bug fixes:
* Fixed pen.setWidth TypeError on Linux
Other changes:
* Added an option to ignore dependency check warning at startup
* Installation configuration viewer: added info on dependency check result
* Ignore when unable to save h5 in ima/sig test scenarios
## Version 2.0.2 ##
The following major changes were introduced with CodraFT V2:
* Fully automated high-level processing features for internal testing purpose, as well
as embedding CodraFT in a third-party software
* Extensive test suite (unit tests and application tests) with 90% feature coverage
* Segmentation fault and Python exception logging
* Customizable annotations for both signals and images
### Release key features ###
* New data visualization and processing features:
| Signal | Image | Feature |
|:------:|:-----:|--------------------------------------------------------|
| | • | Automatic 2D-peak detection |
| | • | Automatic contour extraction (circle/ellipse fit) |
| • | • | Multiple Regions of Interest (ROIs) |
| | • | User-defined annotations (labels and geometric shapes) |
| • | • | "Statistics" computing feature |
* Automation of high-level processing features: added fully automated high-level test
scenarios, and enhanced public API for embedding CodraFT into a third-party application
* Test Driven Development with high quality standards
(pylint score >= 9.8/10, test coverage >= 90%)
### Detailed feature list ###
New data visualization and processing features:
* Image:
* New automatic image contour detection feature returning fitted circle/ellipse
* New automatic 2D peak detection feature (optionally create ROIs)
* "View in a new window": added customizable "Annotations" support for both signal
and image panels - supports user-defined annotations (points, segments, circles,
ellipses, labels,...) which are serialized in image metadata
* Added "Show graphical object titles" option in "View" menu to show or hide the title
(or subtitle) of ROIs or any other graphical object
* Added support for **multiple** Regions of Interest (ROI):
* All "Computing" menu features apply to multiple ROIs
* Computation result arrays now contains ROI index (first column) and one row per ROI
* ROI are merged when summing objects (signals or images)
* ROI can be removed, modified or added at any time
* Added option "Show graphical object titles" ("View" menu) to show or hide ROI titles or any
other geometrical shapes title (or subtitle)
* New computing "Statistics" feature showing a table with statistics on image/signal
and eventually regions of interest (min, max, mean, standard deviation, sum, ...)
New general purpose features:
* Memory management:
* New available memory indicator on main window status bar
* New warning dialog box when trying to open/create data if available memory is below
the "available_memory_threshold" defined in CodraFT configuration file (default: 500MB)
* Error handling:
* New integrated log file viewer
* New warning dialog box at startup suggesting to view log files when logs were
generated during last session
* Logging segmentation faults in ".CodraFT_faulthandler.log"
* Logging Python exceptions in ".CodraFT_traceback.log"
* Signal/Image metadata:
* New copy/paste feature: update object metadata from another one
* New import/export feature: import-export object metadata (JSON text file) using the
new "Import metadata into" / "Export metadata from" entries in "File" menu
* HDF5 browser feature: complete redesign (better compatibility, evolutive design, ...)
* Added support for multiple HDF5 files opening at once
* Added `.CodraFT.ini` configuration file (user home directory):
* New configuration file entry: current working directory
* New configuration file entry: current main window size and position
* New configuration file entry: embedded Python console enable state
* New configuration file entry: available memory alarm threshold
New test-related features:
* Added non-interactive tests, opening the way for unit tests with better coverage
* Added "unattended" and "screenshot" execution modes respectively for testing and documentation purpose
* Added automated high-level test scenarios (signal and image processing)
* Tests are now splitted in two categories: unit tests (`*_unit.py`) and application tests (`*_app.py`).
* Added Coverage.py support
* Added "all_tests.py" to run all tests in unattended mode
New dependencies:
* [scikit-image](https://pypi.org/project/scikit-image/)
* [psutil](https://pypi.org/project/psutil/)
Other changes (on existing features):
* Image and Signal:
* Object properties panel: added data type information (feature refactored upstream to guidata)
* New random signal/image: added support for both Normal and Uniform distributions
* Operations "sum" and "average" now merge metadata results
* Computed titles "s/i000" are now renamed after inserting/removing an object
* Computing results (geometrical shapes: segment, circle, ellipse): numerical results
are now automatically added to metadata (respectively: length, center and radius,
center, a and b)
* Image:
* Added support for image origin and pixel size
* Flat field correction: added threshold parameter
* "New image" now creates an image with the same data type as selected image
* "New image" now supports uint16 data type
* Signal:
* Peak detection: added minimal distance parameter
* Fit dialog / plot: do auto scale at startup
* Peak detection dialog: preselect horizontal cursor at startup
* `codraft.core.gui` code refactoring: added subpackage `core.gui.processor`
* Added "Browse HDF5" action to main window ("Open HDF5" now imports all data)
Bug fixes:
* HDF5 file import: converted `bytes` metadata to `str`
* Added h5py to requirements (setup.py)
* Plot: reintroduced pure white background in light mode (white background was removed
unintentionally when introducing dark mode)
* Image:
* "Clean-up data view" feature was accidently removing grid
* Fixed hard crash when trying to visualize images with NaNs (use case: result of
any filter on `uint8` image)
* Fixed hard crash when using image Z-axis log scale on some images
* Fixed DICOM support
* Fixed hard crash in "to_codraft" (cross section item with empty data)
* Fixed image visualization parameters update from metadata
* MinEnclosingCircle: fixed sqrt(2) error
* Signal:
* "Clean-up data view" feature was accidently removing legend box and grid
* Fixed integral (missing initial point)
* Fixed plotting support for complex data
* Fixed signal visualization parameters update from metadata
## Version 1.7.2 ##
Bug fixes:
* Fixed unit test "app1_test.py" (create a single QApp)
* Fixed progress bar cancel issues (when passing HDF5 files to `app.run` function)
* Fixed random hard crash when opening curve fitting dialog
* Fixed curve fitting dialog parenting
* ROI metadata is now removed (because potentially invalid) after performing a
computation that changes X-axis or Y-axis data (e.g. ROI extraction, image flip,
image rotation, etc.)
* Fixed image creation features (broken since major refactoring)
Other changes:
* Removed deprecated Qt `exec_` calls (replaced by `exec`)
* Added more infos on uninstaller registry keys
* Added documentation on key features
## Version 1.7.1 ##
Added first page of documentation (there is a beginning to everything...).
Bug fixes:
* Cross section tool was working only on first image in item list
* Separate view was broken since major refactoring
## Version 1.7.0 ##
New features:
* Python 3.8 is now the reference Python release
* Dropped Python 2 and PyQt 4 support
* Major code cleaning and refactoring
* Reorganized the whole code base
* Added more unit tests
* Added GUI-based test launcher
* Added isort/black code formatting
* Switched from cx_Freeze to pyinstaller for generating the stand-alone version
* Improved pylint score up to 9.90/10 with strict quality criteria
## Version 1.6.0 ##
New features:
* Added dependencies check on startup: warn the user if at least one dependency
has been altered (i.e. the application has not been qualified in this context)
* Added py3compat (since QtPy is dropping Python 3 support)
## Version 1.5.0 ##
New features:
* Sum, average, difference, multiplication: re-converting data to initial type.
* Now supporting PySide2/PyQt4/PyQt5 compatibility thanks to
guidata >= v1.7.9 (using QtPy).
* Now supporting Python 3.9 and NumPy 1.20.
Bug fixes:
* Fixed cross section retrieval feature: in stand-alone mode, a new CodraFT
window was created (that is not the expected behavior).
* Fixed crash when enabling cross sections on main window (needs PythonQwt 0.9.2).
* Fixed ValueError when generating a 2D-gaussian image with floats.
* HDF5 file import feature:
* Fixed unit processing (parsing) with Python 3.
* Fixed critical bug when clicking on "Check all".
## Version 1.4.4 ##
New experimental features:
* Experimental support for PySide2/PyQt4/PyQt5 thanks to guidata >= v1.7.9 (using QtPy).
* Experimental support for Python 3.9 and NumPy 1.20.
New minor features:
* ZAxisLogTool: update automatically Z-axis scale (+ showing real value)
* Added contrast test (following issues with "eliminate_outliers")
## Version 1.4.3 ##
New minor features:
* New test script for global application test (test_app.py).
* Improved CodraFT launcher (app.py).
## Version 1.4.2 ##
New minor features:
* LMJ-formatted HDF5 file import: tree widget item's tooltip now
shows item data "description".
Bug fixes:
* Fixed runtime warnings when computing centroid coordinates on
an image ROI filled with zeros.
* LMJ-formatted HDF5 file support: fixed truncated units.
## Version 1.4.1 ##
Bug fixes:
* Fixed LMJ-formatted HDF5 files: strings are encoded in "latin-1"
which is not the expected behavior ("utf-8" is the expected
encoding for ensuring better compatibility).
## Version 1.4.0 ##
New features:
* LMJ-formatted HDF5 file import: added support for axis units and labels.
* New curve style behavior (more readable): unselecting items by default,
circling over curve colors when selecting multiple curve items.
Bug fixes:
* Fixed LMJ-formatted HDF5 file support in CodraFT data import feature.
## Version 1.3.1 ##
Bug fixes:
* Improved support for LMJ-formatted HDF5 files.
* Z-axis logscale feature: freeing memory when mode is off.
* CodraFTMainWindow.get_instance: create instance if it doesn't already exist.
* to_codraft: show CodraFT main window on top, if not already visible.
* Patch/guiqwt.histogram: removing histogram curve (if necessary)
when image item has been removed.
## Version 1.3.0 ##
New features:
* Image computations: added "Smallest enclosing circle center" computation.
* Added support for FXD image file type.
Bug fixes:
* Fixed image levels "Log scale" feature for Python 3 compatibility.
## Version 1.2.2 ##
New features:
* Added "Delete all" entry to "Edit" menu: this removes all objects (signals or
images) from current view.
* Added an option "hide_on_close" to CodraFTMainWindow class constructor
(default value is False): when set to True, CodraFT main window will simply
hide when "Close" button is clicked, which is the expected behavior when
embedding CodraFT in another application.
Bug fixes:
* The memory leak fix in app.py was accidentally commented before commit.
## Version 1.2.1 ##
Bug fixes:
* When quitting CodraFT, objects were not deleted: this was causing a memory
leak when embedding CodraFT in another Qt window.
* When canceling HDF5 import dialog box after selecting at least one signal or
image, the progress bar was shown even if no data was being imported.
* When closing HDF5 import dialog box, preview signal/image widgets were not
deleted, hence causing another memory leak.
## Version 1.2.0 ##
New features:
* Added support for uint32 images (converting to int32 data)
* Added "Z-axis logarithmic scale" feature for image items (check out the new
entries in standard image toolbar and context menu)
* Added "HDF5 I/O Toolbar" to avoid a frequently reported user confusion
between HDF5 I/O icons and Signal/Image specific I/O icons (i.e. open and
save actions)
* Cross-section panels are now configured to show only cross-section curves
associated to the currently selected image (instead of showing all curves,
including those associated to hidden images)
* Image subtraction: now handling integer underflow
Bug fixes:
* When "Clean up data view" option was enabled, image histogram was not updated
properly when changing image selection (histogram was the sum of all images
histograms).
* Changed default image levels histogram "eliminate outliers" value: .1% instead
of 2% to avoid display bug for noise background images for example (i.e.
images with high contrast and very narrow histogram levels)
## Version 1.1.2 ##
Bug fixes:
* When the X/Y Cross Section widget is embedded into a main window other than
CodraFT's, clicking on the "Process signal" button will send the signal to
CodraFT's signal panel for further processing, as expected.
## Version 1.1.1 ##
Bug fixes:
* Fixed a bug leading to "None" titles when importing signals/images from HDF5
files created outside CodraFT.
## Version 1.1.0 ##
New features:
* Added new icons.
* Images:
* Added support for SPIRICON image files (single-frame support only).
Bug fixes:
* Fixed a critical bug when opening HDF5 file (bug from "guidata" package).
Now guidata is patched inside CodraFT to take into account the unusual/risky
PyQt patch from Taurus package (PyQt API is set to 2 for QString objects and
instead of raising an ImportError when importing QString from PyQt4.QtCore,
QString still exists and is replaced by "str"...).
* Images:
* Centroid feature: coordinates were mixed up in CodraFT application.
* Signals:
* Curve fitting (gaussian and lorentzian): fixed amplitude initial value
for automatic fitting feature
* FWHM and FW1/e²: fixed amplitude computation for input fit parameters
and output results
## Version 1.0.0 ##
Copyright © 2018 Codra, Pierre Raybaut, licensed under the terms of the
CECILL License v2.1.
First release of `CodraFT`.
New features:
* Added support for both Python 3 and Python 2.7, and both PyQt5 and PyQt4.
* Added HDF5 file reading support, using a new HDF5 browser with embedded
curve and image preview.
* Signal and Image:
* Added menu "Computing" for computing scalar values from signals/images.
* Added "ROI definition" for "Computing" features
* Added absolute value operation.
* Added 10 base logarithm operation.
* Added moving average/median filtering feature.
* Images:
* Added support for Andor SIF image files (support multiple frames).
* Added centroid computing feature.
* Added support for images containing NaN values.
* Signals:
* Added FWHM computing feature (based on curve fitting)
* Added Full Width at 1/e² computing feature (based on gaussian fitting)
* Added derivative and integral computation features.
* Added "lorentzian" and "Voigt" to "new signals" available.
* Added curve fitting feature supporting various models (polynomial,
gaussian, lorentzian, Voigt and multi-gaussian). Computed fitting
parameters are stored in signal's metadata (a new dictionnary item
for the Signal objects)
* Edit menu: added a new "View in a new window" action
* Added standard keyboard shortcuts (new, open, copy, etc.)
* "New image": added new 2D-gaussian creation feature
* Added a GUI-based ROI extraction feature for both signal and image views
* Added a pop-up dialog when double-clicking on a signal/image to allow
visualizing things on a possibly large window
* Added a peak detection feature
* Added centroid coordinates in image statistics tool
* Added support for curve/image titles, axis labels and axis units (those can
be modified through the editable form within the "Properties" groupbox)
* Added support for cross section extraction from the image widget to the
signal tab ; the extracted curve's title shows the associated coordinates
* Added deployment script for building self-consistent executable distribution
using the cx_Freeze tool
* Improved curve visual: background is now flat and white
Bug fixes:
* Console dockwidget is now created after the `View` menu so that it appears
in it, as expected. It is now hidden by default.
* Improved curve visual when selected: instead of adding big black squares
along a selected curve, the curve line is simply broader when selected.
CodraFT-2.2.1/CodraFT.spec 0000664 0000000 0000000 00000003672 14435624103 0015146 0 ustar 00root root 0000000 0000000 # -*- mode: python ; coding: utf-8 -*-
block_cipher = None
import sys
sitepackages = os.path.join(sys.prefix, 'Lib', 'site-packages')
guidata_images = os.path.join(sitepackages, 'guidata', 'images')
guidata_locale = os.path.join(sitepackages, 'guidata', 'locale', 'fr', 'LC_MESSAGES')
guiqwt_images = os.path.join(sitepackages, 'guiqwt', 'images')
guiqwt_locale = os.path.join(sitepackages, 'guiqwt', 'locale', 'fr', 'LC_MESSAGES')
a = Analysis(['codraft\\start.pyw'],
pathex=[],
binaries=[],
datas=[
(guidata_images, 'guidata\\images'),
(guidata_locale, 'guidata\\locale\\fr\\LC_MESSAGES'),
(guiqwt_images, 'guiqwt\\images'),
(guiqwt_locale, 'guiqwt\\locale\\fr\\LC_MESSAGES'),
('codraft\\data', 'codraft\\data'),
('codraft\\locale\\fr\\LC_MESSAGES\\codraft.mo', 'codraft\\locale\\fr\\LC_MESSAGES'),
],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='CodraFT',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None , icon='resources\\codraft.ico')
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='CodraFT')
CodraFT-2.2.1/LICENSE 0000664 0000000 0000000 00000003435 14435624103 0014012 0 ustar 00root root 0000000 0000000 The Licensee is authorized to use the Software according to one of the following
compatible agreements:
* BSD 3-Clause License Agreement (see below)
* CeCILL-B License Agreement (see Licence_CeCILL-B_V1-en.txt)
----------------------------------------------------------------------------------------
BSD 3-Clause License
Copyright (c) 2022, CEA-CODRA
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
CodraFT-2.2.1/Licence_CeCILL-B_V1-en.txt 0000664 0000000 0000000 00000051624 14435624103 0017313 0 ustar 00root root 0000000 0000000
CeCILL-B FREE SOFTWARE LICENSE AGREEMENT
Notice
This Agreement is a Free Software license agreement that is the result
of discussions between its authors in order to ensure compliance with
the two main principles guiding its drafting:
* firstly, compliance with the principles governing the distribution
of Free Software: access to source code, broad rights granted to
users,
* secondly, the election of a governing law, French law, with which
it is conformant, both as regards the law of torts and
intellectual property law, and the protection that it offers to
both authors and holders of the economic rights over software.
The authors of the CeCILL-B (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre])
license are:
Commissariat à l'Energie Atomique - CEA, a public scientific, technical
and industrial research establishment, having its principal place of
business at 25 rue Leblanc, immeuble Le Ponant D, 75015 Paris, France.
Centre National de la Recherche Scientifique - CNRS, a public scientific
and technological establishment, having its principal place of business
at 3 rue Michel-Ange, 75794 Paris cedex 16, France.
Institut National de Recherche en Informatique et en Automatique -
INRIA, a public scientific and technological establishment, having its
principal place of business at Domaine de Voluceau, Rocquencourt, BP
105, 78153 Le Chesnay cedex, France.
Preamble
This Agreement is an open source software license intended to give users
significant freedom to modify and redistribute the software licensed
hereunder.
The exercising of this freedom is conditional upon a strong obligation
of giving credits for everybody that distributes a software
incorporating a software ruled by the current license so as all
contributions to be properly identified and acknowledged.
In consideration of access to the source code and the rights to copy,
modify and redistribute granted by the license, users are provided only
with a limited warranty and the software's author, the holder of the
economic rights, and the successive licensors only have limited liability.
In this respect, the risks associated with loading, using, modifying
and/or developing or reproducing the software by the user are brought to
the user's attention, given its Free Software status, which may make it
complicated to use, with the result that its use is reserved for
developers and experienced professionals having in-depth computer
knowledge. Users are therefore encouraged to load and test the
suitability of the software as regards their requirements in conditions
enabling the security of their systems and/or data to be ensured and,
more generally, to use and operate it in the same conditions of
security. This Agreement may be freely reproduced and published,
provided it is not altered, and that no provisions are either added or
removed herefrom.
This Agreement may apply to any or all software for which the holder of
the economic rights decides to submit the use thereof to its provisions.
Article 1 - DEFINITIONS
For the purpose of this Agreement, when the following expressions
commence with a capital letter, they shall have the following meaning:
Agreement: means this license agreement, and its possible subsequent
versions and annexes.
Software: means the software in its Object Code and/or Source Code form
and, where applicable, its documentation, "as is" when the Licensee
accepts the Agreement.
Initial Software: means the Software in its Source Code and possibly its
Object Code form and, where applicable, its documentation, "as is" when
it is first distributed under the terms and conditions of the Agreement.
Modified Software: means the Software modified by at least one
Contribution.
Source Code: means all the Software's instructions and program lines to
which access is required so as to modify the Software.
Object Code: means the binary files originating from the compilation of
the Source Code.
Holder: means the holder(s) of the economic rights over the Initial
Software.
Licensee: means the Software user(s) having accepted the Agreement.
Contributor: means a Licensee having made at least one Contribution.
Licensor: means the Holder, or any other individual or legal entity, who
distributes the Software under the Agreement.
Contribution: means any or all modifications, corrections, translations,
adaptations and/or new functions integrated into the Software by any or
all Contributors, as well as any or all Internal Modules.
Module: means a set of sources files including their documentation that
enables supplementary functions or services in addition to those offered
by the Software.
External Module: means any or all Modules, not derived from the
Software, so that this Module and the Software run in separate address
spaces, with one calling the other when they are run.
Internal Module: means any or all Module, connected to the Software so
that they both execute in the same address space.
Parties: mean both the Licensee and the Licensor.
These expressions may be used both in singular and plural form.
Article 2 - PURPOSE
The purpose of the Agreement is the grant by the Licensor to the
Licensee of a non-exclusive, transferable and worldwide license for the
Software as set forth in Article 5 hereinafter for the whole term of the
protection granted by the rights over said Software.
Article 3 - ACCEPTANCE
3.1 The Licensee shall be deemed as having accepted the terms and
conditions of this Agreement upon the occurrence of the first of the
following events:
* (i) loading the Software by any or all means, notably, by
downloading from a remote server, or by loading from a physical
medium;
* (ii) the first time the Licensee exercises any of the rights
granted hereunder.
3.2 One copy of the Agreement, containing a notice relating to the
characteristics of the Software, to the limited warranty, and to the
fact that its use is restricted to experienced users has been provided
to the Licensee prior to its acceptance as set forth in Article 3.1
hereinabove, and the Licensee hereby acknowledges that it has read and
understood it.
Article 4 - EFFECTIVE DATE AND TERM
4.1 EFFECTIVE DATE
The Agreement shall become effective on the date when it is accepted by
the Licensee as set forth in Article 3.1.
4.2 TERM
The Agreement shall remain in force for the entire legal term of
protection of the economic rights over the Software.
Article 5 - SCOPE OF RIGHTS GRANTED
The Licensor hereby grants to the Licensee, who accepts, the following
rights over the Software for any or all use, and for the term of the
Agreement, on the basis of the terms and conditions set forth hereinafter.
Besides, if the Licensor owns or comes to own one or more patents
protecting all or part of the functions of the Software or of its
components, the Licensor undertakes not to enforce the rights granted by
these patents against successive Licensees using, exploiting or
modifying the Software. If these patents are transferred, the Licensor
undertakes to have the transferees subscribe to the obligations set
forth in this paragraph.
5.1 RIGHT OF USE
The Licensee is authorized to use the Software, without any limitation
as to its fields of application, with it being hereinafter specified
that this comprises:
1. permanent or temporary reproduction of all or part of the Software
by any or all means and in any or all form.
2. loading, displaying, running, or storing the Software on any or
all medium.
3. entitlement to observe, study or test its operation so as to
determine the ideas and principles behind any or all constituent
elements of said Software. This shall apply when the Licensee
carries out any or all loading, displaying, running, transmission
or storage operation as regards the Software, that it is entitled
to carry out hereunder.
5.2 ENTITLEMENT TO MAKE CONTRIBUTIONS
The right to make Contributions includes the right to translate, adapt,
arrange, or make any or all modifications to the Software, and the right
to reproduce the resulting software.
The Licensee is authorized to make any or all Contributions to the
Software provided that it includes an explicit notice that it is the
author of said Contribution and indicates the date of the creation thereof.
5.3 RIGHT OF DISTRIBUTION
In particular, the right of distribution includes the right to publish,
transmit and communicate the Software to the general public on any or
all medium, and by any or all means, and the right to market, either in
consideration of a fee, or free of charge, one or more copies of the
Software by any means.
The Licensee is further authorized to distribute copies of the modified
or unmodified Software to third parties according to the terms and
conditions set forth hereinafter.
5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION
The Licensee is authorized to distribute true copies of the Software in
Source Code or Object Code form, provided that said distribution
complies with all the provisions of the Agreement and is accompanied by:
1. a copy of the Agreement,
2. a notice relating to the limitation of both the Licensor's
warranty and liability as set forth in Articles 8 and 9,
and that, in the event that only the Object Code of the Software is
redistributed, the Licensee allows effective access to the full Source
Code of the Software at a minimum during the entire period of its
distribution of the Software, it being understood that the additional
cost of acquiring the Source Code shall not exceed the cost of
transferring the data.
5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE
If the Licensee makes any Contribution to the Software, the resulting
Modified Software may be distributed under a license agreement other
than this Agreement subject to compliance with the provisions of Article
5.3.4.
5.3.3 DISTRIBUTION OF EXTERNAL MODULES
When the Licensee has developed an External Module, the terms and
conditions of this Agreement do not apply to said External Module, that
may be distributed under a separate license agreement.
5.3.4 CREDITS
Any Licensee who may distribute a Modified Software hereby expressly
agrees to:
1. indicate in the related documentation that it is based on the
Software licensed hereunder, and reproduce the intellectual
property notice for the Software,
2. ensure that written indications of the Software intended use,
intellectual property notice and license hereunder are included in
easily accessible format from the Modified Software interface,
3. mention, on a freely accessible website describing the Modified
Software, at least throughout the distribution term thereof, that
it is based on the Software licensed hereunder, and reproduce the
Software intellectual property notice,
4. where it is distributed to a third party that may distribute a
Modified Software without having to make its source code
available, make its best efforts to ensure that said third party
agrees to comply with the obligations set forth in this Article .
If the Software, whether or not modified, is distributed with an
External Module designed for use in connection with the Software, the
Licensee shall submit said External Module to the foregoing obligations.
5.3.5 COMPATIBILITY WITH THE CeCILL AND CeCILL-C LICENSES
Where a Modified Software contains a Contribution subject to the CeCILL
license, the provisions set forth in Article 5.3.4 shall be optional.
A Modified Software may be distributed under the CeCILL-C license. In
such a case the provisions set forth in Article 5.3.4 shall be optional.
Article 6 - INTELLECTUAL PROPERTY
6.1 OVER THE INITIAL SOFTWARE
The Holder owns the economic rights over the Initial Software. Any or
all use of the Initial Software is subject to compliance with the terms
and conditions under which the Holder has elected to distribute its work
and no one shall be entitled to modify the terms and conditions for the
distribution of said Initial Software.
The Holder undertakes that the Initial Software will remain ruled at
least by this Agreement, for the duration set forth in Article 4.2.
6.2 OVER THE CONTRIBUTIONS
The Licensee who develops a Contribution is the owner of the
intellectual property rights over this Contribution as defined by
applicable law.
6.3 OVER THE EXTERNAL MODULES
The Licensee who develops an External Module is the owner of the
intellectual property rights over this External Module as defined by
applicable law and is free to choose the type of agreement that shall
govern its distribution.
6.4 JOINT PROVISIONS
The Licensee expressly undertakes:
1. not to remove, or modify, in any manner, the intellectual property
notices attached to the Software;
2. to reproduce said notices, in an identical manner, in the copies
of the Software modified or not.
The Licensee undertakes not to directly or indirectly infringe the
intellectual property rights of the Holder and/or Contributors on the
Software and to take, where applicable, vis-à-vis its staff, any and all
measures required to ensure respect of said intellectual property rights
of the Holder and/or Contributors.
Article 7 - RELATED SERVICES
7.1 Under no circumstances shall the Agreement oblige the Licensor to
provide technical assistance or maintenance services for the Software.
However, the Licensor is entitled to offer this type of services. The
terms and conditions of such technical assistance, and/or such
maintenance, shall be set forth in a separate instrument. Only the
Licensor offering said maintenance and/or technical assistance services
shall incur liability therefor.
7.2 Similarly, any Licensor is entitled to offer to its licensees, under
its sole responsibility, a warranty, that shall only be binding upon
itself, for the redistribution of the Software and/or the Modified
Software, under terms and conditions that it is free to decide. Said
warranty, and the financial terms and conditions of its application,
shall be subject of a separate instrument executed between the Licensor
and the Licensee.
Article 8 - LIABILITY
8.1 Subject to the provisions of Article 8.2, the Licensee shall be
entitled to claim compensation for any direct loss it may have suffered
from the Software as a result of a fault on the part of the relevant
Licensor, subject to providing evidence thereof.
8.2 The Licensor's liability is limited to the commitments made under
this Agreement and shall not be incurred as a result of in particular:
(i) loss due the Licensee's total or partial failure to fulfill its
obligations, (ii) direct or consequential loss that is suffered by the
Licensee due to the use or performance of the Software, and (iii) more
generally, any consequential loss. In particular the Parties expressly
agree that any or all pecuniary or business loss (i.e. loss of data,
loss of profits, operating loss, loss of customers or orders,
opportunity cost, any disturbance to business activities) or any or all
legal proceedings instituted against the Licensee by a third party,
shall constitute consequential loss and shall not provide entitlement to
any or all compensation from the Licensor.
Article 9 - WARRANTY
9.1 The Licensee acknowledges that the scientific and technical
state-of-the-art when the Software was distributed did not enable all
possible uses to be tested and verified, nor for the presence of
possible defects to be detected. In this respect, the Licensee's
attention has been drawn to the risks associated with loading, using,
modifying and/or developing and reproducing the Software which are
reserved for experienced users.
The Licensee shall be responsible for verifying, by any or all means,
the suitability of the product for its requirements, its good working
order, and for ensuring that it shall not cause damage to either persons
or properties.
9.2 The Licensor hereby represents, in good faith, that it is entitled
to grant all the rights over the Software (including in particular the
rights set forth in Article 5).
9.3 The Licensee acknowledges that the Software is supplied "as is" by
the Licensor without any other express or tacit warranty, other than
that provided for in Article 9.2 and, in particular, without any warranty
as to its commercial value, its secured, safe, innovative or relevant
nature.
Specifically, the Licensor does not warrant that the Software is free
from any error, that it will operate without interruption, that it will
be compatible with the Licensee's own equipment and software
configuration, nor that it will meet the Licensee's requirements.
9.4 The Licensor does not either expressly or tacitly warrant that the
Software does not infringe any third party intellectual property right
relating to a patent, software or any other property right. Therefore,
the Licensor disclaims any and all liability towards the Licensee
arising out of any or all proceedings for infringement that may be
instituted in respect of the use, modification and redistribution of the
Software. Nevertheless, should such proceedings be instituted against
the Licensee, the Licensor shall provide it with technical and legal
assistance for its defense. Such technical and legal assistance shall be
decided on a case-by-case basis between the relevant Licensor and the
Licensee pursuant to a memorandum of understanding. The Licensor
disclaims any and all liability as regards the Licensee's use of the
name of the Software. No warranty is given as regards the existence of
prior rights over the name of the Software or as regards the existence
of a trademark.
Article 10 - TERMINATION
10.1 In the event of a breach by the Licensee of its obligations
hereunder, the Licensor may automatically terminate this Agreement
thirty (30) days after notice has been sent to the Licensee and has
remained ineffective.
10.2 A Licensee whose Agreement is terminated shall no longer be
authorized to use, modify or distribute the Software. However, any
licenses that it may have granted prior to termination of the Agreement
shall remain valid subject to their having been granted in compliance
with the terms and conditions hereof.
Article 11 - MISCELLANEOUS
11.1 EXCUSABLE EVENTS
Neither Party shall be liable for any or all delay, or failure to
perform the Agreement, that may be attributable to an event of force
majeure, an act of God or an outside cause, such as defective
functioning or interruptions of the electricity or telecommunications
networks, network paralysis following a virus attack, intervention by
government authorities, natural disasters, water damage, earthquakes,
fire, explosions, strikes and labor unrest, war, etc.
11.2 Any failure by either Party, on one or more occasions, to invoke
one or more of the provisions hereof, shall under no circumstances be
interpreted as being a waiver by the interested Party of its right to
invoke said provision(s) subsequently.
11.3 The Agreement cancels and replaces any or all previous agreements,
whether written or oral, between the Parties and having the same
purpose, and constitutes the entirety of the agreement between said
Parties concerning said purpose. No supplement or modification to the
terms and conditions hereof shall be effective as between the Parties
unless it is made in writing and signed by their duly authorized
representatives.
11.4 In the event that one or more of the provisions hereof were to
conflict with a current or future applicable act or legislative text,
said act or legislative text shall prevail, and the Parties shall make
the necessary amendments so as to comply with said act or legislative
text. All other provisions shall remain effective. Similarly, invalidity
of a provision of the Agreement, for any reason whatsoever, shall not
cause the Agreement as a whole to be invalid.
11.5 LANGUAGE
The Agreement is drafted in both French and English and both versions
are deemed authentic.
Article 12 - NEW VERSIONS OF THE AGREEMENT
12.1 Any person is authorized to duplicate and distribute copies of this
Agreement.
12.2 So as to ensure coherence, the wording of this Agreement is
protected and may only be modified by the authors of the License, who
reserve the right to periodically publish updates or new versions of the
Agreement, each with a separate number. These subsequent versions may
address new issues encountered by Free Software.
12.3 Any Software distributed under a given version of the Agreement may
only be subsequently distributed under the same version of the Agreement
or a subsequent version.
Article 13 - GOVERNING LAW AND JURISDICTION
13.1 The Agreement is governed by French law. The Parties agree to
endeavor to seek an amicable solution to any disagreements or disputes
that may arise during the performance of the Agreement.
13.2 Failing an amicable solution within two (2) months as from their
occurrence, and unless emergency proceedings are necessary, the
disagreements or disputes shall be referred to the Paris Courts having
jurisdiction, by the more diligent Party.
Version 1.0 dated 2006-09-05.
CodraFT-2.2.1/MANIFEST.in 0000664 0000000 0000000 00000000423 14435624103 0014535 0 ustar 00root root 0000000 0000000 recursive-include codraft *.png *.svg *.pot *.po *.mo *.dcm *.ico *.h5 *.chm *.txt *.sig *.csv *.json *.npy *.fxd *.scor-data
recursive-include doc *.py *.rst *.png *.pot *.po
recursive-include nsis *.nsi *.nsh *.bmp *.ico
include MANIFEST.in
include Licence*.*
include *.md
CodraFT-2.2.1/README.md 0000664 0000000 0000000 00000014176 14435624103 0014270 0 ustar 00root root 0000000 0000000 
[](./LICENSE)
[](https://pypi.org/project/codraft/)
[](https://github.com/CODRA-Ingenierie-Informatique/CodraFT)
[](https://pypi.python.org/pypi/codraft/)
CodraFT is [CODRA](https://codra.net/)'s Filtering Tool.

----
## Important note
**CodraFT will soon be replaced by DataLab.**

DataLab is a platform for data processing and visualization, with a focus on
extensibility, automation and reproducibility, thanks to its macro-command
system, a high-level Python API and a plugin system.
See [roadmap](https://codraft.readthedocs.io/en/latest/roadmap.html) section in
documentation for more details.
## Overview
CodraFT is a generic signal and image processing software based on Python scientific
libraries (such as NumPy, SciPy or scikit-image) and Qt graphical user interfaces (thanks to
[guidata](https://pypi.python.org/pypi/guidata) and [guiqwt](https://pypi.python.org/pypi/guiqwt) libraries).
CodraFT stands for "CODRA Filtering Tool".
CodraFT is available as a **stand-alone** application (see for example our all-in-one Windows installer) or as an **addon to your Python-Qt application** thanks to advanced automation and embedding features.
See [home page](https://codra-ingenierie-informatique.github.io/CodraFT/) and
[documentation](https://codraft.readthedocs.io/en/latest/) for more details on
the library and [changelog](CHANGELOG.md) for recent history of changes.
### New in CodraFT 2.0
* New data processing and visualization features (see details in [changelog](CHANGELOG.md))
* Fully automated high-level processing features for internal testing purpose, as well as embedding CodraFT in a third-party software
* Extensive test suite (unit tests and application tests) with 90% feature coverage
### Credits
Copyrights and licensing:
* Copyright © 2018-2022 [CEA](http://www.cea.fr)-[CODRA](https://codra.net/), Pierre Raybaut
* Licensed under the terms of the BSD 3-Clause or the CeCILL-B License. See ``Licence_CeCILL_V2.1-en.txt``.
----
## Key features
### Data visualization
| Signal | Image | Feature |
|:------:|:------:|--------------------------------|
| • | • | Screenshots (save, copy) |
| • | Z-axis | Lin/log scales |
| • | • | Data table editing |
| • | • | Statistics on user-defined ROI |
| • | • | Markers |
| | • | Aspect ratio (1:1, custom) |
| | • | 50+ available colormaps |
| | • | X/Y raw/averaged profiles |
| • | • | User-defined annotations |


### Data processing
| Signal | Image | Feature |
|:------:|:-----:|----------------------------------------------------|
| • | • | Multiple ROI support |
| • | • | Sum, average, difference, product, ... |
| • | • | ROI extraction, Swap X/Y axes |
| • | | Semi-automatic multi-peak detection |
| | • | Rotation (flip, rotate), resize, ... |
| | • | Flat-field correction |
| • | | Normalize, derivative, integral |
| • | • | Linear calibration |
| | • | Thresholding, clipping |
| • | • | Gaussian filter, Wiener filter |
| • | • | Moving average, moving median |
| • | • | FFT, inverse FFT |
| • | | Interactive fit: Gauss, Lorenzt, Voigt, polynomial |
| • | | Interactive multigaussian fit |
| • | • | Computing on custom ROI |
| • | | FWHM, FW @ 1/e² |
| | • | Centroid (robust method w/r noise) |
| | • | Minimum enclosing circle center |
| | • | Automatic 2D-peak detection |
| | • | Automatic contour extraction (circle/ellipse fit) |


----
## Installation
### From the installer
CodraFT is available as a stand-alone application, which does not require any Python
distribution to be installed. Just run the installer and you're good to go!
The installer package is available in the [Releases](https://github.com/CODRA-Ingenierie-Informatique/CodraFT/releases) section.
### From the source package
```bash
python setup.py install
```
----
## Dependencies
### Requirements
* Python 3.7+ (reference is Python 3.8)
* [PyQt5](https://pypi.python.org/pypi/PyQt5) (Python Qt bindings)
* [QtPy](https://pypi.org/project/QtPy/) (abstraction layer for Python-Qt binding libraries)
* [guidata](https://pypi.python.org/pypi/guidata) (set of tools for automatic GUI generation)
* [guiqwt](https://pypi.python.org/pypi/guiqwt) (set of tools for curve and image plotting based on guidata)
* [h5py](https://pypi.org/project/h5py/) (interface to the HDF5 data format)
* [NumPy](https://pypi.org/project/numpy/) (operations on multidimensional arrays)
* [SciPy](https://pypi.org/project/scipy/) (algorithms for scientific computing)
* [scikit-image](https://pypi.org/project/scikit-image/) (algorithms for image processing)
* [psutil](https://pypi.org/project/psutil/) (process and system monitoring)
CodraFT-2.2.1/codraft/ 0000775 0000000 0000000 00000000000 14435624103 0014422 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/codraft/__init__.py 0000664 0000000 0000000 00000002145 14435624103 0016535 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT
=======
CodraFT is a generic signal and image processing software based on Python scientific
libraries (such as NumPy, SciPy or scikit-image) and Qt graphical user interfaces
(thanks to `guidata`_ and `guiqwt`_ libraries).
.. _guidata: https://pypi.python.org/pypi/guidata
.. _guiqwt: https://pypi.python.org/pypi/guiqwt
"""
import os
__version__ = "2.2.1"
__docurl__ = "https://codraft.readthedocs.io/en/latest/"
__homeurl__ = "https://codra-ingenierie-informatique.github.io/CodraFT/"
__supporturl__ = (
"https://github.com/CODRA-Ingenierie-Informatique/CodraFT/issues/new/choose"
)
os.environ["CODRAFT_VERSION"] = __version__
try:
import codraft.core.io # analysis:ignore
import codraft.patch # analysis:ignore
except ImportError:
if not os.environ.get("CODRAFT_DOC"):
raise
# Dear (Debian, RPM, ...) package makers, please feel free to customize the
# following path to module's data (images) and translations:
DATAPATH = LOCALEPATH = ""
CodraFT-2.2.1/codraft/app.py 0000664 0000000 0000000 00000004211 14435624103 0015552 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT launcher module
"""
from guidata.configtools import get_image_file_path
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from codraft.config import Conf
from codraft.core.gui.main import CodraFTMainWindow
from codraft.env import execenv
from codraft.utils.qthelpers import qt_app_context
def create(
splash: bool = True, console: bool = None, objects=None, h5files=None, size=None
) -> CodraFTMainWindow:
"""Create CodraFT application and return mainwindow instance"""
if splash:
# Showing splash screen
pixmap = QG.QPixmap(get_image_file_path("codraft_titleicon.png"))
splashscreen = QW.QSplashScreen(pixmap, QC.Qt.WindowStaysOnTopHint)
splashscreen.show()
window = CodraFTMainWindow(console=console)
if size is not None:
width, height = size
window.resize(width, height)
if splash:
splashscreen.finish(window)
if Conf.main.window_maximized.get(None):
window.showMaximized()
else:
window.showNormal()
if h5files is not None:
window.open_h5_files(h5files, import_all=True)
if objects is not None:
for obj in objects:
window.add_object(obj)
if execenv.h5browser_file is not None:
window.import_h5_file(execenv.h5browser_file)
return window
def run(console=None, objects=None, h5files=None, size=None):
"""Run the CodraFT application
Note: this function is an entry point in `setup.py` and therefore
may not be moved without modifying the package setup script."""
if execenv.h5files:
h5files = ([] if h5files is None else h5files) + execenv.h5files
with qt_app_context(exec_loop=True):
window = create(
splash=True, console=console, objects=objects, h5files=h5files, size=size
)
QW.QApplication.processEvents()
window.check_stable_release()
window.check_dependencies()
window.check_for_previous_crash()
if __name__ == "__main__":
run()
CodraFT-2.2.1/codraft/config.py 0000664 0000000 0000000 00000014441 14435624103 0016245 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
codraft.config
--------------
The `config` module handles `codraft` configuration
(options, images and icons).
"""
import os
import os.path as osp
from guidata import configtools
from guiqwt.config import CONF as GUIQWT_CONF
from codraft.utils import conf, tests
_ = configtools.get_translation("codraft")
CONF_VERSION = "1.0.0"
APP_NAME = "CodraFT"
APP_DESC = _(
"""CodraFT (Codra Filtering Tool) is a
generic signal and image processing software based on Python and Qt"""
)
APP_PATH = osp.dirname(__file__)
DEBUG = len(os.environ.get("DEBUG", "")) > 0
if DEBUG:
print("*** DEBUG mode *** [Reset configuration file, do not redirect std I/O]")
TEST_SEGFAULT_ERROR = len(os.environ.get("TEST_SEGFAULT_ERROR", "")) > 0
if TEST_SEGFAULT_ERROR:
print('*** TEST_SEGFAULT_ERROR mode *** [Enabling test action in "?" menu]')
DATETIME_FORMAT = "%d/%m/%Y - %H:%M:%S"
configtools.add_image_module_path("codraft", osp.join("data", "logo"))
configtools.add_image_module_path("codraft", osp.join("data", "icons"))
class MainSection(conf.Section, metaclass=conf.SectionMeta):
"""Class defining the main configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
traceback_log_path = conf.ConfigPathOption()
traceback_log_available = conf.Option()
faulthandler_enabled = conf.Option()
faulthandler_log_path = conf.ConfigPathOption()
faulthandler_log_available = conf.Option()
window_maximized = conf.Option()
window_position = conf.Option()
window_size = conf.Option()
base_dir = conf.WorkingDirOption()
available_memory_threshold = conf.Option()
ignore_dependency_check = conf.Option()
current_tab = conf.Option()
class ConsoleSection(conf.Section, metaclass=conf.SectionMeta):
"""Classs defining the console configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
enable = conf.Option()
max_line_count = conf.Option()
class IOSection(conf.Section, metaclass=conf.SectionMeta):
"""Class defining the I/O configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
h5_fname_in_title = conf.Option()
h5_fullpath_in_title = conf.Option()
class ProcSection(conf.Section, metaclass=conf.SectionMeta):
"""Class defining the Processing configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
extract_roi_singleobj = conf.Option()
class ViewSection(conf.Section, metaclass=conf.SectionMeta):
"""Class defining the view configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
# String formatting for shape legends
sig_format = conf.Option()
ima_format = conf.Option()
show_label = conf.Option()
show_contrast = conf.Option()
# If True, images are shown with the same LUT range as the first selected image
ima_ref_lut_range = conf.Option()
# Default visualization settings at item creation
# (e.g. see `ImageParam.make_item` in codraft/core/model/image.py)
ima_eliminate_outliers = conf.Option()
# Default visualization settings, persisted in object metadata
# (e.g. see `create_image` in codraft/core/model/image.py)
ima_def_colormap = conf.Option()
ima_def_interpolation = conf.Option()
# Usage (example): Conf.console.enable.get(True)
class Conf(conf.Configuration, metaclass=conf.ConfMeta):
"""Class defining CodraFT configuration structure.
Each class attribute is a section (metaclass is automatically affecting
section names in .INI file based on class attribute names)."""
main = MainSection()
console = ConsoleSection()
view = ViewSection()
proc = ProcSection()
io = IOSection()
def get_old_log_fname(fname):
"""Return old log fname from current log fname"""
return osp.splitext(fname)[0] + ".1.log"
def initialize():
"""Initialize application configuration"""
Conf.initialize(APP_NAME, CONF_VERSION, load=not DEBUG)
Conf.main.traceback_log_path.set(f".{APP_NAME}_traceback.log")
Conf.main.faulthandler_log_path.set(f".{APP_NAME}_faulthandler.log")
def reset():
"""Reset application configuration"""
Conf.reset()
initialize()
initialize()
tests.add_test_module_path("codraft", osp.join("data", "tests"))
GUIQWT_DEFAULTS = {
"plot": {
# "antialiasing": False,
# "title/font/size": 12,
# "title/font/bold": False,
# "marker/curve/text/font/size": 8,
# "marker/curve/text/font/family": "default",
# "marker/curve/text/font/bold": False,
# "marker/curve/text/font/italic": False,
"marker/curve/text/textcolor": "black",
# "marker/curve/text/background_color": "#ffffff",
# "marker/curve/text/background_alpha": 0.8,
# "marker/cross/text/font/family": "default",
# "marker/cross/text/font/size": 8,
# "marker/cross/text/font/bold": False,
# "marker/cross/text/font/italic": False,
"marker/cross/text/textcolor": "black",
# "marker/cross/text/background_color": "#ffffff",
"marker/cross/text/background_alpha": 0.7,
# "marker/cross/line/style": "DashLine",
# "marker/cross/line/color": "yellow",
# "marker/cross/line/width": 1,
# "marker/cursor/text/font/size": 8,
# "marker/cursor/text/font/family": "default",
# "marker/cursor/text/font/bold": False,
# "marker/cursor/text/font/italic": False,
# "marker/cursor/text/textcolor": "#ff9393",
# "marker/cursor/text/background_color": "#ffffff",
# "marker/cursor/text/background_alpha": 0.8,
"shape/drag/symbol/marker": "NoSymbol",
"shape/mask/symbol/size": 5,
"shape/mask/sel_symbol/size": 8,
},
}
GUIQWT_CONF.update_defaults(GUIQWT_DEFAULTS)
CodraFT-2.2.1/codraft/core/ 0000775 0000000 0000000 00000000000 14435624103 0015352 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/codraft/core/__init__.py 0000664 0000000 0000000 00000000002 14435624103 0017453 0 ustar 00root root 0000000 0000000 #
CodraFT-2.2.1/codraft/core/computation/ 0000775 0000000 0000000 00000000000 14435624103 0017714 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/codraft/core/computation/__init__.py 0000664 0000000 0000000 00000000002 14435624103 0022015 0 ustar 00root root 0000000 0000000 #
CodraFT-2.2.1/codraft/core/computation/fit.py 0000664 0000000 0000000 00000006422 14435624103 0021054 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Computation / Curve fitting module
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import abc
import numpy as np
import scipy.special as sps
# ----- Fitting models ---------------------------------------------------------
class FitModel(abc.ABC):
"""Curve fitting model base class"""
@classmethod
@abc.abstractmethod
def func(cls, x, amp, sigma, x0, y0):
"""Return fitting function"""
@classmethod
def get_amp_from_amplitude(
cls, amplitude, sigma
): # pylint: disable=unused-argument
"""Return amp from function amplitude and sigma"""
return amplitude
@classmethod
def amplitude(cls, amp, sigma):
"""Return function amplitude"""
return cls.func(0, amp, sigma, 0, 0)
@classmethod
@abc.abstractmethod
def fwhm(cls, amp, sigma):
"""Return function FWHM"""
@classmethod
def half_max_segment(cls, amp, sigma, x0, y0):
"""Return segment coordinates for y=half-maximum intersection"""
hwhm = 0.5 * cls.fwhm(amp, sigma)
yhm = 0.5 * cls.amplitude(amp, sigma) + y0
return x0 - hwhm, yhm, x0 + hwhm, yhm
class GaussianModel(FitModel):
"""1-dimensional Gaussian fit model"""
@classmethod
def func(cls, x, amp, sigma, x0, y0):
"""Return fitting function"""
return (
amp / (sigma * np.sqrt(2 * np.pi)) * np.exp(-0.5 * ((x - x0) / sigma) ** 2)
+ y0
)
@classmethod
def get_amp_from_amplitude(cls, amplitude, sigma):
"""Return amp from function amplitude and sigma"""
return amplitude * (sigma * np.sqrt(2 * np.pi))
@classmethod
def amplitude(cls, amp, sigma):
"""Return function amplitude"""
return amp / (sigma * np.sqrt(2 * np.pi))
@classmethod
def fwhm(cls, amp, sigma):
"""Return function FWHM"""
return 2 * sigma * np.sqrt(2 * np.log(2))
class LorentzianModel(FitModel):
"""1-dimensional Lorentzian fit model"""
@classmethod
def func(cls, x, amp, sigma, x0, y0):
"""Return fitting function"""
return (amp / (sigma * np.pi)) / (1 + ((x - x0) / sigma) ** 2) + y0
@classmethod
def get_amp_from_amplitude(cls, amplitude, sigma):
"""Return amp from function amplitude and sigma"""
return amplitude * (sigma * np.pi)
@classmethod
def amplitude(cls, amp, sigma):
"""Return function amplitude"""
return amp / (sigma * np.pi)
@classmethod
def fwhm(cls, amp, sigma):
"""Return function FWHM"""
return 2 * sigma
class VoigtModel(FitModel):
"""1-dimensional Voigt fit model"""
@classmethod
def func(cls, x, amp, sigma, x0, y0):
"""Return fitting function"""
# pylint: disable=no-member
z = (x - x0 + 1j * sigma) / (sigma * np.sqrt(2.0))
return y0 + amp * sps.wofz(z).real / (sigma * np.sqrt(2 * np.pi))
@classmethod
def fwhm(cls, amp, sigma):
"""Return function FWHM"""
wg = GaussianModel.fwhm(amp, sigma)
wl = LorentzianModel.fwhm(amp, sigma)
return 0.5346 * wl + np.sqrt(0.2166 * wl**2 + wg**2)
CodraFT-2.2.1/codraft/core/computation/image.py 0000664 0000000 0000000 00000014330 14435624103 0021351 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Computation / Image module
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import numpy as np
import scipy.ndimage as spi
import scipy.ndimage.filters as spf
import scipy.spatial as spt
from numpy import ma
from skimage import measure
def scale_data_to_min_max(data: np.ndarray, zmin, zmax):
"""Scale array `data` to fit [zmin, zmax] dynamic range"""
dmin = data.min()
dmax = data.max()
fdata = np.array(data, dtype=float)
fdata -= dmin
fdata *= float(zmax - zmin) / (dmax - dmin)
fdata += float(zmin)
return np.array(fdata, data.dtype)
def flatfield(rawdata: np.ndarray, flatdata: np.ndarray, threshold: float = None):
"""Compute flat-field correction"""
dtemp = np.array(rawdata, dtype=np.float64, copy=True) * flatdata.mean()
dunif = np.array(flatdata, dtype=np.float64, copy=True)
dunif[dunif == 0] = 1.0
dcorr_all = np.array(dtemp / dunif, dtype=rawdata.dtype)
dcorr = np.array(rawdata, copy=True)
dcorr[rawdata > threshold] = dcorr_all[rawdata > threshold]
return dcorr
def get_centroid_fourier(data: np.ndarray):
"""Return image centroid using Fourier algorithm"""
# Fourier transform method as discussed by Weisshaar et al.
# (http://www.mnd-umwelttechnik.fh-wiesbaden.de/pig/weisshaar_u5.pdf)
rows, cols = data.shape
if rows == 1 or cols == 1:
return 0, 0
i = np.arange(0, rows).reshape(1, rows)
sin_a = np.sin((i - 1) * 2 * np.pi / (rows - 1)).T
cos_a = np.cos((i - 1) * 2 * np.pi / (rows - 1)).T
j = np.arange(0, cols).reshape(cols, 1)
sin_b = np.sin((j - 1) * 2 * np.pi / (cols - 1)).T
cos_b = np.cos((j - 1) * 2 * np.pi / (cols - 1)).T
a = (cos_a * data).sum()
b = (sin_a * data).sum()
c = (data * cos_b).sum()
d = (data * sin_b).sum()
rphi = (0 if b > 0 else 2 * np.pi) if a > 0 else np.pi
cphi = (0 if d > 0 else 2 * np.pi) if c > 0 else np.pi
if a * c == 0.0:
return 0, 0
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
return row, col
def get_absolute_level(data: np.ndarray, level: float):
"""Return absolute level"""
if not isinstance(level, float) or level < 0.0 or level > 1.0:
raise ValueError("Level must be a float between 0. and 1.")
return (float(np.nanmin(data)) + float(np.nanmax(data))) * level
def get_enclosing_circle(data: np.ndarray, level: float = 0.5):
"""Return (x, y, radius) for the circle contour enclosing image
values above threshold relative level (.5 means FWHM)
Raise ValueError if no contour was found"""
data_th = data.copy()
data_th[data <= get_absolute_level(data, level)] = 0.0
contours = measure.find_contours(data_th)
model = measure.CircleModel()
result = None
max_radius = 1.0
for contour in contours:
if model.estimate(contour):
yc, xc, radius = model.params
if radius > max_radius:
result = (int(xc), int(yc), radius)
max_radius = radius
if result is None:
raise ValueError("No contour was found")
return result
def distance_matrix(coords: list) -> np.ndarray:
"""Return distance matrix from coords"""
return np.triu(spt.distance.cdist(coords, coords, "euclidean"))
def get_2d_peaks_coords(
data: np.ndarray, size: int = None, level: float = 0.5
) -> np.ndarray:
"""Detect peaks in image data, return coordinates.
If neighborhoods size is None, default value is the highest value
between 50 pixels and the 1/40th of the smallest image dimension.
Detection threshold level is relative to difference
between data maximum and minimum values.
"""
if size is None:
size = max(min(data.shape) // 40, 50)
data_max = spf.maximum_filter(data, size)
data_min = spf.minimum_filter(data, size)
data_diff = data_max - data_min
diff = (data_max - data_min) > get_absolute_level(data_diff, level)
maxima = data == data_max
maxima[diff == 0] = 0
labeled, _num_objects = spi.label(maxima)
slices = spi.find_objects(labeled)
coords = []
for dy, dx in slices:
x_center = int(0.5 * (dx.start + dx.stop - 1))
y_center = int(0.5 * (dy.start + dy.stop - 1))
coords.append((x_center, y_center))
if len(coords) > 1:
# Eventually removing duplicates
dist = distance_matrix(coords)
for index in reversed(np.unique(np.where((dist < size) & (dist > 0))[1])):
coords.pop(index)
return np.array(coords)
def get_contour_shapes(
data: np.ndarray, shape: str = "ellipse", level: float = 0.5
) -> np.ndarray:
"""Find iso-valued contours in a 2D array, above relative level (.5 means FWHM),
then fit contours with shape ('ellipse' or 'circle')
Return NumPy array containing coordinates of shapes."""
# pylint: disable=too-many-locals
contours = measure.find_contours(data, level=get_absolute_level(data, level))
coords = []
for contour in contours:
if shape == "circle":
model = measure.CircleModel()
if model.estimate(contour):
yc, xc, r = model.params
if r <= 1.0:
continue
coords.append([xc - r, yc, xc + r, yc])
elif shape == "ellipse":
model = measure.EllipseModel()
if model.estimate(contour):
yc, xc, b, a, theta = model.params
if a <= 1.0 or b <= 1.0:
continue
dxa, dya = a * np.cos(theta), a * np.sin(theta)
dxb, dyb = b * np.sin(theta), b * np.cos(theta)
x1, y1, x2, y2 = xc - dxa, yc - dya, xc + dxa, yc + dya
x3, y3, x4, y4 = xc - dxb, yc - dyb, xc + dxb, yc + dyb
coords.append([x1, y1, x2, y2, x3, y3, x4, y4])
else:
raise NotImplementedError(f"Invalid contour model {model}")
return np.array(coords)
CodraFT-2.2.1/codraft/core/computation/signal.py 0000664 0000000 0000000 00000014537 14435624103 0021555 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Computation / Signal module
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import numpy as np
# ----- Filtering functions ----------------------------------------------------
def moving_average(y, n):
"""Compute moving average"""
y_padded = np.pad(y, (n // 2, n - 1 - n // 2), mode="edge")
return np.convolve(y_padded, np.ones((n,)) / n, mode="valid")
# ----- Misc. functions --------------------------------------------------------
def derivative(x, y):
"""Compute numerical derivative"""
dy = np.zeros_like(y)
dy[0:-1] = np.diff(y) / np.diff(x)
dy[-1] = (y[-1] - y[-2]) / (x[-1] - x[-2])
return dy
def normalize(yin, parameter="maximum"):
"""
Normalize input array *yin* with respect to parameter *parameter*
Support values for *parameter*:
'maximum' (default), 'amplitude', 'sum', 'energy'
"""
axis = len(yin.shape) - 1
if parameter == "maximum":
maximum = np.max(yin, axis)
if axis == 1:
maximum = maximum.reshape((len(maximum), 1))
maxarray = np.tile(maximum, yin.shape[axis]).reshape(yin.shape)
return yin / maxarray
if parameter == "amplitude":
ytemp = np.array(yin, copy=True)
minimum = np.min(yin, axis)
if axis == 1:
minimum = minimum.reshape((len(minimum), 1))
ytemp -= minimum
return normalize(ytemp, parameter="maximum")
if parameter == "sum":
return yin / yin.sum()
if parameter == "energy":
return yin / (yin * yin.conjugate()).sum()
raise RuntimeError(f"Unsupported parameter {parameter}")
def xy_fft(x, y, shift=True):
"""Compute FFT on X,Y data.
Args:
x (np.ndarray): X data
y (np.ndarray): Y data
shift (bool, optional): Shift the zero frequency to the center of the spectrum.
Defaults to True.
Returns:
tuple[np.ndarray, np.ndarray]: X,Y data
"""
y1 = np.fft.fft(y)
x1 = np.fft.fftfreq(x.shape[-1], d=x[1] - x[0])
if shift:
x1 = np.fft.fftshift(x1)
y1 = np.fft.fftshift(y1)
return x1, y1
def xy_ifft(x, y, shift=True):
"""Compute iFFT on X,Y data.
Args:
x (np.ndarray): X data
y (np.ndarray): Y data
shift (bool, optional): Shift the zero frequency to the center of the spectrum.
Defaults to True.
Returns:
tuple[np.ndarray, np.ndarray]: X,Y data
"""
x1 = np.fft.fftfreq(x.shape[-1], d=x[1] - x[0])
if shift:
x1 = np.fft.ifftshift(x1)
y = np.fft.ifftshift(y)
y1 = np.fft.ifft(y)
return x1, y1.real
# ----- Peak detection functions -----------------------------------------------
def peak_indexes(y, thres=0.3, min_dist=1, thres_abs=False):
# Copyright (c) 2014 Lucas Hermann Negri
# Unmodified code snippet from PeakUtils 1.3.0
"""Peak detection routine.
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 reduce the number of detected peaks. *y* must be signed.
Parameters
----------
y : ndarray (signed)
1D amplitude data to search for peaks.
thres : float between [0., 1.]
Normalized threshold. Only the peaks with amplitude higher than the
threshold will be detected.
min_dist : int
Minimum distance between each detected peak. The peak with the highest
amplitude is preferred to satisfy this constraint.
thres_abs: boolean
If True, the thres value will be interpreted as an absolute value,
instead of a normalized threshold.
Returns
-------
ndarray
Array containing the numeric indexes of the peaks that were detected
"""
if isinstance(y, np.ndarray) and np.issubdtype(y.dtype, np.unsignedinteger):
raise ValueError("y must be signed")
if not thres_abs:
thres = thres * (np.max(y) - np.min(y)) + np.min(y)
min_dist = int(min_dist)
# compute first order difference
dy = np.diff(y)
# propagate left and right values successively to fill all plateau pixels
# (0-value)
(zeros,) = np.where(dy == 0)
# check if the signal is totally flat
if len(zeros) == len(y) - 1:
return np.array([])
if len(zeros):
# compute first order difference of zero indexes
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
zero_plateaus = np.split(zeros, zeros_diff_not_one)
# fix if leftmost value in dy is zero
if zero_plateaus[0][0] == 0:
dy[zero_plateaus[0]] = dy[zero_plateaus[0][-1] + 1]
zero_plateaus.pop(0)
# fix if rightmost value of dy is zero
if len(zero_plateaus) > 0 and zero_plateaus[-1][-1] == len(dy) - 1:
dy[zero_plateaus[-1]] = dy[zero_plateaus[-1][0] - 1]
zero_plateaus.pop(-1)
# for each chain of zero indexes
for plateau in zero_plateaus:
median = np.median(plateau)
# set leftmost values to leftmost non zero values
dy[plateau[plateau < median]] = dy[plateau[0] - 1]
# set rightmost and middle values to rightmost non zero values
dy[plateau[plateau >= median]] = dy[plateau[-1] + 1]
# find the peaks by using the first order difference
peaks = np.where(
(np.hstack([dy, 0.0]) < 0.0)
& (np.hstack([0.0, dy]) > 0.0)
& (np.greater(y, thres))
)[0]
# handle multiple peaks, respecting the minimum distance
if peaks.size > 1 and min_dist > 1:
highest = peaks[np.argsort(y[peaks])][::-1]
rem = np.ones(y.size, dtype=bool)
rem[peaks] = False
for peak in highest:
if not rem[peak]:
sl = slice(max(0, peak - min_dist), peak + min_dist + 1)
rem[sl] = True
rem[peak] = False
peaks = np.arange(y.size)[~rem]
return peaks
def xpeak(x, y):
"""Return default peak X-position (assuming a single peak)"""
peaks = peak_indexes(y)
if peaks.size == 1:
return x[peaks[0]]
return np.average(x, weights=y)
CodraFT-2.2.1/codraft/core/gui/ 0000775 0000000 0000000 00000000000 14435624103 0016136 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/codraft/core/gui/__init__.py 0000664 0000000 0000000 00000001211 14435624103 0020242 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT core.gui module
This module handles all GUI features which are specific to CodraFT:
* core.gui.main: handles CodraFT main window which relies on signal and image panels
* core.gui.panel: handles CodraFT signal and image panels, relying on:
* core.gui.actionhandler
* core.gui.objectlist
* core.gui.plotitemlist
* core.gui.roieditor
* core.gui.processor
* core.gui.docks: handles CodraFT dockwidgets
* core.gui.h5io: handles HDF5 browser widget and related features
"""
CodraFT-2.2.1/codraft/core/gui/actionhandler.py 0000664 0000000 0000000 00000043276 14435624103 0021337 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Action handler module
This module handles all application actions (menus, toolbars, context menu).
These actions point to CodraFT panels, processors, objectlist, ...
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import abc
import enum
from guidata.configtools import get_icon
from guidata.qthelpers import add_actions, create_action
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from codraft.config import _
from codraft.widgets import fitdialog
class ActionCategory(enum.Enum):
"""Action categories"""
FILE = enum.auto()
EDIT = enum.auto()
VIEW = enum.auto()
OPERATION = enum.auto()
PROCESSING = enum.auto()
COMPUTING = enum.auto()
class BaseActionHandler(metaclass=abc.ABCMeta):
"""Object handling panel GUI interactions: actions, menus, ..."""
OBJECT_STR = "" # e.g. "signal"
def __init__(self, panel, objlist, itmlist, processor, toolbar):
self.panel = panel
self.objlist = objlist
self.itmlist = itmlist
self.processor = processor
self.feature_actions = {}
self.operation_end_actions = None
self.delete_roi_action = None
# Object selection dependent actions
self.actlist_1more = []
self.actlist_2more = []
self.actlist_1 = []
self.actlist_2 = []
self.actlist_cmenu = [] # Context menu
if self.__class__ is not BaseActionHandler:
self.create_all_actions(toolbar)
def get_context_menu_actions(self):
"""Return context menu action list"""
return self.actlist_cmenu
def selection_rows_changed(self):
"""Number of selected rows has changed"""
selrows = self.objlist.get_selected_rows()
nbrows = len(selrows)
for act in self.actlist_1more:
act.setEnabled(nbrows >= 1)
for act in self.actlist_2more:
act.setEnabled(nbrows >= 2)
for act in self.actlist_1:
act.setEnabled(nbrows == 1)
for act in self.actlist_2:
act.setEnabled(nbrows == 2)
self.delete_roi_action.setEnabled(False)
for row in selrows:
obj = self.objlist[row]
if obj.roi is not None:
self.delete_roi_action.setEnabled(True)
break
def create_all_actions(self, toolbar):
"""Setup actions, menus, toolbar"""
featact = self.feature_actions
featact[ActionCategory.FILE] = file_act = self.create_file_actions()
featact[ActionCategory.EDIT] = edit_act = self.create_edit_actions()
featact[ActionCategory.VIEW] = view_act = self.create_view_actions()
featact[ActionCategory.OPERATION] = self.create_operation_actions()
featact[ActionCategory.PROCESSING] = self.create_processing_actions()
featact[ActionCategory.COMPUTING] = self.create_computing_actions()
add_actions(toolbar, file_act + [None] + edit_act + [None] + view_act)
def cra(
self, title, triggered=None, toggled=None, shortcut=None, icon=None, tip=None
):
"""Create action convenience method"""
return create_action(self.panel, title, triggered, toggled, shortcut, icon, tip)
def create_file_actions(self):
"""Create file actions"""
new_act = self.cra(
_("New %s...") % self.OBJECT_STR,
icon=get_icon(f"new_{self.OBJECT_STR}.svg"),
tip=_("Create new %s") % self.OBJECT_STR,
triggered=self.panel.new_object,
shortcut=QG.QKeySequence(QG.QKeySequence.New),
)
open_act = self.cra(
_("Open %s...") % self.OBJECT_STR,
icon=get_icon("libre-gui-import.svg"),
tip=_("Open %s") % self.OBJECT_STR,
triggered=self.panel.open_objects,
shortcut=QG.QKeySequence(QG.QKeySequence.Open),
)
save_act = self.cra(
_("Save %s...") % self.OBJECT_STR,
icon=get_icon("libre-gui-export.svg"),
tip=_("Save selected %s") % self.OBJECT_STR,
triggered=self.panel.save_objects,
shortcut=QG.QKeySequence(QG.QKeySequence.Save),
)
importmd_act = self.cra(
_("Import metadata into %s...") % self.OBJECT_STR,
icon=get_icon("metadata_import.svg"),
tip=_("Import metadata into %s") % self.OBJECT_STR,
triggered=self.panel.import_metadata_from_file,
)
exportmd_act = self.cra(
_("Export metadata from %s...") % self.OBJECT_STR,
icon=get_icon("metadata_export.svg"),
tip=_("Export selected %s metadata") % self.OBJECT_STR,
triggered=self.panel.export_metadata_from_file,
)
self.actlist_1more += [save_act]
self.actlist_cmenu += [save_act]
self.actlist_1 += [importmd_act, exportmd_act]
return [new_act, open_act, save_act, None, importmd_act, exportmd_act]
def create_edit_actions(self):
"""Create edit actions"""
dup_action = self.cra(
_("Duplicate"),
icon=get_icon("libre-gui-copy.svg"),
triggered=self.panel.duplicate_object,
shortcut=QG.QKeySequence(QG.QKeySequence.Copy),
)
cpymeta_action = self.cra(
_("Copy metadata"),
icon=get_icon("metadata_copy.svg"),
triggered=self.panel.copy_metadata,
)
pstmeta_action = self.cra(
_("Paste metadata"),
icon=get_icon("metadata_paste.svg"),
triggered=self.panel.paste_metadata,
)
cleanup_action = self.cra(
_("Clean up data view"),
icon=get_icon("libre-tools-vacuum-cleaner.svg"),
tip=_("Clean up data view before updating plotting panels"),
toggled=self.itmlist.toggle_cleanup_dataview,
)
cleanup_action.setChecked(True)
delm_action = self.cra(
_("Delete object metadata"),
icon=get_icon("metadata_delete.svg"),
tip=_("Delete all that is contained in object metadata"),
triggered=self.panel.delete_metadata,
)
delall_action = self.cra(
_("Delete all"),
shortcut="Shift+Ctrl+Suppr",
icon=get_icon("delete_all.svg"),
triggered=self.panel.delete_all_objects,
)
del_action = self.cra(
_("Remove"),
icon=get_icon("delete.svg"),
triggered=self.panel.remove_object,
shortcut=QG.QKeySequence(QG.QKeySequence.Delete),
)
self.actlist_1more += [
dup_action,
del_action,
delm_action,
pstmeta_action,
delall_action,
]
self.actlist_cmenu += [dup_action, del_action]
self.actlist_1 += [cpymeta_action]
return [
dup_action,
del_action,
delall_action,
None,
cpymeta_action,
pstmeta_action,
delm_action,
]
def create_view_actions(self):
"""Create view actions"""
view_action = self.cra(
_("View in a new window"),
icon=get_icon("libre-gui-binoculars.svg"),
triggered=self.panel.open_separate_view,
)
showlabel_action = self.cra(
_("Show graphical object titles"),
icon=get_icon("show_titles.svg"),
tip=_("Show or hide ROI and other graphical object titles or subtitles"),
toggled=self.panel.toggle_show_titles,
)
showlabel_action.setChecked(False)
self.actlist_1more += [view_action]
self.actlist_cmenu = [view_action, None] + self.actlist_cmenu
return [view_action, showlabel_action]
def create_operation_actions(self):
"""Create operation actions"""
proc = self.processor
sum_action = self.cra(_("Sum"), proc.compute_sum)
average_action = self.cra(_("Average"), proc.compute_average)
diff_action = self.cra(_("Difference"), lambda: proc.compute_difference(False))
qdiff_action = self.cra(
_("Quadratic difference"), lambda: proc.compute_difference(True)
)
prod_action = self.cra(_("Product"), proc.compute_product)
div_action = self.cra(_("Division"), proc.compute_division)
roi_action = self.cra(
_("ROI extraction"),
proc.extract_roi,
icon=get_icon(f"{self.OBJECT_STR}_roi.svg"),
)
swapaxes_action = self.cra(_("Swap X/Y axes"), proc.swap_axes)
abs_action = self.cra(_("Absolute value"), proc.compute_abs)
log_action = self.cra("Log10(y)", proc.compute_log10)
self.actlist_1more += [roi_action, swapaxes_action, abs_action, log_action]
self.actlist_2more += [sum_action, average_action, prod_action]
self.actlist_2 += [diff_action, qdiff_action, div_action]
self.operation_end_actions = [roi_action, swapaxes_action]
return [
sum_action,
average_action,
diff_action,
qdiff_action,
prod_action,
div_action,
None,
abs_action,
log_action,
]
def create_processing_actions(self):
"""Create processing actions"""
proc = self.processor
threshold_action = self.cra(_("Thresholding"), proc.compute_threshold)
clip_action = self.cra(_("Clipping"), proc.compute_clip)
lincal_action = self.cra(_("Linear calibration"), proc.calibrate)
gauss_action = self.cra(_("Gaussian filter"), proc.compute_gaussian)
movavg_action = self.cra(_("Moving average"), proc.compute_moving_average)
movmed_action = self.cra(_("Moving median"), proc.compute_moving_median)
wiener_action = self.cra(_("Wiener filter"), proc.compute_wiener)
fft_action = self.cra(_("FFT"), proc.compute_fft)
ifft_action = self.cra(_("Inverse FFT"), proc.compute_ifft)
for act in (fft_action, ifft_action):
act.setToolTip(_("Warning: only real part is plotted"))
actions = [
threshold_action,
clip_action,
lincal_action,
gauss_action,
movavg_action,
movmed_action,
wiener_action,
fft_action,
ifft_action,
]
self.actlist_1more += actions
return actions
@abc.abstractmethod
def create_computing_actions(self):
"""Create computing actions"""
proc = self.processor
defineroi_action = self.cra(
_("Edit regions of interest..."),
triggered=proc.edit_regions_of_interest,
icon=get_icon("roi.svg"),
)
self.delete_roi_action = self.cra(
_("Remove regions of interest"),
triggered=proc.delete_regions_of_interest,
icon=get_icon("roi_delete.svg"),
)
stats_action = self.cra(
_("Statistics") + "...",
triggered=proc.compute_stats,
icon=get_icon("stats.svg"),
)
self.actlist_1 += [defineroi_action, self.delete_roi_action, stats_action]
self.actlist_cmenu += [
None,
defineroi_action,
self.delete_roi_action,
None,
stats_action,
]
return [defineroi_action, self.delete_roi_action, None, stats_action]
class SignalActionHandler(BaseActionHandler):
"""Object handling signal panel GUI interactions: actions, menus, ..."""
OBJECT_STR = _("signal")
def create_operation_actions(self):
"""Create operation actions"""
base_actions = super().create_operation_actions()
proc = self.processor
peakdetect_action = self.cra(
_("Peak detection"),
proc.detect_peaks,
icon=get_icon("peak_detect.svg"),
)
self.actlist_1more += [peakdetect_action]
roi_actions = self.operation_end_actions
return base_actions + [None, peakdetect_action, None] + roi_actions
def create_processing_actions(self):
"""Create processing actions"""
base_actions = super().create_processing_actions()
proc = self.processor
normalize_action = self.cra(_("Normalize"), proc.normalize)
deriv_action = self.cra(_("Derivative"), proc.compute_derivative)
integ_action = self.cra(_("Integral"), proc.compute_integral)
polyfit_action = self.cra(_("Polynomial fit"), proc.compute_polyfit)
mgfit_action = self.cra(_("Multi-Gaussian fit"), proc.compute_multigaussianfit)
def cra_fit(title, fitdlgfunc):
"""Create curve fitting action"""
return self.cra(title, lambda: proc.compute_fit(title, fitdlgfunc))
gaussfit_action = cra_fit(_("Gaussian fit"), fitdialog.gaussianfit)
lorentzfit_action = cra_fit(_("Lorentzian fit"), fitdialog.lorentzianfit)
voigtfit_action = cra_fit(_("Voigt fit"), fitdialog.voigtfit)
actions1 = [normalize_action, deriv_action, integ_action]
actions2 = [
gaussfit_action,
lorentzfit_action,
voigtfit_action,
polyfit_action,
mgfit_action,
]
self.actlist_1more += actions1 + actions2
return actions1 + [None] + base_actions + [None] + actions2
def create_computing_actions(self):
"""Create computing actions"""
base_actions = super().create_computing_actions()
proc = self.processor
fwhm_action = self.cra(
_("Full width at half-maximum"),
triggered=proc.compute_fwhm,
tip=_("Compute Full Width at Half-Maximum (FWHM)"),
)
fw1e2_action = self.cra(
_("Full width at") + " 1/e²",
triggered=proc.compute_fw1e2,
tip=_("Compute Full Width at Maximum") + "/e²",
)
self.actlist_1more += [fwhm_action, fw1e2_action]
return base_actions + [fwhm_action, fw1e2_action]
class ImageActionHandler(BaseActionHandler):
"""Object handling image panel GUI interactions: actions, menus, ..."""
OBJECT_STR = _("image")
def create_view_actions(self):
"""Create view actions"""
base_actions = super().create_view_actions()
showcontrast_action = self.cra(
_("Show contrast panel"),
icon=get_icon("contrast.png"),
tip=_("Show or hide contrast adjustment panel"),
toggled=self.panel.toggle_show_contrast,
)
showcontrast_action.setChecked(True)
self.actlist_1more += [showcontrast_action]
return base_actions + [showcontrast_action]
def create_operation_actions(self):
"""Create operation actions"""
base_actions = super().create_operation_actions()
proc = self.processor
rotate_menu = QW.QMenu(_("Rotation"), self.panel)
hflip_act = self.cra(
_("Flip horizontally"),
triggered=proc.flip_horizontally,
icon=get_icon("flip_horizontally.svg"),
)
vflip_act = self.cra(
_("Flip vertically"),
triggered=proc.flip_vertically,
icon=get_icon("flip_vertically.svg"),
)
rot90_act = self.cra(
_("Rotate %s right") % "90°", # pylint: disable=consider-using-f-string
triggered=proc.rotate_270,
icon=get_icon("rotate_right.svg"),
)
rot270_act = self.cra(
_("Rotate %s left") % "90°", # pylint: disable=consider-using-f-string
triggered=proc.rotate_90,
icon=get_icon("rotate_left.svg"),
)
rotate_act = self.cra(
_("Rotate arbitrarily..."), triggered=proc.rotate_arbitrarily
)
resize_act = self.cra(_("Resize"), triggered=proc.resize_image)
logp1_act = self.cra("Log10(z+n)", triggered=proc.compute_logp1)
flatfield_act = self.cra(
_("Flat-field correction"), triggered=proc.flat_field_correction
)
self.actlist_2 += [flatfield_act]
self.actlist_1more += [
resize_act,
hflip_act,
vflip_act,
logp1_act,
rot90_act,
rot270_act,
rotate_act,
]
self.actlist_cmenu += [None, hflip_act, vflip_act, rot90_act, rot270_act]
add_actions(
rotate_menu, [hflip_act, vflip_act, rot90_act, rot270_act, rotate_act]
)
roi_actions = self.operation_end_actions
actions = [
logp1_act,
flatfield_act,
None,
rotate_menu,
None,
resize_act,
]
return base_actions + actions + roi_actions
def create_computing_actions(self):
"""Create computing actions"""
base_actions = super().create_computing_actions()
proc = self.processor
# TODO: [P3] Add "Create ROI grid..." action to create a regular grid or ROIs
cent_act = self.cra(
_("Centroid"), proc.compute_centroid, tip=_("Compute image centroid")
)
encl_act = self.cra(
_("Minimum enclosing circle center"),
proc.compute_enclosing_circle,
tip=_("Compute smallest enclosing circle center"),
)
peak_act = self.cra(
_("2D peak detection"),
proc.compute_peak_detection,
tip=_("Compute automatic 2D peak detection"),
)
contour_act = self.cra(
_("Contour detection"),
proc.compute_contour_shape,
tip=_("Compute contour shape fit"),
)
self.actlist_1more += [cent_act, encl_act, peak_act, contour_act]
return base_actions + [cent_act, encl_act, peak_act, contour_act]
CodraFT-2.2.1/codraft/core/gui/docks.py 0000664 0000000 0000000 00000003761 14435624103 0017622 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Dockable widgets
"""
from guidata.qthelpers import is_dark_mode
from guidata.qtwidgets import DockableWidget, DockableWidgetMixin
from guiqwt.plot import ImageWidget
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
class DockablePlotWidget(DockableWidget):
"""Docked plotting widget"""
LOCATION = QC.Qt.RightDockWidgetArea
def __init__(self, parent, plotwidgetclass, toolbar):
super().__init__(parent)
self.toolbar = toolbar
layout = QW.QVBoxLayout()
self.plotwidget = plotwidgetclass()
layout.addWidget(self.plotwidget)
self.setLayout(layout)
self.setup()
def get_plot(self):
"""Return plot instance"""
return self.plotwidget.plot
def setup(self):
"""Setup plotting widget"""
title = self.toolbar.windowTitle()
pwidget = self.plotwidget
pwidget.add_toolbar(self.toolbar, title)
if isinstance(self.plotwidget, ImageWidget):
pwidget.register_all_image_tools()
else:
pwidget.register_all_curve_tools()
# Customizing widget appearances
plot = pwidget.get_plot()
if not is_dark_mode():
for widget in (pwidget, plot, self):
widget.setBackgroundRole(QG.QPalette.Window)
widget.setAutoFillBackground(True)
widget.setPalette(QG.QPalette(QC.Qt.white))
canvas = plot.canvas()
canvas.setFrameStyle(canvas.Plain | canvas.NoFrame)
# ------DockableWidget API
def visibility_changed(self, enable):
"""DockWidget visibility has changed"""
DockableWidget.visibility_changed(self, enable)
self.toolbar.setVisible(enable)
class DockableTabWidget(QW.QTabWidget, DockableWidgetMixin):
"""Docked tab widget"""
LOCATION = QC.Qt.LeftDockWidgetArea
CodraFT-2.2.1/codraft/core/gui/h5io.py 0000664 0000000 0000000 00000011051 14435624103 0017352 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT HDF5 open/save module
"""
import os.path as osp
from qtpy import QtWidgets as QW
from codraft.config import _
from codraft.core.io.base import NativeH5Reader, NativeH5Writer
from codraft.core.io.h5 import H5Importer
from codraft.core.model.signal import SignalParam
from codraft.utils.qthelpers import create_progress_bar, qt_try_loadsave_file
from codraft.widgets.h5browser import H5BrowserDialog
class H5InputOutput:
"""Object handling HDF5 file open/save into/from CodraFT data model/main window"""
def __init__(self, mainwindow):
self.mainwindow = mainwindow
self.h5browser = None
self.uint32_wng = None
self.progressbar = None
self.lmj_metadata = None
@staticmethod
def __progbartitle(fname):
"""Return progress bar title"""
return _("Loading data from %s...") % osp.basename(fname)
def save_file(self, filename):
"""Save all signals and images from CodraFT model into a HDF5 file"""
writer = NativeH5Writer(filename)
for panel in self.mainwindow.panels:
panel.serialize_to_hdf5(writer)
writer.close()
def open_file(self, filename, import_all, reset_all):
"""Open HDF5 file"""
progress = None
try:
reader = NativeH5Reader(filename)
if reset_all:
self.mainwindow.reset_all()
with create_progress_bar(
self.mainwindow, self.__progbartitle(filename), 2
) as progress:
for idx, panel in enumerate(self.mainwindow.panels):
progress.setValue(idx)
QW.QApplication.processEvents()
panel.deserialize_from_hdf5(reader)
if progress.wasCanceled():
break
reader.close()
except KeyError:
if progress is not None:
# KeyError was encoutered when deserializing datasets (CodraFT data
# model is not compatible with this version)
progress.close()
self.import_file(filename, import_all, reset_all)
def __add_object_from_node(self, node):
"""Add CodraFT object from h5 node"""
obj = node.get_object()
self.uint32_wng = self.uint32_wng or node.uint32_wng
if isinstance(obj, SignalParam):
self.mainwindow.signalpanel.add_object(obj)
else:
self.mainwindow.imagepanel.add_object(obj)
def __eventually_show_warnings(self):
"""Eventually show warnings after everything is imported"""
if self.uint32_wng:
QW.QMessageBox.warning(
self.mainwindow, _("Warning"), _("Clipping uint32 data to int32.")
)
def import_file(self, filename, import_all, reset_all):
"""Import HDF5 file"""
if self.h5browser is None:
self.h5browser = H5BrowserDialog(self.mainwindow)
with qt_try_loadsave_file(self.mainwindow, filename, "load"):
self.h5browser.setup(filename)
if not import_all and not self.h5browser.exec():
self.h5browser.cleanup()
return
if import_all:
nodes = self.h5browser.get_all_nodes()
else:
nodes = self.h5browser.get_nodes()
if nodes is None:
self.h5browser.cleanup()
return
if reset_all:
self.mainwindow.reset_all()
with create_progress_bar(
self.mainwindow, self.__progbartitle(filename), len(nodes)
) as progress:
self.uint32_wng = False
for idx, node in enumerate(nodes):
progress.setValue(idx)
QW.QApplication.processEvents()
if progress.wasCanceled():
break
self.__add_object_from_node(node)
self.h5browser.cleanup()
self.__eventually_show_warnings()
def import_dataset_from_file(self, filename, dsetname):
"""Import dataset from HDF5 file"""
h5importer = H5Importer(filename)
try:
node = h5importer.get(dsetname)
self.uint32_wng = False
self.__add_object_from_node(node)
self.__eventually_show_warnings()
except KeyError as exc:
raise KeyError(f"Dataset not found: {dsetname}") from exc
h5importer.close()
CodraFT-2.2.1/codraft/core/gui/main.py 0000664 0000000 0000000 00000076216 14435624103 0017450 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT main window
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import locale
import os
import os.path as osp
import platform
import sys
import time
import webbrowser
from typing import List
import numpy as np
import scipy.ndimage as spi
import scipy.signal as sps
from guidata import __version__ as guidata_ver
from guidata.configtools import get_icon, get_module_data_path, get_module_path
from guidata.qthelpers import add_actions, create_action, win32_fix_title_bar_background
from guidata.widgets.console import DockableConsole
from guiqwt import __version__ as guiqwt_ver
from guiqwt.builder import make
from guiqwt.plot import CurveWidget, ImageWidget
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from qtpy.compat import getopenfilenames, getsavefilename
from qwt import __version__ as qwt_ver
from codraft import __docurl__, __homeurl__, __supporturl__, __version__, env
from codraft.config import APP_DESC, APP_NAME, TEST_SEGFAULT_ERROR, Conf, _
from codraft.core.gui.actionhandler import ActionCategory
from codraft.core.gui.docks import DockablePlotWidget, DockableTabWidget
from codraft.core.gui.h5io import H5InputOutput
from codraft.core.gui.panel import ImagePanel, SignalPanel
from codraft.core.model.image import ImageParam
from codraft.core.model.signal import SignalParam
from codraft.env import execenv
from codraft.utils import dephash
from codraft.utils import qthelpers as qth
from codraft.widgets.instconfviewer import exec_codraft_installconfig_dialog
from codraft.widgets.logviewer import exec_codraft_logviewer_dialog
from codraft.widgets.status import MemoryStatus
DATAPATH = get_module_data_path("codraft", "data")
def get_htmlhelp():
"""Return HTML Help documentation link adapted to locale, if it exists"""
if os.name == "nt":
for suffix in ("_" + locale.getlocale()[0][:2], ""):
path = osp.join(DATAPATH, f"CodraFT{suffix}.chm")
if osp.isfile(path):
return path
return None
class AppProxy:
"""Proxy to CodraFT application: object used from the embedded console
to access CodraFT internal objects"""
def __init__(self, win):
self.win = win
self.s = self.win.signalpanel.objlist
self.i = self.win.imagepanel.objlist
def is_frozen(module_name):
"""Test if module has been frozen (py2exe/cx_Freeze)"""
datapath = get_module_path(module_name)
parentdir = osp.normpath(osp.join(datapath, osp.pardir))
return not osp.isfile(__file__) or osp.isfile(parentdir) # library.zip
class CodraFTMainWindow(QW.QMainWindow):
"""CodraFT main window"""
__instance = None
@staticmethod
def get_instance(console=None, hide_on_close=False):
"""Return singleton instance"""
if CodraFTMainWindow.__instance is None:
return CodraFTMainWindow(console, hide_on_close)
return CodraFTMainWindow.__instance
def __init__(self, console=None, hide_on_close=False):
"""Initialize main window"""
CodraFTMainWindow.__instance = self
super().__init__()
win32_fix_title_bar_background(self)
self.setObjectName(APP_NAME)
self.setWindowIcon(get_icon("codraft.svg"))
self.__restore_pos_and_size()
self.hide_on_close = hide_on_close
self.__old_size = None
self.__memory_warning = False
self.memorystatus = None
self.console = None
self.app_proxy = None
self.signal_toolbar = None
self.image_toolbar = None
self.signalpanel = None
self.imagepanel = None
self.tabwidget = None
self.signal_image_docks = None
self.h5inputoutput = H5InputOutput(self)
self.openh5_action = None
self.saveh5_action = None
self.browseh5_action = None
self.quit_action = None
self.file_menu = None
self.edit_menu = None
self.operation_menu = None
self.processing_menu = None
self.computing_menu = None
self.view_menu = None
self.help_menu = None
self.__is_modified = None
self.set_modified(False)
# Setup actions and menus
if console is None:
console = Conf.console.enable.get(True)
self.setup(console)
@property
def panels(self):
"""Return the tuple of implemented panels (signal, image)"""
return (self.signalpanel, self.imagepanel)
def __set_low_memory_state(self, state):
"""Set memory warning state"""
self.__memory_warning = state
def confirm_memory_state(self): # pragma: no cover
"""Check memory warning state and eventually show a warning dialog"""
if self.__memory_warning:
threshold = Conf.main.available_memory_threshold.get()
answer = QW.QMessageBox.critical(
self,
_("Warning"),
_("Available memory is below %d MB.
Do you want to continue?")
% threshold,
QW.QMessageBox.Yes | QW.QMessageBox.No,
)
return answer == QW.QMessageBox.Yes
return True
def check_stable_release(self): # pragma: no cover
"""Check if this is a stable release"""
if __version__.replace(".", "").isdigit():
# This is a stable release
return
if "b" in __version__:
# This is a beta release
rel = _(
"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 in trying new features before "
"the final release. However, beta software may not behave as "
"expected and will probably have more bugs or performance issues "
"than completed software."
)
else:
# This is an alpha release
rel = _(
"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 specific features by users. "
"Please note that alpha software was not thoroughly tested "
"by the developer before it is released."
)
txtlist = [
f"{APP_NAME} v{__version__}:",
"",
_("This is not a stable release."),
"",
rel,
]
QW.QMessageBox.warning(self, APP_NAME, "
".join(txtlist), QW.QMessageBox.Ok)
def check_dependencies(self): # pragma: no cover
"""Check dependencies"""
if is_frozen("codraft") or Conf.main.ignore_dependency_check.get(False):
# No need to check dependencies if CodraFT has been frozen
return
try:
state = dephash.check_dependencies_hash(DATAPATH)
bad_deps = [name for name in state if not state[name]]
if not bad_deps:
# Everything is OK
return
except IOError:
bad_deps = None
txt0 = _("Non-compliant dependency:")
if bad_deps is None or len(bad_deps) > 1:
txt0 = _("Non-compliant dependencies:")
if bad_deps is None:
txtlist = [
_("CodraFT has not yet been qualified on your operating system."),
]
else:
txtlist = [
"" + txt0 + " " + ", ".join(bad_deps),
"",
"",
_(
"At least one dependency does not comply with CodraFT "
"qualification standard reference (wrong dependency version "
"has been installed, or dependency source code has been "
"modified, or the application has not yet been qualified "
"on your operating system)."
),
]
txtlist += [
"",
_(
"This means that the application has not been officially qualified "
"in this context and may not behave as expected."
),
"",
_(
"Please click on the Ignore button "
"to avoid showing this message at startup."
),
]
txt = "
".join(txtlist)
btn = QW.QMessageBox.information(
self, APP_NAME, txt, QW.QMessageBox.Ok | QW.QMessageBox.Ignore
)
Conf.main.ignore_dependency_check.set(btn == QW.QMessageBox.Ignore)
def check_for_previous_crash(self): # pragma: no cover
"""Check for previous crash"""
if execenv.unattended:
self.show_log_viewer()
elif Conf.main.faulthandler_log_available.get(
False
) or Conf.main.traceback_log_available.get(False):
txt = "
".join(
[
_("Log files were generated during last session."),
"",
_("Do you want to see available log files?"),
]
)
btns = QW.QMessageBox.StandardButton.Yes | QW.QMessageBox.StandardButton.No
choice = QW.QMessageBox.warning(self, APP_NAME, txt, btns)
if choice == QW.QMessageBox.StandardButton.Yes:
self.show_log_viewer()
def take_screenshot(self, name): # pragma: no cover
"""Take main window screenshot"""
self.memorystatus.set_demo_mode(True)
qth.grab_save_window(self, f"{name}")
self.memorystatus.set_demo_mode(False)
def take_menu_screenshots(self): # pragma: no cover
"""Take menu screenshots"""
for panel in self.panels:
self.tabwidget.setCurrentWidget(panel)
for name in (
"file",
"edit",
"view",
"operation",
"processing",
"computing",
"help",
):
menu = getattr(self, f"{name}_menu")
menu.popup(self.pos())
qth.grab_save_window(menu, f"{panel.objectName()}_{name}")
menu.close()
# ------GUI setup
def __restore_pos_and_size(self):
"""Restore main window position and size from configuration"""
pos = Conf.main.window_position.get(None)
if pos is not None:
posx, posy = pos
self.move(QC.QPoint(posx, posy))
size = Conf.main.window_size.get(None)
if size is not None:
width, height = size
self.resize(QC.QSize(width, height))
if pos is not None and size is not None:
sgeo = self.screen().availableGeometry()
out_inf = posx < -int(0.9 * width) or posy < -int(0.9 * height)
out_sup = posx > int(0.9 * sgeo.width()) or posy > int(0.9 * sgeo.height())
if len(QW.QApplication.screens()) == 1 and (out_inf or out_sup):
# Main window is offscreen
posx = min(max(posx, 0), sgeo.width() - width)
posy = min(max(posy, 0), sgeo.height() - height)
self.move(QC.QPoint(posx, posy))
def __save_pos_and_size(self):
"""Save main window position and size to configuration"""
is_maximized = self.windowState() == QC.Qt.WindowMaximized
Conf.main.window_maximized.set(is_maximized)
if not is_maximized:
size = self.size()
Conf.main.window_size.set((size.width(), size.height()))
pos = self.pos()
Conf.main.window_position.set((pos.x(), pos.y()))
def setup(self, console):
"""Setup main window"""
self.statusBar().showMessage(_("Welcome to %s!") % APP_NAME, 5000)
self.memorystatus = MemoryStatus(Conf.main.available_memory_threshold.get(500))
self.memorystatus.SIG_MEMORY_ALARM.connect(self.__set_low_memory_state)
self.statusBar().addPermanentWidget(self.memorystatus)
self.__setup_commmon_actions()
curvewidget = self.__add_signal_panel()
imagewidget = self.__add_image_panel()
self.__add_tabwidget(curvewidget, imagewidget)
self.__add_menus()
if console:
self.__setup_console()
# Update selection dependent actions
self.__update_actions()
self.signal_image_docks[0].raise_()
# Restoring current tab from last session
tab_idx = Conf.main.current_tab.get(None)
if tab_idx is not None:
self.tabwidget.setCurrentIndex(tab_idx)
def __setup_commmon_actions(self):
"""Setup common actions"""
self.openh5_action = create_action(
self,
_("Open HDF5 files..."),
icon=get_icon("h5open.svg"),
tip=_("Open one or several HDF5 files"),
triggered=lambda checked=False: self.open_h5_files(import_all=True),
)
self.saveh5_action = create_action(
self,
_("Save to HDF5 file..."),
icon=get_icon("h5save.svg"),
tip=_("Save to HDF5 file"),
triggered=self.save_to_h5_file,
)
self.browseh5_action = create_action(
self,
_("Browse HDF5 file..."),
icon=get_icon("h5browser.svg"),
tip=_("Browse an HDF5 file"),
triggered=lambda checked=False: self.open_h5_files(import_all=None),
)
h5_toolbar = self.addToolBar(_("HDF5 I/O Toolbar"))
add_actions(
h5_toolbar, [self.openh5_action, self.saveh5_action, self.browseh5_action]
)
# Quit action for "File menu" (added when populating menu on demand)
if self.hide_on_close:
quit_text = _("Hide window")
quit_tip = _("Hide CodraFT window")
else:
quit_text = _("Quit")
quit_tip = _("Quit application")
self.quit_action = create_action(
self,
quit_text,
shortcut=QG.QKeySequence(QG.QKeySequence.Quit),
icon=get_icon("libre-gui-close.svg"),
tip=quit_tip,
triggered=self.close,
)
def __add_signal_panel(self):
"""Setup signal toolbar, widgets and panel"""
self.signal_toolbar = self.addToolBar(_("Signal Processing Toolbar"))
curveplot_toolbar = self.addToolBar(_("Curve Plotting Toolbar"))
curvewidget = DockablePlotWidget(self, CurveWidget, curveplot_toolbar)
curveplot = curvewidget.get_plot()
curveplot.add_item(make.legend("TR"))
self.signalpanel = SignalPanel(
self, curvewidget.plotwidget, self.signal_toolbar
)
self.signalpanel.SIG_STATUS_MESSAGE.connect(self.statusBar().showMessage)
return curvewidget
def __add_image_panel(self):
"""Setup image toolbar, widgets and panel"""
self.image_toolbar = self.addToolBar(_("Image Processing Toolbar"))
imagevis_toolbar = self.addToolBar(_("Image Visualization Toolbar"))
imagewidget = DockablePlotWidget(self, ImageWidget, imagevis_toolbar)
self.imagepanel = ImagePanel(self, imagewidget.plotwidget, self.image_toolbar)
# -----------------------------------------------------------------------------
# # Before eventually disabling the "peritem" mode by default, wait for the
# # guiqwt bug to be fixed (peritem mode is not compatible with multiple image
# # items):
# for cspanel in (
# self.imagepanel.plotwidget.get_xcs_panel(),
# self.imagepanel.plotwidget.get_ycs_panel(),
# ):
# cspanel.peritem_ac.setChecked(False)
# -----------------------------------------------------------------------------
self.imagepanel.SIG_STATUS_MESSAGE.connect(self.statusBar().showMessage)
return imagewidget
def switch_to_signal_panel(self):
"""Switch to signal panel"""
self.tabwidget.setCurrentWidget(self.signalpanel)
def switch_to_image_panel(self):
"""Switch to image panel"""
self.tabwidget.setCurrentWidget(self.imagepanel)
def __add_tabwidget(self, curvewidget, imagewidget):
"""Setup tabwidget with signals and images"""
self.tabwidget = DockableTabWidget()
self.tabwidget.setMaximumWidth(500)
self.tabwidget.addTab(self.signalpanel, get_icon("signal.svg"), _("Signals"))
self.tabwidget.addTab(self.imagepanel, get_icon("image.svg"), _("Images"))
self.__add_dockwidget(self.tabwidget, _("Main panel"))
curve_dock = self.__add_dockwidget(curvewidget, title=_("Curve panel"))
image_dock = self.__add_dockwidget(imagewidget, title=_("Image panel"))
self.tabifyDockWidget(curve_dock, image_dock)
self.signal_image_docks = curve_dock, image_dock
self.tabwidget.currentChanged.connect(self.__tab_index_changed)
self.signalpanel.SIG_OBJECT_ADDED.connect(self.switch_to_signal_panel)
self.imagepanel.SIG_OBJECT_ADDED.connect(self.switch_to_image_panel)
for panel in self.panels:
panel.SIG_OBJECT_ADDED.connect(self.set_modified)
panel.SIG_OBJECT_REMOVED.connect(self.set_modified)
def __add_menus(self):
"""Adding menus"""
self.file_menu = self.menuBar().addMenu(_("File"))
self.file_menu.aboutToShow.connect(self.__update_file_menu)
self.edit_menu = self.menuBar().addMenu(_("&Edit"))
self.operation_menu = self.menuBar().addMenu(_("Operations"))
self.processing_menu = self.menuBar().addMenu(_("Processing"))
self.computing_menu = self.menuBar().addMenu(_("Computing"))
self.view_menu = self.menuBar().addMenu(_("&View"))
self.view_menu.aboutToShow.connect(self.__update_view_menu)
self.help_menu = self.menuBar().addMenu("?")
for menu in (
self.edit_menu,
self.operation_menu,
self.processing_menu,
self.computing_menu,
):
menu.aboutToShow.connect(self.__update_generic_menu)
about_action = create_action(
self,
_("About..."),
icon=get_icon("libre-gui-about.svg"),
triggered=self.__about,
)
homepage_action = create_action(
self,
_("Project home page"),
icon=get_icon("libre-gui-globe.svg"),
triggered=lambda: webbrowser.open(__homeurl__),
)
issue_action = create_action(
self,
_("Bug report or feature request"),
icon=get_icon("libre-gui-globe.svg"),
triggered=lambda: webbrowser.open(__supporturl__),
)
onlinedoc_action = create_action(
self,
_("Online documentation"),
icon=get_icon("libre-gui-help.svg"),
triggered=lambda: webbrowser.open(__docurl__),
)
chmdoc_action = create_action(
self,
_("CHM documentation"),
icon=get_icon("chm.svg"),
triggered=lambda: os.startfile(get_htmlhelp()),
)
chmdoc_action.setVisible(get_htmlhelp() is not None)
logv_action = create_action(
self,
_("Show log files..."),
icon=get_icon("logs.svg"),
triggered=self.show_log_viewer,
)
dep_action = create_action(
self,
_("About CodraFT installation") + "...",
icon=get_icon("logs.svg"),
triggered=lambda: exec_codraft_installconfig_dialog(self),
)
errtest_action = create_action(
self, "Test segfault/Python error", triggered=self.test_segfault_error
)
errtest_action.setVisible(TEST_SEGFAULT_ERROR)
about_action = create_action(
self,
_("About..."),
icon=get_icon("libre-gui-about.svg"),
triggered=self.__about,
)
add_actions(
self.help_menu,
(
onlinedoc_action,
chmdoc_action,
None,
errtest_action,
logv_action,
dep_action,
None,
homepage_action,
issue_action,
about_action,
),
)
def __setup_console(self):
"""Add an internal console"""
self.app_proxy = AppProxy(self)
ns = {
"app": self.app_proxy,
"np": np,
"sps": sps,
"spi": spi,
"os": os,
"sys": sys,
"osp": osp,
"time": time,
}
msg = (
"Example: app.s[0] returns signal object #0\n"
"Modules imported at startup: "
"os, sys, os.path as osp, time, "
"numpy as np, scipy.signal as sps, scipy.ndimage as spi"
)
debug = os.environ.get("DEBUG") == "1"
self.console = DockableConsole(self, namespace=ns, message=msg, debug=debug)
self.console.setMaximumBlockCount(Conf.console.max_line_count.get(5000))
console_dock = self.__add_dockwidget(self.console, _("Console"))
console_dock.hide()
self.console.interpreter.widget_proxy.sig_new_prompt.connect(
lambda txt: self.refresh_lists()
)
# ------GUI refresh
def has_objects(self):
"""Return True if sig/ima panels have any object"""
return sum([len(panel.objlist) for panel in self.panels]) > 0
def set_modified(self, state=True):
"""Set mainwindow modified state"""
state = state and self.has_objects()
self.__is_modified = state
self.setWindowTitle(APP_NAME + ("*" if state else ""))
def __add_dockwidget(self, child, title):
"""Add QDockWidget and toggleViewAction"""
dockwidget, location = child.create_dockwidget(title)
self.addDockWidget(location, dockwidget)
return dockwidget
def refresh_lists(self):
"""Refresh signal/image lists"""
for panel in self.panels:
panel.objlist.refresh_list()
def __update_actions(self):
"""Update selection dependent actions"""
is_signal = self.tabwidget.currentWidget() is self.signalpanel
panel = self.signalpanel if is_signal else self.imagepanel
panel.selection_changed()
self.signal_toolbar.setVisible(is_signal)
self.image_toolbar.setVisible(not is_signal)
def __tab_index_changed(self, index):
"""Switch from signal to image mode, or vice-versa"""
dock = self.signal_image_docks[index]
dock.raise_()
self.__update_actions()
def __update_generic_menu(self, menu=None):
"""Update menu before showing up -- Generic method"""
if menu is None:
menu = self.sender()
menu.clear()
panel = self.tabwidget.currentWidget()
category = {
self.file_menu: ActionCategory.FILE,
self.edit_menu: ActionCategory.EDIT,
self.view_menu: ActionCategory.VIEW,
self.operation_menu: ActionCategory.OPERATION,
self.processing_menu: ActionCategory.PROCESSING,
self.computing_menu: ActionCategory.COMPUTING,
}[menu]
actions = panel.get_category_actions(category)
add_actions(menu, actions)
def __update_file_menu(self):
"""Update file menu before showing up"""
self.saveh5_action.setEnabled(self.has_objects())
self.__update_generic_menu(self.file_menu)
add_actions(
self.file_menu,
[
None,
self.openh5_action,
self.saveh5_action,
self.browseh5_action,
None,
self.quit_action,
],
)
def __update_view_menu(self):
"""Update view menu before showing up"""
self.__update_generic_menu(self.view_menu)
add_actions(self.view_menu, [None] + self.createPopupMenu().actions())
# ------Common features
def reset_all(self):
"""Reset all application data"""
for panel in self.panels:
panel.remove_all_objects()
@staticmethod
def __check_h5file(filename, operation: str):
"""Check HDF5 filename"""
filename = osp.abspath(osp.normpath(filename))
bname = osp.basename(filename)
if operation == "load" and not osp.isfile(filename):
raise IOError(f'File not found "{bname}"')
if not filename.endswith(".h5"):
raise IOError(f'Invalid HDF5 file "{bname}"')
Conf.main.base_dir.set(filename)
return filename
def save_to_h5_file(self, filename=None):
"""Save to a CodraFT HDF5 file"""
if filename is None:
basedir = Conf.main.base_dir.get()
with qth.save_restore_stds():
filters = f'{_("HDF5 files")} (*.h5)'
filename, _filter = getsavefilename(self, _("Save"), basedir, filters)
if not filename:
return
with qth.qt_try_loadsave_file(self.parent(), filename, "save"):
filename = self.__check_h5file(filename, "save")
self.h5inputoutput.save_file(filename)
self.set_modified(False)
def open_h5_files(
self,
h5files: List[str] = None,
import_all: bool = None,
reset_all: bool = None,
) -> None:
"""Open a CodraFT HDF5 file or import from any other HDF5 file
:param h5files: HDF5 filenames (optionally with dataset name, separated by ":")
:param import_all: Import all HDF5 file contents
:param reset_all: Delete all CodraFT signals and images before importing data
"""
if not self.confirm_memory_state():
return
if reset_all is None:
reset_all = False
if self.has_objects():
answer = QW.QMessageBox.question(
self,
_("Warning"),
_(
"Do you want to remove all signals and images "
"before importing data from HDF5 files?"
),
QW.QMessageBox.Yes | QW.QMessageBox.No,
)
if answer == QW.QMessageBox.Yes:
reset_all = True
if h5files is None:
basedir = Conf.main.base_dir.get()
with qth.save_restore_stds():
filters = f'{_("HDF5 files")} (*.h5)'
h5files, _filter = getopenfilenames(self, _("Open"), basedir, filters)
for fname_with_dset in h5files:
if "," in fname_with_dset:
filename, dsetname = fname_with_dset.split(",")
else:
filename, dsetname = fname_with_dset, None
if import_all is None and dsetname is None:
self.import_h5_file(filename, reset_all)
else:
with qth.qt_try_loadsave_file(self, filename, "load"):
filename = self.__check_h5file(filename, "load")
if dsetname is None:
self.h5inputoutput.open_file(filename, import_all, reset_all)
else:
self.h5inputoutput.import_dataset_from_file(filename, dsetname)
reset_all = False
def import_h5_file(self, filename: str, reset_all: bool = None) -> None:
"""Open CodraFT HDF5 browser to Import HDF5 file
:param filename: HDF5 filename
:param reset_all: Delete all CodraFT signals and images before importing data
"""
with qth.qt_try_loadsave_file(self, filename, "load"):
filename = self.__check_h5file(filename, "load")
self.h5inputoutput.import_file(filename, False, reset_all)
def add_object(self, obj, refresh=True):
"""Add object - signal or image"""
if self.confirm_memory_state():
if isinstance(obj, SignalParam):
self.signalpanel.add_object(obj, refresh=refresh)
elif isinstance(obj, ImageParam):
self.imagepanel.add_object(obj, refresh=refresh)
else:
raise TypeError(f"Unsupported object type {type(obj)}")
# ------?
def __about(self): # pragma: no cover
"""About dialog box"""
self.check_stable_release()
QW.QMessageBox.about(
self,
_("About ") + APP_NAME,
f"""{APP_NAME} v{__version__}
{APP_DESC}
%s Pierre Raybaut
Copyright © 2018-2022 CEA-CODRA
PythonQwt {qwt_ver}, guidata {guidata_ver},
guiqwt {guiqwt_ver}
Python {platform.python_version()},
Qt {QC.__version__}, PyQt {QC.PYQT_VERSION_STR}
%s {platform.system()}"""
% (_("Developped by"), _("on")),
)
def show_log_viewer(self):
"""Show error logs"""
exec_codraft_logviewer_dialog(self)
@staticmethod
def test_segfault_error():
"""Generate errors (both fault and traceback)"""
import ctypes # pylint: disable=import-outside-toplevel
ctypes.string_at(0)
raise RuntimeError("!!! Testing RuntimeError !!!")
def show(self):
"""Reimplement QMainWindow method"""
super().show()
if self.__old_size is not None:
self.resize(self.__old_size)
# ------Close window
def closeEvent(self, event):
"""Reimplement QMainWindow method"""
if self.hide_on_close:
self.__old_size = self.size()
self.hide()
else:
if not env.execenv.unattended and self.__is_modified:
answer = QW.QMessageBox.warning(
self,
_("Quit"),
_(
"Do you want to save all signals and images "
"to an HDF5 file before quitting CodraFT?"
),
QW.QMessageBox.Yes | QW.QMessageBox.No | QW.QMessageBox.Cancel,
)
if answer == QW.QMessageBox.Yes:
self.save_to_h5_file()
if self.__is_modified:
event.ignore()
return
elif answer == QW.QMessageBox.Cancel:
event.ignore()
return
if self.console is not None:
try:
self.console.close()
except RuntimeError:
# TODO: [P3] Investigate further why the following error occurs when
# restarting the mainwindow (this is *not* a production case):
# "RuntimeError: wrapped C/C++ object of type DockableConsole
# has been deleted".
# Another solution to avoid this error would be to really restart
# the application (run each unit test in a separate process), but
# it would represent too much effort for an error occuring in test
# configurations only.
pass
self.reset_all()
self.__save_pos_and_size()
# Saving current tab for next session
Conf.main.current_tab.set(self.tabwidget.currentIndex())
event.accept()
CodraFT-2.2.1/codraft/core/gui/objectlist.py 0000664 0000000 0000000 00000014734 14435624103 0020663 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
Object (signal/image) list widgets
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import re
from typing import Tuple
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from codraft.utils.qthelpers import block_signals
class SimpleObjectList(QW.QListWidget):
"""Base object handling panel list widget, object (sig/ima) lists"""
SIG_ITEM_DOUBLECLICKED = QC.Signal(int)
SIG_CONTEXT_MENU = QC.Signal(QC.QPoint)
def __init__(self, panel, parent=None):
parent = panel if parent is None else parent
super().__init__(parent)
self.panel = panel
self.prefix = panel.PREFIX
self.setAlternatingRowColors(True)
self._objects = [] # signals or images
self.itemDoubleClicked.connect(self.item_double_clicked)
def init_from(self, objlist):
"""Init from another SimpleObjectList, without making copies of objects"""
self._objects = objlist.get_objects()
self.refresh_list()
self.setCurrentRow(objlist.currentRow())
def get_objects(self):
"""Get all objects"""
return self._objects
def set_current_row(self, row, extend=False, refresh=True):
"""Set list widget current row"""
if row < 0:
row += self.count()
if extend:
command = QC.QItemSelectionModel.Select
else:
command = QC.QItemSelectionModel.ClearAndSelect
with block_signals(widget=self, enable=not refresh):
self.setCurrentRow(row, command)
def refresh_list(self, new_current_row=None):
"""
Refresh object list
:param new_current_row: New row (if None, new current row is unchanged)
"""
row = self.currentRow()
if new_current_row is not None:
row = new_current_row
self.clear()
for idx, obj in enumerate(self._objects):
item = QW.QListWidgetItem(f"{self.prefix}{idx:03d}: {obj.title}", self)
item.setToolTip(obj.metadata_to_html())
self.addItem(item)
if row < self.count():
self.set_current_row(row)
def item_double_clicked(self, listwidgetitem):
"""Item was double-clicked: open a pop-up plot dialog"""
self.SIG_ITEM_DOUBLECLICKED.emit(self.row(listwidgetitem))
def contextMenuEvent(self, event): # pylint: disable=C0103
"""Override Qt method"""
self.SIG_CONTEXT_MENU.emit(event.globalPos())
class GetObjectDialog(QW.QDialog):
"""Get object dialog box"""
def __init__(self, parent, panel, title):
super().__init__(parent)
self.setWindowTitle(title)
self.setLayout(QW.QVBoxLayout())
self.objlist = SimpleObjectList(panel, parent=parent)
self.objlist.init_from(panel.objlist)
self.objlist.SIG_ITEM_DOUBLECLICKED.connect(lambda row: self.accept())
self.layout().addWidget(self.objlist)
bbox = QW.QDialogButtonBox(QW.QDialogButtonBox.Ok | QW.QDialogButtonBox.Cancel)
bbox.accepted.connect(self.accept)
bbox.rejected.connect(self.reject)
bbox.button(QW.QDialogButtonBox.Ok).setEnabled(self.objlist.count() > 0)
self.layout().addSpacing(10)
self.layout().addWidget(bbox)
def get_object(self):
"""Return current object"""
return self.objlist.get_objects()[self.objlist.currentRow()]
class ObjectList(SimpleObjectList):
"""Object handling panel list widget, object (sig/ima) lists"""
def __init__(self, panel):
super().__init__(panel)
self.setSelectionMode(QW.QListWidget.ExtendedSelection)
def __len__(self):
"""Return number of objects"""
return len(self._objects)
def __getitem__(self, row):
"""Return object at row"""
return self._objects[row]
def __setitem__(self, row, obj):
"""Set object at row"""
self._objects[row] = obj
def __contains__(self, obj):
"""Return True if list contain obj"""
return obj in self._objects
def get_row(self, obj):
"""Return row associated to object obj"""
return self._objects.index(obj)
def __fix_obj_titles(self, row: int, sign: int) -> None:
"""Fix all object titles before adding (sign==1) or removing (sign==-1)
an object at row index"""
pfx = self.prefix
oname = f"{pfx}%03d"
for obj in self:
for match in re.finditer(pfx + "[0-9]{3}", obj.title):
before = match.group()
i_match = int(before[1:])
if sign == -1 and i_match == row:
after = f"{pfx}xxx"
elif (sign == -1 and i_match > row) or (sign == 1 and i_match >= row):
after = oname % (i_match + sign)
else:
continue
obj.title = obj.title.replace(before, after)
def __delitem__(self, row):
"""Del object at row"""
self.__fix_obj_titles(row, -1)
self._objects.pop(row)
def __iter__(self):
"""Return an iterator over objects"""
yield from self._objects
def get_sel_object(self, position=0):
"""
Return currently selected object
:param int position: Position in selection list (0 means first, -1 means last)
:return: Current object or None if there is no selection
"""
rows = self.get_selected_rows()
if rows:
return self[rows[position]]
return None
def get_sel_objects(self):
"""Return selected objects"""
return [self[row] for row in self.get_selected_rows()]
def append(self, obj):
"""Append object"""
self._objects.append(obj)
def insert(self, row, obj):
"""Insert object at row index"""
self.__fix_obj_titles(row, 1)
self._objects.insert(row, obj)
def remove_all(self):
"""Remove all objects"""
self._objects = []
def select_rows(self, rows: Tuple):
"""Select multiple list widget rows"""
for index, row in enumerate(sorted(rows)):
self.set_current_row(row, extend=index != 0, refresh=row == len(rows) - 1)
def select_all_rows(self):
"""Select all widget rows"""
self.selectAll()
def get_selected_rows(self):
"""Return selected rows"""
return [index.row() for index in self.selectionModel().selectedRows()]
CodraFT-2.2.1/codraft/core/gui/panel.py 0000664 0000000 0000000 00000076654 14435624103 0017631 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Panel widgets (core.gui.panel)
Signal and Image Panel widgets relie on components:
* `ObjectProp`: widget handling signal/image properties
using a guidata DataSet
* `core.gui.panel.objectlist.ObjectList`: widget handling signal/image list
* `core.gui.panel.actionhandler.SignalActionHandler` or `ImageActionHandler`:
classes handling Qt actions
* `core.gui.panel.plotitemlist.SignalItemList` or `ImageItemList`:
classes handling guiqwt plot items
* `core.gui.panel.processor.signal.SignalProcessor` or
`core.gui.panel.processor.image.ImageProcessor`: classes handling computing features
* `core.gui.panel.roieditor.SignalROIEditor` or `ImageROIEditor`:
classes handling ROI editor widgets
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import abc
import dataclasses
import os.path as osp
import re
import warnings
from typing import List
import guidata.dataset.qtwidgets as gdq
import numpy as np
from guidata.configtools import get_icon
from guidata.qthelpers import add_actions
from guidata.utils import update_dataset
from guidata.widgets.arrayeditor import ArrayEditor
from guiqwt.io import imread, imwrite, iohandler
from guiqwt.plot import CurveDialog, ImageDialog
from guiqwt.tools import (
AnnotatedCircleTool,
AnnotatedEllipseTool,
AnnotatedPointTool,
AnnotatedRectangleTool,
AnnotatedSegmentTool,
HCursorTool,
LabelTool,
RectangleTool,
SegmentTool,
VCursorTool,
XCursorTool,
)
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy.compat import getopenfilename, getopenfilenames, getsavefilename
from codraft.config import APP_NAME, Conf, _
from codraft.core.gui import actionhandler, objectlist, plotitemlist, roieditor
from codraft.core.gui.processor.image import ImageProcessor
from codraft.core.gui.processor.signal import SignalProcessor
from codraft.core.io.signal import read_signal, write_signal
from codraft.core.model.base import MetadataItem, ObjectItf, ResultShape
from codraft.core.model.image import (
ImageDatatypes,
ImageParam,
create_image,
create_image_from_param,
new_image_param,
)
from codraft.core.model.signal import (
SignalParam,
create_signal_from_param,
new_signal_param,
)
from codraft.utils.qthelpers import (
exec_dialog,
qt_try_except,
qt_try_loadsave_file,
save_restore_stds,
)
# Registering MetadataItem edit widget
gdq.DataSetEditLayout.register(MetadataItem, gdq.ButtonWidget)
class ObjectProp(QW.QWidget):
"""Object handling panel properties"""
def __init__(self, panel, paramclass):
super().__init__(panel)
self.paramclass = paramclass
self.properties = gdq.DataSetEditGroupBox(_("Properties"), paramclass)
self.properties.SIG_APPLY_BUTTON_CLICKED.connect(panel.properties_changed)
self.properties.setEnabled(False)
self.add_prop_layout = QW.QHBoxLayout()
playout = self.properties.edit.layout
playout.addLayout(
self.add_prop_layout, playout.rowCount() - 1, 0, 1, 1, QC.Qt.AlignLeft
)
hlayout = QW.QHBoxLayout()
hlayout.addWidget(self.properties)
vlayout = QW.QVBoxLayout()
vlayout.addLayout(hlayout)
vlayout.addStretch()
self.setLayout(vlayout)
def add_button(self, button):
"""Add additional button on bottom of properties panel"""
self.add_prop_layout.addWidget(button)
def update_properties_from(self, param: ObjectItf = None):
"""Update properties from signal/image dataset"""
self.properties.setDisabled(param is None)
if param is None:
param = self.paramclass()
self.properties.dataset.set_defaults()
update_dataset(self.properties.dataset, param)
self.properties.get()
class BasePanelMeta(type(QW.QSplitter), abc.ABCMeta):
"""Mixed metaclass to avoid conflicts"""
class BasePanel(QW.QSplitter, metaclass=BasePanelMeta):
"""Object handling the item list, the selected item properties and plot"""
PANEL_STR = "" # e.g. "Signal Panel"
PARAMCLASS = SignalParam # Replaced by the right class in child object
DIALOGCLASS = CurveDialog # Idem
ANNOTATION_TOOLS = (
LabelTool,
VCursorTool,
HCursorTool,
XCursorTool,
SegmentTool,
RectangleTool,
)
DIALOGSIZE = (800, 600)
PREFIX = "" # e.g. "s"
OPEN_FILTERS = "" # Qt file open dialog filters
H5_PREFIX = ""
SIG_STATUS_MESSAGE = QC.Signal(str) # emitted by "qt_try_except" decorator
SIG_OBJECT_ADDED = QC.Signal()
SIG_OBJECT_REMOVED = QC.Signal()
SIG_UPDATE_PLOT_ITEM = QC.Signal(int) # Update plot item associated to row number
SIG_UPDATE_PLOT_ITEMS = QC.Signal() # Update plot items associated to selected rows
ROIDIALOGOPTIONS = {}
ROIDIALOGCLASS = roieditor.BaseROIEditor # Replaced in child object
@abc.abstractmethod
def __init__(self, parent, plotwidget, toolbar):
super().__init__(QC.Qt.Vertical, parent)
self.setObjectName(self.PREFIX)
self.mainwindow = parent
self.objprop = ObjectProp(self, self.PARAMCLASS)
self.objlist = objectlist.ObjectList(self)
self.itmlist = None
self.processor = None
self.acthandler = None
self.__metadata_clipboard = {}
self.context_menu = QW.QMenu()
self.__separate_views = {}
def setup_panel(self):
"""Setup panel"""
self.processor.SIG_ADD_SHAPE.connect(self.itmlist.add_shapes)
self.SIG_UPDATE_PLOT_ITEM.connect(self.itmlist.refresh_plot)
self.SIG_UPDATE_PLOT_ITEMS.connect(self.itmlist.refresh_plot)
self.objlist.itemSelectionChanged.connect(self.selection_changed)
self.objlist.SIG_ITEM_DOUBLECLICKED.connect(
lambda row: self.open_separate_view([row])
)
self.objlist.SIG_CONTEXT_MENU.connect(self.__popup_contextmenu)
self.objprop.properties.SIG_APPLY_BUTTON_CLICKED.connect(
self.properties_changed
)
self.addWidget(self.objlist)
self.addWidget(self.objprop)
self.add_results_button()
def get_category_actions(self, category): # pragma: no cover
"""Return actions for category"""
return self.acthandler.feature_actions[category]
def __popup_contextmenu(self, position: QC.QPoint): # pragma: no cover
"""Popup context menu at position"""
# Note: For now, this is completely unnecessary to clear context menu everytime,
# but implementing it this way could be useful in the future in menu contents
# should take into account current object selection
self.context_menu.clear()
add_actions(self.context_menu, self.acthandler.actlist_cmenu)
self.context_menu.popup(position)
# ------Creating, adding, removing objects------------------------------------------
def create_object(self, title=None):
"""Create object (signal or image)
:param str title: Title of the object
"""
obj = self.PARAMCLASS(title=title)
obj.title = title
return obj
@qt_try_except()
def add_object(self, obj, refresh=True):
"""Add signal/image object and return associated plot item"""
obj.check_data()
self.objlist.append(obj)
item = self.itmlist.append(None)
if refresh:
self.objlist.refresh_list(-1)
self.SIG_OBJECT_ADDED.emit()
return item
# TODO: [P2] New feature: move objects up/down
@qt_try_except()
def insert_object(self, obj, row, refresh=True):
"""Insert signal/image object after row"""
obj.check_data()
self.objlist.insert(row, obj)
self.itmlist.insert(row)
if refresh:
self.objlist.refresh_list(new_current_row=row + 1)
self.SIG_OBJECT_ADDED.emit()
def duplicate_object(self):
"""Duplication signal/image object"""
if not self.mainwindow.confirm_memory_state():
return
rows = sorted(self.objlist.get_selected_rows())
row = None
for row in rows:
obj = self.objlist[row]
objcopy = self.create_object()
objcopy.title = obj.title
objcopy.copy_data_from(obj)
self.add_object(objcopy, refresh=False)
self.objlist.refresh_list(new_current_row=-1)
self.SIG_UPDATE_PLOT_ITEMS.emit()
def copy_metadata(self):
"""Copy object metadata"""
row = self.objlist.get_selected_rows()[0]
obj = self.objlist[row]
self.__metadata_clipboard = obj.metadata.copy()
pfx = self.objlist.prefix
new_pref = f"{pfx}{row:03d}_"
for key, value in obj.metadata.items():
if ResultShape.match(key, value):
mshape = ResultShape.from_metadata_entry(key, value)
if not re.match(pfx + r"[0-9]{3}[\s]*", mshape.label):
# Handling additional result (e.g. diameter)
for a_key, a_value in obj.metadata.items():
if isinstance(a_key, str) and a_key.startswith(mshape.label):
self.__metadata_clipboard.pop(a_key)
self.__metadata_clipboard[new_pref + a_key] = a_value
mshape.label = new_pref + mshape.label
# Handling result shape
self.__metadata_clipboard.pop(key)
self.__metadata_clipboard[mshape.key] = value
def paste_metadata(self):
"""Paste metadata to selected object(s)"""
rows = sorted(self.objlist.get_selected_rows(), reverse=True)
row = None
for row in rows:
obj = self.objlist[row]
obj.metadata.update(self.__metadata_clipboard)
self.SIG_UPDATE_PLOT_ITEMS.emit()
def remove_object(self):
"""Remove signal/image object"""
rows = sorted(self.objlist.get_selected_rows(), reverse=True)
for row in rows:
for dlg, obj in self.__separate_views.items():
if obj is self.objlist[row]:
dlg.done(QW.QDialog.DialogCode.Rejected)
del self.objlist[row]
del self.itmlist[row]
self.objlist.refresh_list(max(0, rows[-1] - 1))
self.SIG_UPDATE_PLOT_ITEMS.emit()
self.SIG_OBJECT_REMOVED.emit()
def delete_all_objects(self): # pragma: no cover
"""Confirm before removing all objects"""
if len(self.objlist) == 0:
return
answer = QW.QMessageBox.warning(
self,
_("Delete all"),
_("Do you want to delete all objects (%s)?") % self.PANEL_STR,
QW.QMessageBox.Yes | QW.QMessageBox.No,
)
if answer == QW.QMessageBox.Yes:
self.remove_all_objects()
def remove_all_objects(self):
"""Remove all signal/image objects"""
for dlg in self.__separate_views:
dlg.done(QW.QDialog.DialogCode.Rejected)
self.objlist.remove_all()
self.itmlist.remove_all()
self.objlist.refresh_list(0)
self.SIG_UPDATE_PLOT_ITEMS.emit()
self.SIG_OBJECT_REMOVED.emit()
def delete_metadata(self):
"""Delete object metadata"""
for index, row in enumerate(self.objlist.get_selected_rows()):
self.objlist[row].metadata = {}
if index == 0:
self.selection_changed()
self.SIG_UPDATE_PLOT_ITEMS.emit()
@abc.abstractmethod
def new_object(self, newparam=None, addparam=None, edit=True):
"""Create a new object (signal/image).
:param guidata.dataset.DataSet newparam: new object parameters
:param guidata.dataset.datatypes.DataSet addparam: additional parameters
:param bool edit: Open a dialog box to edit parameters (default: True)
"""
@abc.abstractmethod
def open_object(self, filename: str) -> None:
"""Open object from file (signal/image)"""
def open_objects(self, filenames: List[str] = None) -> None:
"""Open objects from file (signals/images)"""
if not self.mainwindow.confirm_memory_state():
return
if filenames is None: # pragma: no cover
basedir = Conf.main.base_dir.get()
with save_restore_stds():
filenames, _filter = getopenfilenames(
self, _("Open"), basedir, self.OPEN_FILTERS
)
for filename in filenames:
with qt_try_loadsave_file(self.parent(), filename, "load"):
Conf.main.base_dir.set(filename)
self.open_object(filename)
def save_objects(self, filenames: List[str] = None) -> None:
"""Save selected objects to file (signal/image)"""
rows = self.objlist.get_selected_rows()
if filenames is None: # pragma: no cover
filenames = [None] * len(rows)
assert len(filenames) == len(rows)
for index, row in enumerate(rows):
filename = filenames[index]
obj = self.objlist[row]
self.save_object(obj, filename)
@abc.abstractmethod
def save_object(self, obj, filename: str = None) -> None:
"""Save object to file (signal/image)"""
def import_metadata_from_file(self, filename: str = None):
"""Import metadata from file (JSON)"""
if filename is None: # pragma: no cover
basedir = Conf.main.base_dir.get()
with save_restore_stds():
filename, _filter = getopenfilename(
self, _("Import metadata"), basedir, "*.json"
)
if filename:
with qt_try_loadsave_file(self.parent(), filename, "load"):
Conf.main.base_dir.set(filename)
row = self.objlist.get_selected_rows()[0]
obj = self.objlist[row]
obj.import_metadata_from_file(filename)
self.SIG_UPDATE_PLOT_ITEMS.emit()
def export_metadata_from_file(self, filename: str = None):
"""Export metadata to file (JSON)"""
row = self.objlist.get_selected_rows()[0]
obj = self.objlist[row]
if filename is None: # pragma: no cover
basedir = Conf.main.base_dir.get()
with save_restore_stds():
filename, _filt = getsavefilename(
self, _("Export metadata"), basedir, "*.json"
)
if filename:
with qt_try_loadsave_file(self.parent(), filename, "save"):
Conf.main.base_dir.set(filename)
obj.export_metadata_to_file(filename)
# ------Serializing/deserializing objects-------------------------------------------
def serialize_to_hdf5(self, writer):
"""Serialize objects to a HDF5 file"""
with writer.group(self.H5_PREFIX):
for idx, obj in enumerate(self.objlist):
title = re.sub("[^-a-zA-Z0-9_.() ]+", "", obj.title.replace("/", "_"))
name = f"{self.PREFIX}{idx:03d}: {title}"
with writer.group(name):
obj.serialize(writer)
def deserialize_from_hdf5(self, reader):
"""Deserialize objects from a HDF5 file"""
with reader.group(self.H5_PREFIX):
for name in reader.h5.get(self.H5_PREFIX, []):
obj = self.PARAMCLASS()
with reader.group(name):
obj.deserialize(reader)
self.add_object(obj)
QW.QApplication.processEvents()
# ------Refreshing GUI--------------------------------------------------------------
def selection_changed(self):
"""Signal list: selection changed"""
row = self.objlist.currentRow()
sel_objs = self.objlist.get_sel_objects()
if not sel_objs:
row = -1
self.objprop.update_properties_from(self.objlist[row] if row != -1 else None)
self.SIG_UPDATE_PLOT_ITEMS.emit()
self.acthandler.selection_rows_changed()
def properties_changed(self):
"""The properties 'Apply' button was clicked: updating signal"""
row = self.objlist.currentRow()
update_dataset(self.objlist[row], self.objprop.properties.dataset)
self.objlist.refresh_list()
self.SIG_UPDATE_PLOT_ITEMS.emit()
# ------Plotting data in modal dialogs----------------------------------------------
def open_separate_view(self, rows=None) -> QW.QDialog:
"""
Open separate view for visualizing selected objects
:param list rows: List of row indexes for the objects to be shown in dialog
:return: Dialog instance
"""
title = _("Annotations")
if rows is None:
rows = self.objlist.get_selected_rows()
row = rows[0]
obj = self.objlist[row]
dlg = self.create_new_dialog(rows, edit=True, name="new_window")
width, height = self.DIALOGSIZE
dlg.resize(width, height)
dlg.plot_widget.itemlist.setVisible(True)
toolbar = QW.QToolBar(title, self)
dlg.button_layout.insertWidget(0, toolbar)
# dlg.layout().insertWidget(1, toolbar) # other possible location
# dlg.plot_layout.addWidget(toolbar, 1, 0, 1, 1) # other possible location
dlg.add_toolbar(toolbar, id(toolbar))
toolbar.setToolButtonStyle(QC.Qt.ToolButtonTextUnderIcon)
for tool in self.ANNOTATION_TOOLS:
dlg.add_tool(tool, toolbar_id=id(toolbar))
plot = dlg.get_plot()
plot.unselect_all()
for item in plot.items:
item.set_selectable(False)
for item in obj.iterate_shape_items(editable=True):
plot.add_item(item)
self.__separate_views[dlg] = obj
dlg.show()
dlg.finished.connect(self.__separate_view_finished)
return dlg
def __separate_view_finished(self, result: int):
"""Separate view was closed"""
dlg = self.sender()
if result == QW.QDialog.DialogCode.Accepted:
items = dlg.get_plot().get_items()
rw_items = [item for item in items if not item.is_readonly()]
if rw_items:
obj = self.__separate_views[dlg]
obj.set_annotations_from_items(rw_items)
self.selection_changed()
self.SIG_UPDATE_PLOT_ITEMS.emit()
def toggle_show_titles(self, state):
"""Toggle show annotations option"""
Conf.view.show_label.set(state)
for obj in self.objlist:
obj.metadata[obj.METADATA_LBL] = state
self.SIG_UPDATE_PLOT_ITEMS.emit()
def create_new_dialog(
self,
rows,
edit=False,
toolbar=True,
title=None,
tools=None,
name=None,
options=None,
):
"""
Create new pop-up signal/image plot dialog
:param list rows: List of row indexes for the objects to be shown in dialog
:param bool edit: If True, show "OK" and "Cancel" buttons
:param bool toolbar: If True, add toolbar
:param str title: Title of the dialog box
:param list tools: List of plot tools
:param str name: Name of the widget (used as screenshot basename)
:param dict options: Plot options
"""
if title is not None or len(rows) == 1:
if title is None:
title = self.objlist.get_sel_object().title
title = f"{title} - {APP_NAME}"
else:
title = APP_NAME
plot_options = self.itmlist.get_current_plot_options()
if options is not None:
plot_options.update(options)
dlg = self.DIALOGCLASS(
parent=self,
wintitle=title,
edit=edit,
options=plot_options,
toolbar=toolbar,
)
dlg.setWindowIcon(get_icon("codraft.svg"))
dlg.setObjectName(f"{self.PREFIX}_{name}")
if tools is not None:
for tool in tools:
dlg.add_tool(tool)
plot = dlg.get_plot()
for row in rows:
item = self.itmlist.make_item_from_existing(row)
item.set_readonly(True)
plot.add_item(item, z=0)
plot.set_active_item(item)
plot.replot()
return dlg
def create_new_dialog_for_selection(
self, title, name, options=None, toolbar=False, tools=None
):
"""
Create new pop-up dialog for the currently selected signal/image
:param str title: Title of the dialog box
:param str name: Name of the widget (used as screenshot basename)
:param dict options: Plot options
:param list tools: List of plot tools
:return: tuple (dialog, current_object)
"""
row = self.objlist.get_selected_rows()[0]
obj = self.objlist[row]
dlg = self.create_new_dialog(
[row],
edit=True,
toolbar=toolbar,
title=f"{title} - {obj.title}",
tools=tools,
name=name,
options=options,
)
return dlg, obj
def get_roi_dialog(self, extract: bool, singleobj: bool) -> roieditor.ROIEditorData:
"""Get ROI data (array) from specific dialog box"""
roi_s = _("Regions of interest")
options = self.ROIDIALOGOPTIONS
dlg, obj = self.create_new_dialog_for_selection(roi_s, "roi_dialog", options)
plot = dlg.get_plot()
plot.unselect_all()
for item in plot.items:
item.set_selectable(False)
roi_editor = self.ROIDIALOGCLASS(dlg, obj, extract, singleobj)
dlg.plot_layout.addWidget(roi_editor, 1, 0, 1, 1)
if exec_dialog(dlg):
return roi_editor.get_data()
return None
def get_object_dialog(
self, parent: QW.QWidget, title: str
) -> objectlist.GetObjectDialog:
"""Get object dialog"""
dlg = objectlist.GetObjectDialog(parent, self, title)
if exec_dialog(dlg):
return dlg.get_object()
return None
def add_results_button(self):
"""Add 'Show results' button"""
btn = QW.QPushButton(get_icon("show_results.svg"), _("Show results"), self)
btn.setToolTip(_("Show results obtained from previous computations"))
self.objprop.add_button(btn)
btn.clicked.connect(self.show_results)
self.acthandler.actlist_1more.append(btn)
def show_results(self):
"""Show results"""
rows = self.objlist.get_selected_rows()
@dataclasses.dataclass
class ResultData:
"""Result data associated to a shapetype"""
results: List[ResultShape] = None
xlabels: List[str] = None
ylabels: List[str] = None
rdatadict = {}
for idx, row in enumerate(rows):
obj = self.objlist[row]
for key, value in obj.metadata.items():
if ResultShape.match(key, value):
result = ResultShape.from_metadata_entry(key, value)
rdata = rdatadict.setdefault(
result.shapetype, ResultData([], None, [])
)
title = f"{result.label}"
rdata.results.append(result)
rdata.xlabels = result.xlabels
for _i_row_res in range(result.array.shape[0]):
ylabel = f"{self.PREFIX}{idx:03d}: {result.label}"
rdata.ylabels.append(ylabel)
if rdatadict:
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
for rdata in rdatadict.values():
dlg = ArrayEditor(self.parent())
title = _("Results")
dlg.setup_and_check(
np.vstack([result.array for result in rdata.results]),
title,
readonly=True,
xlabels=rdata.xlabels,
ylabels=rdata.ylabels,
)
dlg.setObjectName(f"{self.PREFIX}_results")
dlg.resize(750, 300)
exec_dialog(dlg)
else:
msg = "
".join(
[
_("No result currently available for this object."),
"",
_(
"This feature shows result arrays as displayed after "
'calling one of the computing feature (see "Compute" menu).'
),
]
)
QW.QMessageBox.information(self, APP_NAME, msg)
class SignalPanel(BasePanel):
"""Object handling the item list, the selected item properties and plot,
specialized for Signal objects"""
PANEL_STR = _("Signal List")
PARAMCLASS = SignalParam
DIALOGCLASS = CurveDialog
PREFIX = "s"
OPEN_FILTERS = f'{_("Text files")} (*.txt *.csv)\n{_("NumPy arrays")} (*.npy)'
H5_PREFIX = "CodraFT_Sig"
ROIDIALOGCLASS = roieditor.SignalROIEditor
# pylint: disable=duplicate-code
def __init__(self, parent, plotwidget, toolbar):
super().__init__(parent, plotwidget, toolbar)
self.itmlist = plotitemlist.SignalItemList(self, self.objlist, plotwidget)
self.processor = SignalProcessor(self, self.objlist, plotwidget)
self.acthandler = actionhandler.SignalActionHandler(
self, self.objlist, self.itmlist, self.processor, toolbar
)
self.setup_panel()
# ------Creating, adding, removing objects------------------------------------------
def new_object(self, newparam=None, addparam=None, edit=True):
"""Create a new signal.
:param codraft.core.model.signal.SignalNewParam newparam: new signal parameters
:param guidata.dataset.datatypes.DataSet addparam: additional parameters
:param bool edit: Open a dialog box to edit parameters (default: True)
"""
if not self.mainwindow.confirm_memory_state():
return
curobj = self.objlist.get_sel_object(-1)
if curobj is not None:
newparam = newparam if newparam is not None else new_signal_param()
newparam.size = len(curobj.data)
newparam.xmin = curobj.x.min()
newparam.xmax = curobj.x.max()
signal = create_signal_from_param(
newparam, addparam=addparam, edit=edit, parent=self
)
if signal is not None:
self.add_object(signal)
def open_object(self, filename: str) -> None:
"""Open object from file (signal/image)"""
signal = read_signal(filename)
self.add_object(signal)
def save_object(self, obj, filename: str = None) -> None:
"""Save object to file (signal/image)"""
if filename is None:
basedir = Conf.main.base_dir.get()
with save_restore_stds():
filename, _filter = getsavefilename( # pylint: disable=duplicate-code
self, _("Save as"), basedir, self.OPEN_FILTERS
)
if filename:
with qt_try_loadsave_file(self.parent(), filename, "save"):
Conf.main.base_dir.set(filename)
write_signal(obj, filename)
class ImagePanel(BasePanel):
"""Object handling the item list, the selected item properties and plot,
specialized for Image objects"""
PANEL_STR = _("Image List")
PARAMCLASS = ImageParam
DIALOGCLASS = ImageDialog
DIALOGSIZE = (800, 800)
ANNOTATION_TOOLS = (
AnnotatedCircleTool,
AnnotatedSegmentTool,
AnnotatedRectangleTool,
AnnotatedPointTool,
AnnotatedEllipseTool,
LabelTool,
)
PREFIX = "i"
OPEN_FILTERS = iohandler.get_filters("load", dtype=None)
H5_PREFIX = "CodraFT_Ima"
ROIDIALOGOPTIONS = dict(show_itemlist=True, show_contrast=False)
ROIDIALOGCLASS = roieditor.ImageROIEditor
# pylint: disable=duplicate-code
def __init__(self, parent, plotwidget, toolbar):
super().__init__(parent, plotwidget, toolbar)
self.itmlist = plotitemlist.ImageItemList(self, self.objlist, plotwidget)
self.processor = ImageProcessor(self, self.objlist, plotwidget)
self.acthandler = actionhandler.ImageActionHandler(
self, self.objlist, self.itmlist, self.processor, toolbar
)
self.setup_panel()
# ------Refreshing GUI--------------------------------------------------------------
def properties_changed(self):
"""The properties 'Apply' button was clicked: updating signal"""
row = self.objlist.currentRow()
self.objlist[row].invalidate_maskdata_cache()
super().properties_changed()
# ------Creating, adding, removing objects------------------------------------------
def new_object(self, newparam=None, addparam=None, edit=True):
"""Create a new image.
:param codraft.core.model.image.ImageNewParam newparam: new image parameters
:param guidata.dataset.datatypes.DataSet addparam: additional parameters
:param bool edit: Open a dialog box to edit parameters (default: True)
"""
if not self.mainwindow.confirm_memory_state():
return
curobj = self.objlist.get_sel_object(-1)
if curobj is not None:
newparam = newparam if newparam is not None else new_image_param()
newparam.width, newparam.height = curobj.size
newparam.dtype = ImageDatatypes.from_dtype(curobj.data.dtype)
image = create_image_from_param(
newparam, addparam=addparam, edit=edit, parent=self
)
if image is not None:
self.add_object(image)
def open_object(self, filename: str) -> None:
"""Open object from file (signal/image)"""
data = imread(filename, to_grayscale=False)
reducepath = osp.relpath(filename, osp.join(osp.dirname(filename), osp.pardir))
if filename.lower().endswith(".sif") and len(data.shape) == 3:
for idx in range(data.shape[0]):
image = create_image(reducepath + "_Im" + str(idx), data[idx, ::])
self.add_object(image)
else:
if data.ndim == 3:
# Converting to grayscale
data = data[..., :4].mean(axis=2)
image = create_image(reducepath, data)
if osp.splitext(filename)[1].lower() == ".dcm":
from pydicom import dicomio # pylint: disable=C0415,E0401
image.dicom_template = dicomio.read_file(
filename, stop_before_pixels=True, force=True
)
self.add_object(image)
def save_object(self, obj, filename: str = None) -> None:
"""Save object to file (signal/image)"""
if filename is None:
basedir = Conf.main.base_dir.get()
with save_restore_stds():
filename, _filter = getsavefilename( # pylint: disable=duplicate-code
self,
_("Save as"),
basedir,
iohandler.get_filters(
"save", dtype=obj.data.dtype, template=obj.dicom_template
),
)
if filename:
kwargs = {}
if osp.splitext(filename)[1].lower() == ".dcm":
kwargs["template"] = obj.dicom_template
with qt_try_loadsave_file(self.parent(), filename, "save"):
Conf.main.base_dir.set(filename)
imwrite(filename, obj.data, **kwargs)
def toggle_show_contrast(self, state):
"""Toggle show contrast option"""
Conf.view.show_contrast.set(state)
self.SIG_UPDATE_PLOT_ITEMS.emit()
CodraFT-2.2.1/codraft/core/gui/plotitemlist.py 0000664 0000000 0000000 00000017103 14435624103 0021243 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Plot item list classes
These classes handle guiqwt plot items for signal and image panels.
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from guiqwt.builder import make
from guiqwt.curve import GridItem
from guiqwt.label import LegendBoxItem
from guiqwt.styles import style_generator
from codraft.config import Conf
class BaseItemList:
"""Object handling plot items associated to objects (signals/images)"""
def __init__(self, panel, objlist, plotwidget):
self._enable_cleanup_dataview = True
self.panel = panel
self.objlist = objlist
self.plotwidget = plotwidget
self.plot = plotwidget.get_plot()
self.__plotitems = [] # plot items associated to objects (sig/ima)
self.__shapeitems = []
def __len__(self):
"""Return number of items"""
return len(self.__plotitems)
def __getitem__(self, row):
"""Return item at row"""
return self.__plotitems[row]
def __setitem__(self, row, item):
"""Set item at row"""
self.__plotitems[row] = item
def __delitem__(self, row):
"""Del item at row"""
item = self.__plotitems.pop(row)
self.plot.del_item(item)
def __iter__(self):
"""Return an iterator over items"""
yield from self.__plotitems
def append(self, item):
"""Append item"""
self.__plotitems.append(item)
def insert(self, row):
"""Insert object at row index"""
self.__plotitems.insert(row, None)
def add_item_to_plot(self, row):
"""Add plot item to plot"""
item = self.objlist[row].make_item()
item.set_readonly(True)
if row < len(self):
self[row] = item
else:
self.append(item)
self.plot.add_item(item)
return item
def make_item_from_existing(self, row):
"""Make plot item from existing object/item at row"""
return self.objlist[row].make_item(update_from=self[row])
def update_item(self, row, ref_item=None):
"""Update plot item associated to data"""
self.objlist[row].update_item(self[row], ref_item=ref_item)
def add_shapes(self, row):
"""Add geometric shape items associated to computed results and annotations"""
obj = self.objlist[row]
if obj.metadata:
# Performance optimization: block `guiqwt.baseplot.BasePlot` signals,
# add all items except the last one, unblock signals, then add the last one
# (this avoids some unnecessary refresh process by guiqwt)
items = list(obj.iterate_shape_items(editable=False))
if items:
block = self.plot.blockSignals(True)
for item in items[:-1]:
self.plot.add_item(item)
self.__shapeitems.append(item)
self.plot.blockSignals(block)
self.plot.add_item(items[-1])
self.__shapeitems.append(items[-1])
def remove_all(self):
"""Remove all plot items"""
self.__plotitems = []
self.plot.del_all_items()
def remove_all_shape_items(self):
"""Remove all geometric shapes associated to result items"""
if set(self.__shapeitems).issubset(set(self.plot.items)):
self.plot.del_items(self.__shapeitems)
self.__shapeitems = []
def refresh_plot(self, only_row: int = None):
"""Refresh plot (if row is not None, refresh only plot associated to row)"""
if only_row is None:
rows = self.objlist.get_selected_rows()
if self._enable_cleanup_dataview and len(rows) == 1:
self.cleanup_dataview()
self.remove_all_shape_items()
for item in self:
if item is not None:
item.hide()
else:
rows = [only_row]
title_keys = ("title", "xlabel", "ylabel", "zlabel", "xunit", "yunit", "zunit")
titles_dict = {}
if rows:
ref_item = None
for i_row, row in enumerate(rows):
for key in title_keys:
title = getattr(self.objlist[row], key, "")
value = titles_dict.get(key)
if value is None:
titles_dict[key] = title
elif value != title:
titles_dict[key] = ""
item = self[row]
if item is None:
item = self.add_item_to_plot(row)
else:
if i_row == 0:
make.style = style_generator()
self.update_item(row, ref_item=ref_item)
if ref_item is None:
ref_item = item
self.plot.set_item_visible(item, True, replot=False)
self.plot.set_active_item(item)
item.unselect()
self.add_shapes(row)
self.plot.replot()
else:
for key in title_keys:
titles_dict[key] = ""
tdict = titles_dict
tdict["ylabel"] = (tdict["ylabel"], tdict.pop("zlabel"))
tdict["yunit"] = (tdict["yunit"], tdict.pop("zunit"))
self.plot.set_titles(**titles_dict)
self.plot.do_autoscale()
def toggle_cleanup_dataview(self, state):
"""Toggle clean up data view option"""
self._enable_cleanup_dataview = state
def cleanup_dataview(self):
"""Clean up data view"""
# Performance optimization: using `baseplot.BasePlot.del_items` instead of
# `baseplot.BasePlot.del_item` (avoid emitting unnecessary signals)
self.plot.del_items(
[
item
for item in self.plot.items[:]
if item not in self and not isinstance(item, (LegendBoxItem, GridItem))
]
)
def get_current_plot_options(self):
"""
Return standard signal/image plot options
:return: Dictionary containing plot arguments for CurveDialog/ImageDialog
"""
return dict(
xlabel=self.plot.get_axis_title("bottom"),
ylabel=self.plot.get_axis_title("left"),
xunit=self.plot.get_axis_unit("bottom"),
yunit=self.plot.get_axis_unit("left"),
)
class SignalItemList(BaseItemList):
"""Object handling signal plot items, plot dialogs, plot options"""
# Nothing specific to signals, as of today
class ImageItemList(BaseItemList):
"""Object handling image plot items, plot dialogs, plot options"""
def refresh_plot(self, only_row: int = None):
"""Refresh plot (if row is not None, refresh only plot associated to row)"""
super().refresh_plot(only_row)
self.plotwidget.contrast.setVisible(Conf.view.show_contrast.get(True))
def cleanup_dataview(self):
"""Clean up data view"""
for widget in (self.plotwidget.xcsw, self.plotwidget.ycsw):
widget.hide()
super().cleanup_dataview()
def get_current_plot_options(self):
"""
Return standard signal/image plot options
:return: Dictionary containing plot arguments for CurveDialog/ImageDialog
"""
options = super().get_current_plot_options()
options.update(
dict(
zlabel=self.plot.get_axis_title("right"),
zunit=self.plot.get_axis_unit("right"),
show_contrast=True,
)
)
return options
CodraFT-2.2.1/codraft/core/gui/processor/ 0000775 0000000 0000000 00000000000 14435624103 0020155 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/codraft/core/gui/processor/__init__.py 0000664 0000000 0000000 00000000002 14435624103 0022256 0 ustar 00root root 0000000 0000000 #
CodraFT-2.2.1/codraft/core/gui/processor/base.py 0000664 0000000 0000000 00000041355 14435624103 0021451 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Base Processor GUI module
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import abc
import warnings
from typing import Callable, Dict, List
import guidata.dataset.dataitems as gdi
import guidata.dataset.datatypes as gdt
import numpy as np
from guidata.configtools import get_icon
from guidata.widgets.arrayeditor import ArrayEditor
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from codraft import env
from codraft.config import _
from codraft.core.gui.objectlist import ObjectList
from codraft.core.gui.roieditor import ROIEditorData
from codraft.core.model.base import ResultShape
from codraft.utils import misc
from codraft.utils.qthelpers import create_progress_bar, exec_dialog, qt_try_except
class GaussianParam(gdt.DataSet):
"""Gaussian filter parameters"""
sigma = gdi.FloatItem("σ", default=1.0)
class MovingAverageParam(gdt.DataSet):
"""Moving average parameters"""
n = gdi.IntItem(_("Size of the moving window"), default=3, min=1)
class MovingMedianParam(gdt.DataSet):
"""Moving median parameters"""
n = gdi.IntItem(_("Size of the moving window"), default=3, min=1, even=False)
class ThresholdParam(gdt.DataSet):
"""Threshold parameters"""
value = gdi.FloatItem(_("Threshold"))
class ClipParam(gdt.DataSet):
"""Data clipping parameters"""
value = gdi.FloatItem(_("Clipping value"))
class BaseProcessor(QC.QObject):
"""Object handling data processing: operations, processing, computing"""
SIG_ADD_SHAPE = QC.Signal(int)
EDIT_ROI_PARAMS = False
def __init__(self, panel, objlist: ObjectList, plotwidget):
super().__init__()
self.panel = panel
self.objlist = objlist
self.plotwidget = plotwidget
self.prefix = panel.PREFIX
@qt_try_except()
def compute_sum(self):
"""Compute sum"""
rows = self.objlist.get_selected_rows()
outobj = self.panel.create_object()
outobj.title = "+".join([f"{self.prefix}{row:03d}" for row in rows])
roilist = []
for row in rows:
obj = self.objlist[row]
if obj.roi is not None:
roilist.append(obj.roi)
if outobj.data is None:
outobj.copy_data_from(obj)
else:
outobj.data += np.array(obj.data, dtype=outobj.data.dtype)
outobj.update_resultshapes_from(obj)
if roilist:
outobj.roi = np.vstack(roilist)
self.panel.add_object(outobj)
@qt_try_except()
def compute_average(self):
"""Compute average"""
rows = self.objlist.get_selected_rows()
outobj = self.panel.create_object()
title = ", ".join([f"{self.prefix}{row:03d}" for row in rows])
outobj.title = f'{_("Average")}({title})'
original_dtype = self.objlist.get_sel_object().data.dtype
new_dtype = complex if misc.is_complex_dtype(original_dtype) else float
roilist = []
for row in rows:
obj = self.objlist[row]
if obj.roi is not None:
roilist.append(obj.roi)
if outobj.data is None:
outobj.copy_data_from(obj, dtype=new_dtype)
else:
outobj.data += np.array(obj.data, dtype=outobj.data.dtype)
outobj.update_resultshapes_from(obj)
outobj.data /= float(len(rows))
if misc.is_integer_dtype(original_dtype):
outobj.set_data_type(dtype=original_dtype)
if roilist:
outobj.roi = np.vstack(roilist)
self.panel.add_object(outobj)
@qt_try_except()
def compute_product(self):
"""Compute product"""
rows = self.objlist.get_selected_rows()
outobj = self.panel.create_object()
outobj.title = "*".join([f"{self.prefix}{row:03d}" for row in rows])
for row in rows:
obj = self.objlist[row]
if outobj.data is None:
outobj.copy_data_from(obj)
else:
outobj.data *= np.array(obj.data, dtype=outobj.data.dtype)
self.panel.add_object(outobj)
@qt_try_except()
def compute_difference(self, quad: bool):
"""Compute (quadratic) difference"""
rows = self.objlist.get_selected_rows()
outobj = self.panel.create_object()
outobj.title = "-".join([f"{self.prefix}{row:03d}" for row in rows])
if quad:
outobj.title = f"({outobj.title})/sqrt(2)"
obj0, obj1 = self.objlist.get_sel_object(), self.objlist.get_sel_object(1)
outobj.copy_data_from(obj0)
outobj.data -= np.array(obj1.data, dtype=outobj.data.dtype)
if quad:
outobj.data = outobj.data / np.sqrt(2.0)
if np.issubdtype(outobj.data.dtype, np.unsignedinteger):
outobj.data[obj0.data < obj1.data] = 0
self.panel.add_object(outobj)
@qt_try_except()
def compute_division(self):
"""Compute division"""
rows = self.objlist.get_selected_rows()
outobj = self.panel.create_object()
outobj.title = "/".join([f"{self.prefix}{row:03d}" for row in rows])
obj0, obj1 = self.objlist.get_sel_object(), self.objlist.get_sel_object(1)
outobj.copy_data_from(obj0)
outobj.data = outobj.data / np.array(obj1.data, dtype=outobj.data.dtype)
self.panel.add_object(outobj)
def _get_roieditordata(
self, roidata: np.ndarray = None, singleobj: bool = None
) -> ROIEditorData:
"""Eventually open ROI Editing Dialog, and return ROI editor data"""
# Expected behavior:
# -----------------
# * If 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 roidata is None:
roieditordata = self.edit_regions_of_interest(
extract=True, singleobj=singleobj
)
if roieditordata is not None and roieditordata.roidata is None:
# This only happens in unattended mode (forcing QDialog accept)
return None
else:
roieditordata = ROIEditorData(roidata=roidata, singleobj=singleobj)
return roieditordata
@abc.abstractmethod
def extract_roi(self, roidata: np.ndarray = None) -> None:
"""Extract Region Of Interest (ROI) from data"""
@abc.abstractmethod
def swap_axes(self):
"""Swap data axes"""
@abc.abstractmethod
def compute_abs(self):
"""Compute absolute value"""
@abc.abstractmethod
def compute_log10(self):
"""Compute Log10"""
# ------Data Processing
@abc.abstractmethod
def apply_11_func(self, obj, orig, func, param, message):
"""Apply 11 function: 1 object in --> 1 object out"""
def compute_11(
self,
name: str,
func: Callable,
param: gdt.DataSet = None,
suffix: Callable = None,
func_obj: Callable = None,
edit: bool = True,
):
"""Compute 11 function: 1 object in --> 1 object out"""
if param is not None:
if edit and not param.edit(parent=self.panel.parent()):
return
self._compute_11_subroutine([name], func, [param], suffix, func_obj)
def compute_1n(
self,
names: List,
func: Callable,
params: List = None,
suffix: Callable = None,
func_obj: Callable = None,
edit: bool = True,
):
"""Compute 1n function: 1 object in --> n objects out"""
if params is not None:
group = gdt.DataSetGroup(params, title=_("Parameters"))
if edit and not group.edit(parent=self.panel.parent()):
return
self._compute_11_subroutine(names, func, params, suffix, func_obj)
def _compute_11_subroutine(
self,
names: List,
func: Callable,
params: List,
suffix: Callable,
func_obj: Callable,
):
"""Compute 11 subroutine: used by compute 11 and compute 1n methods"""
rows = self.objlist.get_selected_rows()
with create_progress_bar(
self.panel, names[0], max_=len(rows) * len(params)
) as progress:
for i_row, row in enumerate(rows):
for i_param, (param, name) in enumerate(zip(params, names)):
progress.setValue(i_row * i_param)
progress.setLabelText(name)
QW.QApplication.processEvents()
if progress.wasCanceled():
break
orig = self.objlist[row]
obj = self.panel.create_object()
obj.title = f"{name}({self.prefix}{row:03d})"
if suffix is not None:
obj.title += "|" + suffix(param)
obj.copy_data_from(orig)
message = _("Computing:") + " " + obj.title
self.apply_11_func(obj, orig, func, param, message)
if func_obj is not None:
if param is None:
func_obj(obj)
else:
func_obj(obj, param)
self.panel.add_object(obj)
@abc.abstractmethod
def apply_10_func(self, orig, func, param, message) -> ResultShape:
"""Apply 10 function: 1 object in --> 0 object out (scalar result)"""
def compute_10(
self,
name: str,
func: Callable,
param: gdt.DataSet = None,
suffix: Callable = None,
edit: bool = True,
) -> Dict[int, ResultShape]:
"""Compute 10 function: 1 object in --> 0 object out
(the result of this method is stored in original object's metadata)"""
if param is not None:
if edit and not param.edit(parent=self.panel.parent()):
return None
rows = self.objlist.get_selected_rows()
with create_progress_bar(self.panel, name, max_=len(rows)) as progress:
results = {}
xlabels = None
ylabels = []
title_suffix = "" if suffix is None else "|" + suffix(param)
for idx, row in enumerate(rows):
progress.setValue(idx)
QW.QApplication.processEvents()
if progress.wasCanceled():
break
orig = self.objlist[row]
title = f"{name}{title_suffix}"
message = _("Computing:") + " " + title
result = self.apply_10_func(orig, func, param, message)
if result is None:
continue
results[row] = result
xlabels = result.xlabels
self.SIG_ADD_SHAPE.emit(row)
self.panel.selection_changed()
self.panel.SIG_UPDATE_PLOT_ITEM.emit(row)
for _i_row_res in range(result.array.shape[0]):
ylabel = f"{name}({self.prefix}{idx:03d}){title_suffix}"
ylabels.append(ylabel)
if results:
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
dlg = ArrayEditor(self.panel.parent())
title = _("Results")
res = np.vstack([result.array for result in results.values()])
dlg.setup_and_check(
res, title, readonly=True, xlabels=xlabels, ylabels=ylabels
)
dlg.setObjectName(f"{self.prefix}_results")
dlg.resize(750, 300)
exec_dialog(dlg)
return results
@abc.abstractmethod
@qt_try_except()
def calibrate(self, param=None) -> None:
"""Compute data linear calibration"""
@staticmethod
@abc.abstractmethod
def func_gaussian_filter(x, y, p):
"""Compute gaussian filter"""
@qt_try_except()
def compute_gaussian(self, param: GaussianParam = None) -> None:
"""Compute gaussian filter"""
edit = param is None
if edit:
param = GaussianParam(_("Gaussian filter"))
func = self.func_gaussian_filter
self.compute_11(
"GaussianFilter",
func,
param,
suffix=lambda p: f"σ={p.sigma:.3f} pixels",
edit=edit,
)
@staticmethod
@abc.abstractmethod
def func_moving_average(x, y, p):
"""Moving average computing function"""
@qt_try_except()
def compute_moving_average(self, param: MovingAverageParam = None) -> None:
"""Compute moving average"""
edit = param is None
if edit:
param = MovingAverageParam(_("Moving average"))
func = self.func_moving_average
self.compute_11("MovAvg", func, param, suffix=lambda p: f"n={p.n}", edit=edit)
@staticmethod
@abc.abstractmethod
def func_moving_median(x, y, p):
"""Moving median computing function"""
@qt_try_except()
def compute_moving_median(self, param: MovingMedianParam = None) -> None:
"""Compute moving median"""
edit = param is None
if edit:
param = MovingMedianParam(_("Moving median"))
func = self.func_moving_median
self.compute_11("MovMed", func, param, suffix=lambda p: f"n={p.n}", edit=edit)
@abc.abstractmethod
@qt_try_except()
def compute_wiener(self):
"""Compute Wiener filter"""
@abc.abstractmethod
@qt_try_except()
def compute_fft(self):
"""Compute iFFT"""
@abc.abstractmethod
@qt_try_except()
def compute_ifft(self):
"""Compute FFT"""
# ------Computing
def edit_regions_of_interest(
self, extract: bool = False, singleobj: bool = None
) -> ROIEditorData:
"""Define Region Of Interest (ROI) for computing functions"""
roieditordata = self.panel.get_roi_dialog(extract=extract, singleobj=singleobj)
if roieditordata is not None:
row = self.objlist.get_selected_rows()[0]
obj = self.objlist[row]
roigroup = obj.roidata_to_params(roieditordata.roidata)
if (
env.execenv.unattended
or roieditordata.roidata.size == 0
or not self.EDIT_ROI_PARAMS
or roigroup.edit(parent=self.panel)
):
roidata = obj.params_to_roidata(roigroup)
if roieditordata.modified:
# If ROI has been modified, save ROI (even in "extract mode")
obj.roi = roidata
self.SIG_ADD_SHAPE.emit(row)
self.panel.selection_changed()
self.panel.SIG_UPDATE_PLOT_ITEMS.emit()
return roieditordata
def delete_regions_of_interest(self):
"""Delete Regions Of Interest"""
for row in self.objlist.get_selected_rows():
obj = self.objlist[row]
if obj.roi is not None:
obj.roi = None
self.panel.selection_changed()
self.panel.SIG_UPDATE_PLOT_ITEMS.emit()
@abc.abstractmethod
def _get_stat_funcs(self):
"""Return statistics functions list"""
@qt_try_except()
def compute_stats(self):
"""Compute data statistics"""
row = self.objlist.get_selected_rows()[0]
obj = self.objlist.get_sel_object()
stfuncs = self._get_stat_funcs()
nbcal = len(stfuncs)
roi_nb = 0 if obj.roi is None else obj.roi.shape[0]
res = np.zeros((1 + roi_nb, nbcal))
xlabels = [None] * nbcal
obj_t = f"{self.prefix}{row:03d}"
ylabels = [None] * (roi_nb + 1)
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
with np.errstate(all="ignore"):
for iroi, roi_index in enumerate([None] + list(range(roi_nb))):
for ical, (label, func) in enumerate(stfuncs):
xlabels[ical] = label
res[iroi, ical] = func(obj.get_data(roi_index=roi_index))
if roi_index is None:
ylabels[iroi] = obj_t
else:
ylabels[iroi] = f"{obj_t}|ROI{roi_index:02d}"
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
dlg = ArrayEditor(self.panel.parent())
title = _("Statistics")
dlg.setup_and_check(
res, title, readonly=True, xlabels=xlabels, ylabels=ylabels
)
dlg.setObjectName(f"{self.prefix}_stats")
dlg.setWindowIcon(get_icon("stats.svg"))
dlg.resize(750, 300)
exec_dialog(dlg)
CodraFT-2.2.1/codraft/core/gui/processor/image.py 0000664 0000000 0000000 00000052553 14435624103 0021623 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Image Processor GUI module
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import numpy as np
import scipy.ndimage as spi
import scipy.signal as sps
from guidata.dataset.dataitems import BoolItem, ChoiceItem, FloatItem, IntItem
from guidata.dataset.datatypes import DataSet, DataSetGroup, ValueProp
from guiqwt.widgets.resizedialog import ResizeDialog
from numpy import ma
from qtpy import QtWidgets as QW
from codraft.config import APP_NAME, _
from codraft.core.computation.image import (
distance_matrix,
flatfield,
get_2d_peaks_coords,
get_centroid_fourier,
get_contour_shapes,
get_enclosing_circle,
)
from codraft.core.gui.processor.base import BaseProcessor, ClipParam, ThresholdParam
from codraft.core.model.base import BaseProcParam, ResultShape, ShapeTypes
from codraft.core.model.image import ImageParam, RoiDataGeometries, RoiDataItem
from codraft.utils.qthelpers import create_progress_bar, qt_try_except
class LogP1Param(DataSet):
"""Log10 parameters"""
n = FloatItem("n")
class RotateParam(DataSet):
"""Rotate parameters"""
boundaries = ("constant", "nearest", "reflect", "wrap")
prop = ValueProp(False)
angle = FloatItem(f"{_('Angle')} (°)")
mode = ChoiceItem(
_("Mode"), list(zip(boundaries, boundaries)), default=boundaries[0]
)
cval = FloatItem(
_("cval"),
default=0.0,
help=_(
"Value used for points outside the "
"boundaries of the input if mode is "
"'constant'"
),
)
reshape = BoolItem(
_("Reshape the output array"),
default=True,
help=_(
"Reshape the output array "
"so that the input array is "
"contained completely in the output"
),
)
prefilter = BoolItem(_("Prefilter the input image"), default=True).set_prop(
"display", store=prop
)
order = IntItem(
_("Order"),
default=3,
min=0,
max=5,
help=_("Spline interpolation order"),
).set_prop("display", active=prop)
class ResizeParam(DataSet):
"""Resize parameters"""
boundaries = ("constant", "nearest", "reflect", "wrap")
prop = ValueProp(False)
zoom = FloatItem(_("Zoom"))
mode = ChoiceItem(
_("Mode"), list(zip(boundaries, boundaries)), default=boundaries[0]
)
cval = FloatItem(
_("cval"),
default=0.0,
help=_(
"Value used for points outside the "
"boundaries of the input if mode is "
"'constant'"
),
)
prefilter = BoolItem(_("Prefilter the input image"), default=True).set_prop(
"display", store=prop
)
order = IntItem(
_("Order"),
default=3,
min=0,
max=5,
help=_("Spline interpolation order"),
).set_prop("display", active=prop)
class FlatFieldParam(BaseProcParam):
"""Flat-field parameters"""
threshold = FloatItem(_("Threshold"), default=0.0)
class ZCalibrateParam(DataSet):
"""Image linear calibration parameters"""
a = FloatItem("a", default=1.0)
b = FloatItem("b", default=0.0)
class GenericDetectionParam(DataSet):
"""Generic detection parameters"""
threshold = FloatItem(
_("Relative threshold"),
default=0.5,
min=0.1,
max=0.9,
help=_(
"Detection threshold, relative to difference between "
"data maximum and minimum"
),
)
class PeakDetectionParam(GenericDetectionParam):
"""Peak detection parameters"""
size = IntItem(
_("Neighborhoods size"),
default=10,
min=1,
unit="pixels",
help=_(
"Size of the sliding window used in maximum/minimum filtering algorithm"
),
)
create_rois = BoolItem(_("Create regions of interest"), default=True)
class ContourShapeParam(GenericDetectionParam):
"""Contour shape parameters"""
shapes = (
("ellipse", _("Ellipse")),
("circle", _("Circle")),
)
shape = ChoiceItem(_("Shape"), shapes, default="ellipse")
class ImageProcessor(BaseProcessor):
"""Object handling image processing: operations, processing, computing"""
# pylint: disable=duplicate-code
EDIT_ROI_PARAMS = True
def compute_logp1(self, param: LogP1Param = None) -> None:
"""Compute base 10 logarithm"""
edit = param is None
if edit:
param = LogP1Param("Log10(z+n)")
self.compute_11(
"Log10(z+n)",
lambda z, p: np.log10(z + p.n),
param,
suffix=lambda p: f"n={p.n}",
edit=edit,
)
def rotate_arbitrarily(self, param: RotateParam = None) -> None:
"""Rotate data arbitrarily"""
edit = param is None
if edit:
param = RotateParam(_("Rotation"))
# TODO: [P2] Instead of removing geometric shapes, apply rotation
self.compute_11(
"Rotate",
lambda x, p: spi.rotate(
x,
p.angle,
reshape=p.reshape,
order=p.order,
mode=p.mode,
cval=p.cval,
prefilter=p.prefilter,
),
param,
suffix=lambda p: f"α={p.angle:.3f}°, mode='{p.mode}'",
func_obj=lambda obj, _param: obj.remove_resultshapes(),
edit=edit,
)
def rotate_90(self):
"""Rotate data 90°"""
# TODO: [P2] Instead of removing geometric shapes, apply 90° rotation
self.compute_11(
"Rotate90",
np.rot90,
func_obj=lambda obj: obj.remove_resultshapes(),
)
def rotate_270(self):
"""Rotate data 270°"""
# TODO: [P2] Instead of removing geometric shapes, apply 270° rotation
self.compute_11(
"Rotate270",
lambda x: np.rot90(x, 3),
func_obj=lambda obj: obj.remove_resultshapes(),
)
def flip_horizontally(self):
"""Flip data horizontally"""
# TODO: [P2] Instead of removing geometric shapes, apply horizontal flip
self.compute_11(
"HFlip",
np.fliplr,
func_obj=lambda obj: obj.remove_resultshapes(),
)
def flip_vertically(self):
"""Flip data vertically"""
# TODO: [P2] Instead of removing geometric shapes, apply vertical flip
self.compute_11(
"VFlip",
np.flipud,
func_obj=lambda obj: obj.remove_resultshapes(),
)
def resize_image(self, param: ResizeParam = None) -> None:
"""Resize image"""
obj0 = self.objlist.get_sel_object(0)
for obj in self.objlist.get_sel_objects():
if obj.size != obj0.size:
QW.QMessageBox.warning(
self.panel.parent(),
APP_NAME,
_("Warning:")
+ "\n"
+ _("Selected images do not have the same size"),
)
edit = param is None
if edit:
original_size = obj0.size
dlg = ResizeDialog(
self.plotwidget,
new_size=original_size,
old_size=original_size,
text=_("Destination size:"),
)
if not dlg.exec():
return
param = ResizeParam(_("Resize"))
param.zoom = dlg.get_zoom()
def func_obj(obj, param):
"""Zooming function"""
if obj.dx is not None and obj.dy is not None:
obj.dx, obj.dy = obj.dx / param.zoom, obj.dy / param.zoom
# TODO: [P2] Instead of removing geometric shapes, apply zoom
obj.remove_resultshapes()
self.compute_11(
"Zoom",
lambda x, p: spi.interpolation.zoom(
x,
p.zoom,
order=p.order,
mode=p.mode,
cval=p.cval,
prefilter=p.prefilter,
),
param,
suffix=lambda p: f"zoom={p.zoom:.3f}",
func_obj=func_obj,
edit=edit,
)
def extract_roi(self, roidata: np.ndarray = None, singleobj: bool = None) -> None:
"""Extract Region Of Interest (ROI) from data"""
roieditordata = self._get_roieditordata(roidata, singleobj)
if roieditordata is None or roieditordata.is_empty:
return
obj = self.objlist.get_sel_object()
group = obj.roidata_to_params(roieditordata.roidata)
if roieditordata.singleobj:
def suffix_func(group: DataSetGroup):
if len(group.datasets) == 1:
p = group.datasets[0]
return p.get_suffix()
return ""
def extract_roi_func(data: np.ndarray, group: DataSetGroup):
"""Extract ROI function on data"""
if len(group.datasets) == 1:
p = group.datasets[0]
return data.copy()[p.y0 : p.y1, p.x0 : p.x1]
out = np.zeros_like(data)
for p in group.datasets:
slice1, slice2 = slice(p.y0, p.y1 + 1), slice(p.x0, p.x1 + 1)
out[slice1, slice2] = data[slice1, slice2]
x0 = min([p.x0 for p in group.datasets])
y0 = min([p.y0 for p in group.datasets])
x1 = max([p.x1 for p in group.datasets])
y1 = max([p.y1 for p in group.datasets])
return out[y0:y1, x0:x1]
def extract_roi_func_obj(image: ImageParam, group: DataSetGroup):
"""Extract ROI function on object"""
image.x0 += min([p.x0 for p in group.datasets])
image.y0 += min([p.y0 for p in group.datasets])
image.remove_resultshapes()
# TODO: [P2] Instead of removing geometric shapes, apply ROI extract
self.compute_11(
"ROI",
extract_roi_func,
group,
suffix=suffix_func,
func_obj=extract_roi_func_obj,
edit=False,
)
else:
def extract_roi_func_obj(image: ImageParam, p: DataSet):
"""Extract ROI function on object"""
image.x0 += p.x0
image.y0 += p.y0
image.remove_resultshapes()
if p.geometry is RoiDataGeometries.CIRCLE:
# Circular ROI
image.roi = p.get_single_roi()
# TODO: [P2] Instead of removing geometric shapes, apply roi extract
self.compute_1n(
[f"ROI{iroi}" for iroi in range(len(group.datasets))],
lambda z, p: z.copy()[p.y0 : p.y1, p.x0 : p.x1],
group.datasets,
suffix=lambda p: p.get_suffix(),
func_obj=extract_roi_func_obj,
edit=False,
)
def swap_axes(self):
"""Swap data axes"""
self.compute_11(
"SwapAxes",
lambda z: z.T,
func_obj=lambda obj: obj.remove_resultshapes(),
)
def compute_abs(self):
"""Compute absolute value"""
self.compute_11("Abs", np.abs)
def compute_log10(self):
"""Compute Log10"""
self.compute_11("Log10", np.log10)
@qt_try_except()
def flat_field_correction(self, param: FlatFieldParam = None) -> None:
"""Compute flat field correction"""
edit = param is None
rawdata = self.objlist.get_sel_object().data
flatdata = self.objlist.get_sel_object(1).data
if edit:
param = FlatFieldParam(_("Flat field"))
param.set_from_datatype(rawdata.dtype)
if not edit or param.edit(self.panel.parent()):
rows = self.objlist.get_selected_rows()
robj = self.panel.create_object()
robj.title = (
"FlatField("
+ (",".join([f"{self.prefix}{row:03d}" for row in rows]))
+ f",threshold={param.threshold})"
)
robj.data = flatfield(rawdata, flatdata, param.threshold)
self.panel.add_object(robj)
# ------Image Processing
def apply_11_func(self, obj, orig, func, param, message):
"""Apply 11 function: 1 object in --> 1 object out"""
# (self is used by @qt_try_except)
# pylint: disable=unused-argument
@qt_try_except(message)
def apply_11_func_callback(self, obj, orig, func, param):
"""Apply 11 function callback: 1 object in --> 1 object out"""
if param is None:
obj.data = func(orig.data)
else:
obj.data = func(orig.data, param)
return apply_11_func_callback(self, obj, orig, func, param)
@qt_try_except()
def calibrate(self, param: ZCalibrateParam = None) -> None:
"""Compute data linear calibration"""
edit = param is None
if edit:
param = ZCalibrateParam(_("Linear calibration"), "y = a.x + b")
self.compute_11(
"LinearCal",
lambda x, p: p.a * x + p.b,
param,
suffix=lambda p: "z={p.a}*z+{p.b}",
edit=edit,
)
@qt_try_except()
def compute_threshold(self, param: ThresholdParam = None) -> None:
"""Compute threshold clipping"""
edit = param is None
if edit:
param = ThresholdParam(_("Thresholding"))
self.compute_11(
"Threshold",
lambda x, p: np.clip(x, p.value, x.max()),
param,
suffix=lambda p: f"min={p.value} lsb",
edit=edit,
)
@qt_try_except()
def compute_clip(self, param: ClipParam = None) -> None:
"""Compute maximum data clipping"""
edit = param is None
if edit:
param = ClipParam(_("Clipping"))
self.compute_11(
"Clip",
lambda x, p: np.clip(x, x.min(), p.value),
param,
suffix=lambda p: f"max={p.value} lsb",
edit=edit,
)
@staticmethod
def func_gaussian_filter(x, p): # pylint: disable=arguments-differ
"""Compute gaussian filter"""
return spi.gaussian_filter(x, p.sigma)
@qt_try_except()
def compute_fft(self):
"""Compute FFT"""
self.compute_11("FFT", np.fft.fft2)
@qt_try_except()
def compute_ifft(self):
"Compute iFFT" ""
self.compute_11("iFFT", np.fft.ifft2)
@staticmethod
def func_moving_average(x, p): # pylint: disable=arguments-differ
"""Moving average computing function"""
return spi.uniform_filter(x, size=p.n, mode="constant")
@staticmethod
def func_moving_median(x, p): # pylint: disable=arguments-differ
"""Moving median computing function"""
return sps.medfilt(x, kernel_size=p.n)
@qt_try_except()
def compute_wiener(self):
"""Compute Wiener filter"""
self.compute_11("WienerFilter", sps.wiener)
# ------Image Computing
def apply_10_func(self, orig, func, param, message) -> ResultShape:
"""Apply 10 function: 1 object in --> 0 object out (scalar result)"""
# (self is used by @qt_try_except)
# pylint: disable=unused-argument
@qt_try_except(message)
def apply_10_func_callback(self, orig, func, param):
"""Apply 10 function cb: 1 object in --> 0 object out (scalar result)"""
if param is None:
return func(orig)
return func(orig, param)
return apply_10_func_callback(self, orig, func, param)
@staticmethod
def __apply_origin_size_roi(image, func, *args) -> np.ndarray:
"""Exec computation taking into account image x0, y0, dx, dy and ROIs"""
res = []
for i_roi in image.iterate_roi_indexes():
coords = func(image.get_data(i_roi), *args)
if coords.size:
if image.roi is not None:
x0, y0, _x1, _y1 = RoiDataItem(image.roi[i_roi]).get_rect()
coords[:, ::2] += x0
coords[:, 1::2] += y0
coords[:, ::2] = image.dx * coords[:, ::2] + image.x0
coords[:, 1::2] = image.dy * coords[:, 1::2] + image.y0
idx = np.ones((coords.shape[0], 1)) * i_roi
coords = np.hstack([idx, coords])
res.append(coords)
if res:
return np.vstack(res)
return None
@qt_try_except()
def compute_centroid(self):
"""Compute image centroid"""
def get_centroid_coords(data: np.ndarray):
"""Return centroid coordinates"""
y, x = get_centroid_fourier(data)
return np.array([(x, y)])
def centroid(image: ImageParam):
"""Compute centroid"""
res = self.__apply_origin_size_roi(image, get_centroid_coords)
if res is not None:
return image.add_resultshape("Centroid", ShapeTypes.MARKER, res)
return None
self.compute_10(_("Centroid"), centroid)
@qt_try_except()
def compute_enclosing_circle(self):
"""Compute minimum enclosing circle"""
def get_enclosing_circle_coords(data: np.ndarray):
"""Return diameter coords for the circle contour enclosing image
values above threshold (FWHM)"""
x, y, r = get_enclosing_circle(data)
return np.array([[x - r, y, x + r, y]])
def enclosing_circle(image: ImageParam):
"""Compute minimum enclosing circle"""
res = self.__apply_origin_size_roi(image, get_enclosing_circle_coords)
if res is not None:
return image.add_resultshape("MinEnclosCircle", ShapeTypes.CIRCLE, res)
return None
# TODO: [P2] Find a way to add the circle to the computing results
# as in "enclosingcircle_test.py"
self.compute_10(_("MinEnclosingCircle"), enclosing_circle)
@qt_try_except()
def compute_peak_detection(self, param: PeakDetectionParam = None) -> None:
"""Compute 2D peak detection"""
def peak_detection(image: ImageParam, p: PeakDetectionParam):
"""Compute centroid"""
res = self.__apply_origin_size_roi(
image, get_2d_peaks_coords, p.size, p.threshold
)
if res is not None:
return image.add_resultshape("Peaks", ShapeTypes.POINT, res)
return None
edit = param is None
if edit:
data = self.objlist.get_sel_object().data
param = PeakDetectionParam()
param.size = max(min(data.shape) // 40, 50)
results = self.compute_10(_("Peaks"), peak_detection, param, edit=edit)
if param.create_rois:
with create_progress_bar(
self.panel, _("Create regions of interest"), max_=len(results)
) as progress:
for idx, (row, result) in enumerate(results.items()):
progress.setValue(idx)
QW.QApplication.processEvents()
if progress.wasCanceled():
break
obj = self.objlist[row]
dist = distance_matrix(result.data)
dist_min = dist[dist != 0].min()
assert dist_min > 0
radius = int(0.5 * dist_min / np.sqrt(2) - 1)
assert radius >= 1
roicoords = []
ymax, xmax = obj.data.shape
for x, y in result.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)
self.SIG_ADD_SHAPE.emit(row)
self.panel.selection_changed()
self.panel.SIG_UPDATE_PLOT_ITEM.emit(row)
@qt_try_except()
def compute_contour_shape(self, param: ContourShapeParam = None) -> None:
"""Compute contour shape fit"""
def contour_shape(image: ImageParam, p: ContourShapeParam):
"""Compute contour shape fit"""
res = self.__apply_origin_size_roi(
image, get_contour_shapes, p.shape, p.threshold
)
if res is not None:
shape = ShapeTypes.CIRCLE if p.shape == "circle" else ShapeTypes.ELLIPSE
return image.add_resultshape("Contour", shape, res)
return None
edit = param is None
if edit:
param = ContourShapeParam()
self.compute_10(_("Contour"), contour_shape, param, edit=edit)
def _get_stat_funcs(self):
"""Return statistics functions list"""
# Be careful to use systematically functions adapted to masked arrays
# (e.g. numpy.ma median, and *not* numpy.median)
return [
("min(z)", lambda z: z.min()),
("max(z)", lambda z: z.max()),
("
".join(textlist)
class SignalROIEditor(BaseROIEditor):
"""Signal ROI Editor"""
ICON_NAME = "signal_roi_new.svg"
OBJ_NAME = _("signal")
def setup_widget(self):
"""Setup ROI editor widget"""
super().setup_widget()
info = ROIRangeInfo(self.roi_items)
info_label = make.info_label("BL", info, title=_("Regions of interest"))
self.plot.add_item(info_label)
self.info_label = info_label
self.add_btn.clicked.connect(self.add_roi)
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):
"""Image ROI Editor"""
ICON_NAME = "image_roi_new.svg"
OBJ_NAME = _("image")
def setup_widget(self):
"""Setup ROI editor widget"""
super().setup_widget()
item = self.plot.get_items(item_type=IImageItemType)[0]
item.set_mask_visible(False)
menu = QW.QMenu()
rectact = create_action(
self,
_("Rectangular ROI"),
lambda: self.add_roi(RoiDataGeometries.RECTANGLE),
icon=get_icon("rectangle.png"),
)
circact = create_action(
self,
_("Circular ROI"),
lambda: self.add_roi(RoiDataGeometries.CIRCLE),
icon=get_icon("circle.png"),
)
add_actions(menu, (rectact, circact))
self.add_btn.setMenu(menu)
def add_roi(self, geometry: RoiDataGeometries):
"""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):
"""Update ROI annotation titles"""
super().update_roi_titles()
for index, roi_item in enumerate(self.roi_items):
roi_item.annotationparam.title = f"ROI{index:02d}"
roi_item.annotationparam.update_annotation(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
CodraFT-2.2.1/codraft/core/io/ 0000775 0000000 0000000 00000000000 14435624103 0015761 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/codraft/core/io/__init__.py 0000664 0000000 0000000 00000000412 14435624103 0020067 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT I/O module
"""
# Registering dynamic I/O features:
from codraft.core.io import h5, image # pylint: disable=W0611
CodraFT-2.2.1/codraft/core/io/base.py 0000664 0000000 0000000 00000005106 14435624103 0017247 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Base I/O common module (native HDF5 format)
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from guidata.hdf5io import HDF5Reader, HDF5Writer
from codraft import __version__
H5_VERSION = "CodraFT_Version"
LIST_LENGTH_STR = "__list_length__"
class NativeH5Writer(HDF5Writer):
"""CodraFT signal/image objects HDF5 guidata Dataset Writer class,
supporting dictionary serialization"""
def __init__(self, filename):
super().__init__(filename)
self.h5[H5_VERSION] = __version__
def write_dict(self, val):
"""Write dictionary to h5 file"""
# Keys must be strings
# Values must be h5py supported data types
group = self.get_parent_group()
dict_group = group.create_group(self.option[-1])
for key, value in val.items():
if isinstance(value, dict):
with self.group(key):
self.write_dict(value)
elif isinstance(value, list):
with self.group(key):
with self.group(LIST_LENGTH_STR):
self.write(len(value))
for index, i_val in enumerate(value):
with self.group("elt" + str(index)):
self.write(i_val)
else:
try:
dict_group.attrs[key] = value
except TypeError:
pass
class NativeH5Reader(HDF5Reader):
"""CodraFT signal/image objects HDF5 guidata dataset Writer class,
supporting dictionary deserialization"""
def __init__(self, filename):
super().__init__(filename)
self.version = self.h5[H5_VERSION]
def read_dict(self):
"""Read dictionary from h5 file"""
group = self.get_parent_group()
dict_group = group[self.option[-1]]
dict_val = {}
for key, value in dict_group.attrs.items():
dict_val[key] = value
for key in dict_group:
with self.group(key):
if "__list_length__" in dict_group[key].attrs:
with self.group(LIST_LENGTH_STR):
list_len = self.read()
dict_val[key] = [
dict_group[key]["elt" + str(index)][:]
for index in range(list_len)
]
else:
dict_val[key] = self.read_dict()
return dict_val
CodraFT-2.2.1/codraft/core/io/conv.py 0000664 0000000 0000000 00000001724 14435624103 0017304 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT I/O conversion functions
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from typing import List
import numpy as np
def data_to_xy(data: np.ndarray) -> List[np.ndarray]:
"""Convert 2-D array into a list of 1-D array data (x, y, dx, dy).
This is useful for importing data and creating a CodraFT signal with it."""
rows, cols = data.shape
for colnb in (2, 3, 4):
if cols == colnb and rows > colnb:
data = data.T
break
if len(data) == 1:
data = data.T
if len(data) not in (2, 3, 4):
raise ValueError(f"Invalid data: len(data)={len(data)} (expected 2, 3 or 4)")
x, y = data[:2]
dx, dy = None, None
if len(data) == 3:
dy = data[2]
if len(data) == 4:
dx, dy = data[2:]
return x, y, dx, dy
CodraFT-2.2.1/codraft/core/io/h5/ 0000775 0000000 0000000 00000000000 14435624103 0016275 5 ustar 00root root 0000000 0000000 CodraFT-2.2.1/codraft/core/io/h5/__init__.py 0000664 0000000 0000000 00000000551 14435624103 0020407 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT HDF5 importer module
"""
# Registering dynamic I/O features:
from codraft.core.io.h5 import generic, mos07636 # pylint: disable=W0611
from codraft.core.io.h5.common import H5Importer # pylint: disable=W0611
CodraFT-2.2.1/codraft/core/io/h5/common.py 0000664 0000000 0000000 00000017211 14435624103 0020141 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Common tools for exogenous HDF5 format support
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import abc
import os.path as osp
from typing import Callable, Dict
import h5py
import numpy as np
from codraft.config import Conf
from codraft.core.io.conv import data_to_xy
from codraft.utils.misc import to_string
class BaseNode(metaclass=abc.ABCMeta):
"""Object representing a HDF5 node"""
IS_ARRAY = False
def __init__(self, h5file, dname):
self.h5file = h5file
self.dset = h5file[dname]
self.metadata = {}
self.__obj = None
self.children = []
self.uint32_wng = False
@property
def id(self):
"""Return node id"""
return self.dset.name
@property
def name(self):
"""Return node name, constructed from dataset name"""
return to_string(self.dset.name).split("/")[-1]
@property
def data(self):
"""Data associated to node, if available"""
return None
@property
def icon_name(self):
"""Icon name associated to node"""
@property
def shape_str(self):
"""Return string representation of node shape, if any"""
return ""
@property
def dtype_str(self):
"""Return string representation of node data type, if any"""
return ""
@property
def text(self):
"""Return node textual representation"""
@property
def description(self):
"""Return node description"""
return ""
@classmethod
def match(cls, dset):
"""Return True if h5 dataset match node pattern"""
def create_object(self): # pylint: disable=no-self-use
"""Create native object, if supported"""
return None
def get_object(self):
"""Return native object, if supported"""
if self.__obj is None:
obj = self.create_object() # pylint: disable=assignment-from-none
if obj is not None:
self.__process_metadata(obj)
self.__obj = obj
return self.__obj
def __process_metadata(self, obj):
"""Process metadata from dataset to obj"""
obj.metadata = {}
obj.metadata["HDF5Path"] = self.h5file.filename
obj.metadata["HDF5Dataset"] = self.id
for key, value in self.dset.attrs.items():
if isinstance(value, bytes):
value = to_string(value)
obj.metadata[key] = value
obj.metadata.update(self.metadata)
@property
def object_title(self):
"""Return signal/image object title"""
if Conf.io.h5_fullpath_in_title.get(False):
title = self.id
else:
title = self.name
if Conf.io.h5_fname_in_title.get(True):
title += f" ({osp.basename(self.h5file.filename)})"
return title
def set_signal_data(self, obj):
"""Set signal data (handles various issues)"""
data = self.data
if data.dtype not in (float, np.complex128):
data = np.array(data, dtype=float)
if len(data.shape) == 1:
obj.set_xydata(np.arange(data.size), data)
else:
x, y, dx, dy = data_to_xy(data)
obj.set_xydata(x, y, dx, dy)
def set_image_data(self, obj):
"""Set image data (handles various issues)"""
data = self.data
if data.dtype == np.uint32:
self.uint32_wng = data.max() > np.iinfo(np.int32).max
clipped_data = data.clip(0, np.iinfo(np.int32).max)
data = np.array(clipped_data, dtype=np.int32)
obj.data = data
class H5Importer:
"""CodraFT HDF5 importer class"""
def __init__(self, filename):
self.h5file = h5py.File(filename)
self.__nodes = {}
self.root = RootNode(self.h5file)
self.__nodes[self.root.id] = self.root.dset
self.root.collect_children(self.__nodes)
NODE_FACTORY.run_post_triggers(self)
@property
def nodes(self):
"""Return all nodes"""
return self.__nodes.values()
def get(self, node_id: str):
"""Return node associated to id"""
return self.__nodes[node_id]
def get_relative(self, node: BaseNode, relpath: str, ancestor: int = 0):
"""Return node using relative path to another node"""
path = "/" + (
"/".join(node.id.split("/")[:-ancestor]) + "/" + relpath.strip("/")
).strip("/")
return self.__nodes[path]
def close(self):
"""Close HDF5 file"""
self.__nodes = {}
self.h5file.close()
class NodeFactory:
"""Factory for node classes"""
def __init__(self):
self.__ignored_datasets = []
self.__generic_classes = []
self.__thirdparty_classes = []
self.__post_triggers = {}
def add_ignored_datasets(self, names):
"""Add h5 dataset name to ignore list"""
self.__ignored_datasets.extend(names)
def add_post_trigger(self, nodecls: BaseNode, callback: Callable):
"""Add post trigger function, to be called at the end of the collect process.
Callbacks take only one argument: H5Importer instance."""
triggers = self.__post_triggers.setdefault(nodecls, [])
triggers.append(callback)
def register(self, cls, is_generic=False):
"""Register node class.
Generic classes are processed after specific classes (as a fallback solution)"""
if is_generic:
self.__generic_classes.append(cls)
else:
self.__thirdparty_classes.append(cls)
def get(self, dset):
"""Return node class that matches h5 dataset"""
for name in to_string(dset.name).split("/"):
if name in self.__ignored_datasets:
return None
for cls in self.__thirdparty_classes + self.__generic_classes:
if cls.match(dset):
return cls
if isinstance(dset, h5py.Group):
return GroupNode
return None
def run_post_triggers(self, importer: H5Importer):
"""Run post-collect callbacks"""
for node in importer.nodes:
for nodecls, triggers in self.__post_triggers.items():
if isinstance(node, nodecls):
for func in triggers:
func(node, importer)
NODE_FACTORY = NodeFactory()
class GroupNode(BaseNode):
"""Object representing a HDF5 group node"""
@property
def icon_name(self):
"""Icon name associated to node"""
return "h5group.svg"
def collect_children(self, node_dict: Dict):
"""Construct tree"""
for dset in self.dset.values():
child_cls = NODE_FACTORY.get(dset)
if child_cls is not None:
child = child_cls(self.h5file, dset.name)
node_dict[child.id] = child
self.children.append(child)
if isinstance(child, GroupNode):
child.collect_children(node_dict)
@property
def text(self):
"""Return node textual representation"""
return self.dset.name
class RootNode(GroupNode):
"""Object representing a HDF5 root node"""
def __init__(self, h5file):
super().__init__(h5file, "/")
@property
def icon_name(self):
"""Icon name associated to node"""
return "h5file.svg"
@property
def name(self):
"""Return node name, constructed from dataset name"""
return osp.basename(self.h5file.filename)
@property
def description(self):
"""Return node description"""
return self.h5file.filename
CodraFT-2.2.1/codraft/core/io/h5/generic.py 0000664 0000000 0000000 00000007673 14435624103 0020300 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Generic HDF5 format support
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import h5py
import numpy as np
from codraft.core.io.h5 import common, utils
from codraft.core.model.image import create_image
from codraft.core.model.signal import create_signal
from codraft.utils.misc import to_string
class BaseGenericNode(common.BaseNode):
"""Object representing a generic HDF5 data node"""
@classmethod
def match(cls, dset):
"""Return True if h5 dataset match node pattern"""
return not isinstance(dset, h5py.Group)
@property
def icon_name(self):
"""Icon name associated to node"""
return "h5scalar.svg"
@property
def data(self):
"""Data associated to node, if available"""
return self.dset[()]
@property
def dtype_str(self):
"""Return string representation of node data type, if any"""
return str(self.data.dtype)
@property
def text(self):
"""Return node textual representation"""
return to_string(self.data)
class GenericScalarNode(BaseGenericNode):
"""Object representing a generic scalar HDF5 data node"""
@classmethod
def match(cls, dset):
"""Return True if h5 dataset match node pattern"""
if not super().match(dset):
return False
data = dset[()]
return np.issctype(data) and utils.is_supported_num_dtype(data)
common.NODE_FACTORY.register(GenericScalarNode, is_generic=True)
class GenericTextNode(BaseGenericNode):
"""Object representing a generic text HDF5 data node"""
@classmethod
def match(cls, dset):
"""Return True if h5 dataset match node pattern"""
if not super().match(dset):
return False
data = dset[()]
return isinstance(data, bytes) or utils.is_supported_str_dtype(data)
@property
def dtype_str(self):
"""Return string representation of node data type, if any"""
return "string"
@property
def text(self):
"""Return node textual representation"""
if utils.is_single_str_array(self.data):
return self.data[0]
return to_string(self.data)
common.NODE_FACTORY.register(GenericTextNode, is_generic=True)
class GenericArrayNode(BaseGenericNode):
"""Object representing a generic array HDF5 data node"""
IS_ARRAY = True
@classmethod
def match(cls, dset):
"""Return True if h5 dataset match node pattern"""
if not super().match(dset):
return False
data = dset[()]
return (
utils.is_supported_num_dtype(data)
and isinstance(data, np.ndarray)
and len(data.shape) in (1, 2)
)
@property
def is_signal(self):
"""Return True if array represents a signal"""
shape = self.data.shape
return len(shape) == 1 or shape[0] in (1, 2) or shape[1] in (1, 2)
@property
def icon_name(self):
"""Icon name associated to node"""
return "signal.svg" if self.is_signal else "image.svg"
@property
def shape_str(self):
"""Return string representation of node shape, if any"""
return " x ".join([str(size) for size in self.data.shape])
@property
def dtype_str(self):
"""Return string representation of node data type, if any"""
return str(self.data.dtype)
@property
def text(self):
"""Return node textual representation"""
def create_object(self):
"""Create native object, if supported"""
if self.is_signal:
obj = create_signal(self.object_title)
self.set_signal_data(obj)
else:
obj = create_image(self.object_title)
self.set_image_data(obj)
return obj
common.NODE_FACTORY.register(GenericArrayNode, is_generic=True)
CodraFT-2.2.1/codraft/core/io/h5/mos07636.py 0000664 0000000 0000000 00000022124 14435624103 0020054 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT MOS07636 HDF5 format support
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from guidata.utils import update_dataset
from h5py import Group
from codraft.core.io.h5 import common, utils
from codraft.core.model.base import ANN_KEY
from codraft.core.model.image import create_image
from codraft.core.model.signal import create_signal
from codraft.utils.misc import to_string
# Add ignored dataset names
common.NODE_FACTORY.add_ignored_datasets(("PALETTE",))
class BaseMOS07636Node(common.BaseNode):
"""Object representing a HDF5 node, according to MOS07636"""
ATTR_PATTERN = (None, None)
def __init__(self, h5file, dset):
super().__init__(h5file, dset)
self.xunit = None
self.yunit = None
self.zunit = None
self.xlabel = None
self.ylabel = None
self.zlabel = None
self.__obj_templates = []
self.__metadata_entries = {}
def add_object_default_values(self, **template):
"""Add object default values (object template)"""
self.__obj_templates.append(template)
def update_from_object_default_values(self, obj):
"""Update object (signal/image) from default values (template), if available"""
for template in self.__obj_templates:
update_dataset(obj, template)
def add_metadata_entry(self, key, value):
"""Add metadata entry to object"""
self.__metadata_entries[key] = value
@classmethod
def match(cls, dset):
"""Return True if h5 dataset match node pattern"""
name, value = cls.ATTR_PATTERN
return dset.attrs.get(name) == value
@property
def data(self):
"""Data associated to node, if available"""
if isinstance(self.dset, Group):
return self.dset["valeur"][()]
# This is not a valid dataset according to MOS07636!
return self.dset[()]
@property
def shape_str(self):
"""Return string representation of node shape, if any"""
try:
shape = self.data.shape
if shape:
return " x ".join([str(size) for size in shape])
except AttributeError:
pass
return ""
@property
def dtype_str(self):
"""Return string representation of node data type, if any"""
try:
dstr = str(self.data.dtype)
except AttributeError:
if isinstance(self.data, (str, bytes)):
return "string"
return str(type(self.data))
if dstr.startswith("|S"):
return "string"
return dstr
@property
def description(self):
"""Return node description"""
if isinstance(self.dset, Group):
desc = utils.process_scalar_value(self.dset, "description", utils.fix_ldata)
if desc is not None:
return desc
return super().description
def create_object(self):
"""Create native object, if supported"""
if isinstance(self.dset, Group):
self.xunit, self.yunit, self.zunit = utils.process_label(self.dset, "unite")
self.xlabel, self.ylabel, self.zlabel = utils.process_label(
self.dset, "label"
)
for label in ("description", "source"):
if isinstance(self.dset, Group):
val = utils.process_scalar_value(self.dset, label, utils.fix_ldata)
if val is not None:
self.metadata[label] = val
self.metadata.update(self.__metadata_entries)
class ScalarNode(BaseMOS07636Node):
"""Object representing a scalar HDF5 node, according to MOS07636"""
ATTR_PATTERN = ("CLASS", b"ELEMENTAIRE")
def __init__(self, h5file, dset):
super().__init__(h5file, dset)
if isinstance(self.dset, Group):
self.xunit = utils.process_scalar_value(self.dset, "unite", self._fix_unit)
@staticmethod
def _fix_unit(scdata):
"""Fix unit data"""
data = scdata[0]
if not isinstance(data, bytes):
data = data[0] # Should not be necessary (invalid format)
if data == b"NULL":
return ""
return utils.fix_ldata(data)
@property
def data(self):
"""Data associated to node, if available"""
try:
return super().data
except ValueError:
# Handles invalid scalar datasets...
return self.dset[()]
@property
def icon_name(self):
"""Icon name associated to node"""
return "h5scalar.svg"
@property
def text(self):
"""Return node textual representation"""
text = to_string(self.data)
suffix = "" if self.xunit is None else " " + self.xunit
if not text.endswith(suffix): # Should not be necessary (invalid format)
text += suffix
return text
common.NODE_FACTORY.register(ScalarNode)
class SignalNode(BaseMOS07636Node):
"""Object representing a Signal HDF5 node, according to MOS07636"""
IS_ARRAY = True
ATTR_PATTERN = ("CLASS", b"COURBE")
@property
def icon_name(self):
"""Icon name associated to node"""
return "signal.svg"
@property
def text(self):
"""Return node textual representation"""
def create_object(self):
"""Create native object, if supported"""
super().create_object()
obj = create_signal(
self.object_title,
units=(self.xunit, self.yunit),
labels=(self.xlabel, self.ylabel),
)
self.set_signal_data(obj)
self.update_from_object_default_values(obj)
return obj
common.NODE_FACTORY.register(SignalNode)
class ImageNode(BaseMOS07636Node):
"""Object representing an Image HDF5 node, according to MOS07636"""
IS_ARRAY = True
ATTR_PATTERN = ("CLASS", b"IMAGE")
@property
def icon_name(self):
"""Icon name associated to node"""
return "image.svg"
@property
def text(self):
"""Return node textual representation"""
def create_object(self):
"""Create native object, if supported"""
super().create_object()
obj = create_image(
self.object_title,
units=(self.xunit, self.yunit, self.zunit),
labels=(self.xlabel, self.ylabel, self.zlabel),
)
self.set_image_data(obj)
x0, y0 = utils.process_xy_values(self.dset, "origine")
if x0 is not None and y0 is not None:
obj.x0, obj.y0 = x0, y0
dx, dy = utils.process_xy_values(self.dset, "resolution")
if dx is not None and dy is not None:
obj.dx, obj.dy = dx, dy
self.update_from_object_default_values(obj)
return obj
common.NODE_FACTORY.register(ImageNode)
def handle_margins(node: ImageNode, importer: common.H5Importer):
"""Post-collection trigger handling image margins when available (Vimba Cameras)"""
try:
# Vimba Camera HDF5 / node.id: "/Acquisition/AcquisitionBrute"
margegauche = importer.get_relative(node, "/Parametres_ACQ/MargeGauche", 2)
margehaute = importer.get_relative(node, "/Parametres_ACQ/MargeHaute", 2)
binningx = importer.get_relative(node, "/Parametres_ACQ/BinningX", 2)
binningy = importer.get_relative(node, "/Parametres_ACQ/BinningY", 2)
except KeyError:
try:
# IStar Camera HDF5 / node.id: "/Entrees/Acquisition/AcquisitionBrute"
margegauche = importer.get_relative(node, "/Parametres_IMG/MargeGauche", 2)
margehaute = importer.get_relative(node, "/Parametres_IMG/MargeHaute", 2)
binningx = importer.get_relative(node, "/Parametres_IMG/BinningX", 2)
binningy = importer.get_relative(node, "/Parametres_IMG/BinningY", 2)
except KeyError:
return
node.add_object_default_values(
x0=margegauche.data, y0=margehaute.data, dx=binningx.data, dy=binningy.data
)
common.NODE_FACTORY.add_post_trigger(ImageNode, handle_margins)
def handle_streakcameratimeaxis(node: ImageNode, importer: common.H5Importer):
"""Post-collection trigger handling streak X-axis time conv. when available"""
try:
# Streak Camera HDF5 / node.id: "/Acquisition/AcquisitionCorrigee"
tempspixel = importer.get_relative(node, "/TempsPixel", 1)
offsettemporel = importer.get_relative(node, "/OffsetTemporel", 1)
except KeyError:
return
if node.id.endswith("AcquisitionCorrigee"):
node.add_object_default_values(
x0=offsettemporel.data, dx=tempspixel.data, xunit=tempspixel.xunit
)
common.NODE_FACTORY.add_post_trigger(ImageNode, handle_streakcameratimeaxis)
def handle_annotations(node: ImageNode, importer: common.H5Importer):
"""Post-collection trigger handling annotations when available"""
try:
annotations = importer.get_relative(node, "/Annotations", 1)
except KeyError:
return
node.add_metadata_entry(ANN_KEY, to_string(annotations.data))
common.NODE_FACTORY.add_post_trigger(ImageNode, handle_annotations)
CodraFT-2.2.1/codraft/core/io/h5/utils.py 0000664 0000000 0000000 00000005252 14435624103 0020013 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Utilities for exogenous HDF5 format support
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
import numpy as np
from codraft.utils.misc import to_string
def fix_ldata(fuzzy):
"""Fix label data"""
if fuzzy is not None:
if fuzzy and isinstance(fuzzy, np.void) and len(fuzzy) > 1:
# Shouldn't happen (invalid LMJ fmt)
fuzzy = fuzzy[0]
if isinstance(fuzzy, (np.string_, bytes)):
fuzzy = to_string(fuzzy)
if isinstance(fuzzy, str):
return fuzzy
return None
def fix_ndata(fuzzy):
"""Fix numeric data"""
if fuzzy is not None:
if fuzzy and isinstance(fuzzy, np.void) and len(fuzzy) > 1:
# Shouldn't happen (invalid LMJ fmt)
fuzzy = fuzzy[0]
try:
if float(fuzzy) == int(fuzzy):
return int(fuzzy)
return float(fuzzy)
except (TypeError, ValueError):
pass
return None
def process_scalar_value(dset, name, callback):
"""Process dataset numeric/str value `name`"""
try:
scdata = dset[name][()]
if scdata is not None:
return callback(scdata)
except (KeyError, ValueError):
pass
return None
def process_label(dset, name):
"""Process dataset label `name`"""
try:
ldata = dset[name][()]
if ldata is not None:
xldata, yldata, zldata = None, None, None
if len(ldata) == 2:
xldata, yldata = ldata
elif len(ldata) == 3:
xldata, yldata, zldata = ldata
return fix_ldata(xldata), fix_ldata(yldata), fix_ldata(zldata)
except KeyError:
pass
return None, None, None
def process_xy_values(dset, name):
"""Process dataset x,y values `name`"""
try:
ldata = dset[name][()]
if ldata is not None:
return fix_ndata(ldata[0]), fix_ndata(ldata[1])
except (KeyError, ValueError):
pass
return None, None
def is_supported_num_dtype(data):
"""Return True if data type is a numerical type supported by CodraFT"""
return data.dtype.name.startswith(("int", "uint", "float", "complex"))
def is_single_str_array(data):
"""Return True if data is a single-item string array"""
return np.issctype(data) and data.shape == (1,) and isinstance(data[0], str)
def is_supported_str_dtype(data):
"""Return True if data type is a string type supported by preview"""
return data.dtype.name.startswith("string") or is_single_str_array(data)
CodraFT-2.2.1/codraft/core/io/image.py 0000664 0000000 0000000 00000036272 14435624103 0017427 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause or the CeCILL-B License
# (see codraft/__init__.py for details)
"""
CodraFT Image I/O module
"""
import os
import re
import struct
import time
import numpy as np
from guiqwt.io import _imread_pil, _imwrite_pil, iohandler
from codraft.config import _
from codraft.utils.misc import to_string
# ==============================================================================
# SIF I/O functions
# ==============================================================================
# Original code:
# --------------
# Zhenpeng Zhou