Skip to content
Snippets Groups Projects
Commit dc47e981 authored by s224361's avatar s224361
Browse files

Cleaned up modules

parents 637188e8 071cf913
Branches
No related tags found
No related merge requests found
File added
data/AgamodonSlice.png

87.1 KiB

data/AngustifronsSlice35.png

91.3 KiB

data/BipesSlice4.png

44.1 KiB

data/BipesSlice4NoCropping.png

57.3 KiB

from skimage.graph import route_through_array from skimage.graph import route_through_array
def find_path(cost_image, points): def find_path(cost_image: "numpy.ndarray", points: list) -> list:
"""
Find the optimal path through a cost image between two points.
Parameters:
cost_image (numpy.ndarray): A 2D array representing the cost of traversing each pixel.
points (list): A list containing two tuples, each representing the
(row, column) coordinates of the seed and target points.
Returns:
list: A list of (row, column) tuples representing the path from the seed to the target point.
Raises:
ValueError: If the points list does not contain exactly two points.
"""
if len(points) != 2: if len(points) != 2:
raise ValueError("Points should be a list of 2 points: seed and target.") raise ValueError("Points should be a list of 2 points: seed and target.")
......
import math
from scipy.signal import savgol_filter from scipy.signal import savgol_filter
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsPixmapItem from PyQt5.QtWidgets import QGraphicsScene, QGraphicsPixmapItem
from PyQt5.QtGui import QPixmap, QColor from PyQt5.QtGui import QPixmap, QColor
from PyQt5.QtCore import Qt, QRectF from PyQt5.QtCore import Qt, QRectF, QPoint
import math
import numpy as np import numpy as np
from panZoomGraphicsView import PanZoomGraphicsView from panZoomGraphicsView import PanZoomGraphicsView
from labeledPointItem import LabeledPointItem from labeledPointItem import LabeledPointItem
...@@ -10,6 +10,14 @@ from find_path import find_path ...@@ -10,6 +10,14 @@ from find_path import find_path
class ImageGraphicsView(PanZoomGraphicsView): class ImageGraphicsView(PanZoomGraphicsView):
"""
A custom QGraphicsView for displaying and interacting with an image.
This class extends PanZoomGraphicsView to provide additional functionality
for loading images, adding labeled anchor points, and computing paths
between points based on a cost image.
"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.scene = QGraphicsScene(self) self.scene = QGraphicsScene(self)
...@@ -19,10 +27,10 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -19,10 +27,10 @@ class ImageGraphicsView(PanZoomGraphicsView):
self.image_item = QGraphicsPixmapItem() self.image_item = QGraphicsPixmapItem()
self.scene.addItem(self.image_item) self.scene.addItem(self.image_item)
self.anchor_points = [] # List[(x, y)] self.anchor_points = []
self.point_items = [] # LabeledPointItem self.point_items = []
self.full_path_points = [] # QGraphicsEllipseItems for path self.full_path_points = []
self._full_path_xy = [] # entire path coords (smoothed) self._full_path_xy = []
self.dot_radius = 4 self.dot_radius = 4
self.path_radius = 1 self.path_radius = 1
...@@ -49,16 +57,18 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -49,16 +57,18 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._savgol_window_length = 7 self._savgol_window_length = 7
def set_rainbow_enabled(self, enabled: bool): def set_rainbow_enabled(self, enabled: bool):
"""Enable rainbow coloring of the path."""
self._rainbow_enabled = enabled self._rainbow_enabled = enabled
self._rebuild_full_path() self._rebuild_full_path()
def toggle_rainbow(self): def toggle_rainbow(self):
"""Toggle rainbow coloring of the path."""
self._rainbow_enabled = not self._rainbow_enabled self._rainbow_enabled = not self._rainbow_enabled
self._rebuild_full_path() self._rebuild_full_path()
def set_savgol_window_length(self, wlen: int): def set_savgol_window_length(self, wlen: int):
if wlen < 3: """Set the window length for Savitzky-Golay smoothing."""
wlen = 3 wlen = max(3, wlen)
if wlen % 2 == 0: if wlen % 2 == 0:
wlen += 1 wlen += 1
self._savgol_window_length = wlen self._savgol_window_length = wlen
...@@ -68,7 +78,8 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -68,7 +78,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# LOADING # LOADING
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def load_image(self, path): def load_image(self, path: str):
"""Load an image from a file path."""
pixmap = QPixmap(path) pixmap = QPixmap(path)
if not pixmap.isNull(): if not pixmap.isNull():
self.image_item.setPixmap(pixmap) self.image_item.setPixmap(pixmap)
...@@ -90,7 +101,9 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -90,7 +101,9 @@ class ImageGraphicsView(PanZoomGraphicsView):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# ANCHOR POINTS # ANCHOR POINTS
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _insert_anchor_point(self, idx, x, y, label="", removable=True, z_val=0, radius=4): def _insert_anchor_point(self, idx, x: float, y: float, label="", removable=True,
z_val=0, radius=4):
"""Insert an anchor point at a specific index."""
x_clamped = self._clamp(x, radius, self._img_w - radius) x_clamped = self._clamp(x, radius, self._img_w - radius)
y_clamped = self._clamp(y, radius, self._img_h - radius) y_clamped = self._clamp(y, radius, self._img_h - radius)
...@@ -110,7 +123,7 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -110,7 +123,7 @@ class ImageGraphicsView(PanZoomGraphicsView):
self.scene.addItem(item) self.scene.addItem(item)
def _add_guide_point(self, x, y): def _add_guide_point(self, x, y):
# Ensure we clamp properly """Add a guide point to the path."""
x_clamped = self._clamp(x, self.dot_radius, self._img_w - self.dot_radius) x_clamped = self._clamp(x, self.dot_radius, self._img_w - self.dot_radius)
y_clamped = self._clamp(y, self.dot_radius, self._img_h - self.dot_radius) y_clamped = self._clamp(y, self.dot_radius, self._img_h - self.dot_radius)
...@@ -125,7 +138,8 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -125,7 +138,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._apply_all_guide_points_to_cost() self._apply_all_guide_points_to_cost()
self._rebuild_full_path() self._rebuild_full_path()
def _insert_anchor_between_subpath(self, x_new, y_new): def _insert_anchor_between_subpath(self, x_new: float, y_new: float ):
"""Insert an anchor point between existing anchor points."""
# If somehow we have no path yet # If somehow we have no path yet
if not self._full_path_xy: if not self._full_path_xy:
self._insert_anchor_point(-1, x_new, y_new) self._insert_anchor_point(-1, x_new, y_new)
...@@ -147,9 +161,11 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -147,9 +161,11 @@ class ImageGraphicsView(PanZoomGraphicsView):
return return
def approx_equal(xa, ya, xb, yb, tol=1e-3): def approx_equal(xa, ya, xb, yb, tol=1e-3):
"""Check if two points are approximately equal."""
return (abs(xa - xb) < tol) and (abs(ya - yb) < tol) return (abs(xa - xb) < tol) and (abs(ya - yb) < tol)
def is_anchor(coord): def is_anchor(coord):
"""Check if a point is an anchor point."""
cx, cy = coord cx, cy = coord
for (ax, ay) in self.anchor_points: for (ax, ay) in self.anchor_points:
if approx_equal(ax, ay, cx, cy): if approx_equal(ax, ay, cx, cy):
...@@ -158,23 +174,23 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -158,23 +174,23 @@ class ImageGraphicsView(PanZoomGraphicsView):
# Walk left # Walk left
left_anchor_pt = None left_anchor_pt = None
iL = best_idx i_l = best_idx
while iL >= 0: while i_l >= 0:
px, py = self._full_path_xy[iL] px, py = self._full_path_xy[i_l]
if is_anchor((px, py)): if is_anchor((px, py)):
left_anchor_pt = (px, py) left_anchor_pt = (px, py)
break break
iL -= 1 i_l -= 1
# Walk right # Walk right
right_anchor_pt = None right_anchor_pt = None
iR = best_idx i_r = best_idx
while iR < len(self._full_path_xy): while i_r < len(self._full_path_xy):
px, py = self._full_path_xy[iR] px, py = self._full_path_xy[i_r]
if is_anchor((px, py)): if is_anchor((px, py)):
right_anchor_pt = (px, py) right_anchor_pt = (px, py)
break break
iR += 1 i_r += 1
# If we can't find distinct anchors on left & right, # If we can't find distinct anchors on left & right,
# just insert before E. # just insert before E.
...@@ -221,7 +237,8 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -221,7 +237,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
if self.point_items[i].is_removable(): if self.point_items[i].is_removable():
self._lower_cost_in_circle(ax, ay, self.radius_cost_image) self._lower_cost_in_circle(ax, ay, self.radius_cost_image)
def _lower_cost_in_circle(self, x_f, y_f, radius): def _lower_cost_in_circle(self, x_f: float, y_f: float, radius: int):
"""Lower the cost in a circle centered at (x_f, y_f)."""
if self.cost_image is None: if self.cost_image is None:
return return
h, w = self.cost_image.shape h, w = self.cost_image.shape
...@@ -244,6 +261,7 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -244,6 +261,7 @@ class ImageGraphicsView(PanZoomGraphicsView):
# PATH BUILDING # PATH BUILDING
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _rebuild_full_path(self): def _rebuild_full_path(self):
"""Rebuild the full path based on the anchor points."""
for item in self.full_path_points: for item in self.full_path_points:
self.scene.removeItem(item) self.scene.removeItem(item)
self.full_path_points.clear() self.full_path_points.clear()
...@@ -295,7 +313,8 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -295,7 +313,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
if p_item._text_item: if p_item._text_item:
p_item.setZValue(100) p_item.setZValue(100)
def _compute_subpath_xy(self, xA, yA, xB, yB): def _compute_subpath_xy(self, xA: float, yA: float, xB: float, yB: float):
"""Compute a subpath between two points."""
if self.cost_image is None: if self.cost_image is None:
return [] return []
h, w = self.cost_image.shape h, w = self.cost_image.shape
...@@ -313,7 +332,8 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -313,7 +332,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
# Convert from (row, col) to (x, y) # Convert from (row, col) to (x, y)
return [(c, r) for (r, c) in path_rc] return [(c, r) for (r, c) in path_rc]
def _rainbow_color(self, fraction): def _rainbow_color(self, fraction: float):
"""Get a rainbow color."""
hue = int(300 * fraction) hue = int(300 * fraction)
saturation = 255 saturation = 255
value = 255 value = 255
...@@ -322,7 +342,8 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -322,7 +342,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# MOUSE EVENTS # MOUSE EVENTS
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def mousePressEvent(self, event): def mouse_press_event(self, event):
"""Handle mouse press events for dragging a point or adding a point."""
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
self._mouse_pressed = True self._mouse_pressed = True
self._was_dragging = False self._was_dragging = False
...@@ -341,9 +362,10 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -341,9 +362,10 @@ class ImageGraphicsView(PanZoomGraphicsView):
elif event.button() == Qt.RightButton: elif event.button() == Qt.RightButton:
self._remove_point_by_click(event.pos()) self._remove_point_by_click(event.pos())
super().mousePressEvent(event) super().mouse_press_event(event)
def mouseMoveEvent(self, event): def mouse_move_event(self, event):
"""Handle mouse move events for dragging a point or dragging the view"""
if self._dragging_idx is not None: if self._dragging_idx is not None:
scene_pos = self.mapToScene(event.pos()) scene_pos = self.mapToScene(event.pos())
x_new = scene_pos.x() - self._drag_offset[0] x_new = scene_pos.x() - self._drag_offset[0]
...@@ -368,10 +390,11 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -368,10 +390,11 @@ class ImageGraphicsView(PanZoomGraphicsView):
if dist > self._drag_threshold: if dist > self._drag_threshold:
self._was_dragging = True self._was_dragging = True
super().mouseMoveEvent(event) super().mouse_move_event(event)
def mouseReleaseEvent(self, event): def mouse_release_event(self, event):
super().mouseReleaseEvent(event) """Handle mouse release events for dragging a point or adding a point."""
super().mouse_release_event(event)
if event.button() == Qt.LeftButton and self._mouse_pressed: if event.button() == Qt.LeftButton and self._mouse_pressed:
self._mouse_pressed = False self._mouse_pressed = False
self.setCursor(Qt.ArrowCursor) self.setCursor(Qt.ArrowCursor)
...@@ -394,7 +417,8 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -394,7 +417,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._was_dragging = False self._was_dragging = False
def _remove_point_by_click(self, view_pos): def _remove_point_by_click(self, view_pos: QPoint):
"""Remove a point by clicking on it."""
idx = self._find_item_near(view_pos, threshold=10) idx = self._find_item_near(view_pos, threshold=10)
if idx is None: if idx is None:
return return
...@@ -409,7 +433,8 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -409,7 +433,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._apply_all_guide_points_to_cost() self._apply_all_guide_points_to_cost()
self._rebuild_full_path() self._rebuild_full_path()
def _find_item_near(self, view_pos, threshold=10): def _find_item_near(self, view_pos: QPoint, threshold=10):
"""Find the index of an item near a given position."""
scene_pos = self.mapToScene(view_pos) scene_pos = self.mapToScene(view_pos)
x_click, y_click = scene_pos.x(), scene_pos.y() x_click, y_click = scene_pos.x(), scene_pos.y()
...@@ -431,6 +456,7 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -431,6 +456,7 @@ class ImageGraphicsView(PanZoomGraphicsView):
return max(mn, min(val, mx)) return max(mn, min(val, mx))
def _clear_all_points(self): def _clear_all_points(self):
"""Clear all anchor points and guide points."""
for it in self.point_items: for it in self.point_items:
self.scene.removeItem(it) self.scene.removeItem(it)
self.point_items.clear() self.point_items.clear()
...@@ -442,6 +468,7 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -442,6 +468,7 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._full_path_xy.clear() self._full_path_xy.clear()
def clear_guide_points(self): def clear_guide_points(self):
"""Clear all guide points."""
i = 0 i = 0
while i < len(self.anchor_points): while i < len(self.anchor_points):
if self.point_items[i].is_removable(): if self.point_items[i].is_removable():
...@@ -461,4 +488,5 @@ class ImageGraphicsView(PanZoomGraphicsView): ...@@ -461,4 +488,5 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._rebuild_full_path() self._rebuild_full_path()
def get_full_path_xy(self): def get_full_path_xy(self):
"""Returns the entire path as a list of (x, y) coordinates."""
return self._full_path_xy return self._full_path_xy
import math
from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsTextItem from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsTextItem
from PyQt5.QtGui import QPen, QBrush, QColor, QFont from PyQt5.QtGui import QPen, QBrush, QColor, QFont
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
import math
class LabeledPointItem(QGraphicsEllipseItem): class LabeledPointItem(QGraphicsEllipseItem):
def __init__(self, x, y, label="", radius=4, color=Qt.red, removable=True, z_value=0, parent=None): """
A QGraphicsEllipseItem subclass that represents a labeled point in a 2D space.
This class creates a circular point.
The point can be customized with different colors, sizes, and labels, and can
be marked as removable.
"""
def __init__(self, x: float, y: float, label: str ="", radius:int =4,
color=Qt.red, removable=True, z_value=0, parent=None):
super().__init__(0, 0, 2*radius, 2*radius, parent) super().__init__(0, 0, 2*radius, 2*radius, parent)
self._x = x self._x = x
self._y = y self._y = y
...@@ -30,6 +40,7 @@ class LabeledPointItem(QGraphicsEllipseItem): ...@@ -30,6 +40,7 @@ class LabeledPointItem(QGraphicsEllipseItem):
self.set_pos(x, y) self.set_pos(x, y)
def _scale_text_to_fit(self): def _scale_text_to_fit(self):
"""Scales the text to fit inside the circle."""
if not self._text_item: if not self._text_item:
return return
self._text_item.setScale(1.0) self._text_item.setScale(1.0)
...@@ -43,6 +54,7 @@ class LabeledPointItem(QGraphicsEllipseItem): ...@@ -43,6 +54,7 @@ class LabeledPointItem(QGraphicsEllipseItem):
self._center_label() self._center_label()
def _center_label(self): def _center_label(self):
"""Centers the text inside the circle."""
if not self._text_item: if not self._text_item:
return return
ellipse_w = 2 * self._r ellipse_w = 2 * self._r
...@@ -62,10 +74,14 @@ class LabeledPointItem(QGraphicsEllipseItem): ...@@ -62,10 +74,14 @@ class LabeledPointItem(QGraphicsEllipseItem):
self.setPos(x - self._r, y - self._r) self.setPos(x - self._r, y - self._r)
def get_pos(self): def get_pos(self):
"""Returns the (x, y) coordinates of the center of the circle."""
return (self._x, self._y) return (self._x, self._y)
def distance_to(self, x_other, y_other): def distance_to(self, x_other, y_other):
"""Returns the Euclidean distance from the center
of the circle to another circle."""
return math.sqrt((self._x - x_other)**2 + (self._y - y_other)**2) return math.sqrt((self._x - x_other)**2 + (self._y - y_other)**2)
def is_removable(self): def is_removable(self):
"""Returns True if the point is removable, False otherwise."""
return self._removable return self._removable
import cv2 import cv2
def load_image(path): def load_image(path: str) -> "numpy.ndarray":
"""
Loads an image from the specified file path in grayscale mode.
Args:
path (str): The file path to the image.
Returns:
numpy.ndarray: The loaded grayscale image.
"""
return cv2.imread(path, cv2.IMREAD_GRAYSCALE) return cv2.imread(path, cv2.IMREAD_GRAYSCALE)
\ No newline at end of file
from PyQt5.QtWidgets import QGraphicsView, QSizePolicy from PyQt5.QtWidgets import QGraphicsView, QSizePolicy
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
# A pan & zoom QGraphicsView
class PanZoomGraphicsView(QGraphicsView): class PanZoomGraphicsView(QGraphicsView):
"""
A QGraphicsView subclass that supports panning and zooming with the mouse.
"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setDragMode(QGraphicsView.NoDrag) # We'll handle panning manually self.setDragMode(QGraphicsView.NoDrag) # We'll handle panning manually
...@@ -10,10 +12,10 @@ class PanZoomGraphicsView(QGraphicsView): ...@@ -10,10 +12,10 @@ class PanZoomGraphicsView(QGraphicsView):
self._panning = False self._panning = False
self._pan_start = None self._pan_start = None
# Let it expand in layouts # Expands layout
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def wheelEvent(self, event): def wheel_event(self, event):
""" Zoom in/out with mouse wheel. """ """ Zoom in/out with mouse wheel. """
zoom_in_factor = 1.25 zoom_in_factor = 1.25
zoom_out_factor = 1 / zoom_in_factor zoom_out_factor = 1 / zoom_in_factor
...@@ -23,25 +25,25 @@ class PanZoomGraphicsView(QGraphicsView): ...@@ -23,25 +25,25 @@ class PanZoomGraphicsView(QGraphicsView):
self.scale(zoom_out_factor, zoom_out_factor) self.scale(zoom_out_factor, zoom_out_factor)
event.accept() event.accept()
def mousePressEvent(self, event): def mouse_press_event(self, event):
""" If left button: Start panning (unless overridden). """ """ If left button: Start panning (unless overridden). """
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
self._panning = True self._panning = True
self._pan_start = event.pos() self._pan_start = event.pos()
self.setCursor(Qt.ClosedHandCursor) self.setCursor(Qt.ClosedHandCursor)
super().mousePressEvent(event) super().mouse_press_event(event)
def mouseMoveEvent(self, event): def mouse_move_event(self, event):
""" If panning, translate the scene. """ """ If panning, translate the scene. """
if self._panning and self._pan_start is not None: if self._panning and self._pan_start is not None:
delta = event.pos() - self._pan_start delta = event.pos() - self._pan_start
self._pan_start = event.pos() self._pan_start = event.pos()
self.translate(delta.x(), delta.y()) self.translate(delta.x(), delta.y())
super().mouseMoveEvent(event) super().mouse_move_event(event)
def mouseReleaseEvent(self, event): def mouse_release_event(self, event):
""" End panning. """ """ End panning. """
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
self._panning = False self._panning = False
self.setCursor(Qt.ArrowCursor) self.setCursor(Qt.ArrowCursor)
super().mouseReleaseEvent(event) super().mouse_release_event(event)
from skimage.filters import gaussian from skimage.filters import gaussian
from skimage import exposure from skimage import exposure
def preprocess_image(image, sigma=3, clip_limit=0.01):
# Apply histogram equalization
image_contrasted = exposure.equalize_adapthist(image, clip_limit=clip_limit)
# Apply smoothing def preprocess_image(image: "np.ndarray", sigma: int = 3, clip_limit: float = 0.01) -> "np.ndarray":
"""
Preprocess the input image by applying histogram equalization and Gaussian smoothing.
Args:
image: (ndarray): Input image to be processed.
sigma: (float, optional): Standard deviation for Gaussian kernel. Default is 3.
clip_limit: (float, optional): Clipping limit for contrast enhancement. Default is 0.01.
Returns:
ndarray: The preprocessed image.
"""
# Applies histogram equalization to enhance contrast
image_contrasted = exposure.equalize_adapthist(
image, clip_limit=clip_limit)
# Applies smoothing
smoothed_img = gaussian(image_contrasted, sigma=sigma) smoothed_img = gaussian(image_contrasted, sigma=sigma)
return smoothed_img return smoothed_img
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment