From 6c4934af8e279b305df9319e72ab357bd4308cb6 Mon Sep 17 00:00:00 2001 From: uermel Date: Fri, 9 Oct 2026 11:56:31 -0700 Subject: [PATCH] feat: colour filaments by object, and filament transparency Filament tubes were always coloured by filament ID, with no way to make them transparent. The new `copick filament style [color instance|object] [transparency PERCENT]` command, and the Copick Annotate window's "Colour by instance" checkbox and "Transparency" box, colour all filament sets by instance or by their object's colour and set the tubes' transparency. Instance colours take their alpha from the object's colour, so the transparency is the same in both modes; it reduces the alpha of the object's colour. Both settings are remembered; control points stay opaque. In object mode the filament list's swatches show the object's colour too. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 4 ++ src/cmd/core.py | 25 ++++++++++++ src/docs/user/commands/copick_filaments.html | 20 ++++++++-- src/filaments/controller.py | 31 ++++++++++++++- src/filaments/model.py | 42 ++++++++++++++++++-- src/misc/settings.py | 2 + src/ui/annotation_panels.py | 21 ++++++++++ 7 files changed, 137 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12caea5..4a73e90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,10 @@ synopsis = "Join filaments end to end (default: those selected in the Annotate w category = "General" synopsis = "Cut mode on or off, or cut a filament in two at a point." +[chimerax.command."copick filament style"] +category = "General" +synopsis = "Colour filaments by instance or object, and set their transparency." + [chimerax.command."copick filament save"] category = "General" synopsis = "Save the active filament set, optionally with sampled picks." diff --git a/src/cmd/core.py b/src/cmd/core.py index 19e51a5..dd874d5 100644 --- a/src/cmd/core.py +++ b/src/cmd/core.py @@ -740,6 +740,22 @@ def copick_filament_cut(session, state: str = "toggle", at=None, instance_id: Op ctl.stop_tracing() +def copick_filament_style(session, color: Optional[str] = None, transparency: Optional[float] = None): + """Colour filaments by instance ID or by their object's colour, and set their transparency (percent). + + Applies to every filament set and is remembered. Instance colours take their alpha from the object's colour, so + the transparency is the same in both modes. Without options, reports the current style. + """ + tool = _get_running_tool(session) + if tool is None: + return + ctl = tool.filaments + if color is not None or transparency is not None: + ctl.set_style(color_by_instance=None if color is None else color == "instance", transparency=transparency) + mode = "instance" if ctl.color_by_instance() else "object" + session.logger.info(f"copick: filaments coloured by {mode}, transparency {ctl.transparency():g}%") + + def copick_filament_save( session, user_id: Optional[str] = None, @@ -1157,6 +1173,15 @@ def register_annotation_commands(): ), copick_filament_cut, ) + register( + "copick filament style", + CmdDesc( + keyword=[("color", EnumOf(["instance", "object"])), ("transparency", FloatArg)], + synopsis="Colour filaments by instance or object, and set their transparency.", + url=entity_url, + ), + copick_filament_style, + ) register( "copick filament save", CmdDesc( diff --git a/src/docs/user/commands/copick_filaments.html b/src/docs/user/commands/copick_filaments.html index 1243274..10450bd 100644 --- a/src/docs/user/commands/copick_filaments.html +++ b/src/docs/user/commands/copick_filaments.html @@ -19,7 +19,9 @@

Usage:
copick filament (new | select | focus | go | next | previous | reverse | delete | convert) [ filament-id ]
copick filament save [ userId id ] [ sessionId id ] - [ pickSpacing Å ]

+ [ pickSpacing Å ] +
copick filament style [ color instance | object ] + [ transparency percent ]

Filaments are traced centrelines (e.g. microtubules, actin) of objects declared as filaments @@ -27,6 +29,15 @@

Usage: coloured by its filament ID; picks sampled from a filament carry the same ID and colour.

+ Colour and transparency: copick filament style color object draws all tubes in the object's colour + instead (color instance switches back), and copick filament style transparency percent makes + the tubes transparent. Instance colours take their alpha from the object's colour, so the transparency is the same + in both modes; it reduces the alpha of the object's colour (0 keeps it, 100 hides the tubes). Both settings apply + to all filament sets and are remembered; control points stay opaque. The Copick Annotate window has the same + controls (Colour by instance above the filament list, Transparency). Without options the command + reports the current style. +

+

Tracing: copick filament trace on binds the right mouse button to trace copick filament. Click on the shown tomogram plane (single plane, orthoplanes or tilted slab) to add a control point to the active filament; drag a control point to move it; shift-click a control point to remove it. The centreline is @@ -82,6 +93,8 @@

Usage: reverse [id]Reverse the point order (polarity) of a filament. delete [id]Delete a filament. convert [id]Convert a B-spline fit to an editable Catmull-Rom curve. + stylecopick filament style: colour by instance or by object, and the tubes' + transparency (all sets, remembered). saveSave the active set. With pickSpacing, picks are also sampled every Å along each filament (+Z along the filament, instance ID = filament ID); they replace the whole picks set of the same object, user and session. Session 0 (tool output) cannot be written. @@ -92,10 +105,11 @@

