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

Grayscale property passthrough #99

Merged
merged 3 commits into from
Jul 2, 2024
Merged

Grayscale property passthrough #99

merged 3 commits into from
Jul 2, 2024

Conversation

talmo
Copy link
Contributor

@talmo talmo commented Jun 25, 2024

This PR addresses #97 by implementing the behavior of setting the video.grayscale property so that it gets passed through to the backend appropriately and is used when computing video.shape.

Summary by CodeRabbit

  • New Features

    • Added support for handling grayscale videos and channel assignment in the video functionality.
  • Tests

    • Included new tests for verifying grayscale conversion and properties in video objects.

Copy link
Contributor

coderabbitai bot commented Jun 25, 2024

Walkthrough

The recent changes to the sleap_io repository enhance the handling of grayscale videos. A new property setter method for grayscale has been introduced, updating the backend and related properties accordingly. Additionally, the img_shape property now manages grayscale detection and channel assignment based on the grayscale attribute. These improvements ensure that changes to the grayscale setting are adequately reflected in the video properties.

Changes

Files Change Summary
sleap_io/io/video.py Updated img_shape to handle grayscale and channel assignment; read_test_frame calls _read_frame now.
sleap_io/model/video.py Added @property.setter for grayscale property in the Video class.
tests/model/test_video.py Added tests for verifying grayscale property and shape changes in video objects.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Video
    participant FrameReader

    User->>Video: Set grayscale attribute
    Video->>FrameReader: Update backend to reflect grayscale setting
    Video->>Video: Update img_shape property based on grayscale
    User->>Video: Fetch video properties
    Video->>User: Return updated properties reflecting grayscale change
Loading

Possibly related issues

Poem

In grayscale hues the frames do dance,
A setter's call sparks new romance.
Shapes now heed to grayscale's light,
With tests ensuring all is right.
Pixels align in black and white,
The code now pristinely tight!
🐇✨


Tip

Early access features: enabled

We are currently testing the following features in early access:

  • OpenAI gpt-4o model for code reviews and chat: OpenAI claims that this model is better at understanding and generating code than the previous models. We seek your feedback over the next few weeks before making it generally available.

Note:

  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.
  • Please join our Discord Community to provide feedback and report issues.
  • OSS projects are currently opted into early access features by default.

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Outside diff range and nitpick comments (1)
sleap_io/model/video.py (1)

Line range hint 123-145: Refactor the grayscale property setter for better error handling and consistency.

The setter for the grayscale property directly manipulates the backend and its cache without checking if the backend is initialized. This could lead to AttributeError if self.backend is None. Additionally, ensure consistent handling of the grayscale state within the backend metadata.

def grayscale(self, value: bool):
    """Set the grayscale value and adjust the backend."""
+   if self.backend is None:
+       raise ValueError("Backend not initialized")
    self.backend.grayscale = value
    self.backend._cached_shape = None
    if "grayscale" in self.backend_metadata:
        self.backend_metadata["grayscale"] = value
Tools
Ruff

139-139: Redefinition of unused grayscale from line 124 (F811)

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 5a30110 and 86ea7f3.

Files selected for processing (3)
  • sleap_io/io/video.py (3 hunks)
  • sleap_io/model/video.py (2 hunks)
  • tests/model/test_video.py (1 hunks)
Additional context used
Ruff
tests/model/test_video.py

129-129: Avoid equality comparisons to True; use if video.grayscale: for truth checks (E712)

Replace with video.grayscale

sleap_io/model/video.py

9-9: Redefinition of unused Optional from line 9 (F811)

Remove definition: Optional


47-47: Use X | Y for type annotations (UP007)

Convert to X | Y


49-49: Use X | Y for type annotations (UP007)

Convert to X | Y


62-62: Use X | Y for type annotations (UP007)

Convert to X | Y


63-63: Use X | Y for type annotations (UP007)

Convert to X | Y


