Improve performance of VideoStreamFrame.on_frame
#131
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Context
In the light of our discussion in #123 , before attempting to introduce some convenience features that would slow the GUI down, I sat down to look for some ways to make the
on_frame
method faster (to have more "room" to slow things down later). I discovered several issues that impacted the improvement:ImageTk
;PIL.Image.fromarray
;PIL.Image.fromarray
was ofdtype=np.float32
and required conversion byPIL
.PIL
was very expensive and could be sped up by doing it manually in numpy.I produced several versions of the
on_frame
function with alternative algorithms. Then, using a custom benchmark class, I timed all the methods. I did not use the built-in fps nor interval displays because IMO they are more influenced by inaccuracies in the system clock i.e.self.after(self.frame_delay, self.on_frame)
can wait longer thanself.frame_delay
on threading / system whims. The tables below document selected timings collected on two PCs for various scenarios on the main and this branch. The meanon_frame
times come from the frames # 100–600 displayed in the GUI and have an uncertainty of ~150ms.Branch "main" algorithm timings:
on_frame
timeThen I probed some alternative algorithms on the "main" branch:
on_frame
timenp.partition
rather thannp.percentile
brightness = 0.5
innumpy
rather thanPIL
np.int16
, then clip to (0,255) before PILnp.int16
, then clip to (0,255), then convert tonp.uint8
before PILThis way I found two major improvements. First and foremost, converting and clipping data manually in
numpy
instead of leaving it toPIL
cuts theon_frame
time on average by ~2 ms i.e. ~40%. Interestingly converting toint16
, then clipping integers, then converting touint8
seems the most performant (though it does use the most memory which could be an issue if not the 50 ms to clear RAM in-between frames). Secondly, PIL Image Enhance introduced brightness by linearly scaling the intensity, which we can just as well do in numpy at basically no cost. By implementing these two changes, we get:on_frame
timeI also confirmed this works on my slower microscope PC:
on_frame
timeSome final notes: replacing
PIL
's enhancement with numpy scaling has some advantages and drawbacks. It does behave a bit differently: previously atbrightness = 0.5
no pixel could be lighter than #808080. Now, the lightest 0.5% of pixels might be still white, which in my opinion is an improvement:brightness
can be now used to easily find these lightest spots even if auto-contrast is ON while still keeping the image "dark". Moreover, in this PR I fixed a bug where by setting the dynamic range to the default value, the image would be scaled to fixed value of 256 instead of the dynamic range setting itself. This was the reason why the simulated image was displayed fine at default of 11800 (now changed to 255), but was completely dark at 11799.Minor changes
on_frame
responsible by image display by ~40%Bugfixes
VideoStreamFrame
would scale the display range strictly to 256 whenever it was set tocamera.dynamic_range
(by default 11800, this is also set as the max value).