659 lines
21 KiB
Python
659 lines
21 KiB
Python
"""
|
|
Channel Utilities
|
|
Pack and unpack multiple maps into RGB channels
|
|
"""
|
|
|
|
import torch
|
|
|
|
|
|
class ChannelPacker:
|
|
"""
|
|
Pack 3 grayscale maps into RGB channels
|
|
Common: Pack Roughness (R), Metallic (G), AO (B)
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"red_channel": ("IMAGE",),
|
|
},
|
|
"optional": {
|
|
"green_channel": ("IMAGE",),
|
|
"blue_channel": ("IMAGE",),
|
|
"preset": (["custom", "orm_unity", "orm_unreal", "rma"], {
|
|
"default": "custom",
|
|
"tooltip": "Preset channel layouts: ORM (AO/Roughness/Metallic), RMA (Roughness/Metallic/AO)"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("packed",)
|
|
FUNCTION = "pack"
|
|
CATEGORY = "Texture Alchemist/Channel"
|
|
|
|
def pack(self, red_channel, green_channel=None, blue_channel=None, preset="custom"):
|
|
"""Pack grayscale images into RGB channels"""
|
|
|
|
print("\n" + "="*60)
|
|
print("Channel Packer")
|
|
print("="*60)
|
|
print(f"Preset: {preset}")
|
|
|
|
# Get dimensions from red channel
|
|
batch, height, width, channels = red_channel.shape
|
|
device = red_channel.device
|
|
dtype = red_channel.dtype
|
|
|
|
# Convert to grayscale if needed
|
|
r = self._to_grayscale(red_channel)
|
|
g = self._to_grayscale(green_channel) if green_channel is not None else torch.ones_like(r)
|
|
b = self._to_grayscale(blue_channel) if blue_channel is not None else torch.ones_like(r)
|
|
|
|
# Resize to match if sizes differ
|
|
if green_channel is not None:
|
|
g = self._resize_to_match(g, r)
|
|
if blue_channel is not None:
|
|
b = self._resize_to_match(b, r)
|
|
|
|
# Pack into RGB
|
|
packed = torch.cat([r, g, b], dim=-1)
|
|
|
|
print(f"✓ Channels packed")
|
|
print(f" R: {r.shape}")
|
|
print(f" G: {g.shape}")
|
|
print(f" B: {b.shape}")
|
|
print(f" Output: {packed.shape}")
|
|
print("="*60 + "\n")
|
|
|
|
return (packed,)
|
|
|
|
def _to_grayscale(self, image):
|
|
"""Convert to single channel grayscale"""
|
|
if image.shape[-1] == 1:
|
|
return image
|
|
elif image.shape[-1] == 3:
|
|
# Luminance
|
|
weights = torch.tensor([0.2989, 0.5870, 0.1140],
|
|
device=image.device, dtype=image.dtype)
|
|
return torch.sum(image * weights, dim=-1, keepdim=True)
|
|
else:
|
|
# Just take first channel
|
|
return image[:, :, :, 0:1]
|
|
|
|
def _resize_to_match(self, source, target):
|
|
"""Resize source to match target dimensions"""
|
|
if source.shape[1:3] == target.shape[1:3]:
|
|
return source
|
|
|
|
target_h, target_w = target.shape[1:3]
|
|
source_bchw = source.permute(0, 3, 1, 2)
|
|
|
|
resized = torch.nn.functional.interpolate(
|
|
source_bchw,
|
|
size=(target_h, target_w),
|
|
mode='bilinear',
|
|
align_corners=False
|
|
)
|
|
|
|
return resized.permute(0, 2, 3, 1)
|
|
|
|
|
|
class ChannelUnpacker:
|
|
"""
|
|
Unpack RGB channels into separate grayscale maps
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"packed": ("IMAGE",),
|
|
"output_channels": (["all", "r_only", "g_only", "b_only", "rg", "rb", "gb"], {
|
|
"default": "all",
|
|
"tooltip": "Which channels to extract"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE")
|
|
RETURN_NAMES = ("red", "green", "blue")
|
|
FUNCTION = "unpack"
|
|
CATEGORY = "Texture Alchemist/Channel"
|
|
|
|
def unpack(self, packed, output_channels="all"):
|
|
"""Unpack RGB channels into separate images"""
|
|
|
|
print("\n" + "="*60)
|
|
print("Channel Unpacker")
|
|
print("="*60)
|
|
print(f"Input shape: {packed.shape}")
|
|
print(f"Output channels: {output_channels}")
|
|
|
|
batch, height, width, channels = packed.shape
|
|
device = packed.device
|
|
dtype = packed.dtype
|
|
|
|
# Extract channels
|
|
if channels >= 3:
|
|
r = packed[:, :, :, 0:1]
|
|
g = packed[:, :, :, 1:2]
|
|
b = packed[:, :, :, 2:3]
|
|
elif channels == 2:
|
|
r = packed[:, :, :, 0:1]
|
|
g = packed[:, :, :, 1:2]
|
|
b = torch.zeros((batch, height, width, 1), device=device, dtype=dtype)
|
|
else:
|
|
r = packed
|
|
g = torch.zeros((batch, height, width, 1), device=device, dtype=dtype)
|
|
b = torch.zeros((batch, height, width, 1), device=device, dtype=dtype)
|
|
|
|
# Convert to RGB for display
|
|
r_rgb = r.repeat(1, 1, 1, 3)
|
|
g_rgb = g.repeat(1, 1, 1, 3)
|
|
b_rgb = b.repeat(1, 1, 1, 3)
|
|
|
|
print(f"✓ Channels unpacked")
|
|
print(f" Red: {r_rgb.shape}")
|
|
print(f" Green: {g_rgb.shape}")
|
|
print(f" Blue: {b_rgb.shape}")
|
|
print("="*60 + "\n")
|
|
|
|
return (r_rgb, g_rgb, b_rgb)
|
|
|
|
|
|
class GrayscaleToColor:
|
|
"""
|
|
Convert grayscale image to RGB by repeating the channel
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"grayscale": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("color",)
|
|
FUNCTION = "convert"
|
|
CATEGORY = "Texture Alchemist/Channel"
|
|
|
|
def convert(self, grayscale):
|
|
"""Convert grayscale to RGB"""
|
|
|
|
print("\n" + "="*60)
|
|
print("Grayscale to Color")
|
|
print("="*60)
|
|
print(f"Input shape: {grayscale.shape}")
|
|
|
|
batch, height, width, channels = grayscale.shape
|
|
|
|
# If already RGB or more, return as-is
|
|
if channels >= 3:
|
|
print("✓ Already color (RGB+), returning as-is")
|
|
print("="*60 + "\n")
|
|
return (grayscale,)
|
|
|
|
# Convert to grayscale first if needed
|
|
if channels == 1:
|
|
gray = grayscale
|
|
else:
|
|
# Average multiple channels
|
|
gray = torch.mean(grayscale, dim=-1, keepdim=True)
|
|
|
|
# Repeat to create RGB
|
|
color = gray.repeat(1, 1, 1, 3)
|
|
|
|
print(f"✓ Converted to color")
|
|
print(f" Output shape: {color.shape}")
|
|
print("="*60 + "\n")
|
|
|
|
return (color,)
|
|
|
|
|
|
class ColorToGrayscale:
|
|
"""
|
|
Convert color (RGB) image to grayscale using luminance
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"color": ("IMAGE",),
|
|
"method": (["luminance", "average", "lightness", "red_only", "green_only", "blue_only"], {
|
|
"default": "luminance",
|
|
"tooltip": "Conversion method: luminance (perceptual), average, lightness (min+max/2), or single channel"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("grayscale",)
|
|
FUNCTION = "convert"
|
|
CATEGORY = "Texture Alchemist/Channel"
|
|
|
|
def convert(self, color, method="luminance"):
|
|
"""Convert color to grayscale"""
|
|
|
|
print("\n" + "="*60)
|
|
print("Color to Grayscale")
|
|
print("="*60)
|
|
print(f"Input shape: {color.shape}")
|
|
print(f"Method: {method}")
|
|
|
|
batch, height, width, channels = color.shape
|
|
device = color.device
|
|
dtype = color.dtype
|
|
|
|
# If already grayscale, return as-is (as RGB for display)
|
|
if channels == 1:
|
|
print("✓ Already grayscale, returning as RGB")
|
|
gray = color.repeat(1, 1, 1, 3)
|
|
print("="*60 + "\n")
|
|
return (gray,)
|
|
|
|
# Extract RGB channels (handle RGBA by ignoring alpha)
|
|
r = color[:, :, :, 0:1]
|
|
g = color[:, :, :, 1:2] if channels >= 2 else r
|
|
b = color[:, :, :, 2:3] if channels >= 3 else r
|
|
|
|
# Apply conversion method
|
|
if method == "luminance":
|
|
# Perceptual luminance weights (Rec. 709)
|
|
weights = torch.tensor([0.2126, 0.7152, 0.0722], device=device, dtype=dtype)
|
|
gray_1ch = r * weights[0] + g * weights[1] + b * weights[2]
|
|
elif method == "average":
|
|
# Simple average
|
|
gray_1ch = (r + g + b) / 3.0
|
|
elif method == "lightness":
|
|
# (max + min) / 2
|
|
rgb_stack = torch.cat([r, g, b], dim=-1)
|
|
gray_1ch = (torch.max(rgb_stack, dim=-1, keepdim=True)[0] +
|
|
torch.min(rgb_stack, dim=-1, keepdim=True)[0]) / 2.0
|
|
elif method == "red_only":
|
|
gray_1ch = r
|
|
elif method == "green_only":
|
|
gray_1ch = g
|
|
elif method == "blue_only":
|
|
gray_1ch = b
|
|
else:
|
|
gray_1ch = r * 0.2126 + g * 0.7152 + b * 0.0722
|
|
|
|
# Convert to RGB for display
|
|
grayscale = gray_1ch.repeat(1, 1, 1, 3)
|
|
|
|
print(f"✓ Converted to grayscale")
|
|
print(f" Output shape: {grayscale.shape}")
|
|
print("="*60 + "\n")
|
|
|
|
return (grayscale,)
|
|
|
|
|
|
class ChannelPackerORMA:
|
|
"""
|
|
Pack 4 maps into RGBA: Occlusion (R), Roughness (G), Metallic (B), Alpha (A)
|
|
Advanced ORM with alpha channel support
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"occlusion": ("IMAGE",),
|
|
"roughness": ("IMAGE",),
|
|
"metallic": ("IMAGE",),
|
|
},
|
|
"optional": {
|
|
"alpha": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("orma_packed",)
|
|
FUNCTION = "pack"
|
|
CATEGORY = "Texture Alchemist/Channel"
|
|
|
|
def pack(self, occlusion, roughness, metallic, alpha=None):
|
|
"""Pack ORMA channels"""
|
|
|
|
print("\n" + "="*60)
|
|
print("Channel Packer (ORMA)")
|
|
print("="*60)
|
|
|
|
# Get dimensions from occlusion
|
|
batch, height, width, channels = occlusion.shape
|
|
device = occlusion.device
|
|
dtype = occlusion.dtype
|
|
|
|
# Convert to grayscale if needed
|
|
o = self._to_grayscale(occlusion)
|
|
r = self._to_grayscale(roughness)
|
|
m = self._to_grayscale(metallic)
|
|
|
|
# Resize to match if sizes differ
|
|
r = self._resize_to_match(r, o)
|
|
m = self._resize_to_match(m, o)
|
|
|
|
# Handle alpha channel
|
|
if alpha is not None:
|
|
a = self._to_grayscale(alpha)
|
|
a = self._resize_to_match(a, o)
|
|
else:
|
|
# Default to fully opaque
|
|
a = torch.ones_like(o)
|
|
|
|
# Pack into RGBA
|
|
packed = torch.cat([o, r, m, a], dim=-1)
|
|
|
|
print(f"✓ ORMA packed")
|
|
print(f" Occlusion (R): {o.shape}")
|
|
print(f" Roughness (G): {r.shape}")
|
|
print(f" Metallic (B): {m.shape}")
|
|
print(f" Alpha (A): {a.shape}")
|
|
print(f" Output (RGBA): {packed.shape}")
|
|
print("="*60 + "\n")
|
|
|
|
return (packed,)
|
|
|
|
def _to_grayscale(self, image):
|
|
"""Convert to single channel grayscale"""
|
|
if image.shape[-1] == 1:
|
|
return image
|
|
elif image.shape[-1] >= 3:
|
|
# Luminance
|
|
weights = torch.tensor([0.2989, 0.5870, 0.1140],
|
|
device=image.device, dtype=image.dtype)
|
|
rgb = image[:, :, :, 0:3]
|
|
return torch.sum(rgb * weights, dim=-1, keepdim=True)
|
|
else:
|
|
# Just take first channel
|
|
return image[:, :, :, 0:1]
|
|
|
|
def _resize_to_match(self, source, target):
|
|
"""Resize source to match target dimensions"""
|
|
if source.shape[1:3] == target.shape[1:3]:
|
|
return source
|
|
|
|
target_h, target_w = target.shape[1:3]
|
|
source_bchw = source.permute(0, 3, 1, 2)
|
|
|
|
resized = torch.nn.functional.interpolate(
|
|
source_bchw,
|
|
size=(target_h, target_w),
|
|
mode='bilinear',
|
|
align_corners=False
|
|
)
|
|
|
|
return resized.permute(0, 2, 3, 1)
|
|
|
|
|
|
class ChannelPackerRMA:
|
|
"""
|
|
Pack RMA: Roughness (R), Metallic (G), AO (B)
|
|
Alternative to ORM format
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"roughness": ("IMAGE",),
|
|
"metallic": ("IMAGE",),
|
|
"ao": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("rma_packed",)
|
|
FUNCTION = "pack"
|
|
CATEGORY = "Texture Alchemist/Channel"
|
|
|
|
def pack(self, roughness, metallic, ao):
|
|
"""Pack RMA channels"""
|
|
|
|
print("\n" + "="*60)
|
|
print("Channel Packer (RMA)")
|
|
print("="*60)
|
|
|
|
# Get dimensions from roughness
|
|
batch, height, width, channels = roughness.shape
|
|
device = roughness.device
|
|
dtype = roughness.dtype
|
|
|
|
# Convert to grayscale if needed
|
|
r = self._to_grayscale(roughness)
|
|
m = self._to_grayscale(metallic)
|
|
a = self._to_grayscale(ao)
|
|
|
|
# Resize to match if sizes differ
|
|
m = self._resize_to_match(m, r)
|
|
a = self._resize_to_match(a, r)
|
|
|
|
# Pack into RGB
|
|
packed = torch.cat([r, m, a], dim=-1)
|
|
|
|
print(f"✓ RMA packed")
|
|
print(f" Roughness (R): {r.shape}")
|
|
print(f" Metallic (G): {m.shape}")
|
|
print(f" AO (B): {a.shape}")
|
|
print(f" Output (RGB): {packed.shape}")
|
|
print("="*60 + "\n")
|
|
|
|
return (packed,)
|
|
|
|
def _to_grayscale(self, image):
|
|
"""Convert to single channel grayscale"""
|
|
if image.shape[-1] == 1:
|
|
return image
|
|
elif image.shape[-1] >= 3:
|
|
# Luminance
|
|
weights = torch.tensor([0.2989, 0.5870, 0.1140],
|
|
device=image.device, dtype=image.dtype)
|
|
rgb = image[:, :, :, 0:3]
|
|
return torch.sum(rgb * weights, dim=-1, keepdim=True)
|
|
else:
|
|
# Just take first channel
|
|
return image[:, :, :, 0:1]
|
|
|
|
def _resize_to_match(self, source, target):
|
|
"""Resize source to match target dimensions"""
|
|
if source.shape[1:3] == target.shape[1:3]:
|
|
return source
|
|
|
|
target_h, target_w = target.shape[1:3]
|
|
source_bchw = source.permute(0, 3, 1, 2)
|
|
|
|
resized = torch.nn.functional.interpolate(
|
|
source_bchw,
|
|
size=(target_h, target_w),
|
|
mode='bilinear',
|
|
align_corners=False
|
|
)
|
|
|
|
return resized.permute(0, 2, 3, 1)
|
|
|
|
|
|
class ChannelPackerRMAA:
|
|
"""
|
|
Pack RMAA: Roughness (R), Metallic (G), AO (B), Alpha (A)
|
|
RMA format with alpha channel support
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"roughness": ("IMAGE",),
|
|
"metallic": ("IMAGE",),
|
|
"ao": ("IMAGE",),
|
|
},
|
|
"optional": {
|
|
"alpha": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("rmaa_packed",)
|
|
FUNCTION = "pack"
|
|
CATEGORY = "Texture Alchemist/Channel"
|
|
|
|
def pack(self, roughness, metallic, ao, alpha=None):
|
|
"""Pack RMAA channels"""
|
|
|
|
print("\n" + "="*60)
|
|
print("Channel Packer (RMAA)")
|
|
print("="*60)
|
|
|
|
# Get dimensions from roughness
|
|
batch, height, width, channels = roughness.shape
|
|
device = roughness.device
|
|
dtype = roughness.dtype
|
|
|
|
# Convert to grayscale if needed
|
|
r = self._to_grayscale(roughness)
|
|
m = self._to_grayscale(metallic)
|
|
a = self._to_grayscale(ao)
|
|
|
|
# Resize to match if sizes differ
|
|
m = self._resize_to_match(m, r)
|
|
a = self._resize_to_match(a, r)
|
|
|
|
# Handle alpha channel
|
|
if alpha is not None:
|
|
alpha_ch = self._to_grayscale(alpha)
|
|
alpha_ch = self._resize_to_match(alpha_ch, r)
|
|
else:
|
|
# Default to fully opaque
|
|
alpha_ch = torch.ones_like(r)
|
|
|
|
# Pack into RGBA
|
|
packed = torch.cat([r, m, a, alpha_ch], dim=-1)
|
|
|
|
print(f"✓ RMAA packed")
|
|
print(f" Roughness (R): {r.shape}")
|
|
print(f" Metallic (G): {m.shape}")
|
|
print(f" AO (B): {a.shape}")
|
|
print(f" Alpha (A): {alpha_ch.shape}")
|
|
print(f" Output (RGBA): {packed.shape}")
|
|
print("="*60 + "\n")
|
|
|
|
return (packed,)
|
|
|
|
def _to_grayscale(self, image):
|
|
"""Convert to single channel grayscale"""
|
|
if image.shape[-1] == 1:
|
|
return image
|
|
elif image.shape[-1] >= 3:
|
|
# Luminance
|
|
weights = torch.tensor([0.2989, 0.5870, 0.1140],
|
|
device=image.device, dtype=image.dtype)
|
|
rgb = image[:, :, :, 0:3]
|
|
return torch.sum(rgb * weights, dim=-1, keepdim=True)
|
|
else:
|
|
# Just take first channel
|
|
return image[:, :, :, 0:1]
|
|
|
|
def _resize_to_match(self, source, target):
|
|
"""Resize source to match target dimensions"""
|
|
if source.shape[1:3] == target.shape[1:3]:
|
|
return source
|
|
|
|
target_h, target_w = target.shape[1:3]
|
|
source_bchw = source.permute(0, 3, 1, 2)
|
|
|
|
resized = torch.nn.functional.interpolate(
|
|
source_bchw,
|
|
size=(target_h, target_w),
|
|
mode='bilinear',
|
|
align_corners=False
|
|
)
|
|
|
|
return resized.permute(0, 2, 3, 1)
|
|
|
|
|
|
class ChannelUnpackerRMA:
|
|
"""
|
|
Unpack RMA: Extract Roughness, Metallic, AO from packed RGB
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"rma_packed": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE")
|
|
RETURN_NAMES = ("roughness", "metallic", "ao")
|
|
FUNCTION = "unpack"
|
|
CATEGORY = "Texture Alchemist/Channel"
|
|
|
|
def unpack(self, rma_packed):
|
|
"""Unpack RMA channels"""
|
|
|
|
print("\n" + "="*60)
|
|
print("Channel Unpacker (RMA)")
|
|
print("="*60)
|
|
print(f"Input shape: {rma_packed.shape}")
|
|
|
|
batch, height, width, channels = rma_packed.shape
|
|
device = rma_packed.device
|
|
dtype = rma_packed.dtype
|
|
|
|
# Extract channels
|
|
if channels >= 3:
|
|
roughness = rma_packed[:, :, :, 0:1]
|
|
metallic = rma_packed[:, :, :, 1:2]
|
|
ao = rma_packed[:, :, :, 2:3]
|
|
elif channels == 2:
|
|
roughness = rma_packed[:, :, :, 0:1]
|
|
metallic = rma_packed[:, :, :, 1:2]
|
|
ao = torch.zeros((batch, height, width, 1), device=device, dtype=dtype)
|
|
else:
|
|
roughness = rma_packed
|
|
metallic = torch.zeros((batch, height, width, 1), device=device, dtype=dtype)
|
|
ao = torch.zeros((batch, height, width, 1), device=device, dtype=dtype)
|
|
|
|
# Convert to RGB for display
|
|
roughness_rgb = roughness.repeat(1, 1, 1, 3)
|
|
metallic_rgb = metallic.repeat(1, 1, 1, 3)
|
|
ao_rgb = ao.repeat(1, 1, 1, 3)
|
|
|
|
print(f"✓ RMA unpacked")
|
|
print(f" Roughness: {roughness_rgb.shape}")
|
|
print(f" Metallic: {metallic_rgb.shape}")
|
|
print(f" AO: {ao_rgb.shape}")
|
|
print("="*60 + "\n")
|
|
|
|
return (roughness_rgb, metallic_rgb, ao_rgb)
|
|
|
|
|
|
# Node registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"ChannelPacker": ChannelPacker,
|
|
"ChannelUnpacker": ChannelUnpacker,
|
|
"GrayscaleToColor": GrayscaleToColor,
|
|
"ColorToGrayscale": ColorToGrayscale,
|
|
"ChannelPackerORMA": ChannelPackerORMA,
|
|
"ChannelPackerRMA": ChannelPackerRMA,
|
|
"ChannelPackerRMAA": ChannelPackerRMAA,
|
|
"ChannelUnpackerRMA": ChannelUnpackerRMA,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"ChannelPacker": "Channel Packer (RGB)",
|
|
"ChannelUnpacker": "Channel Unpacker (RGB)",
|
|
"GrayscaleToColor": "Grayscale to Color",
|
|
"ColorToGrayscale": "Color to Grayscale",
|
|
"ChannelPackerORMA": "Channel Packer (ORMA)",
|
|
"ChannelPackerRMA": "Channel Packer (RMA)",
|
|
"ChannelPackerRMAA": "Channel Packer (RMAA)",
|
|
"ChannelUnpackerRMA": "Channel Unpacker (RMA)",
|
|
}
|
|
|