65-65: Use X | Y for type annotations (UP007)

Convert to X | Y


102-102: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple


110-110: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple


118-118: Do not use bare except (E722)


139-139: Redefinition of unused grayscale from line 124 (F811)


197-200: Use return all(Path(f).exists() for f in self.filename) instead of for loop (SIM110)

Replace with return all(Path(f).exists() for f in self.filename)


212-212: Use X | Y for type annotations (UP007)

Convert to X | Y


213-213: Use X | Y for type annotations (UP007)

Convert to X | Y

sleap_io/io/video.py

16-19: Use contextlib.suppress(ImportError) instead of try-except-pass (SIM105)

Replace with contextlib.suppress(ImportError)


21-24: Use contextlib.suppress(ImportError) instead of try-except-pass (SIM105)

Replace with contextlib.suppress(ImportError)


22-22: imageio_ffmpeg imported but unused; consider using importlib.util.find_spec to test for availability (F401)


26-29: Use contextlib.suppress(ImportError) instead of try-except-pass (SIM105)

Replace with contextlib.suppress(ImportError)


27-27: av imported but unused; consider using importlib.util.find_spec to test for availability (F401)


55-55: Use X | Y for type annotations (UP007)

Convert to X | Y


57-57: Use X | Y for type annotations (UP007)

Convert to X | Y


57-57: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple


58-58: Use X | Y for type annotations (UP007)

Convert to X | Y


64-64: Use X | Y for type annotations (UP007)

Convert to X | Y


65-65: Use X | Y for type annotations (UP007)

Convert to X | Y


87-87: Do not compare types, use isinstance() (E721)


90-90: Do not compare types, use isinstance() (E721)


161-161: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple


171-171: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple


490-490: Use X | Y for type annotations (UP007)

Convert to X | Y


496-496: Use X | Y for type annotations (UP007)

Convert to X | Y


497-497: Use X | Y for type annotations (UP007)

Convert to X | Y


532-534: Use a single if statement instead of nested if statements (SIM102)


563-563: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple


581-584: Use ternary operator frame_idx = list(self.frame_map.keys())[0] if self.frame_map else 0 instead of if-else-block (SIM108)

Replace if-else-block with frame_idx = list(self.frame_map.keys())[0] if self.frame_map else 0

Comment on lines +127 to +133
def test_grayscale(centered_pair_low_quality_path):
video = Video.from_filename(centered_pair_low_quality_path)
assert video.grayscale == True
assert video.shape[-1] == 1

video.grayscale = False
assert video.shape[-1] == 3
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure proper boolean checks and optimize the test function.

The test for the grayscale property should avoid direct equality checks against boolean values. Instead, use the truthiness of the value for a cleaner and more Pythonic approach.

- assert video.grayscale == True
+ assert video.grayscale

Additionally, consider adding assertions to ensure that the grayscale property toggles correctly between True and False states, affecting the shape as expected.

Committable suggestion was skipped due to low confidence.

Tools
Ruff

129-129: Avoid equality comparisons to True; use if video.grayscale: for truth checks (E712)

Replace with video.grayscale

Comment on lines 161 to 167
def img_shape(self) -> Tuple[int, int, int]:
"""Shape of a single frame in the video."""
height, width, channels = self.get_frame(0).shape
height, width, channels = self.read_test_frame().shape
if self.grayscale is False:
channels = 3
elif self.grayscale is True:
channels = 1
Copy link
Contributor

Choose a reason for hiding this comment

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

Refine the implementation of img_shape property.

The method to determine the img_shape based on the grayscale property is overly complex and can be simplified. Additionally, this method should not directly read a frame to determine the shape; it should utilize cached values to improve efficiency.

