Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dials.image_viewer: add option to display in the "best fit" frame #1716

Merged
merged 12 commits into from
May 21, 2021

Conversation

dagewa
Copy link
Member

@dagewa dagewa commented May 19, 2021

  • This should be moved so that it is done once per detector, not every time an image is displayed
  • It should also be made optional with a checkbox to control

- This should be moved so that it is done once per detector, not
  every time an image is displayed
- It should also be made optional with a checkbox to control
@dagewa
Copy link
Member Author

dagewa commented May 19, 2021

Also, need to check if this breaks the beam centre and other overlays

@graeme-winter
Copy link
Contributor

Also, need to check if this breaks the beam centre and other overlays

Checking now

@graeme-winter
Copy link
Contributor

@dagewa

Screenshot 2021-05-19 at 16 11 26

Looking good!

@graeme-winter
Copy link
Contributor

2 theta > 90° though

Grey-Area fix-image-viewer :( $ dials.image_viewer split_12.*
DIALS (2018) Acta Cryst. D74, 85-97. https://doi.org/10.1107/S2059798317017235
The following parameters have been modified:

input {
  experiments = split_12.expt
  reflections = split_12.refl
}

Traceback (most recent call last):
  File "/Users/graeme/git/dials/build/../modules/dials/command_line/image_viewer.py", line 208, in <module>
    run()
  File "/Users/graeme/git/dials/conda_base/python.app/Contents/lib/python3.8/contextlib.py", line 75, in inner
    return func(*args, **kwds)
  File "/Users/graeme/git/dials/build/../modules/dials/command_line/image_viewer.py", line 204, in run
    show_image_viewer(params=params, reflections=reflections, experiments=experiments)
  File "/Users/graeme/git/dials/build/../modules/dials/command_line/image_viewer.py", line 169, in show_image_viewer
    wrapper.display(experiments=experiments, reflections=reflections)
  File "/Users/graeme/git/dials/modules/dials/util/image_viewer/spotfinder_wrap.py", line 81, in display
    self.frame.load_image(chooser_wrapper(imagesets[0], 0))
  File "/Users/graeme/git/dials/modules/dials/util/image_viewer/spotfinder_frame.py", line 591, in load_image
    super().load_image(
  File "/Users/graeme/git/dials/modules/dials/util/image_viewer/slip_viewer/frame.py", line 423, in load_image
    self.pyslip.tiles.set_image(
  File "/Users/graeme/git/dials/modules/dials/util/image_viewer/slip_viewer/tile_generation.py", line 306, in set_image
    self.flex_image = get_flex_image_multipanel(
  File "/Users/graeme/git/dials/modules/dials/util/image_viewer/slip_viewer/tile_generation.py", line 116, in get_flex_image_multipanel
    beam_center /= npanels / 1e-3
  File "/Users/graeme/git/dials/modules/cctbx_project/scitbx/matrix/__init__.py", line 154, in __truediv__
    return rec([e/other for e in self.elems], self.n)
  File "/Users/graeme/git/dials/modules/cctbx_project/scitbx/matrix/__init__.py", line 154, in <listcomp>
    return rec([e/other for e in self.elems], self.n)
ZeroDivisionError: Please report this error to [email protected]: float division by zero

@graeme-winter
Copy link
Contributor

Looking at that one now

@graeme-winter
Copy link
Contributor

OK, beam centre nonsense is baked in everywhere which causes massive failures if the two-theta angle is > 90 - because there is no beam centre

However this current state is a long way along the road to fixing. But the rest of the road is a rocky and pothole strewn mudbath, so may be hard going.

@phyy-nx
Copy link
Member

phyy-nx commented May 19, 2021

I re-ran my rotation tests from previous attempts to fix this. Here are the commands again:

dials.import `libtbx.find_in_repositories dials_regression`/image_examples/ALS_831/q315r_lyso_001.img output.experiments=singlepanel.expt
dials.import `libtbx.find_in_repositories dials_regression`/image_examples/LCLS_cspad_nexus/idx-20130301060858401.cbf output.experiments=multipanel.expt
libtbx.python `libtbx.find_in_repositories dials`/util/image_viewer/slip_viewer/rotate_detector.py singlepanel.expt
mv rotated.expt singlepanel_rotated.expt
libtbx.python `libtbx.find_in_repositories dials`/util/image_viewer/slip_viewer/rotate_detector.py multipanel.expt
mv rotated.expt multipanel_rotated.expt

Then use dials.image_viewer on each of the .expt files in turn to see what happens. On both master and this project_2d branch, both single and multi panel images appear rotated correctly.

👍

Also, calculate only when this setting is toggled, not every time
an image is displayed. Slightly nasty approach by assigning to
an attribute of the detector, but avoids modifying
get_flex_image_multipanel signature.
@dagewa dagewa marked this pull request as ready for review May 19, 2021 19:55
@dagewa
Copy link
Member Author

dagewa commented May 19, 2021

I completed additional work as per the first comment.

It might be a bit messy still. For instance, I tried setting the default to "image" rather than "lab", but it still initially displays the "lab" form. Maybe this is because the calculation is not properly performed until the option is toggled at least once? Anyway, I leave that to anyone with more idea about the image viewer code to look at.

The whole image may shift between the two modes. This is a minor annoyance I don't have time to look at. Finally, as @graeme-winter points out, there are still issues at high angle, which are not due to the best-frame projection but due to problems calculating beam centre.

@dagewa dagewa changed the title Project to 2D "best fit" frame first. dials.image_viewer: add option to display in the "best fit" frame May 19, 2021
@dagewa
Copy link
Member Author

dagewa commented May 19, 2021

Demo:
rotated-cspad

@codecov
Copy link

codecov bot commented May 19, 2021

Codecov Report

Merging #1716 (a783f29) into main (802c9d6) will increase coverage by 4.96%.
The diff coverage is 74.86%.

❗ Current head a783f29 differs from pull request most recent head f323b1b. Consider uploading reports for the commit f323b1b to get more accurate results

@@            Coverage Diff             @@
##             main    #1716      +/-   ##
==========================================
+ Coverage   66.65%   71.62%   +4.96%     
==========================================
  Files         615      867     +252     
  Lines       68872    90876   +22004     
  Branches     9572    11314    +1742     
==========================================
+ Hits        45905    65087   +19182     
- Misses      21042    23769    +2727     
- Partials     1925     2020      +95     

Copy link
Member

@phyy-nx phyy-nx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I don't have the 2theta tests. On my rotation tests things look good.

@graeme-winter
Copy link
Contributor

@dagewa taking a quick look at the default

@graeme-winter
Copy link
Contributor

Some confusion in the code...

        self.settings.project_onto = self.params.project_onto

will set it as a string value however

            self.settings.project_onto = self.projection_ctrl.GetSelection()

later on assigns as the index of the selection. I do not think that this feature is unique to this new option.

@graeme-winter
Copy link
Contributor

Some confusion in the code...

        self.settings.project_onto = self.params.project_onto

will set it as a string value however

            self.settings.project_onto = self.projection_ctrl.GetSelection()

later on assigns as the index of the selection. I do not think that this feature is unique to this new option.

Fortunately this is never actually read anyway 🙄

@dagewa
Copy link
Member Author

dagewa commented May 20, 2021

Yes, I admit to getting confused by this while copy-and-pasting, but I could not resolve in the time frame. I would welcome anyone with a better idea of what is going on in the image viewer code to tidy up, but I fear starting down that route might end up like an episode of Hoarder Homes.

@graeme-winter
Copy link
Contributor

Yes, I admit to getting confused by this while copy-and-pasting, but I could not resolve in the time frame. I would welcome anyone with a better idea of what is going on in the image viewer code to tidy up, but I fear starting down that route might end up like an episode of Hoarder Homes.

Am looking

@rjgildea
Copy link
Contributor

The issue is that the attribute detector.projected_2d is only set as a consequence of calling frame.update_settings():

self.update_settings()

however this is only called after the image is generated here:
self.pyslip.tiles.set_image(
file_name_or_data=img,
metrology_matrices=self.metrology_matrices,
get_image_data=get_image_data,
show_saturated=(
self.settings.display == "image"
and self.settings.image_type == "corrected"
),
)

Unfortunately the method can't be called before this line, as it appears to depend on attributes defined as part of generating the image...

@rjgildea
Copy link
Contributor

The reason it can't be called before the image is created, is because this PR explicitly adds an attribute to self.pyslip.tiles.raw_image.get_detector() which does not exist until self.pyslip.tiles.set_image() has been called:
https://github.com/dials/dials/pull/1716/files#diff-3b34f595b35ea14a7777f614778f9da3c90d11f059de766d5d7d3c4d3ec9ff41R1018-R1025

@rjgildea
Copy link
Contributor

Right, so what happens is we call SpotFrame.load_image() which calls XrayFrame.load_image() which calls SpotFrame.update_settings() which is the method that sets the detector.projected_2d attribute. However, update_settings() is only called after calling self.pyslip.tiles.set_image which is what results in self.pyslip.tiles.raw_image being set, which is required by update_settings(), meaning that this can't be called before setting the image.

@graeme-winter
Copy link
Contributor

Right, so what happens is we call SpotFrame.load_image() which calls XrayFrame.load_image() which calls SpotFrame.update_settings() which is the method that sets the detector.projected_2d attribute. However, update_settings() is only called after calling self.pyslip.tiles.set_image which is what results in self.pyslip.tiles.raw_image being set, which is required by update_settings(), meaning that this can't be called before setting the image.

So... if we want this to work the only sane option is to evaluate it as part of the image loading. That seems a long way from ideal.

@graeme-winter
Copy link
Contributor

I hold the opinion that though this change set is not perfect, the image viewer is a lot better with the changes in than out -> am minded to suggest we merge in it's current form and add a new issue about the annoying behaviour.

I welcome other perspectives.

Copy link
Contributor

@graeme-winter graeme-winter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valuable if not perfect improvement, and I appreciate the effort @dagewa made in making these changes. Thank you.

Probably should work something about closing #1715 into the commit messages as this makes it less fundamentally broken.

@@ -32,6 +32,8 @@
.type = int
color_scheme = *grayscale rainbow heatmap invert
.type = choice
project_onto = *lab image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably I would prefer projection than project_onto

@@ -0,0 +1 @@
``dials.image_viewer``: Add an option to display in a best-fit "image" coordinate frame.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth @ndevenish poking at this a little to get some kind of "snapped to grid" idea in there. I have no inspiration otherwise would do myself. Worth making this readable by the general reader.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, should also now say that the option is the new default

@@ -524,70 +524,7 @@ def get_key(self, file_name_or_data):
return super().get_key(file_name_or_data)

def update_settings(self, layout=True):
# XXX The zoom level from the settings panel are not taken into
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rjgildea improving his -ve line count, approve!

This aliasing did completely confuse me while I was trying to change the default, so appreciate it being removed.

That's a couple of haunted trees felled anyway.

# Calculate 2D origin, fast and slow vectors for detector projected onto
# a best fit frame. FIXME this should be done once for the detector, not
# here for every image we want to display!
try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EAFP is OK, but wonder if this is a little harder to read than just a "hasattr" or even setting the attribute to None when the object is created (which would involve a little more exploration but helps to leave breadcrumbs for a longer term improvement)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh the FIXME comment is out of date too btw. It came from the first commit where project_2d was called every time this function run rather than checking the detector for pre-calculated results. I did do this in a rush yesterday...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

panels is a misnomer here, AFAICT. It is actually a Detector object. We could assign this attribute when the experiments are first loaded in, to ensure it is there whenever it matters.

An alternative is what I suggested at the bottom of cctbx/dxtbx#224 (comment), which is that the project_2d function gets moved to the Detector object itself. Then any detector can tell a viewer how to lay out its panels in 2D. This would have the advantage of providing a method that could be overridden in special cases, like that of the P12M, avoiding the special case code here in the image viewer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment was aimed at the try/except AttributeError pattern, but agree on moving it to Detector as proposed. Could be worth considering if we can do sooner rather than later 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, but actually it would probably have to be in the Format, because I don't think a Detector knows what detector it is...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

until then:
projected_axes = getattr(panels, "projected_2d", None)
replaces the 4 lines

# Calculate 2D origin, fast and slow vectors for detector projected onto
# a best fit frame. FIXME this should be done once for the detector, not
# here for every image we want to display!
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

until then:
projected_axes = getattr(panels, "projected_2d", None)
replaces the 4 lines

Comment on lines 1020 to 1023
if not hasattr(detector, "projected_2d"):
detector.projected_2d = project_2d(detector)
elif detector.projected_2d is None:
detector.projected_2d = project_2d(detector)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not hasattr(detector, "projected_2d"):
detector.projected_2d = project_2d(detector)
elif detector.projected_2d is None:
detector.projected_2d = project_2d(detector)
if not getattr(detector, "projected_2d", None):
detector.projected_2d = project_2d(detector)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ this is kinda what I was getting at 🙂

I appreciate I am not always clear though

@graeme-winter
Copy link
Contributor

Following the latest change set @dagewa something is looking good - imported / integrated multi-two-theta offset scan with overlay of reflections:

Screenshot 2021-05-21 at 09 57 59
Screenshot 2021-05-21 at 09 58 05
Screenshot 2021-05-21 at 09 58 15
Screenshot 2021-05-21 at 09 58 22
Screenshot 2021-05-21 at 09 58 27

bonus points - for reasons that are opaque no longer crashes for two theta > 90° 🤔 🤷‍♂️ 👍

@dagewa
Copy link
Member Author

dagewa commented May 21, 2021

I am rather puzzled by the fact that the fix fixes more than I thought it fixed 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants