interactive-pipe 0.5.6__tar.gz → 0.5.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/PKG-INFO +4 -4
  2. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/pyproject.toml +1 -1
  3. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/readme.md +3 -3
  4. interactive_pipe-0.5.7/src/interactive_pipe/__init__.py +4 -0
  5. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/core/filter.py +12 -4
  6. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/core/graph.py +18 -2
  7. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/data_objects/curves.py +3 -1
  8. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/gui.py +4 -2
  9. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/qt_gui.py +20 -3
  10. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/headless/pipeline.py +26 -8
  11. interactive_pipe-0.5.7/src/interactive_pipe/helper/_private.py +2 -0
  12. interactive_pipe-0.5.7/src/interactive_pipe/helper/choose_backend.py +39 -0
  13. interactive_pipe-0.5.7/src/interactive_pipe/helper/filter_decorator.py +111 -0
  14. interactive_pipe-0.5.7/src/interactive_pipe/helper/keyword_args_analyzer.py +72 -0
  15. interactive_pipe-0.5.7/src/interactive_pipe/helper/pipeline_decorator.py +32 -0
  16. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe.egg-info/PKG-INFO +4 -4
  17. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe.egg-info/SOURCES.txt +5 -1
  18. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_decorator.py +1 -1
  19. interactive_pipe-0.5.6/src/interactive_pipe/__init__.py +0 -3
  20. interactive_pipe-0.5.6/src/interactive_pipe/helper/decorator.py +0 -90
  21. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/LICENSE +0 -0
  22. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/setup.cfg +0 -0
  23. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/core/__init__.py +0 -0
  24. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/core/cache.py +0 -0
  25. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/core/engine.py +0 -0
  26. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/core/pipeline.py +0 -0
  27. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/core/signature.py +0 -0
  28. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/data_objects/__init__.py +0 -0
  29. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/data_objects/data.py +0 -0
  30. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/data_objects/image.py +0 -0
  31. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/data_objects/parameters.py +0 -0
  32. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/__init__.py +0 -0
  33. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/mpl_control.py +0 -0
  34. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/mpl_gui.py +0 -0
  35. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/mpl_window.py +0 -0
  36. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/nb_control.py +0 -0
  37. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/nb_gui.py +0 -0
  38. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/qt_control.py +0 -0
  39. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/graphical/window.py +0 -0
  40. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/headless/__init__.py +0 -0
  41. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/headless/control.py +0 -0
  42. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/headless/keyboard.py +0 -0
  43. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/helper/__init__.py +0 -0
  44. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/helper/control_abbreviation.py +0 -0
  45. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/thirdparty/images_openai_api.py +0 -0
  46. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe/thirdparty/music_spotify.py +0 -0
  47. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe.egg-info/dependency_links.txt +0 -0
  48. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/src/interactive_pipe.egg-info/top_level.txt +0 -0
  49. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_cache.py +0 -0
  50. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_control_abbreviations.py +0 -0
  51. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_controller.py +0 -0
  52. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_core.py +0 -0
  53. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_curves.py +0 -0
  54. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_engine.py +0 -0
  55. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_filter.py +0 -0
  56. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_headless.py +0 -0
  57. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_image.py +0 -0
  58. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_parameters.py +0 -0
  59. {interactive_pipe-0.5.6 → interactive_pipe-0.5.7}/test/test_recorder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: interactive_pipe
3
- Version: 0.5.6
3
+ Version: 0.5.7
4
4
  Summary: Library to create flexible interactive image processing pipelines and automatically add a graphical user interface without knowing anything about GUI coding!
5
5
  Author-email: Balthazar Neveu <balthazarneveu@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/balthazarneveu/interactive_pipe
@@ -26,8 +26,8 @@ License-File: LICENSE
26
26
  | Science notebook | Toddler DIY Jukebox on a raspberry Pi |
27
27
  |:-----: |:-----:|
28
28
  | ![science notebook](/doc/images/scientific_notebook.jpg) | ![jukebox](/doc/images/jukebox.jpg)
