Hello, I try to set guides on an image displayed in a FloatCanvas. The application I’m building is quite large but I reduced it as much as possible to show the problem. The next picture shows the startup screen of this reduced test code.
When zoomed-in you can see that the guide does not coincide with the pixel boundaries.
Sorry, but the picture here was not allowed (new user).
Zooming is done with Ctrl key and mouse wheel. After zooming in and out several times, you may be lucky and get the desired result.
Sorry, but the picture here was not allowed (new user).
My code for testing is the file pixzoomtst.py. I tried to upload it but since I’m new it is not allowed.
Maybe pasting can:
#! python3
# -*- coding: utf-8 -*-
import wx
import wx.lib.floatcanvas.FloatCanvas
import wx.lib.floatcanvas.FCEvents
import wx.lib.floatcanvas.FCObjects
import numpy as N
from wx.lib.floatcanvas.Utilities import BBox
class ScBitmap(wx.lib.floatcanvas.FCObjects.ScaledBitmap2):
def __init__(self,
Bitmap,
XY,
Height,
Width=None,
Position = 'tl',
InForeground = False):
wx.lib.floatcanvas.FCObjects.ScaledBitmap2.__init__(self, Bitmap,
XY, Height, Width, Position, InForeground)
def _DrawSubBitmap(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc):
"""
Subsets just the part of the bitmap that is visible
then scales and draws that.
"""
BBworld = BBox.asBBox(self._Canvas.ViewPortBB)
BBbitmap = BBox.fromPoints(self.WorldToBitmap(BBworld))
XYs = WorldToPixel(self.XY)
# figure out subimage:
# fixme: this should be able to be done more succinctly!
if BBbitmap[0,0] < 0:
Xb = 0
elif BBbitmap[0,0] > self.bmpWH[0]: # off the bitmap
Xb = 0
else:
Xb = int(BBbitmap[0,0])
XYs[0] = 0 # draw at origin
if BBbitmap[0,1] < 0:
Yb = 0
elif BBbitmap[0,1] > self.bmpWH[1]: # off the bitmap
Yb = 0
ShouldDraw = False
else:
Yb = int(BBbitmap[0,1])
XYs[1] = 0 # draw at origin
if BBbitmap[1,0] < 0:
#off the screen -- This should never happen!
Wb = 0
elif BBbitmap[1,0] > self.bmpWH[0]:
Wb = self.bmpWH[0] - Xb
else:
Wb = int(BBbitmap[1,0]) - Xb
if BBbitmap[1,1] < 0:
# off the screen -- This should never happen!
Hb = 0
ShouldDraw = False
elif BBbitmap[1,1] > self.bmpWH[1]:
Hb = self.bmpWH[1] - Yb
else:
Hb = int(BBbitmap[1,1]) - Yb
FullHeight = ScaleWorldToPixel(self.Height)[0]
scale = float(FullHeight) / float(self.bmpWH[1])
Ws = int(scale * Wb + 0.5) # add the 0.5 to round
Hs = int(scale * Hb + 0.5)
if (self.ScaledBitmap is None) or (self.ScaledBitmap[0] != (Xb, Yb, Wb, Hb, Ws, Ws)):
Img = self.Image.GetSubImage(wx.Rect(Xb, Yb, Wb, Hb))
#print("rescaling with High quality")
if scale < 1.0:
Img.Rescale(int(Ws), int(Hs), quality=wx.IMAGE_QUALITY_HIGH)
else:
Img.Rescale(int(Ws), int(Hs), quality=wx.IMAGE_QUALITY_NORMAL)
bmp = wx.Bitmap(Img)
self.ScaledBitmap = ((Xb, Yb, Wb, Hb, Ws, Ws), bmp)# this defines the cached bitmap
#XY = self.ShiftFun(XY[0], XY[1], W, H)
#fixme: get the shiftfun working!
else:
#print("Using cached bitmap")
##fixme: The cached bitmap could be used if the one needed is the same scale, but
## a subset of the cached one.
bmp = self.ScaledBitmap[1]
dc.DrawBitmap(bmp, XYs, True)
if HTdc and self.HitAble:
HTdc.SetPen(self.HitPen)
HTdc.SetBrush(self.HitBrush)
HTdc.DrawRectangle(XYs, (Ws, Hs) )
class Guide(wx.lib.floatcanvas.FCObjects.Line):
"""
Draws a guide line vertical or horizontal.
It will draw a straight vertical line at the x-coordinate
or a straight horizontal line at the y-coordinate.
These lines will extend to the viewport boundaries.
"""
def __init__(self, Points,
LineColor = "Blue",
LineStyle = "ShortDash",
LineWidth = 1,
InForeground = True,
GuideMode = wx.VERTICAL):
"""
Default class constructor.
:param `Points`: takes a 2-tuple, or a (2,)
`NumPy <http://www.numpy.org/>`_ array of point coordinates
:param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor`
:param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle`
:param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth`
:param boolean `InForeground`: should object be in foreground
"""
wx.lib.floatcanvas.FCObjects.DrawObject.__init__(self, InForeground)
self.Points = Points
self.CalcBoundingBox()
self.LineColor = LineColor
self.LineStyle = LineStyle
self.LineWidth = LineWidth
self.GuideMode = GuideMode
self.SetPen(LineColor, LineStyle, LineWidth)
self.HitLineWidth = max(LineWidth, self.MinHitLineWidth)
def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
# calculate guide extension and map guide to pixel boundaries
psz = self._Canvas.PanelSize
if self.GuideMode == wx.VERTICAL:
self.Points[0][1] = self._Canvas.PixelToWorld((0, 0))[1]
self.Points[1][1] = self._Canvas.PixelToWorld(psz)[1]
else:
self.Points[0][0] = self._Canvas.PixelToWorld((0, 0))[0]
self.Points[1][0] = self._Canvas.PixelToWorld(psz)[0]
Points = WorldToPixel(self.Points)
dc.SetPen(self.Pen)
dc.DrawLines(Points)
if HTdc and self.HitAble:
HTdc.SetPen(self.HitPen)
HTdc.DrawLines(Points)
class ImagePanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
# get a sizer
vbsz = wx.BoxSizer(wx.VERTICAL)
# add a canvas for image viewing
self.canvas = wx.lib.floatcanvas.FloatCanvas.FloatCanvas(self,
id = wx.ID_ANY, BackgroundColor = '#888888')
vbsz.Add(self.canvas, 1, wx.EXPAND)
self.SetSizer(vbsz)
# define the zoom range
self.zoomRange = [1/32, 1/23, 1/16, 1/11, 1/8, 2/11, 1/4, 1/3, 1/2, 2/3]
self.zoomRange.append(1.0)
self.zoomRange.extend([3/2, 2/1, 3/1, 4/1, 11/2, 8/1, 11/1, 16/1, 25/1,
32/1])
self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.startGuide)
self.canvas.Bind(wx.EVT_LEAVE_WINDOW, self.stopGuide)
self.canvas.Bind(wx.lib.floatcanvas.FloatCanvas.EVT_MOUSEWHEEL,
self.onWheel)
wx.CallAfter(self.imageView)
self.gmode = 'Von'
def imageView(self):
# load the image
img = wx.Image('ZAZ5829_j0258.jpg', wx.BITMAP_TYPE_ANY)
self.iw = img.GetWidth()
self.ih = img.GetHeight()
# add image as a scaled bitmap to the canvas
## self.scbmap = wx.lib.floatcanvas.FloatCanvas.ScaledBitmap(
self.scbmap = ScBitmap(
img, XY = (self.iw // -2, self.ih // 2), Height = self.ih,
Position = 'tl')
self.canvas.AddObject(self.scbmap)
# go fit the image on the canvas
self.imageFit()
def imageFit(self):
# get the actual canvas size
cw, ch = self.canvas.PanelSize
# find best zoom factor and zoom index to display the whole image
self.zf = self.zoomRange[0]
self.zi = 0
for z in self.zoomRange[1:]:
if (z * self.iw) > cw or (z * self.ih) > ch:
break
else:
self.zf = z
self.zi += 1
# set the scale to fit whole image
self.canvas.Scale = self.zf
self.canvas.SetToNewScale()
def startGuide(self, event):
if event.Entering:
# get the current canvas w x h pixel size
cps = self.canvas.PanelSize
# convert to canvas w x h world size
self.cws = self.canvas.PixelToWorld(cps)
# get world coordinates of canvas origine (top-left)
self.cwo = self.canvas.PixelToWorld((0, 0))
# get the coordinate for a V- or H-guide
sp = self.canvas.PixelToWorld(event.GetPosition())
sp = (round(sp[0]), round(sp[1]))
# draw the guide
if self.gmode == 'Von':
# draw a vertical guide line
self.cg = Guide(N.array(((sp[0], self.cwo[1]),
(sp[0], self.cws[1]))),
GuideMode = wx.VERTICAL)
elif self.gmode == 'Hon':
# draw a horizontal guide line
self.cg = Guide(N.array(((self.cwo[0], sp[1]),
(self.cws[0], sp[1]))),
GuideMode = wx.HORIZONTAL)
else:
print('event handled by showGuide without valid guide mode')
return
# put the guide on the canvas
self.canvas.AddObject(self.cg)
# keep the current position for later moving
self.cp = sp
self.canvas.Bind(wx.lib.floatcanvas.FloatCanvas.EVT_MOTION,
self.onMove)
def stopGuide(self, event):
if event.Leaving:
# remove last drawn guide
self.canvas.RemoveObject(self.cg)
self.cg = None
self.canvas.Draw()
self.canvas.Unbind(wx.lib.floatcanvas.FloatCanvas.EVT_MOTION)
def onMove(self, event):
cx, cy = event.Coords
# round to nearest integer for mapping to image pixel boudaries
cx = round(cx)
cy = round(cy)
# update the current guide position with a delta
if self.gmode == 'Von':
self.cg.Points += (cx - self.cp[0], 0)
self.cg.CalcBoundingBox()
elif self.gmode == 'Hon':
self.cg.Points += (0, cy - self.cp[1])
self.cg.CalcBoundingBox()
# move the guide on the canvas
self.canvas.Draw(True)
# update the current position
self.cp = (cx, cy)
event.Skip()
def onWheel(self, event):
if wx.GetKeyState(wx.WXK_CONTROL):
pos = event.GetPosition()
oldzf = self.zf
if event.GetWheelRotation() < 0:
if self.zi > 0:
self.zi -= 1
else:
if self.zi < len(self.zoomRange) - 1:
self.zi += 1
self.zf = self.zoomRange[self.zi]
self.canvas.Zoom(self.zf / oldzf, pos, 'Pixel',
keepPointInPlace = True)
class TopWindow(wx.Frame):
def __init__(self, sz):
wx.Frame.__init__(self, parent = None, id = wx.ID_ANY,
title = "Pixel zoom test", size = sz)
self.panel = wx.Panel(self)
self.fsizer = wx.BoxSizer(wx.VERTICAL)
# add the image panel
self.imP = ImagePanel(self.panel)
self.fsizer.Add(self.imP, 1, wx.EXPAND|wx.ALL, 1)
self.panel.SetSizerAndFit(self.fsizer)
if __name__ == '__main__':
app = wx.App(False)
w, h = wx.GetDisplaySize()
frame = TopWindow((int(w*0.5), int(h*0.35)))
frame.Show(True)
app.MainLoop()
Any help to point me in the direction of a solution is appreciated.
I need to use ScaledBitmap2 because images can be large. The subclass is needed to avoid a fatal error when passing floats into the method to cut out a rectangle and also to get the correct scaling quality for viewing individual pixels for zoom levels > 1.0.
Thanks in advance.