Skip to content

Commit

Permalink
feat: Add zoom tool
Browse files Browse the repository at this point in the history
* Image and mask consider the zoom and shift to draw on screen
* Setting global variable zoom
* Cursor cross calculates the correct point that is clicked
* Added Zoom Tool on Views
* When on zoom mode can shift the image using cursor
  • Loading branch information
Caio-Coldebella committed Dec 15, 2023
1 parent 442659d commit 7144160
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 48 deletions.
3 changes: 3 additions & 0 deletions micview/controllers/hooks/toolframe_states_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ def toolIsSetHook(* args: Any) -> None:
models.states['toolframe_states'].selected_tool = "none"

def transparencyLevelHook(* args: Any) -> None:
models.states['image_canvas_states'].update_all_childs = True

def zoomHook(* args: Any) -> None:
models.states['image_canvas_states'].update_all_childs = True
69 changes: 43 additions & 26 deletions micview/controllers/services/image_viewer/ImageCanvasController.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
from typing import Any, Tuple, List
from micview.models.getters import states, data
from micview.controllers.services.volume.controller import changeCurrentPoint, getImageSlices, getMaskSlices
from micview.controllers.services.image_viewer.services import calcCanvasImageSize, calcProportionFactor, get3DCoordinate, getEquivalentPoint
from micview.controllers.services.image_viewer.services import calcCanvasImageSize, get3DCoordinate, getEquivalentPoint, getInverseEquivalentPoint
from micview.controllers.validations.validate_point import getNearestValidPoint

class ImageCanvasController:
def __init__(self, master: tk.Canvas) -> None:
super().__init__()
self.master: tk.Canvas = master
self.id: int = self.master.id
self.shift: List[int, int] = [0, 0]
self.last_clicked_coord: List[int, int] = [0,0]
self.canvas_image_size = False
self.proportion_factor = False
self.image_data = None
Expand All @@ -26,6 +28,7 @@ def eventHandler(self, type: str) -> None: #when other child suffer click
if(type == "action_on_child"):
self.refresh()
elif(type == "update_all_childs"):
self.actual_tool = states['toolframe_states'].selected_tool
self.resize()

def resize(self, e: Any = None) -> None:#Resizes the canvas widget
Expand All @@ -34,22 +37,29 @@ def resize(self, e: Any = None) -> None:#Resizes the canvas widget
self.refresh()

def click(self, e: Any) -> None:#Handles clicks on canvas
self.proportion_factor: Tuple[float, float] = calcProportionFactor(id=self.id, canvas_image_size=self.canvas_image_size)
new_point_x, new_point_y = getEquivalentPoint(canvas_shape=self.canvas_shape, canvas_image_size=self.canvas_image_size, proportion_factor=self.proportion_factor, e=e)
if(new_point_x == -1 and new_point_y == -1):
return
valid_points: List[int] = get3DCoordinate(id=self.id, x=new_point_x, y=new_point_y)
valid_points = getNearestValidPoint(x=valid_points[0], y=valid_points[1], z=valid_points[2])
valid_points[self.id] = -1
changeCurrentPoint(axis0=valid_points[0] ,axis1=valid_points[1], axis2=valid_points[2])
self.master.delete('cross')
if(states['toolframe_states'].selected_tool == "cursor"):
if(self.actual_tool == "zoom"):
if(int(e.type) == 4):
self.last_clicked_coord = [e.x, e.y]
elif(int(e.type) == 6):
self.shift[0] -= int((e.x - self.last_clicked_coord[0])/2)
self.last_clicked_coord[0] = e.x
self.shift[1] -= int((e.y - self.last_clicked_coord[1])/2)
self.last_clicked_coord[1] = e.y
states['image_canvas_states'].update_all_childs = True
else:
new_point_x, new_point_y = getEquivalentPoint(canvas_shape=self.canvas_shape, canvas_image_size=self.canvas_image_size, zoom_area=self.zoom_area, e=e)
if(new_point_x == -1 and new_point_y == -1):
return
valid_points: List[int] = get3DCoordinate(id=self.id, x=new_point_x, y=new_point_y)
valid_points = getNearestValidPoint(x=valid_points[0], y=valid_points[1], z=valid_points[2])
valid_points[self.id] = -1
changeCurrentPoint(axis0=valid_points[0] ,axis1=valid_points[1], axis2=valid_points[2])
self.master.delete('cross')
self.drawCross()
states['image_canvas_states'].action_on_child = self.id
states['image_canvas_states'].action_on_child = self.id

