# -*- coding: utf-8 -*-
# This file is part of SLOTH - stick/like object tracking in high-resolution.
# Copyright (C) 2012 Monika Kauer
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#@authors: M.Kauer, B.Kauer
import struct, array, StringIO
[docs]class Nd2File:
"Read Nikon ND2 format as produced by NIS-Elements AR 4.00.03 (Build 775)"
def __init__(self, f):
self.f = f
self.chunks = self.read_chunkmap()
self.meta = {}
for name in filter(lambda x: x.endswith("LV!") or "LV|" in x, self.chunks.keys()):
self.meta.update(self.read_lv_encoding(self.read_chunk(self.chunks[name]), 1))
self.attr = self.meta["SLxImageAttributes"]
def read_chunk(self, chunk):
self.f.seek(chunk[0])
h = struct.unpack("IIQ", self.f.read(16))
assert h[0] == 0xabeceda, "invalid magic %x"%h[0]
self.f.seek(chunk[0]+16+h[1])
return self.f.read(h[2])
[docs] def read_chunkmap(self):
"read the map of the chunks at the end of the file"
self.f.seek(-40, 2)
mapptr=struct.unpack("32sQ", self.f.read(40))
assert mapptr[0]=="ND2 CHUNK MAP SIGNATURE 0000001!"
data = self.read_chunk(mapptr[1:])
pos = 0
res = {}
while True:
p = data.index("!",pos)+1
res[data[pos:p]] = struct.unpack("QQ", data[p : p+16])
# abort if we found the magic end
if data[pos:p] == mapptr[0]:
break
pos = p + 16
return res
[docs] def read_coordinates(self):
"""read the microscope coordinates and temperatures
Missing: get chunknames and types from xml metadata"""
res = {}
fmt_double = ["X","Y", "Z", "Z1", "Z2", "AcqTimesCache", "CameraTemp1", "CameraTemp2"]
fmt_int = ["AcqFramesCache", "PFS_OFFSET", "PFS_STATUS"]
for postfix in fmt_double+fmt_int:
name = "CustomData|%s!"%postfix
if name in self.chunks:
st = self.read_chunk(self.chunks[name])
res[postfix] = array.array(postfix in fmt_int and "I" or "d", st)
return res
def read_lv_encoding(self, data, count):
data = StringIO.StringIO(data)
res = {}
for c in range(count):
lastpos = data.tell()
hdr = data.read(2)
if not hdr:
break
typ = ord(hdr[0])
name = data.read(2*ord(hdr[1]))
name = name.decode("utf16")[:-1].encode("utf8")
if typ == 1:
value, = struct.unpack("B", data.read(1))
elif typ in [2, 3]:
value, = struct.unpack("I", data.read(4))
elif typ == 5:
value, = struct.unpack("Q", data.read(8))
elif typ == 6:
value, = struct.unpack("d", data.read(8))
elif typ == 8:
value = data.read(2)
while value[-2:] != "\x00\x00":
value += data.read(2)
value = value.decode("utf16")[:-1].encode("utf8")
elif typ == 9:
cnt, = struct.unpack("Q", data.read(8))
value = array.array("B", data.read(cnt))
elif typ == 11:
newcount,length = struct.unpack("<IQ", data.read(12))
length -= data.tell()-lastpos
value = self.read_lv_encoding(data.read(length), newcount)
# XXX do not know for what these offsets? are
unknown = array.array("I", data.read(newcount*8))
else:
assert 0, "%s hdr %x:%x unknown"%(name, ord(hdr[0]), ord(hdr[1]))
if not name in res:
res[name] = value
else:
if type(res[name]) != type([]):
res[name] = [res[name]]
res[name].append(value)
x = data.read()
assert not x, "skip %d %s"%(len(x), repr(x[:30]))
return res
def get_image(self, nr):
assert nr >= 0 and nr < self.attr["uiSequenceCount"]
assert self.attr["ePixelType"] == 1
d = self.read_chunk(self.chunks["ImageDataSeq|%d!"%nr])
acqtime = struct.unpack("d", d[:8])[0]
res = [acqtime]
for i in range(self.attr["uiComp"]):
a = array.array("H", d)
res.append(a[4+i::self.attr["uiComp"]])
return res
def get_ppm(self, nr, layer):
x = self.get_image(nr)
layer = x[layer+1]
m = max(layer)
layer.byteswap()
return "P5\n%d %d\n%d\n"%(self.attr["uiWidth"], self.attr["uiHeight"], m) + layer.tostring()
class SeedDetection:
def __init__(self, **args):
self.args = args
def debug(self, *x):
print >>sys.stderr, " ".join(map(str, x))
def draw_line(self, image, width, color, x1, y1, x2, y2):
"simple bresenham"
deltax, deltay = abs(x2 - x1), -abs(y2 - y1)
err = deltax + deltay
while True:
image[y1*width+x1] = color
if x1 == x2 and y1 == y2:
break
e = 2*err
if e > deltay:
err += deltay
x1 += x1 < x2 and 1 or -1
if e < deltax:
err += deltax
y1 += y1 < y2 and 1 or -1
def new_object(self, objects, hints, y, xmin, xmax):
"add a new horizontal line segment as new object and merge with old objects"
v = (y, xmin, xmax)
objectnr = None
for nr in hints.get(y-1, []):
items = objects.get(nr, [])
for i in range(len(items)-1, -1, -1):
if items[i][0] < (y - 1): break
if items[i][0] == y: continue
assert items[i][0] == y -1
if max(xmin, items[i][1]) <= min(xmax, items[i][2]):
if not objectnr:
objectnr = nr
else:
objects[objectnr] = list(set(objects[objectnr]).union(items))
objects[objectnr].sort()
del objects[nr]
break
if objectnr:
objects[objectnr].append(v)
else:
objectnr = (y<<16)+xmin
assert objectnr not in objects
objects[objectnr] = [v]
hints.setdefault(y, set())
hints[y].add(objectnr)
def detect_objects(self, image, width, height):
"detect objects line by line by checking for a certain limit"
use_green = self.args["use_green"]
parameter = self.args["parameter"]
maximum = max(image[1]) + (use_green and max(image[2]) or 0)
minimum = min(image[1]) + (use_green and min(image[2]) or 0)
objects = {}
hints = {}
output = self.args["output_ppm"] and use_green
layer = image[1]
prev = False
diff = (maximum-minimum) / parameter
for i in range(height*width):
value = image[1][i]
if use_green:
value += image[2][i]
v = (value-minimum) > diff
if v != prev:
if not v:
end = i - 1
y = end/width
while start/width != y:
self.new_object(objects, hints, start/width, start % width, width-1)
start += width - (start % width)
self.new_object(objects, hints, y, start % width, end%width)
prev = v
start = i
if output:
layer[i] = value # v and max(maximum/2,value) or value
return layer, objects
def filter_objects(self, objects, width, height):
"filter objects that are to short, to thick or out of bounds"
accounting = {"count": 0, "length": 0, "bounds":0, "thick": 0}
res = []
for k in objects:
items = objects[k]
pymin = (items[ 0][1],items[ 0][0])
pymax = (items[-1][1],items[-1][0])
pxmin = pymin
pxmax = pymin
for i in items:
if i[2] > pxmax[0]: pxmax = (i[2],i[0])
if i[1] < pxmin[0]: pxmin = (i[1],i[0])
vx = (pxmax[0] - pxmin[0]+1)**2
vy = (pymax[1] - pymin[1]+1)**2
l1 = (pxmin[0]-pymin[0])**2+(pxmin[1]-pymin[1])**2
l2 = (pymin[0]-pxmax[0])**2+(pymin[1]-pxmax[1])**2
l3 = (pxmax[0]-pymax[0])**2+(pxmax[1]-pymax[1])**2
l4 = (pymax[0]-pxmin[0])**2+(pymax[1]-pxmin[1])**2
thickness = min(max(l1, l3), max(l2,l4), vx, vy)**0.5
length = (vx+vy)**0.5
# guarantee a minimum length
if length < self.args["minlength"]:
accounting["length"]+=1
#self.debug("(%3d %3d)"%pymin,"(%3d %3d)"%pymax, "%6.2f"%length, "skip length")
reason = 1
# make sure it is not outside the picture
elif not pxmin[0] or pxmax[0] == width-1 or not pymin[1] or pymax[1] == height-1:
accounting["bounds"]+=1
#self.debug("(%3d %3d)"%pxmin,"(%3d %3d)"%pxmax, "(%3d %3d)"%pymin,"(%3d %3d)"%pymax, "skip bounds")
reason = 2
# and that it is not to thick
elif thickness > self.args["maxthickness"]:
accounting["thick"]+=1
#self.debug("(%3d %3d)"%pxmin,"(%3d %3d)"%pxmax, "(%3d %3d)"%pymin,"(%3d %3d)"%pymax, "%6.2f"%thickness, "skip thickness")
reason = 3
else:
self.debug("(%3d %3d)"%pxmin,"(%3d %3d)"%pxmax, "(%3d %3d)"%pymin,"(%3d %3d)"%pymax, "len", "%6.2f"%length, "thick", "%6.2f"%thickness, "ok")
accounting["count"]+=1
reason = 0
res.append((pxmin, pxmax, pymin, pymax, items, reason))
return accounting, res
def detect(self, image, width, height, nr):
self.debug("start picture", nr, self.args)
layer,objects = self.detect_objects(image, width, height)
accounting,objects = self.filter_objects(objects, width, height)
self.debug("end picture", nr, accounting)
if not self.args["output_ppm"]: return
m = max(max(layer), 0x100)
# draw the lines into the picture
for pxmin,pxmax, pymin, pymax, items, reason in objects:
for y,xmin,xmax in items:
self.draw_line(layer, width, [0, m, m/2, m/2][reason], xmin, y, xmax, y)
# diagonals
self.draw_line(layer, width, m, pxmin[0], pxmin[1], pxmax[0], pxmax[1])
self.draw_line(layer, width, m, pymin[0], pymin[1], pymax[0], pymax[1])
# boundary
#self.draw_line(layer, width, m, pxmin[0], pxmin[1], pymin[0], pymin[1])
#self.draw_line(layer, width, m, pymin[0], pymin[1], pxmax[0], pxmax[1])
#self.draw_line(layer, width, m, pxmax[0], pxmax[1], pymax[0], pymax[1])
#self.draw_line(layer, width, m, pymax[0], pymax[1], pxmin[0], pxmin[1])
# output the PPM file
layer.byteswap()
sys.stdout.write("P5\n%d %d\n%d\n"%(width, height, m) + layer.tostring())
if __name__ == "__main__":
import sys, pprint
filename = filter(lambda x: not x.startswith("-"), sys.argv[1:])[0]
nd = Nd2File(open(filename))
if "-m" in sys.argv:
pprint.pprint(nd.meta)
elif "-c" in sys.argv:
keys = nd.chunks.keys()
keys.sort()
for k in keys:
print k
elif "-x" in sys.argv:
import xml.dom.minidom as xml
for name in filter(lambda x:x.startswith("CustomDataVar|"), nd.chunks):
x = xml.parseString(nd.read_chunk(nd.chunks[name]))
print x.toprettyxml().encode("utf8"),
elif "-t" in sys.argv:
for n in nd.meta["SLxImageTextInfo"]:
if nd.meta["SLxImageTextInfo"][n]:
print nd.meta["SLxImageTextInfo"][n]
elif "-a0" in sys.argv:
for i in range(nd.attr["uiSequenceCount"]):
sys.stdout.write(nd.get_ppm(i, 0))
elif "-a1" in sys.argv:
for i in range(nd.attr["uiSequenceCount"]):
sys.stdout.write(nd.get_ppm(i, 1))
elif "-p" in sys.argv:
for i in range(nd.attr["uiComp"]):
sys.stdout.write(nd.get_ppm(12, i))
elif "-z" in sys.argv:
detect = SeedDetection(parameter=4.25, minlength=15, maxthickness=15, use_green=False, output_ppm=True)
detect.detect(nd.get_image(0), nd.attr["uiWidth"], nd.attr["uiHeight"], 0)
detect.args["use_green"] = True
for i in range(min(1000, nd.attr["uiSequenceCount"])):
detect.detect(nd.get_image(i), nd.attr["uiWidth"], nd.attr["uiHeight"], i)
else:
print "Usage %s -[cmxtap] FILE"%sys.argv[0]