Skip to content

Commit

Permalink
Add annotation in OTIO reader for Live Review (AcademySoftwareFoundat…
Browse files Browse the repository at this point in the history
…ion#612)

### Add annotation in OTIO reader for Live Review

### Summarize your change.

- [X] Add the paint and point schemas used to defined an annotation
drawn by a presenter
- [X] Add an annotation hook to handle the display of annotations sent
from an OTIO string
- [X] Handle the exception where a source is not active by manually
setting the aspect ratio
- [X] Add a parameter to set the node to use in findAnnotatedFrames

### Describe the reason for the change.

A participant to a Live Review session is now able to see annotations
drawn by the presenter.

### Describe what you have tested and on which operating system.

The changes were tested on MacOS arm64.

---------

Signed-off-by: Éloïse Brosseau <[email protected]>
  • Loading branch information
eloisebrosseau authored Nov 7, 2024
1 parent e055db3 commit db17195
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 10 deletions.
3 changes: 3 additions & 0 deletions docs/rv-packages/rv-otio-reader.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ The RV package is installed in the usual Python package installation location: `
The package also uses a number of files located in `Plugins/SupportFiles/otio_reader` and detailed below.

- `manifest.json`: Provides examples of schemas and hooks used in the import process.
- `annotation_hook.py`: An example of a hook called before importing an annotation.
- `annotation_schema.py` An example schema of an annotation.
- `cdlExportHook.py`: An example of exporting an RVLinearize to a custom CDL effect in OTIO.
- `cdlHook.py`: An example of importing a custom CDL effect in OTIO into an RVLinearize node.
- `cdlSchema.py` An example schema of a CDL effect.
- `clipHook.py`: An example of a hook called before importing a clip.
- `customTransitionHook.py`: An example of importing a custom transition in OTIO into RV.
- `effectHook.py`: A helper file for adding and setting RV properties from OTIO.
- `paint_schema.py`: An example schema for a paint annotation.
- `point_schema.py`: An example schema for a paint annotation point.
- `sourcePostExportHook.py`: A hook called after an RVSourceNodeGroup has been exported to a `Clip`. This can be used to add custom effects for other nodes within the same source group. The RVLinearize node is provided as an example.
- `retimeExportHook.py`: A hook for exporting an RVRetime node to OTIO schemas LinearTimeWarp or FreezeFrame.
- `timeWarpHook.py`: A hook for importing OTIO's LinearTimeWarp and FreezeFrame schemas.
Expand Down
10 changes: 6 additions & 4 deletions src/lib/app/mu_rvui/extra_commands.mu
Original file line number Diff line number Diff line change
Expand Up @@ -980,19 +980,21 @@ Locate the input in the eval path at frame starting at node and return its ui na
}

documentation: """
Returns an array of all annotated frames relative to the view node. The array
is not sorted and some frames may appear more than once.
Returns an array of all annotated frames relative to the node passed to the function.
If there is no node, the view node is used instead. The array is not sorted and some
frames may appear more than once.
""";

\: findAnnotatedFrames (int[];)
\: findAnnotatedFrames (int[]; string node = nil)
{
string[] tempProps;
let seqb = sequenceBoundaries();
let testFrames = if seqb.empty() then int[](frameStart()) else seqb;
if (node eq nil) node = viewNode();

for_each (f; testFrames)
{
for_each (info; metaEvaluate(f, viewNode()))
for_each (info; metaEvaluate(f, node))
{
let {name, nodeType, eframe} = info;

Expand Down
26 changes: 23 additions & 3 deletions src/plugins/rv-packages/otio_reader/PACKAGE
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package: OTIO Reader
author: Contributors to the OpenTimelineIO project
organization: OpenTimelineIO project
contact: [email protected]
version: 1.1
version: 1.2
url: http://opentimeline.io
rv: 4.0.8
openrv: 1.0.0
rv: 2024.2.0
openrv: 2.1.0

modes:
- file: otio_reader_plugin.py
Expand All @@ -20,6 +20,8 @@ files:
location: SupportFiles/$PACKAGE
- file: annotation_schema.py
location: SupportFiles/$PACKAGE
- file: annotation_hook.py
location: SupportFiles/$PACKAGE
- file: cdlSchema.py
location: SupportFiles/$PACKAGE
- file: cdlHook.py
Expand All @@ -42,6 +44,10 @@ files:
location: SupportFiles/$PACKAGE
- file: multiRepPostExportHook.py
location: SupportFiles/$PACKAGE
- file: paint_schema.py
location: SupportFiles/$PACKAGE
- file: point_schema.py
location: SupportFiles/$PACKAGE

description: |
<p>
Expand Down Expand Up @@ -134,6 +140,20 @@ description: |
</p>
</li>

<li>
<p>
paint_schema.py
An example schema for a paint annotation.
</p>
</li>

<li>
<p>
point_schema.py
An example schema for a paint annotation point.
</p>
</li>

<li>
<p>
timeWarpHook.py
Expand Down
77 changes: 77 additions & 0 deletions src/plugins/rv-packages/otio_reader/annotation_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# *****************************************************************************
# Copyright 2024 Autodesk, Inc. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# *****************************************************************************


import effectHook
import opentimelineio as otio
from rv import commands, extra_commands


def hook_function(in_timeline, argument_map=None) -> None:
for layer in in_timeline.layers:
if layer.name == "Paint":
if type(layer.layer_range) is otio._opentime.TimeRange:
range = layer.layer_range
else:
range = otio.opentime.TimeRange(
layer.layer_range["start_time"], layer.layer_range["duration"]
)

relative_time = range.end_time_inclusive()
frame = relative_time.to_frames()

source_node = argument_map.get("source_group")
paint_node = extra_commands.nodesInGroupOfType(source_node, "RVPaint")[0]
paint_component = f"{paint_node}.paint"
stroke_id = commands.getIntProperty(f"{paint_component}.nextId")[0]
pen_component = f"{paint_node}.pen:{stroke_id}:{frame}:annotation"
frame_component = f"{paint_node}.frame:{frame}"

# Set properties on the paint component of the RVPaint node
effectHook.set_rv_effect_props(
paint_component, {"nextId": stroke_id + 1, "show": True}
)

# Add and set properties on the pen component of the RVPaint node
effectHook.add_rv_effect_props(
pen_component,
{
"color": [float(x) for x in layer.rgba],
"brush": layer.brush,
"debug": 1,
"join": 3,
"cap": 2,
"splat": 1,
"mode": 0 if layer.type.lower() == "color" else 1,
},
)

if not commands.propertyExists(f"{frame_component}.order"):
commands.newProperty(f"{frame_component}.order", commands.StringType, 1)

commands.insertStringProperty(
f"{frame_component}.order", [f"pen:{stroke_id}:{frame}:annotation"]
)

global_scale = argument_map.get("global_scale")
points_property = f"{pen_component}.points"
width_property = f"{pen_component}.width"

if not commands.propertyExists(points_property):
commands.newProperty(points_property, commands.FloatType, 2)
if not commands.propertyExists(width_property):
commands.newProperty(width_property, commands.FloatType, 1)

global_width = 2 / 15 # 0.133333...
for point in layer.points:
commands.insertFloatProperty(
points_property,
[point.x * global_scale.x, point.y * global_scale.y],
)
commands.insertFloatProperty(
width_property, [point.width * global_width]
)
19 changes: 19 additions & 0 deletions src/plugins/rv-packages/otio_reader/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,27 @@
"name" : "CDL",
"execution_scope" : "in process",
"filepath" : "cdlSchema.py"
},
{
"OTIO_SCHEMA" : "SchemaDef.1",
"name" : "Paint",
"execution_scope" : "in process",
"filepath" : "paint_schema.py"
},
{
"OTIO_SCHEMA" : "SchemaDef.1",
"name" : "Point",
"execution_scope" : "in process",
"filepath" : "point_schema.py"
}
],
"hook_scripts" : [
{
"OTIO_SCHEMA" : "HookScript.1",
"name" : "load_annotation",
"execution_scope" : "in_process",
"filepath" : "annotation_hook.py"
},
{
"OTIO_SCHEMA" : "HookScript.1",
"name" : "create_cdl",
Expand Down Expand Up @@ -72,6 +90,7 @@

],
"hooks" : {
"Annotation_to_rv" : ["load_annotation"],
"CDL_to_rv" : ["create_cdl"],
"Effect_to_rv" : ["create_effect"],
"export_RVLinearize" : ["export_cdl"],
Expand Down
11 changes: 8 additions & 3 deletions src/plugins/rv-packages/otio_reader/otio_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# such that we can use this both interactively as well as standalone
#

import logging
from rv import commands
from rv import extra_commands

Expand Down Expand Up @@ -653,9 +654,13 @@ def _add_source_bounds(media_ref, src, context=None):
# A width of 1.0 in RV means draw to the aspect ratio, so scale the
# width by the inverse of the aspect ratio
#
media_info = commands.sourceMediaInfo(src)
height = media_info["height"]
aspect_ratio = 1.0 if height == 0 else media_info["width"] / height
try:
media_info = commands.sourceMediaInfo(src)
height = media_info["height"]
aspect_ratio = media_info["width"] / height
except Exception:
logging.exception("Unable to determine aspect ratio, using default value of 16:9")
aspect_ratio = 1920 / 1080

translate = bounds.center() * global_scale - global_translate
scale = (bounds.max - bounds.min) * global_scale
Expand Down
126 changes: 126 additions & 0 deletions src/plugins/rv-packages/otio_reader/paint_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# *****************************************************************************
# Copyright 2024 Autodesk, Inc. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# *****************************************************************************

"""
For our OTIO output to effectively interface with other programs
using the OpenTimelineIO Python API, our custom schema need to be
specified and registered with the API.
As per OTIO documentation, a class such as this one must be created,
the schema must be registered with a PluginManifest, and the path to that
manifest must be added to $OTIO_PLUGIN_MANIFEST_PATH; then the schema
is ready to be used.
```python
Example:
myObject = otio.schemadef.Paint.Paint(
name, points, rgba, type, brush, layer_range, hold, ghost
)
"""

import opentimelineio as otio


@otio.core.register_type
class Paint(otio.core.SerializableObject):
"""A schema for the start of an annotation"""

_serializable_label = "Paint.1"
_name = "Paint"

def __init__(
self,
name: str = "",
points: list | None = None,
rgba: list | None = None,
type: str = "",
brush: str = "",
layer_range: dict | None = None,
hold: bool = False,
ghost: bool = False,
) -> None:
super().__init__()
self.name = name
self.points = points
self.rgba = rgba
self.type = type
self.brush = brush
self.layer_range = layer_range
self.hold = hold
self.ghost = ghost

name = otio.core.serializable_field(
"name", required_type=str, doc=("Name: expects a string")
)

_points = otio.core.serializable_field(
"points", required_type=list, doc=("Points: expects a list of point objects")
)

@property
def points(self) -> list:
return self._points

@points.setter
def points(self, val: list) -> None:
self._points = val

_rgba = otio.core.serializable_field(
"rgba", required_type=list, doc=("RGBA: expects a list of four floats")
)

@property
def rgba(self) -> list:
return self._rgba

@rgba.setter
def rgba(self, val: list) -> list:
self._rgba = val

type = otio.core.serializable_field(
"type", required_type=str, doc=("Type: expects a string")
)

brush = otio.core.serializable_field(
"brush", required_type=str, doc=("Brush: expects a string")
)

_layer_range = otio.core.serializable_field(
"layer_range",
required_type=otio.opentime.TimeRange,
doc=("Layer_range: expects a TimeRange object"),
)

@property
def layer_range(self) -> otio.opentime.TimeRange:
return self._layer_range

@layer_range.setter
def layer_range(self, val) -> otio.opentime.TimeRange:
self._layer_range = val

_hold = otio.core.serializable_field(
"hold", required_type=bool, doc=("Hold: expects either true or false")
)

_ghost = otio.core.serializable_field(
"ghost", required_type=bool, doc=("Ghost: expects either true or false")
)

def __str__(self) -> str:
return (
f"Paint({self.name}, {self.points}, {self.rgba}, {self.type}, "
f"{self.brush}, {self.layer_range}, {self.hold}, {self.ghost})"
)

def __repr__(self) -> str:
return (
f"otio.schema.Paint(name={self.name!r}, points={self.points!r}, "
f"rgba={self.rgba!r}, type={self.type!r}, brush={self.brush!r}, "
f"layer_range={self.layer_range!r}, hold={self.hold!r}, "
f"ghost={self.ghost!r})"
)
Loading

0 comments on commit db17195

Please sign in to comment.