Coverage for /opt/conda/envs/apienv/lib/python3.10/site-packages/daiquiri/core/components/imageviewer/__init__.py: 39%
649 statements
« prev ^ index » next coverage.py v7.6.5, created at 2024-11-15 02:12 +0000
« prev ^ index » next coverage.py v7.6.5, created at 2024-11-15 02:12 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3import json
4import time
5import os
6import pprint
7from datetime import datetime
8from typing import Any, Dict, List
9import gevent
10from marshmallow import fields
11from flask import g
12import numpy
13from PIL import Image
15from daiquiri.core import marshal, require_control
16from daiquiri.core.logging import log
17from daiquiri.core.components import (
18 Component,
19 ComponentResource,
20 actor,
21 ComponentActorKilled,
22)
23from daiquiri.core.schema import ErrorSchema, MessageSchema
24from daiquiri.core.schema.components.imageviewer import (
25 ImageSource,
26 SourceSettings,
27 MapAdditionalSchema,
28 MapSettings,
29 MoveToReferenceSchema,
30 SelectMatrixSchema,
31 ExportReferenceSchema,
32)
33from daiquiri.core.schema.metadata import paginated
34from daiquiri.core.components.dcutilsmixin import DCUtilsMixin
35from daiquiri.core.components.imageviewer.source import Source
36from daiquiri.core.components.imageviewer.annotate import AnnotateImage
37from daiquiri.core.components.imageviewer.transform import (
38 calculate_transform_matrix,
39 export_reference_to_sampleimage,
40)
42import logging
44logger = logging.getLogger(__name__)
45pp = pprint.PrettyPrinter()
47# For large mosaics need to disable pixel check
48# https://stackoverflow.com/questions/51152059/pillow-in-python-wont-let-me-open-image-exceeds-limit
49Image.MAX_IMAGE_PIXELS = None
52class SourcesResource(ComponentResource):
53 @marshal(out=[[200, paginated(ImageSource), "A list of image/video sources"]])
54 def get(self, **kwargs):
55 """Get a list of image sources defined in the current 2d viewer"""
56 return self._parent.get_sources()
59class SourcesSettingsResource(ComponentResource):
60 @marshal(out=[[200, SourceSettings(), "Source Settings"]])
61 def get(self):
62 return {
63 "has_fine": self._parent.origin_defining_source.has_fine,
64 "fine_fixed": self._parent.origin_defining_source.fine_fixed,
65 "coarse_fixed": self._parent.origin_defining_source.coarse_fixed,
66 "config": self._parent.origin_defining_source.config,
67 }
69 @marshal(
70 inp=SourceSettings,
71 out=[
72 [200, SourceSettings(), "Updated source settings"],
73 [400, ErrorSchema(), "Could not update source settings"],
74 ],
75 )
76 def patch(self, fine_fixed: bool = None, coarse_fixed: bool = None, **kwargs):
77 if fine_fixed is not None:
78 self._parent.origin_defining_source.fine_fixed = fine_fixed
79 if coarse_fixed is not None:
80 self._parent.origin_defining_source.coarse_fixed = coarse_fixed
82 return {
83 "has_fine": self._parent.origin_defining_source.has_fine,
84 "fine_fixed": self._parent.origin_defining_source.fine_fixed,
85 "coarse_fixed": self._parent.origin_defining_source.coarse_fixed,
86 "config": self._parent.origin_defining_source.config,
87 }
90class SourceImageResource(ComponentResource):
91 @marshal(
92 inp={
93 "sampleid": fields.Int(
94 metadata={"description": "The sampleid"}, required=True
95 ),
96 "subsampleid": fields.Int(
97 metadata={"description": "Optionally a subsampleid"}
98 ),
99 },
100 out=[
101 [200, MessageSchema(), "Source image created"],
102 [400, ErrorSchema(), "Could not create source image"],
103 ],
104 )
105 def post(self, **kwargs):
106 """Capture an image from a source"""
107 try:
108 ret = self._parent.save_sample_image(**kwargs)
109 if ret:
110 return {"message": "Source image created"}, 200
111 else:
112 return {"error": "Could not create source image"}, 400
114 except Exception as e:
115 logger.exception("Could not save image")
116 log.get("user").exception("Could not save source image", type="hardware")
117 return {"error": f"Could not create source image: {str(e)}"}, 400
120class GenerateMapsResource(ComponentResource):
121 @marshal(
122 inp={
123 "datacollectionid": fields.Int(
124 metadata={"description": "Optionally a datacollectionid"},
125 required=False,
126 ),
127 },
128 out=[
129 [200, MessageSchema(), "Maps generated"],
130 [400, ErrorSchema(), "Could not create maps"],
131 ],
132 )
133 def post(self, subsampleid, **kwargs):
134 """Generate a new map for a subsampleid"""
135 maps = self._parent.generate_maps(
136 subsampleid, datacollectionid=kwargs.get("datacollectionid", None)
137 )
138 if maps:
139 return {"message": "Maps created"}, 200
140 else:
141 return {"error": "Could not create maps"}, 400
144class CreateMapAdditionalResource(ComponentResource):
145 @marshal(
146 inp=MapAdditionalSchema,
147 out=[
148 [200, MessageSchema(), "Map generated"],
149 [400, ErrorSchema(), "Could not create map"],
150 ],
151 )
152 def post(self, **kwargs):
153 """Generate a new map from additional scalars"""
154 amap = self._parent.generate_additional_map(**kwargs)
155 if amap:
156 return {"message": "Map created"}, 200
157 else:
158 return {"error": "Could not create additional map"}, 400
161class MoveResource(ComponentResource):
162 @require_control
163 @marshal(
164 inp={
165 "x": fields.Float(required=True, metadata={"title": "X Position"}),
166 "y": fields.Float(required=True, metadata={"title": "Y Position"}),
167 },
168 out=[
169 [200, MessageSchema(), "Move to position ok"],
170 [400, ErrorSchema(), "Could not move to position"],
171 ],
172 )
173 def post(self, **kwargs):
174 """Move the cursor position to the current origin marking position"""
175 try:
176 self._parent.move(kwargs)
177 return {"message": "ok"}, 200
178 except Exception as e:
179 message = f"Couldn't move to position: {str(e)}"
180 log.get("user").error(message, type="hardware")
181 return {"error": message}, 400
184class MoveToResource(ComponentResource):
185 @require_control
186 @marshal(
187 out=[
188 [200, MessageSchema(), "Moved to subsample"],
189 [400, ErrorSchema(), "Could not move to subsample"],
190 ],
191 )
192 def post(self, subsampleid, **kwargs):
193 """Move the specified subsample to the current origin marking position"""
194 try:
195 self._parent.move_to(subsampleid)
196 return {"message": "ok"}, 200
197 except Exception as e:
198 message = f"Couldn't move to subsample `{subsampleid}`: {str(e)}"
199 log.get("user").error(message, type="hardware")
200 return {"error": message}, 400
203class MoveToReferenceResource(ComponentResource):
204 @require_control
205 @marshal(
206 inp={
207 "x": fields.Float(required=True, metadata={"title": "X Position"}),
208 "y": fields.Float(required=True, metadata={"title": "Y Position"}),
209 "execute": fields.Bool(),
210 },
211 out=[
212 [200, MoveToReferenceSchema(), "Moved to position"],
213 [400, ErrorSchema(), "Could not move to position"],
214 ],
215 )
216 def post(self, **kwargs):
217 """Move the cursor position to a position from a reference image"""
218 try:
219 execute = kwargs.pop("execute", True)
220 positions = self._parent.move_to_reference(kwargs, execute=execute)
221 print("positions", positions)
222 return {"moveid": f"move{time.time()}", "positions": positions}, 200
223 except Exception as e:
224 logger.exception("Couldnt move to reference position")
225 return {"error": str(e)}, 400
228class SelectReferenceMatrixResource(ComponentResource):
229 @require_control
230 @marshal(
231 inp={
232 "sampleactionid": fields.Int(
233 required=True, metadata={"title": "Sample Action"}
234 ),
235 },
236 out=[
237 [200, SelectMatrixSchema(), "Calculated transformation matrix"],
238 [400, ErrorSchema(), "Could not calculate transformation matrix"],
239 ],
240 )
241 def post(self, sampleactionid, **kwargs):
242 """Select a sampleaction and calculate transformation matrix"""
243 try:
244 self._parent.select_reference_matrix(sampleactionid)
245 return {"matrixid": f"matrix{time.time()}"}, 200
246 except Exception as e:
247 logger.exception("Couldnt select reference matrix")
248 return {"error": str(e)}, 400
251class ExportReferenceResource(ComponentResource):
252 @require_control
253 @marshal(
254 inp=ExportReferenceSchema,
255 out=[
256 [200, MessageSchema(), "Exported reference to sample image"],
257 [400, ErrorSchema(), "Could not export reference to sample image"],
258 ],
259 )
260 def post(self, sampleactionid, crop=None):
261 """Export a reference image to a sample image
263 Transforms the image into the current 2dview coordinate space
264 using the selected transformation matrix
265 """
266 try:
267 self._parent.export_reference_to_sampleimage(sampleactionid, crop=crop)
268 return {"message": "Exported reference to sample image"}, 200
269 except Exception as e:
270 logger.exception("Could not export reference to sample image")
271 return {"error": str(e)}, 400
274class MosaicResource(ComponentResource):
275 @require_control
276 @actor("mosaic", enqueue=False, preprocess=True)
277 def post(self, **kwargs):
278 """Create a tiled mosaic actor"""
279 pass
281 def preprocess(self, **kwargs):
282 kwargs["absol"] = self._parent.get_absolute_fp(
283 {"x": kwargs["x1"], "y": kwargs["y1"]},
284 {"x": kwargs["x2"], "y": kwargs["y2"]},
285 )
287 sample = self._metadata.get_samples(sampleid=kwargs["sampleid"])
288 if not sample:
289 raise AttributeError(f"No such sample {kwargs['sampleid']}")
291 sessionid = g.blsession.get("sessionid")
293 def save_image(x, y):
294 return self._parent.save_image(
295 sessionid=sessionid,
296 sampleid=kwargs["sampleid"],
297 file_prefix=f"mosaic_{x}_{y}_",
298 )
300 kwargs["sessionid"] = sessionid
301 kwargs["save"] = save_image
302 kwargs["camera"] = self._parent.origin_defining_source.device
304 return kwargs
307class UploadImage(ComponentResource):
308 @marshal(
309 inp={
310 "image": fields.Str(
311 required=True, metadata={"description": "Base64 encoded image"}
312 ),
313 "sampleid": fields.Int(
314 required=True, metadata={"description": "Sample this image belongs to"}
315 ),
316 },
317 out=[
318 [200, MessageSchema(), "Image uploaded"],
319 [400, ErrorSchema(), "Could not upload image"],
320 ],
321 )
322 def post(self, **kwargs):
323 """Upload an image and send it to an actor"""
324 success = self._parent.upload_image(**kwargs)
325 if success:
326 return {"message": "Image uploaded"}, 200
327 else:
328 return {"error": "Could not upload image"}, 400
331class AutoFocusImageResource(ComponentResource):
332 @require_control
333 @actor("autofocus", enqueue=False, preprocess=True)
334 def post(self, **kwargs):
335 """Autofocus the sample image"""
336 pass
338 def preprocess(self, **kwargs):
339 kwargs["camera"] = self._parent.origin_defining_source.device
340 kwargs["z_increment"] = self._parent.origin_defining_source._get_from_config(
341 "motor_z_autofocus_increment"
342 )
343 kwargs["z_iterations"] = self._parent.origin_defining_source._get_from_config(
344 "motor_z_autofocus_iterations"
345 )
346 kwargs["z_motor"] = self._parent.origin_defining_source._get_hwobj_from_config(
347 "motor_z"
348 )
350 return kwargs
353class ExportSubSamplesResource(ComponentResource):
354 @marshal(
355 inp={
356 "subsampleids": fields.List(
357 fields.Int(),
358 required=True,
359 metadata={"description": "A list of subsamples to export"},
360 )
361 },
362 out=[
363 [200, MessageSchema(), "Subsamples exported"],
364 [400, ErrorSchema(), "Could not export subsamples"],
365 ],
366 )
367 def post(self, **kwargs):
368 """Export the selected subsamples to json"""
369 try:
370 dirname = self._parent.export_subsamples(kwargs["subsampleids"])
371 message = f"Sub samples exported to '{dirname}'"
372 log.get("user").info(message, type="actor")
373 return {"message": message}, 200
374 except Exception as e:
375 return {"error": f"Could not export subsamples: {str(e)}"}, 400
378class MapSettingsResource(ComponentResource):
379 @marshal(out=[[200, MapSettings(), "Map Settings"]])
380 def get(self):
381 return {
382 "during_scan": self._parent._generate_during_scan,
383 "scalar_maps": self._parent._scalar_maps,
384 }
386 @marshal(
387 inp=MapSettings,
388 out=[
389 [200, MapSettings(), "Updated map settings"],
390 [400, ErrorSchema(), "Could not update map settings"],
391 ],
392 )
393 def patch(self, **kwargs):
394 if kwargs.get("during_scan") is not None:
395 self._parent._generate_during_scan = kwargs["during_scan"]
397 if kwargs.get("scalar_maps") is not None:
398 self._parent._scalar_maps = kwargs["scalar_maps"]
400 return {
401 "during_scan": self._parent._generate_during_scan,
402 "scalar_maps": self._parent._scalar_maps,
403 }, 200
406class ReferenceImageResource(ComponentResource):
407 @require_control
408 @actor("reference", enqueue=False, preprocess=True)
409 def post(self, **kwargs):
410 """Import a reference image of the sample"""
411 pass
413 def preprocess(self, **kwargs):
414 if "sampleid" not in kwargs:
415 raise AttributeError("No sample provided")
417 sample = self._metadata.get_samples(sampleid=kwargs["sampleid"])
418 if not sample:
419 raise AttributeError(f"No such sample {kwargs['sampleid']}")
421 kwargs["sessionid"] = g.blsession.get("sessionid")
422 return kwargs
425class Imageviewer(Component, DCUtilsMixin):
426 _actors = [
427 "createmap",
428 "mosaic",
429 "move",
430 "upload_canvas",
431 "autofocus",
432 "export",
433 "reference",
434 ]
435 _config_export = ["options", "scantypes", "upload_canvas"]
437 def setup(self):
438 self._scan_actors = []
439 self._map_actors = []
440 self._in_generate = False
441 self._generate_during_scan = self._config.get("generate_maps_during_scan", True)
442 self._scalar_maps = self._config.get("automatic_scalar_maps", [])
444 self.register_route(SourcesResource, "/sources")
445 self.register_route(SourcesSettingsResource, "/sources/origin")
446 self.register_route(SourceImageResource, "/sources/image")
447 self.register_route(MapSettingsResource, "/maps")
448 self.register_route(GenerateMapsResource, "/maps/generate/<int:subsampleid>")
449 self.register_route(CreateMapAdditionalResource, "/maps/additional")
450 self.register_route(MoveResource, "/move")
451 self.register_route(MoveToReferenceResource, "/move/reference")
452 self.register_route(SelectReferenceMatrixResource, "/move/reference/matrix")
453 self.register_route(MoveToResource, "/move/<int:subsampleid>")
454 self.register_route(UploadImage, "/image/<int:sampleid>")
455 self.register_route(ExportSubSamplesResource, "/export")
456 self.register_actor_route(MosaicResource, "/mosaic")
457 self.register_actor_route(ReferenceImageResource, "/reference")
458 self.register_actor_route(ExportReferenceResource, "/reference/export")
459 self.register_actor_route(AutoFocusImageResource, "/sources/autofocus")
460 self._generate_scan_actors()
461 self._sources: List[Source] = []
462 for i, src in enumerate(self._config["sources"]):
463 self._sources.append(
464 Source(
465 src,
466 i + 1,
467 self._hardware,
468 self.emit,
469 config_file=self._config.resource,
470 )
471 )
472 self._create_maps = self._config.get("createmaps", {})
473 self._check_running_actors = True
474 self._check_actor_thread = gevent.spawn(self.check_running_actors)
476 self._reference_matrix = None
477 self._reference_inverse_matrix = None
479 def reload(self):
480 self._generate_scan_actors()
481 for i, src in enumerate(self._config["sources"]):
482 if i < len(self._sources):
483 self._sources[i].update_config(src)
484 else:
485 self._sources.append(
486 Source(
487 src,
488 i + 1,
489 self._hardware,
490 self.emit,
491 config_file=self._config.resource,
492 )
493 )
495 def _generate_scan_actors(self):
496 """Dynamically generate scan actor resources"""
498 def post(self, **kwargs):
499 pass
501 def preprocess(self, **kwargs):
502 subsample = self._parent._metadata.get_subsamples(kwargs["subsampleid"])
503 if not subsample:
504 raise AttributeError(f"No such subsample {kwargs['subsampleid']}")
505 kwargs["sampleid"] = subsample["sampleid"]
506 kwargs["sample"] = subsample["sample"]
507 sample = self._parent._metadata.get_samples(kwargs["sampleid"])
508 kwargs["extrametadata"] = {
509 **(sample["extrametadata"] if sample["extrametadata"] else {}),
510 "subsample": subsample["extrametadata"],
511 }
512 kwargs["sessionid"] = g.blsession.get("sessionid")
513 # Check whether position can be reached before marking the subsample queued
514 # as this can raise
515 absol = self._parent.get_absolute(kwargs["subsampleid"])
516 (
517 kwargs["containerqueuesampleid"],
518 kwargs["datacollectionplanid"],
519 ) = self._parent._metadata.queue_subsample(
520 kwargs["subsampleid"], scanparameters=kwargs
521 )
522 kwargs["absol"] = absol
523 kwargs["before_scan_starts"] = self._parent.before_scan_starts
524 kwargs["update_datacollection"] = self._parent.update_datacollection
525 kwargs["next_datacollection"] = self._parent.next_datacollection
526 kwargs["generate_maps"] = self._parent.generate_maps
527 kwargs["open_attachment"] = self._parent._open_dc_attachment
528 kwargs["add_scanqualityindicators"] = self._parent.add_scanqualityindicators
529 kwargs["scans"] = self._parent.get_component("scans")
530 kwargs["beamsize"] = self._parent.beamsize
532 def get_rois():
533 return {
534 "rois": self._metadata.get_xrf_map_rois(
535 sampleid=kwargs["sampleid"],
536 no_context=True,
537 )["rows"],
538 "conversion": self._parent.get_component("scans")._config["mca"][
539 "conversion"
540 ],
541 }
543 kwargs["get_rois"] = get_rois
545 kwargs["enqueue"] = kwargs.get("enqueue", True)
546 return kwargs
548 for key, scans in self._config.get("scantypes", {}).items():
549 for scanname in scans:
550 if scanname in self._actors:
551 continue
553 self._actors.append(scanname)
554 self._scan_actors.append(scanname)
555 act_res = type(
556 scanname,
557 (ComponentResource,),
558 {
559 "post": require_control(actor(scanname, preprocess=True)(post)),
560 "preprocess": preprocess,
561 },
562 )
563 self.register_actor_route(act_res, f"/scan/{scanname}")
565 def before_scan_starts(self, actor, save_image=True):
566 """Saving directory is created"""
567 if actor.get("datacollectionid"):
568 self._save_dc_params(actor)
569 if save_image:
570 self._save_dc_image(actor)
572 def _save_dc_image(self, actor):
573 """Save an image for a datacollection
575 Includes origin and scalebar, and subsample location.
576 Should ideally be called once the subsample has been moved to the
577 origin location
578 """
579 try:
580 details = self.save_image(
581 sessionid=actor["sessionid"],
582 sampleid=actor["sampleid"],
583 subsampleid=actor["subsampleid"],
584 savemeta=False,
585 annotate=True,
586 file_prefix="snapshot1_",
587 )
589 extra = {}
590 if details.get("subsample"):
591 subsample = details["subsample"]
592 extra["pixelspermicronx"] = details["scale"]["x"] * 1e-9 / 1e-6
593 extra["pixelspermicrony"] = details["scale"]["y"] * 1e-9 / 1e-6
594 extra["snapshot_offsetxpixel"] = subsample["x"]
595 extra["snapshot_offsetypixel"] = subsample["y"]
597 self.update_datacollection(
598 actor, xtalsnapshotfullpath1=details["path"], **extra
599 )
600 except Exception:
601 logger.exception("Could not save image")
602 log.get("user").exception(
603 "Could not save data collection image", type="actor"
604 )
606 def save_sample_image(self, **kwargs):
607 """Saves a sample image
609 Sets up file saving, and saves an image from the source image
611 Kwargs:
612 sampleid (int): Sample id
613 Returns:
614 path (str): Path to the new image
615 """
616 self._saving.set_filename(
617 set_metadata=False,
618 extra_saving_args=self._config.get(
619 "sample_image_saving", {"data_filename": "{sampleid.name}_image{time}"}
620 ),
621 **{
622 "sampleid": kwargs["sampleid"],
623 "sessionid": g.blsession.get("sessionid"),
624 "time": int(time.time()),
625 },
626 )
627 self._saving.create_root_path()
628 return self.save_image(
629 sampleid=kwargs.get("sampleid", None),
630 subsampleid=kwargs.get("subsampleid", None),
631 sessionid=g.blsession.get("sessionid"),
632 file_prefix="sampleimage_",
633 )
635 def upload_image(self, **kwargs):
636 """Send an image to an actor
638 Kwargs:
639 sampleid (int): The associated sampleid
640 image (str): The base64 encoded image
641 """
642 sample = self._metadata.get_samples(sampleid=kwargs["sampleid"])
643 self.actor(
644 "upload_canvas",
645 error=self._upload_failed,
646 spawn=True,
647 actargs={"image": kwargs["image"], "sample": sample},
648 )
650 return True
652 def _upload_failed(self, actid, exception, actor):
653 logger.error(
654 f"Could not upload image for {actor['sampleid']} exception was {exception}"
655 )
656 log.get("user").exception(
657 f"Could not upload image for {actor['sampleid']} exception was {exception}",
658 type="queue",
659 )
661 def actor_started(self, actid, actor):
662 """Callback when an actor starts
664 For scan actors this will generate a datacollection
665 """
666 if actor.name in self._scan_actors:
667 self.start_datacollection(actor)
669 if actor.name in ["mosaic", "reference"]:
670 args = {
671 "sessionid": actor["sessionid"],
672 "sampleid": actor["sampleid"],
673 "starttime": datetime.now(),
674 "actiontype": actor.name,
675 }
676 sampleaction = self._metadata.add_sampleaction(**args, no_context=True)
677 actor.update(sampleactionid=sampleaction["sampleactionid"])
679 self._saving.set_filename(
680 extra_saving_args=actor.saving_args, **actor.all_data
681 )
682 self._saving.create_root_path(wait_exists=True)
683 actor.update(base_path=self._saving.dirname)
685 logger.info(f"Actor '{actor.name}' with id '{actid}' started")
687 def actor_success(self, actid, response, actor):
688 """Callback when an actor finishes successfully
690 For scan actors this update the datacollection with the endtime and
691 'success' status
693 For ROI scans it will launch map generation
694 """
695 if actor.name in self._scan_actors:
696 self.update_datacollection(
697 actor, endtime=datetime.now(), runstatus="Successful", emit_end=True
698 )
699 self._save_dc_log(actor)
701 if actor.name in self._create_maps:
702 self.generate_maps(actor["subsampleid"], actor["datacollectionid"])
704 if actor.name in ["mosaic", "reference"]:
705 snapshot = {}
706 if actor.get("full"):
707 snapshot["xtalsnapshotafter"] = self.save_full_mosaic(actor["full"])
709 self._metadata.update_sampleaction(
710 sampleactionid=actor["sampleactionid"],
711 no_context=True,
712 endtimestamp=datetime.now(),
713 status="success",
714 **snapshot,
715 )
717 logger.info(f"Actor '{actor.name}' with id '{actid}' finished")
719 def save_full_mosaic(self, image):
720 """Saves the full mosaic image
722 Also creates a thumbnail
724 Args:
725 image (PIL.Image): The image to save
727 Returns
728 path (str): Path to the newly saved image
729 """
730 directory = self._saving.dirname
731 if self._config.get("image_subdirectory"):
732 directory = os.path.join(directory, self._config.get("image_subdirectory"))
733 if not os.path.exists(directory):
734 os.makedirs(directory)
736 filename = os.extsep.join([f"mosaic_full_{time.time()}", "png"])
737 path = os.path.join(directory, filename)
738 image.save(path)
739 self._generate_thumb(path)
740 return path
742 def actor_error(self, actid, exception, actor):
743 """Callback when an actor fails
745 For scan actors this will update the datacollection with the end time and
746 'failed' status
747 """
748 status = "Aborted" if isinstance(exception, ComponentActorKilled) else "Failed"
749 if actor.name in self._scan_actors:
750 self.update_datacollection(actor, endtime=datetime.now(), runstatus=status)
751 self._save_dc_log(actor)
752 if status == "Failed":
753 self._save_dc_exception(actor, exception)
754 if actor.name in ["mosaic", "reference"]:
755 self._metadata.update_sampleaction(
756 sampleactionid=actor["sampleactionid"],
757 no_context=True,
758 endtimestamp=datetime.now(),
759 status="error",
760 message=str(exception),
761 )
762 if status == "Failed":
763 logger.error(f"Actor '{actor.name}' with id '{actid}' failed")
764 else:
765 logger.info(f"Actor '{actor.name}' with id '{actid}' aborted")
767 def actor_remove(self, actid, actor):
768 """Callback when an actor is removed from the queue
770 For scan actors this will remove the item from the database queue
771 """
772 if actor.name in self._scan_actors:
773 self._metadata.unqueue_subsample(
774 actor["subsampleid"],
775 containerqueuesampleid=actor["containerqueuesampleid"],
776 no_context=True,
777 )
779 def check_running_actors(self):
780 """Periodicically check for any running actors
782 This is used to trigger automated downstream procesing, i.e.
783 map generation so that long scans can be followed
784 """
785 logger.debug("Starting periodic actor checker")
786 while self._check_running_actors:
787 if self._generate_during_scan:
788 running_copy = self._running_actors.copy()
789 for actid, actall in running_copy.items():
790 actor = actall[0]
791 if actor.name in self._create_maps:
792 if (
793 actor.get("subsampleid")
794 and actor.get("datacollectionid")
795 and actor.get("datacollectionnumber")
796 ):
797 logger.debug(
798 f"Re/generating maps for {actor.name} dcid:{actor['datacollectionid']}"
799 )
800 self.generate_maps(
801 actor["subsampleid"],
802 actor["datacollectionid"],
803 auto=True,
804 )
805 try:
806 time.sleep(self._config["regenerate_interval"])
807 except KeyError:
808 time.sleep(60)
810 def generate_maps(self, subsampleid, datacollectionid=None, auto=False):
811 """Launch a series of actors to generate maps for each of the MCA ROIs"""
812 self._in_generate = True
814 dcs = self._metadata.get_datacollections(
815 datacollectionid, subsampleid=subsampleid, no_context=True, ungroup=True
816 )
817 scans = self.get_component("scans")
819 if datacollectionid:
820 dcs = [dcs]
821 else:
822 dcs = dcs["rows"]
824 existing = self._metadata.get_xrf_maps(
825 subsampleid=subsampleid, no_context=True
826 )["rows"]
828 if not auto:
829 self.emit(
830 "message",
831 {"type": "generate_maps", "status": "started"},
832 )
833 log.get("user").info(
834 "Starting map generation",
835 type="actor",
836 )
838 count = 0
839 for dc in dcs:
840 running = False
841 for actid in self._map_actors:
842 actall = self._running_actors.get(actid)
843 if not actall:
844 continue
846 actor = actall[0]
848 if (
849 actor["subsampleid"] == subsampleid
850 and actor["datacollectionid"] == dc["datacollectionid"]
851 ):
852 running = True
853 break
855 if running:
856 logger.info(
857 f"Generate map actor already running for subsample {subsampleid} datacollection {dc['datacollectionid']}"
858 )
859 continue
861 rois = self._metadata.get_xrf_map_rois(
862 sampleid=dc["sampleid"], no_context=True
863 )["rows"]
864 spectra = scans.get_scan_spectra(dc["datacollectionnumber"], allpoints=True)
865 scalars = scans.get_scan_data(
866 dc["datacollectionnumber"], per_page=1e10, all_scalars=True
867 )
868 if spectra and scalars:
869 self._map_actors.append(
870 self.actor(
871 "createmap",
872 spawn=True,
873 success=self._append_map,
874 error=self._map_failed,
875 actargs={
876 "datacollectionid": dc["datacollectionid"],
877 "datacollectionnumber": dc["datacollectionnumber"],
878 "subsampleid": subsampleid,
879 "rois": rois,
880 "spectra": spectra,
881 "scalars": scalars,
882 },
883 )
884 )
885 count += 1
886 else:
887 logger.warning(
888 f"Generate Map: Cant get spectra for scan {dc['datacollectionnumber']} (datacollectionid: {dc['datacollectionid']})"
889 )
890 log.get("user").warning(
891 f"Cant get spectra for scan {dc['datacollectionnumber']} (datacollectionid: {dc['datacollectionid']})",
892 type="queue",
893 )
895 # Update scalar maps
896 if scalars:
897 for scalar_name in self._scalar_maps:
898 for existing_map in existing:
899 if (
900 existing_map["scalar"] == scalar_name
901 and existing_map["datacollectionid"]
902 == dc["datacollectionid"]
903 ):
904 break
905 else:
906 self.generate_additional_map(
907 datacollectionid=dc["datacollectionid"],
908 scalars=[scalar_name],
909 no_context=True,
910 )
912 for m in existing:
913 if m["scalar"] and m["datacollectionid"] == dc["datacollectionid"]:
914 new_data = self._get_additional_map(
915 m["scalar"], scalars=scalars
916 )
917 if new_data:
918 points = len(new_data) - new_data.count(-1)
919 self._metadata.update_xrf_map(
920 mapid=m["mapid"],
921 data=new_data,
922 points=points,
923 no_context=True,
924 )
926 self._in_generate = False
928 if count == 0 and not auto:
929 self.emit(
930 "message",
931 {
932 "type": "generate_maps",
933 "status": "warning",
934 "message": "No maps to generate, check the log",
935 },
936 )
937 log.get("user").info(
938 "No maps to generation",
939 type="actor",
940 )
942 return True
944 def _map_failed(self, actid, exception, actor):
945 logger.error(
946 f"Could not generate map for scan {actor['datacollectionnumber']} (datacollectionid: {actor['datacollectionid']}), exception was {exception}"
947 )
948 log.get("user").exception(
949 f"Could not generate map for scan {actor['datacollectionnumber']} (datacollectionid: {actor['datacollectionid']})",
950 type="queue",
951 )
952 self._update_map_actor_status(actid)
954 def _append_map(self, actid, maps, actor):
955 """Add new map to the maplist
957 Will try to updating an existing map if it matched dcid and maproiid
958 """
959 dc = self._metadata.get_datacollections(
960 actor["datacollectionid"], subsampleid=actor["subsampleid"], no_context=True
961 )
963 existing = self._metadata.get_xrf_maps(
964 subsampleid=dc["subsampleid"], no_context=True
965 )["rows"]
967 if not maps:
968 logger.info("No maps generated to append")
969 return
971 # Create / update ROI maps
972 for m in maps[0]["maps"]:
973 exists = False
974 for ex in existing:
975 if (
976 dc["datacollectionid"] == ex["datacollectionid"]
977 and m["maproiid"] == ex["maproiid"]
978 ):
979 mapid = ex["mapid"]
980 points = len(m["data"]) - m["data"].count(-1)
981 self._metadata.update_xrf_map(
982 mapid=ex["mapid"],
983 data=m["data"],
984 points=points,
985 no_context=True,
986 )
987 exists = True
988 break
990 if not exists:
991 newmap = self._metadata.add_xrf_map(
992 maproiid=m["maproiid"],
993 datacollectionid=dc["datacollectionid"],
994 data=m["data"],
995 no_context=True,
996 )
997 if not newmap:
998 continue
1000 mapid = newmap["mapid"]
1002 self.emit(
1003 "message",
1004 {
1005 "type": "map",
1006 "mapid": mapid,
1007 "sampleid": dc["sampleid"],
1008 "subsampleid": dc["subsampleid"],
1009 },
1010 )
1012 self._update_map_actor_status(actid)
1014 def _update_map_actor_status(self, actid):
1015 self._map_actors.remove(actid)
1017 if self._in_generate:
1018 return
1020 if len(self._map_actors) == 0:
1021 self.emit(
1022 "message",
1023 {"type": "generate_maps", "status": "finished"},
1024 )
1026 log.get("user").info(
1027 "Map generation complete",
1028 type="actor",
1029 )
1030 else:
1031 self.emit(
1032 "message",
1033 {
1034 "type": "generate_maps",
1035 "status": "progress",
1036 "remaining": len(self._map_actors),
1037 },
1038 )
1040 def _get_additional_map(self, scalar, scalars):
1041 """Get data for an additional map
1043 Args:
1044 scalar (str): The key for scalar data to use
1045 scalars (dict): Dict of scan data scalars
1047 Returns:
1048 data (ndarray): The map data
1049 """
1050 if scalar in scalars["data"]:
1051 data = scalars["data"][scalar]["data"]
1053 if not data:
1054 logger.warning(f"Scalar {scalar} data length is zero")
1055 return
1057 if len(data) < scalars["npoints"]:
1058 missing = scalars["npoints"] - len(data)
1059 data.extend([-1 for x in range(missing)])
1061 return data
1063 else:
1064 logger.warning(f"Cannot find scalar {scalar} in scan data")
1066 def generate_additional_map(self, **kwargs):
1067 """Generate additional maps based on scan scalars"""
1068 dc = self._metadata.get_datacollections(
1069 datacollectionid=kwargs["datacollectionid"],
1070 no_context=kwargs.get("no_context"),
1071 )
1073 if not dc:
1074 return
1076 scans = self.get_component("scans")
1077 scalars = scans.get_scan_data(
1078 scanid=dc["datacollectionnumber"], per_page=1e10, scalars=kwargs["scalars"]
1079 )
1081 if not scalars:
1082 logger.warning(f"Scan id {dc['datacollectionnumber']} is not available")
1083 return
1085 for scalar in kwargs["scalars"]:
1086 data = self._get_additional_map(scalar=scalar, scalars=scalars)
1087 if data:
1088 roi = self._metadata.add_xrf_map_roi_scalar(scalar=scalar)
1089 self._metadata.add_xrf_map(
1090 maproiid=roi["maproiid"],
1091 datacollectionid=dc["datacollectionid"],
1092 data=data,
1093 no_context=True,
1094 )
1096 return True
1098 def get_sources(self):
1099 """Return list of image sources"""
1100 sources = [src.info() for src in self._sources]
1101 return {"total": len(sources), "rows": sources}
1103 def move(self, args):
1104 """Move the source image"""
1105 absol = self.get_absolute_fp(args)
1106 self.actor("move", spawn=True, actargs={"absol": absol})
1107 return True
1109 def _get_matched_positions(self, sampleactionid: int):
1110 positions = self._metadata.get_sampleaction_positions(
1111 sampleactionid=sampleactionid
1112 )
1114 refs = {}
1115 reals = {}
1116 for position in positions["rows"]:
1117 if position["type"] == "reference":
1118 refs[position["id"]] = position
1119 if position["type"] == "real":
1120 reals[position["id"]] = position
1122 ref_keys = set(refs.keys())
1123 real_keys = set(reals.keys())
1124 if ref_keys != real_keys:
1125 raise RuntimeError(
1126 f"Real and Reference positions do not match: missing refs: {list(real_keys - ref_keys)}, missing reals: {list(ref_keys - real_keys)}"
1127 )
1129 # Stolen from:
1130 # https://gitlab.esrf.fr/id16/LineControl/-/blob/master/src/linecontrol/widget/cool/FluoSampleRegistration.py#L263
1131 nbref = len(ref_keys)
1132 pfrom = numpy.ones((nbref, 3), dtype=float)
1133 pto = numpy.ones((nbref, 3), dtype=float)
1134 for i, position_id in enumerate(refs.keys()):
1135 pfrom[i, 0:2] = (refs[position_id]["posx"], refs[position_id]["posy"])
1136 pto[i, 0:2] = (
1137 reals[position_id]["posx"],
1138 reals[position_id]["posy"],
1139 )
1141 return pfrom, pto
1143 def select_reference_matrix(self, sampleactionid):
1144 """Select a sampleaction (reference image) to calculate the transfomation matrix from"""
1145 pfrom, pto = self._get_matched_positions(sampleactionid)
1146 self._reference_matrix = calculate_transform_matrix(pfrom, pto)
1148 try:
1149 self._reference_inverse_matrix = numpy.linalg.inv(self._reference_matrix)
1150 except Exception:
1151 raise RuntimeError("Could not calculate inverse matrix")
1153 with numpy.printoptions(precision=3, suppress=True):
1154 log.get("user").info(
1155 f"Transformation matrix calculated:\n{self._reference_matrix}",
1156 type="actor",
1157 )
1159 src = self.origin_defining_source
1160 src.set_reference_inverse_matrix(self._reference_inverse_matrix)
1162 def move_to_reference(self, pos, execute):
1163 """Move the source image to a reference position"""
1164 if not isinstance(self._reference_matrix, numpy.ndarray):
1165 raise RuntimeError("No reference matrix computed")
1167 transformed_pos = numpy.dot(self._reference_matrix, (pos["x"], pos["y"], 1))
1168 absol = self.get_absolute_fp({"x": transformed_pos[0], "y": transformed_pos[1]})
1169 if execute:
1170 self.actor("move", spawn=True, actargs={"absol": absol})
1172 positions = {}
1173 for motor_id, motor in absol["fixed"].items():
1174 positions[motor_id] = {
1175 "motor": motor["motor"].__repr__(),
1176 "destination": round(motor["destination"], 3),
1177 "unit": motor["unit"],
1178 }
1180 if not execute:
1181 log.get("user").info(
1182 f"""Move to:\n{positions['x']['motor']}: {positions['x']['destination']}\n{positions['y']['motor']}: {positions['y']['destination']}""",
1183 type="hardware",
1184 )
1186 return positions
1188 def export_reference_to_sampleimage(self, sampleactionid, crop=None) -> int:
1189 sampleaction = self._metadata.get_sampleactions(sampleactionid=sampleactionid)
1190 refs, reals = self._get_matched_positions(sampleactionid)
1192 export = export_reference_to_sampleimage(
1193 original_path=sampleaction["xtalsnapshotbefore"],
1194 snapshot_path=sampleaction["xtalsnapshotafter"],
1195 reference_points=refs,
1196 vlm_points=reals,
1197 )
1199 sampleimage = self._metadata.add_sampleimage(
1200 no_context=True,
1201 sampleid=sampleaction["sampleid"],
1202 offsetx=int(export.center_x),
1203 # `add_sampleimage` expects offsety to be negative
1204 offsety=int(-1 * export.center_y),
1205 scalex=float(export.scale_factor) * 1e3,
1206 # Because we inverted offsety, invert scaley to flip back
1207 scaley=float(-1 * export.scale_factor) * 1e3,
1208 file=export.image_path,
1209 )
1211 return sampleimage["sampleimageid"]
1213 def _add_exif(self, image_filename: str, metadata: Dict[str, Any]) -> None:
1214 """Write pixel size and offset to exif"""
1215 metadata_string = json.dumps(metadata, indent=2)
1217 img = Image.open(image_filename)
1218 exif = img.getexif()
1219 # TODO: This is Maker, could not get MakerNote to work
1220 # https://github.com/python-pillow/Pillow/blob/main/src/PIL/ExifTags.py#L155
1221 exif.update([(271, metadata_string)])
1222 img.save(image_filename, exif=exif)
1224 def save_image(
1225 self,
1226 sessionid=None,
1227 sampleid=None,
1228 subsampleid=None,
1229 savemeta=True,
1230 annotate=False,
1231 file_prefix="",
1232 ):
1233 """Save an image from the source device, like objects, images are marked
1234 relative to the origin marking position
1236 Args:
1237 sessionid (int): The session id
1238 sampleid (int): The sample id
1239 subsampleid (int): Optionally a subsample id
1240 savemeta (bool): Whether to save this as a sample image
1241 annotate (bool): Whether to annotate this image (origin, scalebar, etc)
1243 Returns:
1244 path (str): The path of the saved image
1245 """
1246 directory = self._saving.dirname
1247 for src in self._sources:
1248 if not src.origin:
1249 continue
1251 filename = os.extsep.join([f"{file_prefix}{time.time()}", "png"])
1253 if self._config.get("image_subdirectory"):
1254 directory = os.path.join(
1255 directory, self._config.get("image_subdirectory")
1256 )
1257 if not os.path.exists(directory):
1258 os.makedirs(directory)
1260 path = os.path.join(directory, filename)
1262 if not src.device.online():
1263 raise RuntimeError("Cannot save image, camera is offline")
1265 src.device.call("save", path)
1267 image_info = src.canvas.vlm_image_info
1268 beam = src.canvas.beam_info
1270 subsample = None
1271 if annotate:
1272 if subsampleid:
1273 subsample = self._metadata.get_subsamples(
1274 subsampleid=subsampleid, no_context=True
1275 )
1276 ann = AnnotateImage(path)
1277 details = ann.annotate(
1278 image_info["center"],
1279 beam["position"],
1280 image_info["pixelsize"],
1281 src.unit,
1282 subsample,
1283 )
1284 subsample = details["subsample"]
1286 if sampleid and savemeta:
1287 self._metadata.add_sampleimage(
1288 no_context=True,
1289 sampleid=sampleid,
1290 offsetx=int(image_info["center"][0]),
1291 offsety=int(image_info["center"][1]),
1292 scalex=float(image_info["pixelsize"][0]),
1293 scaley=float(image_info["pixelsize"][1]),
1294 file=path,
1295 positions=src.get_additional(),
1296 )
1297 self._add_exif(
1298 path,
1299 {
1300 "mppx": float(image_info["pixelsize"][0]),
1301 "mppy": float(image_info["pixelsize"][1]),
1302 "offsetx": int(image_info["center"][0]),
1303 "offsety": int(image_info["center"][1]),
1304 },
1305 )
1307 self._generate_thumb(path)
1308 return {
1309 "path": path,
1310 "subsample": subsample,
1311 "scale": {
1312 "x": float(image_info["pixelsize"][0]),
1313 "y": float(image_info["pixelsize"][1]),
1314 },
1315 }
1317 def move_to(self, subsampleid):
1318 """Move to a specific subsample
1320 Args:
1321 subsampleid (int): The subsample id
1323 Returns:
1324 success (bool): Whether the move was successful
1325 """
1326 absol = self.get_absolute(subsampleid)
1327 self.actor("move", spawn=True, actargs={"absol": absol})
1328 return True
1330 @property
1331 def origin_defining_source(self) -> Source:
1332 for src in self._sources:
1333 if src.origin is True:
1334 return src
1336 @property
1337 def beamsize(self):
1338 src = self.origin_defining_source
1339 if src:
1340 return src.beamsize
1342 def get_absolute_fp(self, pos, pos2=None):
1343 """Return absolute motor positions to bring a position to the centre of view
1345 Args:
1346 pos (dict): Dictionary containing 'x' and 'y' positions
1348 Returns:
1349 absolute (dict): Absolute positions and their associated motors
1350 """
1351 src = self.origin_defining_source
1352 if src is None:
1353 return None
1355 if pos2:
1356 absol = src.canvas.canvas_to_motor(
1357 [[pos["x"], -pos["y"]], [pos2["x"], -pos2["y"]]]
1358 )
1359 else:
1360 absol = src.canvas.canvas_to_motor([pos["x"], -pos["y"]])
1362 absol["axes"] = self.find_axes_from_variable(absol["variable"])
1363 absol["move_to"] = self.move_to_absol
1365 # print("------ get_absolute_fp ------")
1366 # pp.pprint(pos)
1367 # pp.pprint(absol)
1369 return absol
1371 def get_absolute(self, subsampleid):
1372 """Return absolute motor positions to bring a subsample id to the origin marking
1374 Args:
1375 subsampleid (int): The subsample to get the position of
1377 Returns:
1378 absolute (dict): A dictionary of the absolute positions for the subsample
1379 """
1380 src = self.origin_defining_source
1381 if src is None:
1382 return None
1384 obj = self._metadata.get_subsamples(subsampleid)
1385 if obj is None:
1386 return
1388 if obj["type"] == "loi" or obj["type"] == "roi":
1389 pos = [[obj["x"], -obj["y"]], [obj["x2"], -obj["y2"]]]
1390 else:
1391 pos = [obj["x"], -obj["y"]]
1392 absol = src.canvas.canvas_to_motor(pos)
1393 # src.canvas.sampleposition=....
1395 # print("------ get_absolute ------")
1396 # pp.pprint(obj)
1397 # pp.pprint(absol)
1399 if "z" in absol["fixed"]:
1400 del absol["fixed"]["z"]
1402 absol["axes"] = self.find_axes_from_variable(absol["variable"])
1403 absol["move_to_additional"] = src.move_to_additional
1404 absol["positions"] = obj["positions"]
1405 absol["move_to"] = self.move_to_absol
1407 return absol
1409 def find_axes_from_variable(self, variable):
1410 axes = {}
1411 for key, obj in variable.items():
1412 for axis in ["x", "y", "z"]:
1413 if key.startswith(axis):
1414 axes[axis] = obj
1416 return axes
1418 def move_to_absol(self, absol, sequential=False):
1419 all_objs = list(absol["fixed"].values()) + list(
1420 absol.get("variable", {}).values()
1421 )
1422 for obj in all_objs:
1423 if isinstance(obj["destination"], list):
1424 obj["motor"].move(obj["destination"][0])
1425 else:
1426 obj["motor"].move(obj["destination"])
1428 if sequential:
1429 obj["motor"].wait()
1431 for obj in all_objs:
1432 obj["motor"].wait()
1434 def _generate_thumb(self, path):
1435 size = (250, 250)
1436 thumb = Image.open(path)
1437 thumb.thumbnail(size, Image.LANCZOS)
1438 thumb.save(path.replace(".png", "t.png"))
1440 def export_subsamples(self, subsampleids):
1441 subsamples = []
1442 sampleid = None
1443 for subsampleid in subsampleids:
1444 obj = self._metadata.get_subsamples(subsampleid)
1445 sampleid = obj["sampleid"]
1446 sample = self._metadata.get_samples(sampleid)
1447 abs = self.get_absolute(subsampleid)
1449 motor_types = {}
1450 motor_positions = {}
1451 for move_type in ("fixed", "variable"):
1452 motor_positions[move_type] = {}
1454 for motor_type, details in abs[move_type].items():
1455 motor_types[motor_type] = details["motor"].name()
1456 motor_positions[move_type][details["motor"].name()] = {
1457 "destination": details["destination"],
1458 "unit": details["unit"],
1459 "unit_exponent": details["unit_exponent"],
1460 }
1462 subsamples.append(
1463 {
1464 "subsampleid": subsampleid,
1465 "type": obj["type"],
1466 "comments": obj["comments"],
1467 "extrametadata": {
1468 **(sample["extrametadata"] if sample["extrametadata"] else {}),
1469 "subsample": obj["extrametadata"],
1470 },
1471 "additional": abs["positions"],
1472 "motors": motor_positions,
1473 }
1474 )
1476 actor, greenlet = self.actor(
1477 "export",
1478 start=self._set_export_path,
1479 spawn=True,
1480 return_actor=True,
1481 actargs={
1482 "motor_types": motor_types,
1483 "subsamples": subsamples,
1484 "sessionid": g.blsession.get("sessionid"),
1485 "sampleid": sampleid,
1486 "time": int(time.time()),
1487 },
1488 )
1490 greenlet.join()
1491 if actor._failed:
1492 raise actor._exception
1494 return actor["dirname"]
1496 def _set_export_path(self, actid, actor):
1497 self._saving.set_filename(
1498 set_metadata=False,
1499 extra_saving_args=actor.saving_args,
1500 **actor.all_data,
1501 )
1502 self._saving.create_root_path(wait_exists=True)
1503 actor.update(dirname=self._saving.dirname)