1- import zmq
1+ from time import sleep
2+
3+ import appose
4+ from appose .service import ResponseType
25import numpy as np
36
4- from cellprofiler_core .module .image_segmentation import ImageSegmentation
7+ from cellprofiler_core .module ._module import Module
58from cellprofiler_core .setting .do_something import DoSomething
6- from cellprofiler_core .setting .text import Integer
7- from cellprofiler_core .object import Objects
8-
9- HELLO = "Hello"
10- ACK = "Acknowledge"
11- DENIED = "Denied"
9+ from cellprofiler_core .setting .subscriber import ImageSubscriber
10+ from cellprofiler_core .setting .text import Text
1211
1312__doc__ = """\
1413 ApposeDemo
3130
3231cite_paper_link = "https://doi.org/10.1016/1047-3203(90)90014-M"
3332
34- class ApposeDemo (ImageSegmentation ):
33+ qt_setup = """
34+ # CRITICAL: Qt must run on main thread on macOS.
35+
36+ from qtpy.QtWidgets import QApplication
37+ from qtpy.QtCore import Qt, QTimer
38+ import threading
39+ import sys
40+
41+ # Configure Qt for macOS before any QApplication creation
42+ QApplication.setAttribute(Qt.AA_MacPluginApplication, True)
43+ QApplication.setAttribute(Qt.AA_PluginApplication, True)
44+ QApplication.setAttribute(Qt.AA_DisableSessionManager, True)
45+
46+ # Create QApplication on main thread.
47+ qt_app = QApplication(sys.argv)
48+
49+ # Prevent Qt from quitting when last Qt window closes; we want napari to stay running.
50+ qt_app.setQuitOnLastWindowClosed(False)
51+
52+ task.export(qt_app=qt_app)
53+ task.update()
54+
55+ # Run Qt event loop on main thread.
56+ qt_app.exec()
57+ """
58+
59+ qt_shutdown = """
60+ # Signal main thread to quit.
61+ qt_app.quit()
62+ """
63+
64+ napari_show = """
65+ import napari
66+ import numpy as np
67+
68+ from superqt import ensure_main_thread
69+
70+ @ensure_main_thread
71+ def show(narr):
72+ napari.imshow(narr)
73+
74+ narr = np.random.random([512, 384])
75+ show(narr)
76+ task.outputs["shape"] = narr.shape
77+ """
78+
79+ READY = False
80+
81+ class ApposeDemo (Module ):
3582 category = "Image Processing"
3683
3784 module_name = "ApposeDemo"
@@ -40,137 +87,81 @@ class ApposeDemo(ImageSegmentation):
4087
4188 def create_settings (self ):
4289 super ().create_settings ()
90+
91+ self .x_name = ImageSubscriber (
92+ "Select the input image" , doc = "Select the image you want to use."
93+ )
4394
44- self .context = None
45- self .server_socket = None
46-
47- # TODO: launch server automatically, if necessary
48- self .server_port = Integer (
49- text = "Server port number" ,
50- value = 7878 ,
51- minval = 0 ,
52- doc = """\
53- The port number which the server is listening on. The server must be launched manually first.
54- """ ,
95+ self .package_path = Text (
96+ "Path to apposednapari environment" ,
97+ "/Users/Nodar/Developer/CellProfiler/apposednapari/.pixi/envs/default" ,
5598 )
56-
57- # TODO: perform handshake automatically, if necessary
58- self .server_handshake = DoSomething (
59- "" ,
60- "Perform Server Handshake" ,
61- self .do_server_handshake ,
99+ self .doit = DoSomething (
100+ "Do the thing" ,
101+ "Do it" ,
102+ self .do_it ,
62103 doc = f"""\
63- Press this button to do an initial handshake with the server.
64- This must be done manually, once.
104+ Press this button to do the job.
65105""" ,
66106 )
67107
68108 def settings (self ):
69- return super ().settings () + [self .server_port , self .server_handshake ]
109+ return super ().settings () + [self .package_path , self .doit ]
70110
71- # ImageSegmentation defines this so we have to overide it
72111 def visible_settings (self ):
73112 return self .settings ()
74113
75- # ImageSegmentation defines this so we have to overide it
76114 def volumetric (self ):
77- return False
115+ return True
78116
79117 def run (self , workspace ):
80118 x_name = self .x_name .value
81119
82- y_name = self .y_name .value
83-
84120 images = workspace .image_set
85121
86122 x = images .get_image (x_name )
87123
88- dimensions = x .dimensions
89-
90124 x_data = x .pixel_data
91125
92- y_data = self .do_server_execute (x_data )
93-
94- y = Objects ()
95-
96- y .segmented = y_data
97-
98- y .parent_image = x .parent_image
99-
100- objects = workspace .object_set
101-
102- objects .add_objects (y , y_name )
103-
104- self .add_measurements (workspace )
105-
106126 if self .show_window :
107- workspace .display_data .x_data = x_data
108-
109- workspace .display_data .y_data = y_data
110-
111- workspace .display_data .dimensions = dimensions
112-
113- def do_server_handshake (self ):
114- port = str (self .server_port .value )
115- domain = "localhost"
116- socket_addr = f"tcp://{ domain } :{ port } "
117-
118- if self .context :
119- self .context .destroy ()
120- self .server_socket = None
121-
122- self .context = zmq .Context ()
123- self .server_socket = self .context .socket (zmq .PAIR )
124- self .server_socket .copy_threshold = 0
125- c = self .server_socket .connect (socket_addr )
126-
127- print ("Setup socket at" , socket_addr , "connected to" , c )
128-
129- self .server_socket .send_string (HELLO )
130- response = self .server_socket .recv_string ()
131-
132- if response == ACK :
133- print ("Received correct response" , response )
134- else :
135- print ("Received unexpected response" , response )
136-
137- def do_server_execute (self , im_data ):
138- dummy_data = lambda : np .array ([[]])
139-
140- socket = self .server_socket
141- header = np .lib .format .header_data_from_array_1_0 (im_data )
142-
143- socket .send_json (header )
144-
145- ack = socket .recv_string ()
146- if ack == ACK :
147- print ("header acknowledged:" , ack )
148- else :
149- print ("unexpected response" , ack )
150- return dummy_data ()
151-
152- socket .send (im_data , copy = False )
153-
154- ack = socket .recv_string ()
155- if ack == ACK :
156- print ("image data acknowledged" , ack )
157- elif ack == DENIED :
158- print ("image data denied, aborting" , ack )
159- return dummy_data ()
160- else :
161- print ("unknown response to image data" , ack )
162- return dummy_data ()
163-
164- return_header = socket .recv_json ()
165- print ("received return header" , return_header )
166-
167- print ("acknowledging header reciept" )
168- socket .send_string (ACK )
169-
170- print ("waiting for image data" )
171-
172- label_data_buf = socket .recv (copy = False )
173- labels = np .frombuffer (label_data_buf , dtype = return_header ['descr' ])
174- labels .shape = return_header ['shape' ]
175- print ("returning label data" , labels .shape )
176- return labels
127+ ...
128+
129+ def do_it (self ):
130+ env = appose .base (str (self .package_path )).build ()
131+ with env .python () as python :
132+ # Print Appose events verbosely, for debugging purposes.
133+ python .debug (print )
134+
135+ # Start the Qt application event loop in the worker process.
136+ print ("Starting Qt app event loop" )
137+ setup = python .task (qt_setup , queue = "main" )
138+ def check_ready (event ):
139+ if event .response_type == ResponseType .UPDATE :
140+ print ("Got update event! Marking Qt as ready" )
141+ global READY
142+ READY = True
143+ print ("Ready..." , READY )
144+ print ("attempting to start to listen" , flush = True )
145+ setup .listen (check_ready )
146+ print ("attempting start setup" , flush = True )
147+ setup .start ()
148+ print ("Waiting for Qt startup..." , flush = True )
149+ global READY
150+ while not READY :
151+ print ("sleeping" , READY )
152+ sleep (0.1 )
153+ print ("Qt is ready!" , flush = True )
154+
155+ # Create a test image in shared memory.
156+ ndarr = appose .NDArray (dtype = "float64" , shape = [512 , 384 ])
157+ # Fill the array with random values.
158+ # There's probably a slicker way without needing to slice/copy...
159+ ndarr .ndarray ()[:] = np .random .random (ndarr .shape )
160+
161+ # Actually do a real thing with napari: create and show an image.
162+ print ("Showing image with napari..." , flush = True )
163+ task = python .task (napari_show , inputs = {"ndarr" : ndarr })
164+
165+ task .wait_for ()
166+ shape = task .outputs ["shape" ]
167+ print (f"Task complete! Got shape: { shape } " , flush = True )
0 commit comments