File size: 7,898 Bytes
433dab5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | """Colormap loading and management for CPT/ACT/RGB files."""
import os
import re
from typing import List, Tuple, Optional
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
def parse_cpt_file(filepath: str) -> LinearSegmentedColormap:
"""
Load a GMT-style CPT (Color Palette Table) file.
Args:
filepath: Path to the CPT file
Returns:
matplotlib LinearSegmentedColormap
"""
colors = []
positions = []
with open(filepath, 'r') as f:
lines = f.readlines()
# Parse CPT format
for line in lines:
line = line.strip()
# Skip comments and empty lines
if line.startswith('#') or not line or line.startswith('B') or line.startswith('F') or line.startswith('N'):
continue
parts = line.split()
if len(parts) >= 8:
# CPT format: z0 r0 g0 b0 z1 r1 g1 b1
try:
z0, r0, g0, b0, z1, r1, g1, b1 = map(float, parts[:8])
# Normalize RGB values if they're in 0-255 range
if r0 > 1 or g0 > 1 or b0 > 1:
r0, g0, b0 = r0/255, g0/255, b0/255
if r1 > 1 or g1 > 1 or b1 > 1:
r1, g1, b1 = r1/255, g1/255, b1/255
colors.extend([(r0, g0, b0), (r1, g1, b1)])
positions.extend([z0, z1])
except ValueError:
continue
if not colors:
raise ValueError(f"No valid color data found in {filepath}")
# Normalize positions to 0-1 range
positions = np.array(positions)
if len(set(positions)) > 1:
positions = (positions - positions.min()) / (positions.max() - positions.min())
else:
positions = np.linspace(0, 1, len(positions))
# Create colormap
cmap_data = list(zip(positions, colors))
cmap_data.sort(key=lambda x: x[0]) # Sort by position
# Remove duplicates
unique_data = []
seen_positions = set()
for pos, color in cmap_data:
if pos not in seen_positions:
unique_data.append((pos, color))
seen_positions.add(pos)
positions, colors = zip(*unique_data)
# Create the colormap
name = os.path.splitext(os.path.basename(filepath))[0]
return LinearSegmentedColormap.from_list(name, list(zip(positions, colors)))
def parse_act_file(filepath: str) -> ListedColormap:
"""
Load an Adobe Color Table (ACT) file.
Args:
filepath: Path to the ACT file
Returns:
matplotlib ListedColormap
"""
with open(filepath, 'rb') as f:
data = f.read()
# ACT files are 768 bytes (256 colors * 3 RGB values)
if len(data) != 768:
raise ValueError(f"Invalid ACT file size: {len(data)} bytes (expected 768)")
colors = []
for i in range(0, 768, 3):
r, g, b = data[i], data[i+1], data[i+2]
colors.append((r/255.0, g/255.0, b/255.0))
name = os.path.splitext(os.path.basename(filepath))[0]
return ListedColormap(colors, name=name)
def parse_rgb_file(filepath: str) -> ListedColormap:
"""
Load a simple RGB text file (one color per line).
Args:
filepath: Path to the RGB file
Returns:
matplotlib ListedColormap
"""
colors = []
with open(filepath, 'r') as f:
lines = f.readlines()
for line in lines:
line = line.strip()
# Skip comments and empty lines
if line.startswith('#') or not line:
continue
# Parse RGB values
parts = line.split()
if len(parts) >= 3:
try:
r, g, b = map(float, parts[:3])
# Normalize RGB values if they're in 0-255 range
if r > 1 or g > 1 or b > 1:
r, g, b = r/255, g/255, b/255
colors.append((r, g, b))
except ValueError:
continue
if not colors:
raise ValueError(f"No valid color data found in {filepath}")
name = os.path.splitext(os.path.basename(filepath))[0]
return ListedColormap(colors, name=name)
def load_cpt(filepath: str) -> LinearSegmentedColormap:
"""Load a CPT colormap file."""
return parse_cpt_file(filepath)
def load_act(filepath: str) -> ListedColormap:
"""Load an ACT colormap file."""
return parse_act_file(filepath)
def load_rgb(filepath: str) -> ListedColormap:
"""Load an RGB colormap file."""
return parse_rgb_file(filepath)
def load_colormap(filepath: str) -> mcolors.Colormap:
"""
Load a colormap from file, auto-detecting the format.
Args:
filepath: Path to the colormap file
Returns:
matplotlib Colormap
"""
_, ext = os.path.splitext(filepath.lower())
if ext == '.cpt':
return load_cpt(filepath)
elif ext == '.act':
return load_act(filepath)
elif ext in ['.rgb', '.txt']:
return load_rgb(filepath)
else:
# Try to guess based on content
try:
return load_cpt(filepath)
except:
try:
return load_rgb(filepath)
except:
raise ValueError(f"Cannot determine colormap format for {filepath}")
def get_matplotlib_colormaps() -> List[str]:
"""Get list of available matplotlib colormaps."""
return sorted(plt.colormaps())
def create_diverging_colormap(name: str, colors: List[str], center: float = 0.5) -> LinearSegmentedColormap:
"""
Create a diverging colormap.
Args:
name: Name for the colormap
colors: List of color names/hex codes
center: Position of the center color (0-1)
Returns:
LinearSegmentedColormap
"""
return LinearSegmentedColormap.from_list(name, colors)
def reverse_colormap(cmap: mcolors.Colormap) -> mcolors.Colormap:
"""Reverse a colormap."""
if hasattr(cmap, 'reversed'):
return cmap.reversed()
else:
# For custom colormaps
if isinstance(cmap, LinearSegmentedColormap):
return LinearSegmentedColormap.from_list(
f"{cmap.name}_r",
cmap(np.linspace(0, 1, 256))[::-1]
)
elif isinstance(cmap, ListedColormap):
return ListedColormap(
cmap.colors[::-1],
name=f"{cmap.name}_r"
)
else:
return cmap
def colormap_to_cpt(cmap: mcolors.Colormap, filepath: str, n_colors: int = 256):
"""
Save a matplotlib colormap as a CPT file.
Args:
cmap: matplotlib Colormap
filepath: Output file path
n_colors: Number of color levels
"""
colors = cmap(np.linspace(0, 1, n_colors))
with open(filepath, 'w') as f:
f.write(f"# Color palette: {cmap.name}\n")
f.write("# Created by TensorView\n")
for i in range(len(colors) - 1):
r0, g0, b0 = colors[i][:3]
r1, g1, b1 = colors[i+1][:3]
z0 = i / (n_colors - 1)
z1 = (i + 1) / (n_colors - 1)
f.write(f"{z0:.6f}\t{r0*255:.0f}\t{g0*255:.0f}\t{b0*255:.0f}\t")
f.write(f"{z1:.6f}\t{r1*255:.0f}\t{g1*255:.0f}\t{b1*255:.0f}\n")
def get_colormap_info(cmap: mcolors.Colormap) -> dict:
"""Get information about a colormap."""
info = {
'name': getattr(cmap, 'name', 'unknown'),
'type': type(cmap).__name__,
'n_colors': getattr(cmap, 'N', 'continuous')
}
if isinstance(cmap, ListedColormap):
info['n_colors'] = len(cmap.colors)
return info |