def refresh(self) -> None:# Refreshs the canvas objects
self.image_data = ImageTk.PhotoImage(image=Image.fromarray(obj=getImageSlices(axis=self.id), mode='L').resize(size=self.canvas_image_size, resample=Image.NEAREST))
self.proportion_factor: Tuple[float, float] = calcProportionFactor(id=self.id, canvas_image_size=self.canvas_image_size)
self.zoomed_image()
self.drawImage()
self.master.delete('orient_text')
self.drawOrientText()
Expand All @@ -60,10 +70,25 @@ def refresh(self) -> None:# Refreshs the canvas objects
red, green, blue = data.T[0:3]
mask_areas = (red > 0) | (blue > 0) | (green > 0)
data[:,:,3][mask_areas.T] = int(255*states["toolframe_states"].transparency_level)
self.mask_data = ImageTk.PhotoImage(image=Image.fromarray(obj=data, mode='RGBA').resize(size=self.canvas_image_size, resample=Image.NEAREST))
self.mask_image = Image.fromarray(obj=data, mode='RGBA')
self.zoomed_mask()
self.drawMask()
if(states['toolframe_states'].selected_tool == "cursor"):
self.drawCross()

def zoomed_image(self):
self.original_image = Image.fromarray(obj=getImageSlices(axis=self.id), mode='L')
original_image_size = self.original_image.size
w,h = original_image_size[0]//2, original_image_size[1]//2
const_x, const_y = (w + self.shift[0], h + self.shift[1])
zoom = states['toolframe_states'].zoom
self.zoom_area = (const_x - w/zoom, const_y - h/zoom, const_x + w/zoom, const_y + h/zoom)
self.zoomed_img = self.original_image.crop(box=self.zoom_area)
self.image_data = ImageTk.PhotoImage(image=self.zoomed_img.resize(size=self.canvas_image_size, resample=Image.NEAREST))

def zoomed_mask(self):
self.zoomed_msk = self.mask_image.crop(box=self.zoom_area)
self.mask_data = ImageTk.PhotoImage(image=self.zoomed_msk.resize(size=self.canvas_image_size, resample=Image.NEAREST))

def drawImage(self) -> None:
self.drawn_image: int = self.master.create_image((self.master.centerX, self.master.centerY), image=self.image_data, anchor="center", tags=("image",))
Expand All @@ -80,18 +105,10 @@ def drawOrientText(self) -> None:
self.drawn_orient_text: int = self.master.create_text((self.master.centerX,self.canvas_shape[1] -3), text=f"{self.orient_text[3]}", fill="#EA2027", font=('Cambria',13,'bold'), anchor="s", tags=("orient_text",))

def drawCross(self) -> None:
points = data["cursor_data"].current_point
pointX, pointY = 0, 0
if(self.id == 0):
pointX, pointY = points[2], points[1]
elif(self.id == 1):
pointX, pointY = points[2], points[0]
elif(self.id == 2):
pointX, pointY = points[1], points[0]

