Maintaining view/scroll position in QGraphicsView when swapping images - python

I'm having trouble with zooming TIFF images loaded into a QGraphicsView with QGraphicsPixmapItem.
The problem is more maintaining image quality along with having a zoom speed that doesn't make the application choke. To begin with I was just replacing the image with a scaled QPixmap - I used Qt.FastTransformation while the user was holding down a horizontal slider and then when the slider was released replaced the pixmap again with another scaled pixmap using Qt.SmoothTransformation. This gave a nice quality zoomed image but the zooming was jerky after the image size started to increase to larger than its original size; zooming out of the image was fine.
Using QTransform.fromScale() on the QGraphicsView gives much smoother zooming but a lower quality image, even when applying .setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing) to the QGraphicsView.
My latest approach is to combine the two methods and use a QTransform on the QGraphicsView to have the smooth zooming but when the user releases the slider replace the image in the QGraphicsView with a scaled pixmap. This works great, but the position in the view is lost - the user zooms in to one area and because the scaled pixmap is larger the view jumps to another location when the slider is released and the higher quality scaled pixmap replaces the previous image.
I figured that as the width height ratio is the same in both images I could take the percentages of the scrollbars before the image swap and apply the same percentages after the swap and things should work out fine. This works well mostly, but there are still times when the view 'jumps' after swapping the image.
I'm pretty sure I'm doing something quite wrong here. Does anybody know of a better way to do this, or can anyone spot something in the code below that could cause this jumping?
This is the code to save/restore the scrollbar location. They are methods of a subclassed QGraphicsView:
def store_scrollbar_position(self):
x_max = self.horizontalScrollBar().maximum()
if x_max:
x = self.horizontalScrollBar().sliderPosition()
self.scroll_x_percentage = x * (100 / float(x_max))
y_max = self.verticalScrollBar().maximum()
if y_max:
y = self.verticalScrollBar().sliderPosition()
self.scroll_y_percentage = y * (100 / float(y_max))
def restore_scrollbar_position(self):
x_max = self.horizontalScrollBar().maximum()
if self.scroll_x_percentage and x_max:
x = x_max * (float(self.scroll_x_percentage) / 100)
self.horizontalScrollBar().setSliderPosition(x)
y_max = self.verticalScrollBar().maximum()
if self.scroll_y_percentage and y_max:
y = y_max * (float(self.scroll_y_percentage) / 100)
self.verticalScrollBar().setSliderPosition(y)
And here is how I'm doing the scaling. self.imageFile is a QPixmap and self.image is my QGraphicsPixmapItem. Again, part of a subclassed QGraphicsView. The method is attached to the slider movement with the highQuality parameter set to False. It is called again on slider release with highQuality as True to swap the image.
def setImageScale(self, scale=None, highQuality=True):
if self.imageFile.isNull():
return
if scale is None:
scale = self.scale
self.scale = scale
self.image.setPixmap(self.imageFile)
self.scene.setSceneRect(self.image.boundingRect())
self.image.setPos(0, 0)
if not highQuality:
self.setTransform(QTransform.fromScale(self.scaleFactor, self.scaleFactor))
self.store_scrollbar_position()
else:
self.image.setPixmap(self.imageFile.scaled(self.scaleFactor * self.imageFile.size(),
Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.setTransform(self.transform)
self.scene.setSceneRect(self.image.boundingRect())
self.image.setPos(0, 0)
self.restore_scrollbar_position()
return
Any help would be appreciated. I'm starting to get quite frustrated with this now.

I found a solution that works better than the code I first posted. It's still not perfect, but is much improved. Just in case anyone else is trying to solve a similar problem...
When setting the low quality image I call this method added to my QGraphicsView:
def get_scroll_state(self):
"""
Returns a tuple of scene extents percentages.
"""
centerPoint = self.mapToScene(self.viewport().width()/2,
self.viewport().height()/2)
sceneRect = self.sceneRect()
centerWidth = centerPoint.x() - sceneRect.left()
centerHeight = centerPoint.y() - sceneRect.top()
sceneWidth = sceneRect.width()
sceneHeight = sceneRect.height()
sceneWidthPercent = centerWidth / sceneWidth if sceneWidth != 0 else 0
sceneHeightPercent = centerHeight / sceneHeight if sceneHeight != 0 else 0
return sceneWidthPercent, sceneHeightPercent
This gets stored in self.scroll_state. When setting the high quality image I call another function to restore the percentages used in the previous function.
def set_scroll_state(self, scroll_state):
sceneWidthPercent, sceneHeightPercent = scroll_state
x = (sceneWidthPercent * self.sceneRect().width() +
self.sceneRect().left())
y = (sceneHeightPercent * self.sceneRect().height() +
self.sceneRect().top())
self.centerOn(x, y)
This sets the center position to the same location (percentage-wise) as I was at before swapping the image.

Related

Scaling QPolygon on its origin

I'm trying to scale a QPolygonF that is on a QGraphicsScene's QGraphicsView on its origin.
However, even after translating the polygon (poly_2) to its origin (using QPolygon.translate() and the center coordinates of the polygon received via boundingRect (x+width)/2 and (y+height)/2), the new polygon is still placed on the wrong location.
The blue polygon should be scaled according to the origin of poly_2 (please see the image below, black is the original polygon, blue polygon is the result of the code below, and the orange polygon is representing the intended outcome)
I thought that the issue might be that coordinates are from global and should be local, yet this does solve the issue unfortunately.
Here's the code:
import PyQt5
from PyQt5 import QtCore
import sys
import PyQt5
from PyQt5.QtCore import *#QPointF, QRectF
from PyQt5.QtGui import *#QPainterPath, QPolygonF, QBrush,QPen,QFont,QColor, QTransform
from PyQt5.QtWidgets import *#QApplication, QGraphicsScene, QGraphicsView, QGraphicsSimpleTextItem
poly_2_coords= [PyQt5.QtCore.QPointF(532.35, 274.98), PyQt5.QtCore.QPointF(525.67, 281.66), PyQt5.QtCore.QPointF(518.4, 292.58), PyQt5.QtCore.QPointF(507.72, 315.49), PyQt5.QtCore.QPointF(501.22, 326.04), PyQt5.QtCore.QPointF(497.16, 328.47), PyQt5.QtCore.QPointF(495.53, 331.71), PyQt5.QtCore.QPointF(488.24, 339.02), PyQt5.QtCore.QPointF(480.94, 349.56), PyQt5.QtCore.QPointF(476.09, 360.1), PyQt5.QtCore.QPointF(476.89, 378.76), PyQt5.QtCore.QPointF(492.3, 393.35), PyQt5.QtCore.QPointF(501.22, 398.21), PyQt5.QtCore.QPointF(527.17, 398.21), PyQt5.QtCore.QPointF(535.28, 390.1), PyQt5.QtCore.QPointF(540.96, 373.89), PyQt5.QtCore.QPointF(539.64, 356.93), PyQt5.QtCore.QPointF(541.46, 329.0), PyQt5.QtCore.QPointF(543.39, 313.87), PyQt5.QtCore.QPointF(545.83, 300.89), PyQt5.QtCore.QPointF(545.83, 276.56), PyQt5.QtCore.QPointF(543.39, 267.64), PyQt5.QtCore.QPointF(537.81, 268.91)]
def main():
app = QApplication(sys.argv)
scene = QGraphicsScene()
view = QGraphicsView(scene)
pen = QPen(QColor(0, 20, 255))
scene.addPolygon(QPolygonF(poly_2_coords))
poly_2 = QPolygonF(poly_2_coords)
trans = QTransform().scale(1.5,1.5)
#poly_22 = trans.mapToPolygon(QRect(int(poly_2.boundingRect().x()),int(poly_2.boundingRect().y()),int(poly_2.boundingRect().width()),int(poly_2.boundingRect().height())))
#trans.mapToPolygon()
#scene.addPolygon(QPolygonF(poly_22),QPen(QColor(0, 20, 255)))
poly_2.translate((poly_2.boundingRect().x()+poly_2.boundingRect().width())/2,(poly_2.boundingRect().y()+poly_2.boundingRect().height())/2)
print(f'poly_2.boundingRect().x() {poly_2.boundingRect().x()}+poly_2.boundingRect().width(){poly_2.boundingRect().width()}')
trans = QTransform().scale(1.4,1.4)
#poly_2.setTransformOriginPoint()
poly_22 = trans.map(poly_2)
scene.addPolygon(poly_22,QPen(QColor(0, 20, 255)))
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Edit: I've tried saving the polygon as a QGraphicsItem, and set its transformation origin point according the bbox's middle X,Y and then mapped from Global to Scene, yet no luck: the new polygon is still drawn to the wrong place.
poly_2 = QPolygonF(poly_2_coords)
poly = scene.addPolygon(poly_2)
point = QPoint((poly_2.boundingRect().x()+poly_2.boundingRect().width())/2,(poly_2.boundingRect().y()+poly_2.boundingRect().height())/2)
poly.setTransformOriginPoint(point)
poly.setScale(3)
If replacing point to equal only X,Y of the bounding rectangle, the result seems to be closer to what I need. However, in this case the origin point is obviously wrong. Is this just random luck that this answer seems to be closer to what I need?
Before considering the problem of the translation, there is a more important aspect that has to be considered: if you want to create a transformation based on the center of a polygon, you must find that center. That point is called centroid, the geometric center of any polygon.
While there are simple formulas for all basic geometric shapes, finding the centroid of a (possibly irregular) polygon with an arbitrary number of vertices is a bit more complex.
Using the arithmetic mean of vertices is not a viable option, as even in a simple square you might have multiple points on a single side, which would move the computed "center" towards those points.
The formula can be found in the Wikipedia article linked above, while a valid python implementation is available in this answer.
I modified the formula of that answer in order to accept a sequence of QPoints, while improving readability and performance, but the concept remains the same:
def centroid(points):
if len(points) < 3:
raise ValueError('At least 3 points are required')
# https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
# https://en.wikipedia.org/wiki/Shoelace_formula
# computation uses concatenated pairs from the sequence, with the
# last point paired to the first one:
# (p[0], p[1]), (p[1], p[2]) [...] (p[n], p[0])
area = cx = cy = 0
p1 = points[0]
for p2 in points[1:] + [p1]:
shoelace = p1.x() * p2.y() - p2.x() * p1.y()
area += shoelace
cx += (p1.x() + p2.x()) * shoelace
cy += (p1.y() + p2.y()) * shoelace
p1 = p2
A = 0.5 * area
factor = 1 / (6 * A)
return cx * factor, cy * factor
Then, you have two options, depending on what you want to do with the resulting item.
Scale the item
In this case, you create a QGraphicsPolygonItem like the original one, then set its transform origin point using the formula above, and scale it:
poly_2 = QtGui.QPolygonF(poly_2_coords)
item2 = scene.addPolygon(poly_2, QtGui.QPen(QtGui.QColor(0, 20, 255)))
item2.setTransformOriginPoint(*centroid(poly_2_coords))
item2.setScale(1.5)
Use a QTransform
With Qt transformations some special care must be taken, as scaling always uses 0, 0 as origin point.
To scale around a specified point, you must first translate the matrix to that point, then apply the scale, and finally restore the matrix translation to its origin:
poly_2 = QtGui.QPolygonF(poly_2_coords)
cx, cy = centroid(poly_2_coords)
trans = QtGui.QTransform()
trans.translate(cx, cy)
trans.scale(1.5, 1.5)
trans.translate(-cx, -cy)
poly_2_scaled = trans.map(poly_2)
scene.addPolygon(poly_2_scaled, QtGui.QPen(QtGui.QColor(0, 20, 255)))
This is exactly what QGraphicsItems do when using the basic setScale() and setRotation() transformations.
Shape origin point and item position
Remember that QGraphicsItems are always created with their position at 0, 0.
This might not seem obvious especially for basic shapes: when you create a QGraphicsRectItem giving its x, y, width, height, the position will still be 0, 0. When dealing with complex geometry management, it's usually better to create basic shapes with the origin/reference at 0, 0 and then move the item at x, y.
For complex polygons like yours, a possibility could be to translate the centroid of the polygon at 0, 0, and then move it at the actual centroid coordinates:
item = scene.addPolygon(polygon.translated(-cx, -cy))
item.setPos(cx, cy)
item.setScale(1.5)
This might make things easier for development (the mapped points will always be consistent with the item position), and the fact that you don't need to change the transform origin point anymore makes reverse mapping even simpler.

Moving the cursor in relation to a certain fix-point in Python

is there any possibility to move the cursor in Python with % starting from a certain coordinate like (1,1)?
I am using pyautogui atm to automate and I thought it would be quite convenient if this is independent from the monitor size making it universal.
Thanks for your help in advance!
It's possible indirectly. As detailed at the top of the Mouse Control Functions page of the documentation, you can get the screen size using the size() function (which returns a tuple of (X, Y)). You can then do the math to figure out how many screen pixels equal the percentage you're looking for, then call moveTo() to go there.
# script to move mouse 50% to the right and down
import pyautogui as pag
percentage = 0.5
cur_X, cur_Y = pag.position() # current X and Y coordinates
size_X, size_Y = pag.size() # screen size
goto_X = (size_X - cur_X) * percentage + cur_X # current location plus half
goto_Y = (size_Y - cur_Y) * percentage + cur_Y # the distance to the edge
pag.moveTo(goto_X, goto_Y, 1) # move to new position, taking 1 second

Rotate Rect along line

First project in qt.
I'm having trouble translating/rotating a rect along a line. Basically i would want to align the rect with the position of the line. When i change position of the circle the rect should translate along the line. See images below.
What i have at the moment
w_len = len(str(weight)) / 3 * r + r / 3
weight_v = Vector(r if w_len <= r else w_len, r)
weight_rectangle = QRectF(*(mid - weight_v), *(2 * weight_v))
painter.drawRect(weight_rectangle)
*mid is just a vector with coordinates at half of the link , weight_v is a vector based on the text size.
Any pointers , should i look at adding a translate to the painter ? Whenever i try to add translation to the painter it breaks the other shapes as well.
t = QTransform()
t.translate(-5 ,-5)
t.rotate(90)
painter.setTransform(t)
painter.drawRect(weight_rectangle)
painter.resetTransform()
Update:
With below answer i was able to fix the rotation. Many thanks, looks like my text is not displaying correctly.
I have the following code:
painter.translate(center_of_rec_x, center_of_rec_y);
painter.rotate(- link_paint.angle());
rx = -(weight_v[0] * 0.5)
ry = -(weight_v[1] )
new_rec = QRect(rx , ry, weight_v[0], 2 * weight_v[1])
painter.drawRect(QRect(rx , ry, weight_v[0] , 2 * weight_v[1] ))
painter.drawText(new_rec, Qt.AlignCenter, str(weight))
Update2:
All is fine , was a mistake in my code. I was taking the wrong link angle.
Thx.
Rotation is always done according to the origin point (0, 0), so you need to translate to the origin point of the rotation and then apply it.
Also, when applying any temporary change to the painter, save() and restore() should be used: in this way the current state of the painter is stored, and that state will be restored afterwards (including any transformation applied in the meantime). Painter states can be nested, and one could save multiple times to apply multiple "layers" of painter state modifications. Just remember that the all states must be restored to the base status before releasing (ending) the painter.
Since you didn't provide an MRE, I created a small widget to show how this works:
class AngledRect(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.setMinimumSize(200, 200)
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
contents = self.contentsRect()
# draw a line from the top left to the bottom right of the widget
line = QtCore.QLineF(contents.topLeft(), contents.bottomRight())
qp.drawLine(line)
# save the current state of the painter
qp.save()
# translate to the center of the painting rectangle
qp.translate(contents.center())
# apply an inverted rotation, since the line angle is counterclockwise
qp.rotate(-line.angle())
# create a rectangle that is centered at the origin point
rect = QtCore.QRect(-40, -10, 80, 20)
qp.setPen(QtCore.Qt.white)
qp.setBrush(QtCore.Qt.black)
qp.drawRect(rect)
qp.drawText(rect, QtCore.Qt.AlignCenter, '{:.05f}'.format(line.angle()))
qp.restore()
# ... other painting...
For simple transformations, using translate and rotate is usually enough, but the above is almost identical to:
transform = QtGui.QTransform()
transform.translate(contents.center().x(), contents.center().y())
transform.rotate(-line.angle())
qp.save()
qp.setTransform(transform)
# ...

Pyglet. How to change picture(animate) for vertices dynamically. OpenGL

Environment:
Python: 3.6.6
pyglet version: 1.3.2
Code base:
abstract_model.py
import pyglet
def get_texture_group(file, order_group_index):
texture = pyglet.image.load(file).texture
order_group = pyglet.graphics.OrderedGroup(order_group_index)
return pyglet.graphics.TextureGroup(texture, order_group)
class AbstractModel(object):
def _create_as_vertex(self):
v_x = self.cell_data.get("x") * 32
v_y = self.cell_data.get("y") * -1 * 32
texture_group = self.map_type_iamge.get(self.cell_data.get("t"))
x_offset = self.x_offset * self.scale
x, y, z = v_x + x_offset, v_y, self.z
x_ = (texture_group.texture.width * self.scale + x_offset + v_x)
y_ = (texture_group.texture.height * self.scale + v_y)
tex_coords = ('t2f', (0, 0, 1, 0, 1, 1, 0, 1))
self.vertices = self.batch.add(
4, pyglet.gl.GL_QUADS,
texture_group,
('v3f', (x, y, z,
x_, y, z,
x_, y_, z,
x, y_, z)),
tex_coords)
def _animate(self, dt):
# lets assume that I have list of pyglet.graphics.TextureGroup
# and they should somehow be drawn one after other
print("I need change image. dt=", dt, self)
pyglet.clock.schedule_once(self._animate, 1)
ground3d.py
import os
import pyglet
import settings
from models import abstract_model
GROUND_DIR = os.path.join(settings.STATIC_DIR, "ground")
order_group_index = 0
map_type_iamge = {
1: abstract_model.get_texture_group(os.path.join(GROUND_DIR, "w1.png"), order_group_index),
2: abstract_model.get_texture_group(os.path.join(GROUND_DIR, "t1.png"), order_group_index),
1001: abstract_model.get_texture_group(os.path.join(GROUND_DIR, "t1_direction.png"), order_group_index),
}
class Ground3D(abstract_model.AbstractModel):
def __init__(self, cell_data, batch):
self.batch = batch
self.cell_data = cell_data
self.map_type_iamge = map_type_iamge
self.scale = 1
self.x_offset = 0
self.z = 0
self.entity = None
self._create_as_vertex()
pyglet.clock.schedule_once(self._animate, 1)
Explanation:
I have models(just flat rect for an example) which should be placed on 3 dimensions. And these models should be animated, like picture_1, after second picture_2, ... etc.
As I understood from my previous question using pyglet.sprite.Sprite() in 3D batch is not a good idea.
Question:
How I can change pictures(using TextureGroup or any other approaches) on self.vertices?
Or which arroach/classes I use use to implement it. I can't find any examples for such (as for my simple vision) usual case as animation for some flat models in 3 dimensions.
There are many example about rotating/moving/resizing of vertices, but how to build a correct question(is animation aspect) for getting answer in google - I don't know.
PS: If you, reader, have any usefull links on this subject(for pyglet or just for OpenGL) I would be very appreciated you share this link(s) in comment.
Texture coordinates.
You should have a single texture atlas for all frames of all different things that are animated ever.
Preferably, everything should have same animation speed and same amount of frames all the time.
Let's say there's two sprites that have 2 frames for entire animation, and they are stored in 64x64 texture atlas. (EDIT: sorry for ambiguity, 64x64 PIXELS, just because it could imply that we have 64x64 tile atlas, same everywhere else where I mention this)
Now, you need to have a global timer with global value which indicates current animation frame, not game frame. It should be independent of framerate.
Said value should be updated every once in a while at your desired speed like this:
current_frame = (current_frame + 1) % animation_length
Since we have 2 frames in this example, it will turn out like this:
# init
animation_length = 2
current_frame = 0
# updates:
current_frame = (0 + 1) % 2 # 1 % 2 -> 1
current_frame = (1 + 1) % 2 # 2 % 2 -> 0
...
Now, you need to update UV's of all your sprites only when the frame changes.
UV's start from left right and go from 0 to 1 (as far as I remember, for the sake of this example, they do, shhh).
Since we have 2 frames each, we can calculate "tiles" in the UV coordinates like this:
tile_width = 1.0 / frames # 2 frames each, width will be 0.5
tile_height = 1.0 / sprites # 2 sprites each, height will be 0.5 too, perfect
Now, on first frame, you generate your UV's like normal, you just take vertical ID or something, and use tile_height * sprite_id to get current V coordinate, and your U is calculated like tile_width * current_frame.
This assumes that you already have sprite batching so what you do is go over every sprite on update, and basically just recalculate new UV's with new frame, meaning all sprites change their frame to the next one, yay!
If you want to have systems that are independent of eachother, say, very slow animations for some, and faster for others, you'll need different sprite batches or proper calculation on from where to where you need to update UV's in vertex buffer array. Everything else is exactly the same, except now current_frame won't be global but rather contained, preferebly in some list or separate object that manages timers for animations.
You don't need to change anything in your shaders, they just need right UV's for the frames and you're set.
By the way, this is very basic, you could apply some logic yourself so you could instead have 16x16 grid of 32x32 pixels in your texture, each line of sprites having 4 different animations, these could be either sprite's states (attack, run, etc), but how you do it is entirely on you, most importantly, get it to work. Goodluck.
But if you do it the way I said, then state will be another integer, and UV for state, assuming all states have exactly the same width, it would be like this:
state_width = 1 / states
tile_width = 1 / (states * frames_per_state)
U = state_width * current_state + tile_width * current_frame
Now, one issue arises, player could start his animation at the last attack frame.
It's normal, entities with actions should all have individual timers, what I described above, is for tons of sprites that are just background, like grass. Now when you divided it up, you could have a proper way to reset current frame to 0 when new state is assigned.
If your entities are objects, you could write proper methods that recalculate UV's every time you rebuild the sprite batch using those sprites, and then timers itselves could be contained in objects.
We need to draw something? Check animation state, has it changed, no? Send UV's that were calculated before, otherwise, wait a bit, we need to recalculate, only then add those to VBO, and well, render your thing, in the end, it will appear as if you have animations, even though really, it's just a simple, but great UV manipulation.
Goodluck.

Correct method and Python package that can find width of an image's feature

The input is a spectrum with colorful (sorry) vertical lines on a black background. Given the approximate x coordinate of that band (as marked by X), I want to find the width of that band.
I am unfamiliar with image processing. Please direct me to the correct method of image processing and a Python image processing package that can do the same.
I am thinking PIL, OpenCV gave me an impression of being overkill for this particular application.
What if I want to make this an expert system that can classify them in the future?
I'll give a complete minimal working example (as suggested by sega_sai). I don't have access to your original image, but you'll see it doesn't really matter! The peak distributions found by the code below are:
Mean values at: 26.2840960523 80.8255092125
import Image
from scipy import *
from scipy.optimize import leastsq
# Load the picture with PIL, process if needed
pic = asarray(Image.open("band2.png"))
# Average the pixel values along vertical axis
pic_avg = pic.mean(axis=2)
projection = pic_avg.sum(axis=0)
# Set the min value to zero for a nice fit
projection /= projection.mean()
projection -= projection.min()
# Fit function, two gaussians, adjust as needed
def fitfunc(p,x):
return p[0]*exp(-(x-p[1])**2/(2.0*p[2]**2)) + \
p[3]*exp(-(x-p[4])**2/(2.0*p[5]**2))
errfunc = lambda p, x, y: fitfunc(p,x)-y
# Use scipy to fit, p0 is inital guess
p0 = array([0,20,1,0,75,10])
X = xrange(len(projection))
p1, success = leastsq(errfunc, p0, args=(X,projection))
Y = fitfunc(p1,X)
# Output the result
print "Mean values at: ", p1[1], p1[4]
# Plot the result
from pylab import *
subplot(211)
imshow(pic)
subplot(223)
plot(projection)
subplot(224)
plot(X,Y,'r',lw=5)
show()
Below is a simple thresholding method to find the lines and their width, it should work quite reliably for any number of lines. The yellow and black image below was processed using this script, the red/black plot illustrates the found lines using parameters of threshold = 0.3, min_line_width = 5)
The script averages the rows of an image, and then determines the basic start and end positions of each line based on a threshold (which you can set between 0 and 1), and a minimum line width (in pixels). By using thresholding and minimum line width you can easily filter your input images to get the lines out of them. The first function find_lines returns all the lines in an image as a list of tuples containing the start, end, center, and width of each line. The second function find_closest_band_width is called with the specified x_position, and returns the width of the closest line to this position (assuming you want distance to centre for each line). As the lines are saturated (255 cut-off per channel), their cross-sections are not far from a uniform distribution, so I don't believe trying to fit any kind of distribution is really going to help too much, just unnecessarily complicates.
import Image, ImageStat
def find_lines(image_file, threshold, min_line_width):
im = Image.open(image_file)
width, height = im.size
hist = []
lines = []
start = end = 0
for x in xrange(width):
column = im.crop((x, 0, x + 1, height))
stat = ImageStat.Stat(column)
## normalises by 2 * 255 as in your example the colour is yellow
## if your images start using white lines change this to 3 * 255
hist.append(sum(stat.sum) / (height * 2 * 255))
for index, value in enumerate(hist):
if value > threshold and end >= start:
start = index
if value < threshold and end < start:
if index - start < min_line_width:
start = 0
else:
end = index
center = start + (end - start) / 2.0
width = end - start
lines.append((start, end, center, width))
return lines
def find_closest_band_width(x_position, lines):
distances = [((value[2] - x_position) ** 2) for value in lines]
index = distances.index(min(distances))
return lines[index][3]
## set your threshold, and min_line_width for finding lines
lines = find_lines("8IxWA_sample.png", 0.7, 4)
## sets x_position to 59th pixel
print 'width of nearest line:', find_closest_band_width(59, lines)
I don't think that you need anything fancy for you particular task.
I would just use PIL + scipy. That should be enough.
Because you essentially need to take your image, make a 1D-projection of it
and then fit a Gaussian or something like that to it. The information about the approximate location of the band should be used a first guess for the fitter.

Categories