@property
def img_shape(self) -> tuple[int, int, int]:
-    height, width, channels = self.read_test_frame().shape
+    if self._cached_shape is not None:
+        return self._cached_shape[1:]  # Extract height, width, channels from cached shape
-    if self.grayscale is False:
-        channels = 3
-    elif self.grayscale is True:
-        channels = 1
-    return int(height), int(width), int(channels)
+    raise ValueError("Shape not cached")

Committable suggestion was skipped due to low confidence.

Tools
Ruff

161-161: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Outside diff range and nitpick comments (5)
sleap_io/model/video.py (5)

Line range hint 9-9: Remove redundant import.

The Optional type is imported twice, which is unnecessary and could lead to confusion.

- from typing import Tuple, Optional, Optional
+ from typing import Tuple, Optional

Line range hint 47-47: Modernize type annotations.

Several type annotations in the code use the older typing module syntax. Update these to use the more modern and concise | syntax for unions, which is recommended in Python 3.10 and later.

- from typing import Tuple, Optional
+ from typing import Tuple
-    backend: Optional[VideoBackend] = None
+    backend: VideoBackend | None = None
-    source_video: Optional[Video] = None
+    source_video: Video | None = None
-    dataset: Optional[str] = None,
+    dataset: str | None = None,
-    grayscale: Optional[bool] = None,
+    grayscale: bool | None = None,
-    source_video: Optional[Video] = None,
+    source_video: Video | None = None,
-    dataset: Optional[str] = None,
+    dataset: str | None = None,
-    grayscale: Optional[str] = None,
+    grayscale: str | None = None,

Also applies to: 49-49, 62-62, 63-63, 65-65, 211-211, 212-212


Line range hint 102-102: Update type annotations to use built-in types.

The tuple type annotations should use the built-in tuple type instead of Tuple from the typing module, as recommended in recent Python versions.

-    def shape(self) -> Tuple[int, int, int, int] | None:
+    def shape(self) -> tuple[int, int, int, int] | None:
-    def _get_shape(self) -> Tuple[int, int, int, int] | None:
+    def _get_shape(self) -> tuple[int, int, int, int] | None:

Also applies to: 110-110


Line range hint 118-118: Add specific exception handling.

Using a bare except is not recommended as it can catch unexpected exceptions and make debugging difficult. Specify the type of exceptions you expect here.

118      try:
119          return self.backend.shape
120      except Exception as e:  # Specify the expected exceptions, e.g., AttributeError
121          if "shape" in self.backend_metadata:
122              return self.backend_metadata["shape"]
123          return None

Line range hint 196-199: Optimize existence check with a generator expression.

The loop for checking file existence can be simplified and optimized using a generator expression.

196      if isinstance(self.filename, list):
197          if check_all:
-               for f in self.filename:
-                   if not Path(f).exists():
-                       return False
-               return True
+               return all(Path(f).exists() for f in self.filename)
198          else:
199              return Path(self.filename[0]).exists()
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 86ea7f3 and 96412ff.

Files selected for processing (1)
  • sleap_io/model/video.py (1 hunks)
Additional context used
Ruff
sleap_io/model/video.py

9-9: Redefinition of unused Optional from line 9 (F811)

Remove definition: Optional


47-47: Use X | Y for type annotations (UP007)

Convert to X | Y


49-49: Use X | Y for type annotations (UP007)

Convert to X | Y


62-62: Use X | Y for type annotations (UP007)

Convert to X | Y


63-63: Use X | Y for type annotations (UP007)

Convert to X | Y


65-65: Use X | Y for type annotations (UP007)

Convert to X | Y


102-102: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple


110-110: Use tuple instead of Tuple for type annotation (UP006)

Replace with tuple


118-118: Do not use bare except (E722)


196-199: Use return all(Path(f).exists() for f in self.filename) instead of for loop (SIM110)

Replace with return all(Path(f).exists() for f in self.filename)


211-211: Use X | Y for type annotations (UP007)

Convert to X | Y


212-212: Use X | Y for type annotations (UP007)

Convert to X | Y

Comment on lines +134 to +143
return self.backend_metadata.get("grayscale", None)

@grayscale.setter
def grayscale(self, value: bool):
"""Set the grayscale value and adjust the backend."""
if self.backend is not None:
self.backend.grayscale = value
self.backend._cached_shape = None

self.backend_metadata["grayscale"] = value
Copy link
Contributor

Choose a reason for hiding this comment

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

Implement robust error handling in grayscale setter.

The setter for the grayscale property lacks error handling, which could lead to issues if an incorrect type or a null value is passed. Consider adding type checks and handling potential exceptions to ensure robustness.

136  @grayscale.setter
137  def grayscale(self, value: bool):
138      """Set the grayscale value and adjust the backend."""
139      if not isinstance(value, bool):
140          raise ValueError("grayscale must be a boolean value.")
141      if self.backend is not None:
142          self.backend.grayscale = value
143          self.backend._cached_shape = None
144  
145      self.backend_metadata["grayscale"] = value
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return self.backend_metadata.get("grayscale", None)
@grayscale.setter
def grayscale(self, value: bool):
"""Set the grayscale value and adjust the backend."""
if self.backend is not None:
self.backend.grayscale = value
self.backend._cached_shape = None
self.backend_metadata["grayscale"] = value
return self.backend_metadata.get("grayscale", None)
@grayscale.setter
def grayscale(self, value: bool):
"""Set the grayscale value and adjust the backend."""
if not isinstance(value, bool):
raise ValueError("grayscale must be a boolean value.")
if self.backend is not None:
self.backend.grayscale = value
self.backend._cached_shape = None
self.backend_metadata["grayscale"] = value

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 96412ff and 99403ca.

Files selected for processing (2)
  • sleap_io/io/video.py (3 hunks)
  • tests/model/test_video.py (1 hunks)
Additional context used
Ruff
tests/model/test_video.py

129-129: Avoid equality comparisons to True; use if video.grayscale: for truth checks

Replace with video.grayscale

(E712)


137-137: Avoid equality comparisons to False; use if not video.grayscale: for false checks

Replace with not video.grayscale

(E712)


143-143: Avoid equality comparisons to True; use if video.grayscale: for truth checks

Replace with video.grayscale

(E712)

sleap_io/io/video.py

16-19: Use contextlib.suppress(ImportError) instead of try-except-pass

Replace with contextlib.suppress(ImportError)

(SIM105)


21-24: Use contextlib.suppress(ImportError) instead of try-except-pass

Replace with contextlib.suppress(ImportError)

(SIM105)


22-22: imageio_ffmpeg imported but unused; consider using importlib.util.find_spec to test for availability

(F401)


26-29: Use contextlib.suppress(ImportError) instead of try-except-pass

Replace with contextlib.suppress(ImportError)

(SIM105)


27-27: av imported but unused; consider using importlib.util.find_spec to test for availability

(F401)


55-55: Use X | Y for type annotations

Convert to X | Y

(UP007)


57-57: Use X | Y for type annotations

Convert to X | Y

(UP007)


57-57: Use tuple instead of Tuple for type annotation

Replace with tuple

(UP006)


58-58: Use X | Y for type annotations

Convert to X | Y

(UP007)


64-64: Use X | Y for type annotations

Convert to X | Y

(UP007)


65-65: Use X | Y for type annotations

Convert to X | Y

(UP007)


87-87: Do not compare types, use isinstance()

(E721)


90-90: Do not compare types, use isinstance()

(E721)


161-161: Use tuple instead of Tuple for type annotation

Replace with tuple

(UP006)


173-173: Use tuple instead of Tuple for type annotation

Replace with tuple

(UP006)


492-492: Use X | Y for type annotations

Convert to X | Y

(UP007)


498-498: Use X | Y for type annotations

Convert to X | Y

(UP007)


499-499: Use X | Y for type annotations

Convert to X | Y

(UP007)


