|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import copy |
3 | 4 | import functools |
4 | 5 | import os |
| 6 | +from pathlib import Path |
5 | 7 | from typing import Sequence, Type |
6 | 8 |
|
7 | 9 | from scitbx.array_family import flex |
@@ -340,3 +342,190 @@ def get_imageset( |
340 | 342 | _add_static_mask_to_iset(format_instance, iset) |
341 | 343 |
|
342 | 344 | return iset |
| 345 | + |
| 346 | + @classmethod |
| 347 | + def create_imagesequence( |
| 348 | + cls, |
| 349 | + filenames: Sequence[str], |
| 350 | + single_file_indices: Sequence[int] | None = None, |
| 351 | + format_kwargs: dict | None = None, |
| 352 | + ) -> ImageSequence: |
| 353 | + """Create an ImageSequence by reading the image file""" |
| 354 | + return cls.get_imageset( |
| 355 | + filenames, |
| 356 | + single_file_indices=single_file_indices, |
| 357 | + format_kwargs=format_kwargs, |
| 358 | + as_sequence=True, |
| 359 | + ) |
| 360 | + |
| 361 | + @classmethod |
| 362 | + def reload_imagesequence( |
| 363 | + cls, |
| 364 | + filenames: Sequence[str], |
| 365 | + single_file_indices: Sequence[int], |
| 366 | + beam: model.Beam, |
| 367 | + detector: model.Detector, |
| 368 | + goniometer: model.Goniometer | None = None, |
| 369 | + scan: model.Scan | None = None, |
| 370 | + format_kwargs: dict | None = None, |
| 371 | + ) -> ImageSequence: |
| 372 | + """ |
| 373 | + Construct an ImageSequence from existing data |
| 374 | + """ |
| 375 | + # Although this accepts filenames as a sequence, for Liskov substitution |
| 376 | + # reasons, since this is a FormatMultiImage we only support _one_ file |
| 377 | + # that contains many images in a single dataset, not multiple files. |
| 378 | + if len(filenames) == 0: |
| 379 | + raise ValueError("Error: Cannot make an ImageSequence out of no files") |
| 380 | + elif len(filenames) > 1: |
| 381 | + raise ValueError( |
| 382 | + "Error: FormatMultiImage.get_imagesequence only supports single multiimage files" |
| 383 | + ) |
| 384 | + |
| 385 | + # Default empty kwargs |
| 386 | + format_kwargs = format_kwargs or {} |
| 387 | + # Make filenames absolute |
| 388 | + filename = Path(filenames[0]).absolute() |
| 389 | + # WIP: Ensure that we can't use this again |
| 390 | + del filenames |
| 391 | + |
| 392 | + assert single_file_indices, ( |
| 393 | + "We must have single file indices to reload ImageSequence" |
| 394 | + ) |
| 395 | + num_images = len(single_file_indices) |
| 396 | + |
| 397 | + # Get the format instance |
| 398 | + # assert len(filenames) == 1 |
| 399 | + # cls._current_filename_ = None |
| 400 | + # cls._current_instance_ = None |
| 401 | + # format_instance = cls.get_instance(filename, **format_kwargs) |
| 402 | + |
| 403 | + # if num_images is None: |
| 404 | + # # As we now have the actual format class we can get the number |
| 405 | + # # of images from here. This saves having to create another |
| 406 | + # # format class instance in the Reader() constructor |
| 407 | + # # NOTE: Having this information breaks internal assumptions in |
| 408 | + # # *Lazy classes, so they have to figure this out in |
| 409 | + # # their own time. |
| 410 | + # num_images = format_instance.get_num_images() |
| 411 | + |
| 412 | + # Get some information from the format class |
| 413 | + reader = cls.get_reader()([filename], num_images=num_images, **format_kwargs) |
| 414 | + |
| 415 | + # WIP: This should always be safe if we have the actual format class, |
| 416 | + # and the assumption we have in this version is that we do |
| 417 | + assert not cls.is_abstract() |
| 418 | + # WIP: ... BUT, we don't have a FormatClass instance (and probably |
| 419 | + # don't need to have one), so maybe we need to change all these |
| 420 | + # existing examples to ClassMethod? Or, check_format=False always |
| 421 | + # just set this blank, so maybe we never need this after initial |
| 422 | + # load. |
| 423 | + vendor = "" |
| 424 | + |
| 425 | + if not single_file_indices: |
| 426 | + raise ValueError( |
| 427 | + "single_file_indices can not be empty to reload FormatMultiImage ImageSequence" |
| 428 | + ) |
| 429 | + single_file_indices = flex.size_t(single_file_indices) |
| 430 | + |
| 431 | + # Check indices are sequential |
| 432 | + if ( |
| 433 | + max(single_file_indices) - min(single_file_indices) |
| 434 | + == len(single_file_indices) - 1 |
| 435 | + ): |
| 436 | + raise ValueError("single_file_idices are not sequential") |
| 437 | + num_images = len(single_file_indices) |
| 438 | + |
| 439 | + # Check the scan makes sense - we must want to use <= total images |
| 440 | + if scan is not None: |
| 441 | + assert scan.get_num_images() <= num_images |
| 442 | + |
| 443 | + # Create the masker |
| 444 | + format_instance: FormatMultiImage = None |
| 445 | + |
| 446 | + loader = DeferredLoader( |
| 447 | + cls, |
| 448 | + filename, |
| 449 | + format_kwargs=format_kwargs, |
| 450 | + gonimeter=goniometer, |
| 451 | + ) |
| 452 | + |
| 453 | + isetdata = ImageSetData( |
| 454 | + reader=reader, |
| 455 | + masker=loader.load_dynamic_mask, |
| 456 | + vendor=vendor, |
| 457 | + params=format_kwargs, |
| 458 | + format=cls, |
| 459 | + template=filename, |
| 460 | + ) |
| 461 | + |
| 462 | + # Create the sequence |
| 463 | + iset = ImageSequence( |
| 464 | + isetdata, |
| 465 | + beam=beam, |
| 466 | + detector=detector, |
| 467 | + goniometer=goniometer, |
| 468 | + scan=scan, |
| 469 | + indices=single_file_indices, |
| 470 | + ) |
| 471 | + |
| 472 | + # Handle merging detector static mask... if necessary |
| 473 | + assert iset.external_lookup.mask.data.empty(), ( |
| 474 | + "Deferred static mask loading assumes nothing loaded already" |
| 475 | + ) |
| 476 | + |
| 477 | + # Set up deferred instantiation and loading of the format class |
| 478 | + iset.external_lookup.mask.set_data_generator(loader.load_static_mask) |
| 479 | + return iset |
| 480 | + |
| 481 | + # WIP: Things reloading reads involving format_instance |
| 482 | + # - [x] _add_static_mask_to_iset(format_instance, iset) |
| 483 | + # - Added set_data_generator to ExternalLookupItem |
| 484 | + # - [x] masker = format_instance.get_masker(goniometer=goniometer) |
| 485 | + # - Masker is now a callable that returns the masker on request |
| 486 | + # - [x] vendor = format_instance.get_vendortype() |
| 487 | + # - Have just removed when reloading, for now |
| 488 | + # - [x] reader = cls.get_reader()(filenames, num_images=num_images, **format_kwargs) |
| 489 | + # - Reader now lazily loads rather than eagerly loads |
| 490 | + |
| 491 | + |
| 492 | +class DeferredLoader: |
| 493 | + """ |
| 494 | + Single object to hold data related to deferred loading |
| 495 | +
|
| 496 | + We don't want to eagerly access the raw image data on disk unless |
| 497 | + it's actively requested. This class holds the information to do this, |
| 498 | + and instances of it's methods passed into appropriate places so that |
| 499 | + they can load on demand. |
| 500 | +
|
| 501 | + Also, the objects this is going on are likely to be passed through a |
| 502 | + pickly boundary, so needs to be statically defined. |
| 503 | + """ |
| 504 | + |
| 505 | + def __init__( |
| 506 | + self, |
| 507 | + format: Type[Format], |
| 508 | + filename: str, |
| 509 | + format_kwargs: dict, |
| 510 | + gonimeter: model.Goniometer, |
| 511 | + ): |
| 512 | + self.format = format |
| 513 | + self.filename = filename |
| 514 | + self.format_kwargs = copy.deepcopy(format_kwargs or {}) |
| 515 | + self.goniometer = gonimeter |
| 516 | + |
| 517 | + def load_static_mask(self) -> ImageBool | None: |
| 518 | + print("Deferred loading of static mask") |
| 519 | + static_mask = self.format.get_instance( |
| 520 | + self.filename, **self.format_kwargs |
| 521 | + ).get_static_mask() |
| 522 | + if static_mask: |
| 523 | + return ImageBool(static_mask) |
| 524 | + return None |
| 525 | + |
| 526 | + def load_dynamic_mask(self) -> dxtbx.masking.GoniometerShadowMasker | None: |
| 527 | + """Lazy reading of dynamic masking""" |
| 528 | + print("Rendering masker") |
| 529 | + return self.format.get_instance(self.filename, **self.format_kwargs).get_masker( |
| 530 | + goniometer=self.goniometer |
| 531 | + ) |
0 commit comments