Usage:
copick open filaments microtubule:tracer/1
copick new filaments microtubule
copick filament trace on
- copick filament save sessionId manual-2 pickSpacing 82
+ copick filament save sessionId manual-2 pickSpacing 82
+ copick filament style color object transparency 50

The Filaments tab, the Copick Edit toolbar tab and the shortcuts - tf nf fn fp rv xf run the same commands.

+ tf cf nf fn fp rv jf xf run the same commands.

See also: copick instance, copick open / show / hide, diff --git a/src/filaments/controller.py b/src/filaments/controller.py index 8626418..af738d7 100644 --- a/src/filaments/controller.py +++ b/src/filaments/controller.py @@ -98,6 +98,33 @@ def edit_session(self) -> Optional[FilamentEditSession]: def tube_scale(self) -> float: return float(getattr(self.tool.settings, "filament_tube_scale", 0.5)) + def color_by_instance(self) -> bool: + return bool(getattr(self.tool.settings, "filament_color_by_instance", True)) + + def transparency(self) -> float: + return float(getattr(self.tool.settings, "filament_transparency", 0.0)) + + def _new_model(self, edit: FilamentEditSession) -> FilamentSetModel: + return FilamentSetModel( + self.session, + edit, + tube_scale=self.tube_scale(), + color_by_instance=self.color_by_instance(), + transparency=self.transparency(), + ) + + def set_style(self, color_by_instance: Optional[bool] = None, transparency: Optional[float] = None) -> None: + """Colour every filament set by instance or by object, and set the tubes' transparency (percent; saved).""" + settings = self.tool.settings + if color_by_instance is not None: + settings.filament_color_by_instance = bool(color_by_instance) + if transparency is not None: + settings.filament_transparency = min(max(float(transparency), 0.0), 100.0) + for _edit, model in list(self.entries.values()): + if not model.deleted: + model.set_style(self.color_by_instance(), self.transparency()) + self.notify() + # -- open / close ---------------------------------------------------------------------------------------------- def show(self, copick_filaments: Any) -> FilamentSetModel: @@ -107,7 +134,7 @@ def show(self, copick_filaments: Any) -> FilamentSetModel: step = copick_filaments.voxel_spacing or self._tomogram_voxel_size() or 10.0 edit = FilamentEditSession.from_filaments(copick_filaments, step=step) self._track_history(edit) - model = FilamentSetModel(self.session, edit, tube_scale=self.tube_scale()) + model = self._new_model(edit) self.session.models.add([model]) model.redraw() self.entries[copick_filaments] = (edit, model) @@ -135,7 +162,7 @@ def new(self, object_name: str, user_id: str, session_id: str) -> FilamentEditSe step = self._tomogram_voxel_size() or 10.0 edit = FilamentEditSession(run, object_name, user_id, session_id, step=step) self._track_history(edit) - model = FilamentSetModel(self.session, edit, tube_scale=self.tube_scale()) + model = self._new_model(edit) self.session.models.add([model]) self.entries[edit] = (edit, model) self.active_key = edit diff --git a/src/filaments/model.py b/src/filaments/model.py index 7c3d11d..b97e6b8 100644 --- a/src/filaments/model.py +++ b/src/filaments/model.py @@ -35,11 +35,20 @@ class FilamentSetModel(Model): """Tubes for the filaments of one ``FilamentEditSession``.""" - def __init__(self, session, edit_session, tube_scale: float = 0.5): + def __init__( + self, + session, + edit_session, + tube_scale: float = 0.5, + color_by_instance: bool = True, + transparency: float = 0.0, + ): name = f"{edit_session.object_name} filaments ({edit_session.user_id}/{edit_session.session_id})" Model.__init__(self, name, session) self.edit_session = edit_session self.tube_scale = float(tube_scale) + self.color_by_instance = bool(color_by_instance) + self.transparency = min(max(float(transparency), 0.0), 100.0) self._tubes: Dict[int, Surface] = {} self._controls: Optional[Surface] = None self._handle_radius: Optional[float] = None @@ -70,8 +79,30 @@ def handle_radius(self) -> float: tubes = [self.tube_radius(i) for i in self.edit_session.filaments] or [self.radius * self.tube_scale] return max(HANDLE_OVER_TUBE * max(tubes), HANDLE_MIN_STEPS * self.edit_session.step) + @property + def single_rgba(self) -> np.ndarray: + """The set's single colour: the object's colour, its alpha reduced by ``transparency`` (percent).""" + rgba = self.base_rgba.astype(float) + rgba[3] *= (100.0 - self.transparency) / 100.0 + return np.round(rgba).astype(np.uint8) + def color_of(self, instance_id: int) -> np.ndarray: - return instance_colors([instance_id], self.base_rgba, dtype=np.uint8)[0] + """A tube's colour: its instance colour, with the alpha of the single colour, or the single colour itself.""" + single = self.single_rgba + if not self.color_by_instance: + return single + return instance_colors([instance_id], single, dtype=np.uint8)[0] + + def set_style(self, color_by_instance: Optional[bool] = None, transparency: Optional[float] = None) -> None: + """Recolour the tubes (and handles) without rebuilding them.""" + if color_by_instance is not None: + self.color_by_instance = bool(color_by_instance) + if transparency is not None: + self.transparency = min(max(float(transparency), 0.0), 100.0) + for instance_id, surf in self._tubes.items(): + if not surf.deleted: + surf.color = self.color_of(instance_id) + self._draw_controls() # ---------------------------------------------------------------------------------------------------------------- # Drawing @@ -136,7 +167,12 @@ def _draw_controls(self) -> None: k = ACTIVE_HANDLE_SCALE if i == active else 1.0 places.append(Place(axes=np.eye(3) * k, origin=p)) self._controls.positions = Places(places) - colors = instance_colors(ids, self.base_rgba, dtype=np.uint8) + opaque = self.base_rgba.copy() + opaque[3] = 255 # handles stay opaque, so editing stays readable on transparent tubes + if self.color_by_instance: + colors = instance_colors(ids, opaque, dtype=np.uint8) + else: + colors = np.tile(opaque, (len(ids), 1)) colors[ids == active] = (255, 255, 255, 255) # the active filament's handles are white self._controls.colors = colors self._controls.display = True diff --git a/src/misc/settings.py b/src/misc/settings.py index c4babbc..e191be3 100644 --- a/src/misc/settings.py +++ b/src/misc/settings.py @@ -14,6 +14,8 @@ class CoPickSettings(Settings): "spotlight_particles": False, # Also hide particles outside the spotlight sphere "filament_tube_scale": 0.5, # Filament tube radius as a fraction of the object radius "filament_pick_spacing": 0.0, # Default spacing of picks sampled along filaments (0 = object radius) + "filament_color_by_instance": True, # Colour filament tubes by filament ID (False: the object's colour) + "filament_transparency": 0.0, # Transparency of filament tubes, percent (0 = the object colour's alpha) "paint_radius": 50.0, # Instance paint brush radius (Å) "color_picks_by_instance": False, # Colour every particle list by instance ID (filaments always are) } diff --git a/src/ui/annotation_panels.py b/src/ui/annotation_panels.py index b084ecf..25c5a75 100644 --- a/src/ui/annotation_panels.py +++ b/src/ui/annotation_panels.py @@ -146,12 +146,21 @@ def __init__(self, tool: Any, parent=None): lambda: self._cmd("copick filament convert"), ) self.save_btn = _button("💾", "Save the filament set…", self._save) + self.transparency_spin = QSpinBox() + self.transparency_spin.setRange(0, 100) + self.transparency_spin.setSuffix(" %") + self.transparency_spin.setKeyboardTracking(False) # one command per committed value + self.transparency_spin.setToolTip( + "Transparency of the filament tubes, whether coloured by instance or by object (all filament sets)", + ) + self.transparency_spin.valueChanged.connect(lambda v: self._cmd(f"copick filament style transparency {v}")) layout.addWidget( _flow_row( self.trace_btn, self.cut_btn, _labelled("Filament", self.active_spin), _labelled("New points", self.mode_combo), + _labelled("Transparency", self.transparency_spin), self.convert_btn, self.save_btn, ), @@ -164,6 +173,10 @@ def __init__(self, tool: Any, parent=None): reverse=True, merge=True, merge_tip="Join the selected filaments end to end (into the current one)", + color_toggle=True, + ) + self.browser.color_by_instance_toggled.connect( + lambda on: self._cmd(f"copick filament style color {'instance' if on else 'object'}"), ) self.browser.current_changed.connect(lambda k: self._cmd(f"copick filament select {k}")) self.browser.focus_requested.connect(lambda k: self._cmd(f"copick filament focus {k}")) @@ -231,6 +244,10 @@ def refresh(self) -> None: edit = self.ctl.edit_session from ..filaments.controller import CUT_MODE, TRACE_MODE + self.browser.set_color_by_instance(self.ctl.color_by_instance()) + self.transparency_spin.blockSignals(True) + self.transparency_spin.setValue(round(self.ctl.transparency())) + self.transparency_spin.blockSignals(False) right = self.ctl.right_mode if self.ctl.editing else None for btn, mode in ((self.trace_btn, TRACE_MODE), (self.cut_btn, CUT_MODE)): btn.blockSignals(True) @@ -261,6 +278,10 @@ def refresh(self) -> None: from copick_shared_ui.util.instances import InstanceRow, rows_from_filaments rows = rows_from_filaments(edit.to_list()) + if not self.ctl.color_by_instance(): # the swatches show what the tubes show + rgba = tuple(float(c) / 255 for c in (getattr(edit.object, "color", None) or (255, 255, 255, 255))[:4]) + for r in rows: + r.color = rgba known = {r.instance_id for r in rows} rows += [InstanceRow(i, count=len(c), label="pending") for i, c in edit.pending.items() if i not in known] self.browser.set_rows(sorted(rows, key=lambda r: r.instance_id), kind="filaments")