534-536: Use a single if statement instead of nested if statements

(SIM102)


565-565: Use tuple instead of Tuple for type annotation

Replace with tuple

(UP006)


583-586: Use ternary operator frame_idx = list(self.frame_map.keys())[0] if self.frame_map else 0 instead of if-else-block

Replace if-else-block with frame_idx = list(self.frame_map.keys())[0] if self.frame_map else 0

(SIM108)

video.grayscale = True
video.close()
video.open()
assert video.grayscale == True
Copy link
Contributor

Choose a reason for hiding this comment

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

Optimize boolean check.

Avoid direct equality comparisons to True; use truthiness checks for a cleaner and more Pythonic approach.

-    assert video.grayscale == True
+    assert video.grayscale
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert video.grayscale == True
assert video.grayscale
Tools
Ruff

143-143: Avoid equality comparisons to True; use if video.grayscale: for truth checks

Replace with video.grayscale

(E712)


video.close()
video.open()
assert video.grayscale == False
Copy link
Contributor

Choose a reason for hiding this comment

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

Optimize boolean check.

Avoid direct equality comparisons to False; use falsiness checks for a cleaner and more Pythonic approach.

-    assert video.grayscale == False
+    assert not video.grayscale
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert video.grayscale == False
assert not video.grayscale
Tools
Ruff

137-137: Avoid equality comparisons to False; use if not video.grayscale: for false checks

Replace with not video.grayscale

(E712)


def test_grayscale(centered_pair_low_quality_path):
video = Video.from_filename(centered_pair_low_quality_path)
assert video.grayscale == True
Copy link
Contributor

Choose a reason for hiding this comment

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

Optimize boolean check.

Avoid direct equality comparisons to True; use truthiness checks for a cleaner and more Pythonic approach.

-    assert video.grayscale == True
+    assert video.grayscale
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert video.grayscale == True
assert video.grayscale
Tools
Ruff

129-129: Avoid equality comparisons to True; use if video.grayscale: for truth checks

Replace with video.grayscale

(E712)

Comment on lines +163 to +169
height, width, channels = self.read_test_frame().shape
if self.grayscale is None:
self.detect_grayscale()
if self.grayscale is False:
channels = 3
elif self.grayscale is True:
channels = 1
Copy link
Contributor

Choose a reason for hiding this comment

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

Optimize the implementation of img_shape property.

The method to determine the img_shape based on the grayscale property is overly complex and can be simplified. Additionally, this method should not directly read a frame to determine the shape; it should utilize cached values to improve efficiency.

@property
def img_shape(self) -> tuple[int, int, int]:
-    height, width, channels = self.read_test_frame().shape
+    if self._cached_shape is not None:
+        return self._cached_shape[1:]  # Extract height, width, channels from cached shape
-    if self.grayscale is None:
-        self.detect_grayscale()
-    if self.grayscale is False:
-        channels = 3
-    elif self.grayscale is True:
-        channels = 1
-    return int(height), int(width), int(channels)
+    raise ValueError("Shape not cached")

Committable suggestion was skipped due to low confidence.

Copy link

codecov bot commented Jul 2, 2024

Codecov Report

Attention: Patch coverage is 93.33333% with 1 line in your changes missing coverage. Please review.

Project coverage is 96.11%. Comparing base (5a30110) to head (99403ca).

Files Patch % Lines
sleap_io/io/video.py 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #99      +/-   ##
==========================================
+ Coverage   96.04%   96.11%   +0.06%     
==========================================
  Files          17       17              
  Lines        2022     2032      +10     
==========================================
+ Hits         1942     1953      +11     
+ Misses         80       79       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@talmo talmo merged commit 95b9c81 into main Jul 2, 2024
7 of 8 checks passed
@talmo talmo deleted the talmo/grayscale-prop branch July 2, 2024 21:25
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.

Changing video grayscale attribute after load doesn't get reflected in downstream properties
1 participant