x,y = getInverseEquivalentPoint(id=self.id, canvas_shape=self.canvas_shape, canvas_image_size=self.canvas_image_size, zoom_area=self.zoom_area)
imgTop = self.canvas_shape[1]/2 - self.canvas_image_size[1]/2
imgBottom = self.canvas_shape[1]/2 + self.canvas_image_size[1]/2
imgLeft = self.canvas_shape[0]/2 - self.canvas_image_size[0]/2
imgRight = self.canvas_shape[0]/2 + self.canvas_image_size[0]/2
self.master.create_line((pointX/self.proportion_factor[0]+imgLeft, imgTop, pointX/self.proportion_factor[0]+imgLeft, imgBottom), fill="#759fe6", dash=(3,2), tags=("cross",))
self.master.create_line((imgLeft, pointY/self.proportion_factor[1]+imgTop, imgRight, pointY/self.proportion_factor[1]+imgTop), fill="#759fe6", dash=(3,2), tags=("cross",))
self.master.create_line((x, imgTop, x, imgBottom), fill="#759fe6", dash=(3,2), tags=("cross",))
self.master.create_line((imgLeft, y, imgRight, y), fill="#759fe6", dash=(3,2), tags=("cross",))
50 changes: 31 additions & 19 deletions micview/controllers/services/image_viewer/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,6 @@ def calcCanvasImageSize(canvas_shape: Tuple[int,int]=(0,0), image_shape: Tuple[i
return (int(canvas_shape[1]), int(canvas_shape[1]))
mult_factor: float = canvas_shape[1]/image_shape[max_side]
return (int(mult_factor*image_shape[1]), int(mult_factor*image_shape[0]))

def calcProportionFactor(id: int ,canvas_image_size: Tuple[float, float]) -> Tuple[float, float]:
'''
Calcs the proportion factor between image in canvas and volume
'''
volume_shape: List[float] = data['changed_volume_data'].changed_image_volume.shape
shift = 0
if(len(volume_shape) > 3):
shift = 1
if(id == 0):
return (volume_shape[2 + shift]/canvas_image_size[0], volume_shape[1 + shift]/canvas_image_size[1])
if(id == 1):
return (volume_shape[2 + shift]/canvas_image_size[0], volume_shape[0 + shift]/canvas_image_size[1])
if(id == 2):
return (volume_shape[1 + shift]/canvas_image_size[0], volume_shape[0 + shift]/canvas_image_size[1])

def get3DCoordinate(id:int, x:int, y:int) -> List[int]:
'''
Expand All @@ -38,15 +23,42 @@ def get3DCoordinate(id:int, x:int, y:int) -> List[int]:
if(id == 2):
return [y, x, 0]

def getEquivalentPoint(canvas_shape: List[int], canvas_image_size: Tuple[float, float], proportion_factor: float, e: Any) -> Tuple[int, int]:
def getEquivalentPoint(canvas_shape: List[int], canvas_image_size: Tuple[float, float], zoom_area: Tuple[float, float, float, float], e: Any) -> Tuple[int, int]:
'''
Gets equivalent point of canvas on volume surface
'''
zoom = states['toolframe_states'].zoom
if(zoom >= 1):
zoom = 1
center: Tuple[float, float] = (canvas_shape[0]/2, canvas_shape[1]/2)
if(e.x < center[0] - canvas_image_size[0]/2 or e.x > center[0] + canvas_image_size[0]/2 or e.y < center[1] - canvas_image_size[1]/2 or e.y > center[1] + canvas_image_size[1]/2):
offsetx: float = (canvas_shape[0] - canvas_image_size[0])/2
offsety: float = (canvas_shape[1] - canvas_image_size[1])/2
if(e.x < center[0] - (canvas_image_size[0]/2)*zoom or e.x > center[0] + (canvas_image_size[0]/2)*zoom or e.y < center[1] - (canvas_image_size[1]/2)*zoom or e.y > center[1] + (canvas_image_size[1]/2)*zoom):
return -1, -1
new_point_x: int = (e.x - offsetx)*(zoom_area[2] - zoom_area[0])/canvas_image_size[0] + zoom_area[0]
new_point_y: int = (e.y - offsety)*(zoom_area[3] - zoom_area[1])/canvas_image_size[1] + zoom_area[1]
return new_point_x, new_point_y

def getInverseEquivalentPoint(id: int, canvas_shape: List[int], canvas_image_size: Tuple[float, float], zoom_area: Tuple[float, float, float, float]) -> Tuple[int, int]:
points = data["cursor_data"].current_point
x,y = 0,0
if(id == 0):
x,y = points[2], points[1]
if(id == 1):
x,y = points[2], points[0]
if(id == 2):
x,y = points[1], points[0]

offsetx: float = (canvas_shape[0] - canvas_image_size[0])/2
new_point_x: int = (e.x - offsetx)*proportion_factor[0]
offsety: float = (canvas_shape[1] - canvas_image_size[1])/2
new_point_y: int = (e.y - offsety)*proportion_factor[1]
new_point_x: int = (x - zoom_area[0])*canvas_image_size[0]/(zoom_area[2] - zoom_area[0]) + offsetx
new_point_y: int = (y - zoom_area[1])*canvas_image_size[1]/(zoom_area[3] - zoom_area[1]) + offsety
if(new_point_x < 0):
new_point_x = 0
if(new_point_y < 0):
new_point_y = 0
if(new_point_x > canvas_shape[0]):
new_point_x = canvas_shape[0]
if(new_point_y > canvas_shape[1]):
new_point_y = canvas_shape[1]
return new_point_x, new_point_y
3 changes: 3 additions & 0 deletions micview/controllers/services/tools/zoom_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def changeZoom(zoom: float) -> None:
from micview.models.getters import states
states['toolframe_states'].zoom = zoom
15 changes: 13 additions & 2 deletions micview/models/states/toolframe_states.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import tkinter as tk
from micview.controllers.hooks.toolframe_states_hooks import channelSelectHook, selectedToolHook, toolIsSetHook, transparencyLevelHook
from micview.controllers.hooks.toolframe_states_hooks import channelSelectHook, selectedToolHook, toolIsSetHook, transparencyLevelHook, zoomHook

class ToolframeStatesClass:
def __init__(self, master: tk.Tk) -> None:
Expand All @@ -12,6 +12,8 @@ def __init__(self, master: tk.Tk) -> None:
self.__tool_is_set.trace(mode='w', callback=toolIsSetHook)
self.__transparency_level = tk.DoubleVar(master=master, value=1.0, name="transparency_level")
self.__transparency_level.trace(mode='w', callback=transparencyLevelHook)
self.__zoom = tk.DoubleVar(master=master, value=1.0, name="zoom")
self.__zoom.trace(mode='w', callback=zoomHook)

@property
def channel_select(self) -> int:
Expand Down Expand Up @@ -51,4 +53,13 @@ def transparency_level(self) -> float:
@transparency_level.setter
def transparency_level(self, value: float) -> None:
assert type(value) is float
self.__transparency_level.set(value=value)
self.__transparency_level.set(value=value)

@property
def zoom(self) -> float:
return self.__zoom.get()

@zoom.setter
def zoom(self, value: float) -> None:
assert type(value) is float
self.__zoom.set(value=value)
43 changes: 42 additions & 1 deletion micview/views/components/toolframe/ZoomTool.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import tkinter as tk
import importlib
from types import ModuleType
from typing import Any

from micview.controllers.services.tools.zoom_tool import changeZoom
models: ModuleType = importlib.import_module(name='micview.models.getters')

class ZoomTool:
def __init__(self, master: tk.Tk):
super().__init__()
super().__init__()
self.master: tk.Tk = master
self.createVars()
self.createWidgets()

def createVars(self) -> None:
initial_value = models.states['toolframe_states'].zoom
self.zoom = tk.DoubleVar(master=self.master, value=initial_value, name="zoom_local")
self.zoom.trace_add(mode="write", callback=self.updateZoom)

def createWidgets(self) -> None:
self.title = tk.Label(master=self.master, text="Zoom Tool", font=('Cambria', 13, 'bold'), bg="#f1f2f6")
self.title.place(x=5, y=10)
self.zoomTitle = tk.Label(master=self.master, text="Zoom", font=('Cambria', 11, 'bold'), bg="#f1f2f6")
self.zoomTitle.place(x=5, y=60)
self.box = tk.Spinbox(master=self.master, from_=0.1, to=10, increment=0.1, width=10, bg="#f1f2f6", textvariable=self.zoom, state="readonly", font=('Cambria', 14))
self.box.place(x=5, y=85)
self.zoomButton1 = tk.Button(master=self.master, text="1x", font=('Cambria', 10, 'bold'), bg="#f1f2f6", command=self.set1)
self.zoomButton1.place(x=5, y=125)
self.zoomButton5 = tk.Button(master=self.master, text="5x", font=('Cambria', 10, 'bold'), bg="#f1f2f6", command=self.set5)
self.zoomButton5.place(x=55, y=125)
self.zoomButton10 = tk.Button(master=self.master, text="10x", font=('Cambria', 10, 'bold'), bg="#f1f2f6", command=self.set10)
self.zoomButton10.place(x=105, y=125)

def updateZoom(self, *args: Any) -> None:
value = self.zoom.get()
changeZoom(zoom=value)

def set1(self) -> None:
self.zoom.set(value=float(1))
changeZoom(float(1))

def set5(self) -> None:
self.zoom.set(value=float(5))
changeZoom(float(5))

def set10(self) -> None:
self.zoom.set(value=float(10))
changeZoom(float(10))

0 comments on commit 7144160

Please sign in to comment.