29
- | Sliders are added automatically in your jupyter notebook. This works on Google Collab and the code takes about 40 lines of code.| Plays some music when you touch the icon. Images can be generated using the OpenAI Dall-E API helpers. Text overlay is added using OpenCV |
30
- | [Sample notebook](https://colab.research.google.com/drive/1AwHyjZH8MnzZqwsvbmxBoB15btuMIwtk?usp=sharing) | :soon: sample code |
29
+ | Sliders are added automatically in your jupyter notebook. This works on Google Collab and the code takes about 40 lines of code. No Widgets, event handlers or matplotlib knowledge required.| Plays some music when you touch the icon. Images can be generated using the OpenAI [Dall-E API helpers](https://github.com/balthazarneveu/interactive_pipe/blob/master/src/interactive_pipe/thirdparty/images_openai_api.py) . Caption added through the title mechanism. Music samples generated by prompting [MusicGen](https://huggingface.co/spaces/facebook/MusicGen) |
30
+ | [Demo notebook on collab](https://colab.research.google.com/drive/1AwHyjZH8MnzZqwsvbmxBoB15btuMIwtk?usp=sharing) | [jukebox.py demo code](/demo/jukebox.py)|
31
31
 
32
32
  ----------------------
33
33
 
@@ -55,7 +55,7 @@ License-File: LICENSE
55
55
  ----------------------
56
56
 
57
57
  ## :scroll: Features
58
- **Version 0.5.6**
58
+ **Version 0.5.7**
59
59
  - Modular multi-image processing filters
60
60
  - Declarative: Easily make graphical user interface without having to learn anything about pyQt or matplotlib
61
61
  - Support in jupyter notebooks
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "interactive_pipe"
8
- version = "0.5.6"
8
+ version = "0.5.7"
9
9
  authors = [
10
10
  { name="Balthazar Neveu", email="balthazarneveu@gmail.com" },
11
11
  ]
@@ -12,8 +12,8 @@
12
12
  | Science notebook | Toddler DIY Jukebox on a raspberry Pi |
13
13
  |:-----: |:-----:|
14
14
  | ![science notebook](/doc/images/scientific_notebook.jpg) | ![jukebox](/doc/images/jukebox.jpg)
15
- | Sliders are added automatically in your jupyter notebook. This works on Google Collab and the code takes about 40 lines of code.| Plays some music when you touch the icon. Images can be generated using the OpenAI Dall-E API helpers. Text overlay is added using OpenCV |
16
- | [Sample notebook](https://colab.research.google.com/drive/1AwHyjZH8MnzZqwsvbmxBoB15btuMIwtk?usp=sharing) | :soon: sample code |
15
+ | Sliders are added automatically in your jupyter notebook. This works on Google Collab and the code takes about 40 lines of code. No Widgets, event handlers or matplotlib knowledge required.| Plays some music when you touch the icon. Images can be generated using the OpenAI [Dall-E API helpers](https://github.com/balthazarneveu/interactive_pipe/blob/master/src/interactive_pipe/thirdparty/images_openai_api.py) . Caption added through the title mechanism. Music samples generated by prompting [MusicGen](https://huggingface.co/spaces/facebook/MusicGen) |
16
+ | [Demo notebook on collab](https://colab.research.google.com/drive/1AwHyjZH8MnzZqwsvbmxBoB15btuMIwtk?usp=sharing) | [jukebox.py demo code](/demo/jukebox.py)|
17
17
 
18
18
  ----------------------
19
19
 
@@ -41,7 +41,7 @@
41
41
  ----------------------
42
42
 
43
43
  ## :scroll: Features
44
- **Version 0.5.6**
44
+ **Version 0.5.7**
45
45
  - Modular multi-image processing filters
46
46
  - Declarative: Easily make graphical user interface without having to learn anything about pyQt or matplotlib
47
47
  - Support in jupyter notebooks
@@ -0,0 +1,4 @@
1
+ from interactive_pipe.helper.pipeline_decorator import pipeline, interactive_pipeline
2
+ from interactive_pipe.headless.control import Control
3
+ from interactive_pipe.headless.keyboard import KeyboardControl
4
+ from interactive_pipe.helper.filter_decorator import interactive, interact
@@ -27,14 +27,14 @@ class PureFilter:
27
27
  assert isinstance(new_global_params, dict)
28
28
  self._global_params = new_global_params
29
29
 
30
-
31
-
32
30
  def check_apply_signature(self):
33
31
  if not hasattr(self, "__args_names") or not hasattr(self, "__kwargs_names"):
34
32
  self.__args_names, self.__kwargs_names = analyze_apply_fn_signature(
35
33
  self.apply)
34
+ self.signature = (self.__args_names, self.__kwargs_names)
36
35
  else: # skip computing signature
37
36
  pass
37
+
38
38
  def __initialize_default_values(self):
39
39
  assert not hasattr(self, "_values")
40
40
  self.check_apply_signature()
@@ -50,7 +50,6 @@ class PureFilter:
50
50
  def values(self, new_values):
51
51
  assert isinstance(
52
52
  new_values, dict), f"{new_values} is not a dictionary"
53
-
54
53
  self._values = {**self._values, **new_values}
55
54
 
56
55
  def run(self, *imgs) -> Tuple[Any]:
@@ -120,4 +119,13 @@ class FilterCore(PureFilter):
120
119
  descr += "(" + (", ".join([f"{it}" for it in self.inputs])) + ")"
121
120
  descr += " -> " + "(" + ", ".join([f"{it}" for it in self.outputs]) + ")"
122
121
  # descr += "\n"
123
- return descr
122
+ return descr
123
+
124
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
125
+ control_dict = {}
126
+ if hasattr(self, "controls"):
127
+ for ctrl_name, ctrl in self.controls.items():
128
+ control_dict[ctrl_name] = ctrl.value
129
+ merged_kwargs = {**kwargs, **control_dict}
130
+ out = self.apply(*args, **merged_kwargs)
131
+ return out
@@ -2,6 +2,7 @@ import ast
2
2
  import inspect
3
3
  import logging
4
4
  from typing import List, Callable, Tuple, Optional, Union
5
+ from interactive_pipe.core.filter import FilterCore
5
6
  from interactive_pipe.core.signature import analyze_apply_fn_signature
6
7
 
7
8
  def get_name(node: ast.NodeVisitor) -> Union[str, List[str], None]:
@@ -57,10 +58,25 @@ def get_call_graph(func:Callable, global_context=None) -> dict:
57
58
  input_names = [get_name(arg) for arg in value.args]
58
59
  output_names = flatten_target_names(targets, mapping_function=get_name)
59
60
  function_object = global_context[function_name]
60
- sig = analyze_apply_fn_signature(function_object)
61
+ if isinstance(function_object, FilterCore):
62
+ # Case of a filter instance
63
+ function_object.check_apply_signature()
64
+ # ---> forcing filter instance .name to be the name of the object used in the func
65
+ function_object.name = function_name
66
+ # you could use instead: # function_name = function_object.name to keep the original name
67
+ sig = function_object.signature
68
+ function_object.inputs = input_names
69
+ function_object.outputs = output_names
70
+ function_apply = function_object
71
+ elif isinstance(function_object, Callable):
72
+ # Case of a decorated function
73
+ sig = analyze_apply_fn_signature(function_object)
74
+ function_apply = global_context[function_name]
75
+ else:
76
+ raise TypeError(f"Not supported {function_name} - should be function or FilterCore")
61
77
  results.append({
62
78
  "function_name": function_name,
63
- "function_object": global_context[function_name],
79
+ "function_object": function_apply,
64
80
  "signature" : {"args": sig[0], "kwargs": sig[1]},
65
81
  "args": input_names,
66
82
  "returns": output_names,
@@ -419,7 +419,9 @@ class Curve(Data):
419
419
 
420
420
  @staticmethod
421
421
  def _update_plot(data, plt_obj, ax=None):
422
- texts = ax.get_legend().get_texts()
422
+ legend = ax.get_legend()
423
+ if legend is not None:
424
+ texts = legend.get_texts()
423
425
  linear_index = 0
424
426
  for curve_idx, curve in enumerate(data["curves"]):
425
427
  if curve.data.get("x", None) is not None:
@@ -33,9 +33,11 @@ class InteractivePipeGUI():
33
33
  self.audio = audio
34
34
  self.name = name
35
35
  self.size = size
36
+ merged_controls = []
37
+ merged_controls+= controls
36
38
  if hasattr(pipeline, "controls"):
37
- controls += pipeline.controls
38
- self.controls = controls
39
+ merged_controls += pipeline.controls
40
+ self.controls = merged_controls
39
41
  if self.pipeline.outputs:
40
42
  if not isinstance(self.pipeline.outputs[0], list):
41
43
  self.pipeline.outputs = [self.pipeline.outputs]
@@ -56,7 +56,11 @@ import time
56
56
 
57
57
  class InteractivePipeQT(InteractivePipeGUI):
58
58
  def init_app(self, **kwargs):
59
- self.app = QApplication(sys.argv)
59
+ if not QApplication.instance():
60
+ self.app = QApplication(sys.argv)
61
+ else:
62
+ self.app = QApplication.instance()
63
+
60
64
  if self.audio:
61
65
  self.audio_player()
62
66
  self.window = MainWindow(controls=self.controls, name=self.name, pipeline=self.pipeline, size=self.size, main_gui=self, **kwargs)
@@ -365,8 +369,20 @@ class MainWindow(QWidget, InteractivePipeWindow):
365
369
  elif img_widget is not None:
366
370
  img_widget.setParent(None)
367
371
 
368
- def update_image(self, image_array, row, col):
369
- if isinstance(image_array, np.ndarray):
372
+ def update_image(self, image_array_original, row, col):
373
+ if isinstance(image_array_original, np.ndarray):
374
+ if len(image_array_original.shape) == 2:
375
+ # Consider black & white
376
+ image_array = image_array_original.copy()
377
+ c = 3
378
+ image_array = np.expand_dims(image_array, axis=-1)
379
+ image_array = np.repeat(image_array, c, axis=-1)
380
+ elif len(image_array_original.shape) == 3:
381
+ assert isinstance(image_array_original, np.ndarray)
382
+ assert(image_array_original.shape[-1]) == 3
383
+ image_array = image_array_original
384
+ else:
385
+ raise NotImplementedError(f"{image_array_original.shape}4 dimensions image or more like burst are not supported")
370
386
  h, w, c = image_array.shape
371
387
  bytes_per_line = c * w
372
388
  image = QImage(image_array.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
@@ -374,6 +390,7 @@ class MainWindow(QWidget, InteractivePipeWindow):
374
390
  image_label = self.image_canvas[row][col]["image"]
375
391
  image_label.setPixmap(pixmap)
376
392
  else:
393
+ image_array = image_array_original
377
394
  if MPL_SUPPORT and isinstance(image_array, Curve):
378
395
  image_label = FigureCanvas(Figure())
379
396
  if self.image_canvas[row][col]["ax_placeholder"] is None:
@@ -74,14 +74,22 @@ class HeadlessPipeline(PipelineCore):
74
74
  inputs_filt = filt_dict["args"]
75
75
  outputs_filt = filt_dict["returns"]
76
76
  logging.debug("----------------->", filt_name, inputs_filt, outputs_filt)
77
- filter = FilterCore(
78
- name=filt_name,
79
- inputs=inputs_filt,
80
- outputs = outputs_filt,
81
- apply_fn=filt_dict["function_object"],
82
- )
83
- func_kwargs = analyze_apply_fn_signature(filt_dict["function_object"])[1]
84
- params_to_analyze = {**func_kwargs, **Control.get_controls(filt_name)}
77
+ if isinstance(filt_dict["function_object"], FilterCore):
78
+ filter = filt_dict["function_object"]
79
+ filter.inputs = inputs_filt
80
+ filter.outputs = outputs_filt
81
+ filter.name = filt_name
82
+ params_to_analyze = filter.controls
83
+ else:
84
+ # when using the @interactive decorator
85
+ filter = FilterCore(
86
+ name=filt_name,
87
+ inputs=inputs_filt,
88
+ outputs = outputs_filt,
89
+ apply_fn=filt_dict["function_object"],
90
+ )
91
+ func_kwargs = analyze_apply_fn_signature(filt_dict["function_object"])[1]
92
+ params_to_analyze = {**func_kwargs, **Control.get_controls(filt_name)}
85
93
  for param_name, param_value in params_to_analyze.items():
86
94
  if isinstance(param_value, Control):
87
95
  param_value.connect_filter(filter, param_name)
@@ -146,7 +154,17 @@ class HeadlessPipeline(PipelineCore):
146
154
  ret += "}"
147
155
  return ret
148
156
 
157
+ def update_parameters_from_controls(self):
158
+ if not hasattr(self, "controls"):
159
+ # Not having .controls attribute
160
+ # This happens for headless pipelines which have no list of controls
161
+ return
162
+ for ctrl in self.controls:
163
+ logging.info(f"{ctrl.filter_to_connect.name}, {ctrl.parameter_name_to_connect}, {ctrl.value}")
164
+ self.parameters = {ctrl.filter_to_connect.name: {ctrl.parameter_name_to_connect: ctrl.value}}
165
+
149
166
  def __run(self):
167
+ self.update_parameters_from_controls()
150
168
  result_full = super().run()
151
169
  if self.outputs is not None:
152
170
  output_indexes = self.outputs
@@ -0,0 +1,2 @@
1
+ registered_controls_names = []
2
+ auto_gui = None
@@ -0,0 +1,39 @@
1
+ from interactive_pipe.helper import _private
2
+ import logging
3
+
4
+ def get_interactive_pipeline_class(gui="auto"):
5
+ selected_gui = None
6
+ if gui is None or gui == "auto":
7
+ if _private.auto_gui is not None:
8
+
9
+ selected_gui = _private.auto_gui #skip selection, used cached one
10
+ logging.debug(f"auto gui already selected {selected_gui}")
11
+ else:
12
+ try:
13
+ __IPYTHON__ # noqa: F821
14
+ selected_gui = "nb"
15
+ except Exception:
16
+ try:
17
+ from interactive_pipe.graphical.qt_gui import InteractivePipeQT as ChosenGui
18
+ selected_gui = "qt"
19
+ except Exception as qt_import_execption:
20
+ try:
21
+ import matplotlib.pyplot as plt
22
+ selected_gui = "mpl"
23
+ except Exception as mpl_import_exception:
24
+ raise NameError("Error in auto backend choice:\n"+
25
+ f"Could not import Qt: {qt_import_execption}\n"+
26
+ f"Could not import Matplotlib {mpl_import_exception}"
27
+ )
28
+ _private.auto_gui = selected_gui
29
+ else:
30
+ selected_gui = gui
31
+ if selected_gui == "qt":
32
+ from interactive_pipe.graphical.qt_gui import InteractivePipeQT as ChosenGui
33
+ elif selected_gui == "mpl":
34
+ from interactive_pipe.graphical.mpl_gui import InteractivePipeMatplotlib as ChosenGui
35
+ elif selected_gui == "nb":
36
+ from interactive_pipe.graphical.nb_gui import InteractivePipeJupyter as ChosenGui
37
+ else:
38
+ raise NotImplementedError(f"Gui {gui} not available")
39
+ return ChosenGui
@@ -0,0 +1,111 @@
1
+ from interactive_pipe.headless.control import Control
2
+ import functools
3
+ import inspect
4
+ from interactive_pipe.helper.keyword_args_analyzer import get_controls_from_decorated_function_declaration
5
+ from interactive_pipe.helper import _private #import registered_controls_names
6
+ from interactive_pipe.headless.pipeline import HeadlessPipeline
7
+
8
+ from interactive_pipe.core.filter import FilterCore
9
+ import logging
10
+
11
+ from interactive_pipe.helper.choose_backend import get_interactive_pipeline_class
12
+
13
+ class EnhancedFilterCore(FilterCore):
14
+ """Internally creates a pipeline with a single filter
15
+ so you can graphically test a single filter without instantiating a pipeline.
16
+ Internally used by `@interact` decorator.
17
+ """
18
+ def get_gui(self, gui="auto", output_routing=None):
19
+ self.inputs = list(range(len(self.signature[0])))
20
+ if output_routing is None:
21
+ logging.warning("Single output assumed, cannot deduce the number of outputs from your code, please provide output_routing!")
22
+ self.outputs = ["output",]
23
+ else:
24
+ self.outputs = output_routing
25
+ pipeline = HeadlessPipeline(filters=[self], inputs=[], outputs=None)
26
+ for ctrl_name, _ctrl in self.controls.items():
27
+ self.controls[ctrl_name].filter_to_connect = self
28
+ self.controls[ctrl_name].parameter_name_to_connect = ctrl_name
29
+ controls = [ctrl for _, ctrl in self.controls.items()]
30
+ pipeline.controls = controls
31
+ return get_interactive_pipeline_class(gui=gui)(pipeline=pipeline)
32
+
33
+ def run_gui(self, *args, gui="auto", output_routing=None):
34
+ try:
35
+ self.inputs = args
36
+ out = self.run(*args)
37
+ output_routing = [f"output {idx}" for idx in range(len(out))]
38
+ self.outputs = output_routing
39
+ except Exception as exc:
40
+ logging.warning(f"cannot automatically deduce the output routing\n{exc}")
41
+ gui_pipeline = self.get_gui(gui=gui, output_routing=output_routing)
42
+ gui_pipeline.pipeline.inputs_routing = list(range(len(self.signature[0])))
43
+ gui_pipeline(*args)
44
+ gui_pipeline.close()
45
+ gui_pipeline.controls = []
46
+ del gui_pipeline
47
+
48
+
49
+ def interact(*decorator_args, gui="auto", disable=False, output_routing = None, **decorator_controls):
50
+ """interact decorator allows you to launch a GUI from a single function
51
+
52
+ This will directly launch a GUI.
53
+ This is a "One shot decorator
54
+
55
+ ___________________________________________________________________________
56
+
57
+ Note: `@interact` is a lot inspired by the `iPyWidget` interact function you'd usually use...
58
+ except that you can return numpy arrays & Curve class instances to automatically deal with the plot mechanism
59
+ Which avoids writing a lot of matplotlib boiler plate code
60
+ """
61
+ ommitted_parentheses_flag = False
62
+ def wrapper(func):
63
+ if disable:
64
+ return func
65
+ # this will run the GUI automatically
66
+ _private.registered_controls_names = []
67
+ filter_instance = filter_from_function(func, **decorator_controls)
68
+ filter_instance.run_gui(*(decorator_args if not ommitted_parentheses_flag else decorator_args[1:]), gui=gui, output_routing=output_routing)
69
+ # return the original function if you want to keep using it afterwards
70
+ return func
71
+
72
+
73
+ if len(decorator_args)==1 and callable(decorator_args[0]):
74
+ ommitted_parentheses_flag = True
75
+ return wrapper(decorator_args[0]) # no parenthesis
76
+ return wrapper
77
+
78
+
79
+ def filter_from_function(apply_fn, default_params={}, **kwargs) -> EnhancedFilterCore:
80
+ controls = get_controls_from_decorated_function_declaration(apply_fn, kwargs)
81
+ filter_instance = EnhancedFilterCore(apply_fn=apply_fn, default_params=default_params)
82
+ filter_instance.controls = controls
83
+ return filter_instance
84
+
85
+
86
+ def interactive(**decorator_controls):
87
+ """Decorator to declare some controls linked to keyword arguments.
88
+ Parameters will become "variable" and sliders automatically appear in the GUI.
89
+
90
+ `@interactive` differs from `interact` in the sense that it will not launch a gui.
91
+ It is simply used to declare some sliders and allows re-using these functions afterwards.
92
+ Function decorator to add some controls
93
+ """
94
+ def wrapper(func):
95
+ controls = get_controls_from_decorated_function_declaration(func, decorator_controls)
96
+
97
+ @functools.wraps(func)
98
+ def inner(*args, **kwargs):
99
+ # Combine args and kwargs into a single dictionary
100
+ merged_kwargs = {**kwargs, **controls}
101
+ bound_args = inspect.signature(func).bind(*args, **merged_kwargs)
102
+ bound_args.apply_defaults()
103
+
104
+ for k, v in bound_args.arguments.items():
105
+ if isinstance(v, Control):
106
+ bound_args.arguments[k] = v.value
107
+
108
+ # Call the original function with the processed arguments
109
+ return func(*bound_args.args, **bound_args.kwargs)
110
+ return inner
111
+ return wrapper
@@ -0,0 +1,72 @@
1
+ from interactive_pipe.headless.control import Control
2
+ from interactive_pipe.helper.control_abbreviation import control_from_tuple
3
+ from typing import Callable, Union
4
+ from interactive_pipe.core.graph import analyze_apply_fn_signature
5
+ from interactive_pipe.helper import _private
6
+
7
+
8
+ def __create_control_from_keyword_argument(
9
+ param_name: str,
10
+ unknown_keyword_arg: Union[Control, list, tuple]
11
+ ) -> Union[None, Control]:
12
+ """Create a Control from a given keyword argument named param_name with value unknown_keyword_arg
13
+
14
+ - If unknown_keyword_arg is already a Control, nothing to do.
15
+ - If unknown_keyword_arg is a tuple or a list or something else,
16
+ guess the Slider declaration automatically (see `control_from_tuple`)
17
+
18
+ You cannot have several controls which have the same attribute .name
19
+ See https://github.com/balthazarneveu/interactive_pipe/issues/35 for more details
20
+ """
21
+ chosen_control = None
22
+ if isinstance(unknown_keyword_arg, Control): # This includes KeyboardControl aswell!!
23
+ if unknown_keyword_arg.name is None or unknown_keyword_arg._auto_named:
24
+ unknown_keyword_arg.name = param_name
25
+ chosen_control = unknown_keyword_arg
26
+ else:
27
+ if isinstance(unknown_keyword_arg, list) or isinstance(unknown_keyword_arg, tuple):
28
+ try:
29
+ chosen_control = control_from_tuple(unknown_keyword_arg, param_name=param_name)
30
+ except Exception as exc_1:
31
+ try:
32
+ chosen_control = control_from_tuple((unknown_keyword_arg,), param_name=param_name)
33
+ except Exception as exc:
34
+ raise Exception(exc)
35
+ # NOTE: for keyword args, setting a boolean will not trigger a tickmark (although it is possible)
36
+ # Use (True) instead of True if you want to make a tickbox
37
+ if chosen_control is not None:
38
+ assert chosen_control.name not in _private.registered_controls_names, f"{chosen_control.name} already attributed - {_private.registered_controls_names}"
39
+ _private.registered_controls_names.append(chosen_control.name)
40
+ return chosen_control
41
+
42
+ def get_controls_from_decorated_function_declaration(func: Callable, decorator_controls: dict):
43
+ controls = {}
44
+ keyword_args = analyze_apply_fn_signature(func)[1]
45
+ keyword_names = list(keyword_args.keys())
46
+
47
+ # Analyze at 2 levels (function keyword args & decorator keyword args) then register controls when necessary.
48
+ #-------------------------------------------
49
+ # @interactive(param_2=Control(...), )
50
+ # def func(img1, img2, param_1=Control(...)):
51
+ #-------------------------------------------
52
+
53
+ # 1. Analyzing function keyword args
54
+ # def func(img1, img2, param_1=Control(...))
55
+
56
+ for param_name, unknown_keyword_arg in keyword_args.items():
57
+ chosen_control = __create_control_from_keyword_argument(param_name, unknown_keyword_arg)
58
+ if chosen_control is not None:
59
+ controls[param_name] = chosen_control
60
+
61
+
62
+ # 2. Analyzing decorator keyword args
63
+ # @interactive(param_2=Control(...))
64
+ for param_name, unknown_keyword_arg in decorator_controls.items():
65
+ assert param_name in keyword_names, f"typo: control {param_name} passed through the decorator does not match any of the function keyword args {keyword_names}"
66
+ chosen_control = __create_control_from_keyword_argument(param_name, unknown_keyword_arg)
67
+ if chosen_control is not None:
68
+ controls[param_name] = chosen_control
69
+
70
+ for param_name, unknown_control in controls.items():
71
+ Control.register(func.__name__, param_name, controls[param_name])
72
+ return controls
@@ -0,0 +1,32 @@
1
+ from interactive_pipe.headless.pipeline import HeadlessPipeline
2
+ from interactive_pipe.graphical.gui import InteractivePipeGUI
3
+
4
+ import functools
5
+ from typing import Callable,Union
6
+ from interactive_pipe.helper.choose_backend import get_interactive_pipeline_class
7
+
8
+
9
+ def pipeline(pipeline_function:Callable, **kwargs) -> HeadlessPipeline:
10
+ return HeadlessPipeline.from_function(pipeline_function, **kwargs)
11
+
12
+
13
+ def interactive_pipeline(gui="auto", cache=False, output_canvas=None, **kwargs_gui) -> Union[HeadlessPipeline, InteractivePipeGUI]:
14
+ """@interactive_pipeline
15
+
16
+ Function decorator to add some controls @interactive_pipeline
17
+ """
18
+ def wrapper(pipeline_function):
19
+ headless_pipeline = HeadlessPipeline.from_function(pipeline_function, cache=cache)
20
+ if output_canvas is not None:
21
+ headless_pipeline.outputs = output_canvas
22
+ if gui is None:
23
+ return headless_pipeline
24
+ else:
25
+ InteractivePipeGui = get_interactive_pipeline_class(gui)
26
+ gui_pipeline = InteractivePipeGui(pipeline=headless_pipeline, name=pipeline_function.__name__, **kwargs_gui)
27
+
28
+ @functools.wraps(pipeline_function)
29
+ def inner(*args, **kwargs):
30
+ return gui_pipeline.__call__(*args, **kwargs)
31
+ return inner
32
+ return wrapper
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: interactive-pipe
3
- Version: 0.5.6
3
+ Version: 0.5.7
4
4
  Summary: Library to create flexible interactive image processing pipelines and automatically add a graphical user interface without knowing anything about GUI coding!
5
5
  Author-email: Balthazar Neveu <balthazarneveu@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/balthazarneveu/interactive_pipe
@@ -26,8 +26,8 @@ License-File: LICENSE
26
26
  | Science notebook | Toddler DIY Jukebox on a raspberry Pi |
27
27
  |:-----: |:-----:|
28
28
  | ![science notebook](/doc/images/scientific_notebook.jpg) | ![jukebox](/doc/images/jukebox.jpg)
29
- | Sliders are added automatically in your jupyter notebook. This works on Google Collab and the code takes about 40 lines of code.| Plays some music when you touch the icon. Images can be generated using the OpenAI Dall-E API helpers. Text overlay is added using OpenCV |
30
- | [Sample notebook](https://colab.research.google.com/drive/1AwHyjZH8MnzZqwsvbmxBoB15btuMIwtk?usp=sharing) | :soon: sample code |
29
+ | Sliders are added automatically in your jupyter notebook. This works on Google Collab and the code takes about 40 lines of code. No Widgets, event handlers or matplotlib knowledge required.| Plays some music when you touch the icon. Images can be generated using the OpenAI [Dall-E API helpers](https://github.com/balthazarneveu/interactive_pipe/blob/master/src/interactive_pipe/thirdparty/images_openai_api.py) . Caption added through the title mechanism. Music samples generated by prompting [MusicGen](https://huggingface.co/spaces/facebook/MusicGen) |
30
+ | [Demo notebook on collab](https://colab.research.google.com/drive/1AwHyjZH8MnzZqwsvbmxBoB15btuMIwtk?usp=sharing) | [jukebox.py demo code](/demo/jukebox.py)|
31
31
 
32
32
  ----------------------
33
33
 
@@ -55,7 +55,7 @@ License-File: LICENSE
55
55
  ----------------------
56
56
 
57
57
  ## :scroll: Features
58
- **Version 0.5.6**
58
+ **Version 0.5.7**
59
59
  - Modular multi-image processing filters
60
60
  - Declarative: Easily make graphical user interface without having to learn anything about pyQt or matplotlib
61
61
  - Support in jupyter notebooks
@@ -33,8 +33,12 @@ src/interactive_pipe/headless/control.py
33
33
  src/interactive_pipe/headless/keyboard.py
34
34
  src/interactive_pipe/headless/pipeline.py
35
35
  src/interactive_pipe/helper/__init__.py
36
+ src/interactive_pipe/helper/_private.py
37
+ src/interactive_pipe/helper/choose_backend.py
36
38
  src/interactive_pipe/helper/control_abbreviation.py
37
- src/interactive_pipe/helper/decorator.py
39
+ src/interactive_pipe/helper/filter_decorator.py
40
+ src/interactive_pipe/helper/keyword_args_analyzer.py
41
+ src/interactive_pipe/helper/pipeline_decorator.py
38
42
  src/interactive_pipe/thirdparty/images_openai_api.py
39
43
  src/interactive_pipe/thirdparty/music_spotify.py
40
44
  test/test_cache.py
@@ -1,5 +1,5 @@
1
1
  from interactive_pipe.headless.control import Control
2
- from interactive_pipe import interactive
2
+ from interactive_pipe.helper.filter_decorator import interactive
3
3
  import numpy as np
4
4
 
5
5
  @interactive(
@@ -1,3 +0,0 @@
1
- from interactive_pipe.helper.decorator import interactive, pipeline, interactive_pipeline
2
- from interactive_pipe.headless.control import Control
3
- from interactive_pipe.headless.keyboard import KeyboardControl
@@ -1,90 +0,0 @@
1
- from interactive_pipe.headless.control import Control
2
- from interactive_pipe.helper.control_abbreviation import control_from_tuple
3
- from interactive_pipe.headless.pipeline import HeadlessPipeline
4
- from interactive_pipe.graphical.gui import InteractivePipeGUI
5
- from interactive_pipe.core.graph import analyze_apply_fn_signature
6
- import functools
7
- import inspect
8
- from typing import Callable,Union
9
- import logging
10
-
11
- def interactive(**decorator_controls):
12
- """Function decorator to add some controls
13
- """
14
- def wrapper(func):
15
- controls = {}
16
- keyword_args = analyze_apply_fn_signature(func)[1]
17
- keyword_names = list(keyword_args.keys())
18
- for param_name, unknown_keyword_arg in keyword_args.items():
19
- if isinstance(unknown_keyword_arg, Control):
20
- if unknown_keyword_arg.name is None or unknown_keyword_arg._auto_named:
21
- unknown_keyword_arg.name = param_name
22
- controls[param_name] = unknown_keyword_arg
23
- else:
24
- if isinstance(unknown_keyword_arg, list) or isinstance(unknown_keyword_arg, tuple):
25
- try:
26
- controls[param_name] = control_from_tuple(unknown_keyword_arg, param_name=param_name)
27
- except Exception as exc_1:
28
- try:
29
- controls[param_name] = control_from_tuple((unknown_keyword_arg,), param_name=param_name)
30
- except Exception as exc:
31
- print(exc_1)
32
- raise Exception(exc)
33
- # NOTE: for keyword args, setting a boolean will not trigger a tickmark (although it is possible)
34
- # Use (True) instead of True if you want to make a tickbox
35
-
36
-
37
- for param_name, unknown_control in decorator_controls.items():
38
- assert param_name in keyword_names, f"typo: control {param_name} passed through the decorator does not match any of the function keyword args {keyword_names}"
39
- if isinstance(unknown_control, Control):
40
- controls[param_name] = unknown_control
41
- else: # you may get a tuple, list or a boolean
42
- controls[param_name] = control_from_tuple(unknown_control, param_name=param_name)
43
-
44
- for param_name, unknown_control in controls.items():
45
- Control.register(func.__name__, param_name, controls[param_name])
46
- @functools.wraps(func)
47
- def inner(*args, **kwargs):
48
- # Combine args and kwargs into a single dictionary
49
- merged_kwargs = {**kwargs, **controls}
50
- bound_args = inspect.signature(func).bind(*args, **merged_kwargs)
51
- bound_args.apply_defaults()
52
-
53
- for k, v in bound_args.arguments.items():
54
- if isinstance(v, Control):
55
- bound_args.arguments[k] = v.value
56
-
57
- # Call the original function with the processed arguments
58
- return func(*bound_args.args, **bound_args.kwargs)
59
- return inner
60
- return wrapper
61
-
62
-
63
- def pipeline(pipeline_function:Callable, **kwargs) -> HeadlessPipeline:
64
- return HeadlessPipeline.from_function(pipeline_function, **kwargs)
65
-
66
-
67
- def interactive_pipeline(gui=None, cache=False, output_canvas=None, **kwargs_gui) -> Union[HeadlessPipeline, InteractivePipeGUI]:
68
- """Function decorator to add some controls
69
- """
70
- def wrapper(pipeline_function):
71
- headless_pipeline = HeadlessPipeline.from_function(pipeline_function, cache=cache)
72
- if output_canvas is not None:
73
- headless_pipeline.outputs = output_canvas
74
- if gui is None:
75
- return headless_pipeline
76
- if gui == "qt":
77
- from interactive_pipe.graphical.qt_gui import InteractivePipeQT as InteractivePipeGui
78
- elif gui == "mpl":
79
- from interactive_pipe.graphical.mpl_gui import InteractivePipeMatplotlib as InteractivePipeGui
80
- elif gui == "nb":
81
- from interactive_pipe.graphical.nb_gui import InteractivePipeJupyter as InteractivePipeGui
82
- else:
83
- raise NotImplementedError(f"Gui {gui} not available")
84
- gui_pipeline = InteractivePipeGui(pipeline=headless_pipeline, name=pipeline_function.__name__, **kwargs_gui)
85
-
86
- @functools.wraps(pipeline_function)
87
- def inner(*args, **kwargs):
88
- return gui_pipeline.__call__(*args, **kwargs)
89
- return inner
90
- return wrapper