Python Lepton 3.5 How to Properly Colorize GRAY16_LE Raw Data from GStreamer V4L2 on Linux

2,630 views
Skip to first unread message

Phillip Class

unread,
Apr 18, 2019, 4:05:51 PM4/18/19
to Flir Lepton
I have a GroupGets PureThermal 2 fully radiometric Lepton and am programming on Linux ( Ubuntu 18.04 ). The uvc-radiometry.py script works great. Now I want to get the data using V4L2 into Python with GStreamer's backend for Python OpenCv. I understand the V4L2 GRAY16_LE format is the raw intensity sensor data for radiometry and palettization. It works and I can get the frames and display them and "see" a scene. However, all of my attempts at properly colorizing the frame with a GroupGets palette have turned up less than desirable. It doesn't look as good as if the Lepton sends its own Palettized frames with V4L2 as RGB ( instead of GRAY16_LE ), and it also looks palettized.

Here are some examples:

image1 (1).jpegimage1.jpeg



















Here is my code so far:

def launchPipelineAndSendFrames(self,scaledIRWidth=320,scaledIRHeight=240):

irPipeline = 'v4l2src device=%s ! video/x-raw, width=(int)160, height=(int)120,format=GRAY16_LE ! videoconvert ! appsink' % (self.videoDevice)
#irPipeline = 'v4l2src device=%s ! appsink' % (self.videoDevice)
self.debugPrint("IR Pipeline: %s"%(irPipeline))
irVideo = cv2.VideoCapture(irPipeline, cv2.CAP_GSTREAMER)
irWidth = irVideo.get(cv2.CAP_PROP_FRAME_WIDTH)
irHeight = irVideo.get(cv2.CAP_PROP_FRAME_HEIGHT)
irFps = irVideo.get(cv2.CAP_PROP_FPS)
if irVideo.isOpened():
self.numFramesCaptured = 0
colorMap = self.generateColourMap()
startCaptureTime = time.time()
didConfigureGPU = False
while True:
try:
irCheck, irFrame = irVideo.read()
if irCheck:

#print(irFrame.shape)

# Should be grayscale
#irFrame = cv2.cvtColor(irFrame, cv2.COLOR_BGR2GRAY)

# Change to rescale to frame size
#irFrame = cv2.resize(irFrame, (scaledIRWidth, scaledIRHeight))

minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(irFrame)

#cv2.normalize(irFrame, irFrame, 0, 255, cv2.NORM_MINMAX, -1)

#np.right_shift(irFrame, 8, irFrame)

#irFrame = self.palettize_frame(irFrame)

#print(irFrame.shape)

### To grayscale and normalize
#mask_gray = cv2.cvtColor(irFrame, cv2.COLOR_BGR2GRAY)
irFrame = cv2.normalize(src=irFrame, dst=None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.COLOR_BGR2GRAY)

irFrame = cv2.LUT(irFrame, colorMap)

encodeParam = [int(cv2.IMWRITE_JPEG_QUALITY), 100]
encoded, buffer = cv2.imencode('.jpg', irFrame, encodeParam)
encodedJpeg = base64.b64encode(buffer)
self.senderSocket.send_json({"frame": encodedJpeg.decode("utf-8")})

else:
self.debugPrint("Frame not available")
self.debugPrint("%s" % (irVideo.isOpened()))
except Exception as err:
self.debugPrint("ERROR: %s"%(err))
continue
else:
self.debugPrint("Cannot open Camera!")

Here is my Palettize and LUT Function:

# Palettize the Received Lepton Frame
def palettize_frame(self, lepton_buf):
curPal_array = np.array(self.colormap_rainbow)
indexes = np.stack([3 * lepton_buf, 3 * lepton_buf + 1, 3 * lepton_buf + 2], axis=-1)
colors = curPal_array[indexes.ravel()]
palettizedFrame = colors.reshape(lepton_buf.shape[0], lepton_buf.shape[1], 3).astype(np.uint8)
return palettizedFrame

def generateColourMap(self):
"""
Conversion of the colour map from GetThermal to a numpy LUT:
https://github.com/groupgets/GetThermal/blob/bb467924750a686cc3930f7e3a253818b755a2c0/src/dataformatter.cpp#L6
"""

lut = np.zeros((256, 1, 3), dtype=np.uint8)

colormapIronBlack = [
255, 255, 255, 253, 253, 253, 251, 251, 251, 249, 249, 249, 247, 247,
247, 245, 245, 245, 243, 243, 243, 241, 241, 241, 239, 239, 239, 237,
237, 237, 235, 235, 235, 233, 233, 233, 231, 231, 231, 229, 229, 229,
227, 227, 227, 225, 225, 225, 223, 223, 223, 221, 221, 221, 219, 219,
219, 217, 217, 217, 215, 215, 215, 213, 213, 213, 211, 211, 211, 209,
209, 209, 207, 207, 207, 205, 205, 205, 203, 203, 203, 201, 201, 201,
199, 199, 199, 197, 197, 197, 195, 195, 195, 193, 193, 193, 191, 191,
191, 189, 189, 189, 187, 187, 187, 185, 185, 185, 183, 183, 183, 181,
181, 181, 179, 179, 179, 177, 177, 177, 175, 175, 175, 173, 173, 173,
171, 171, 171, 169, 169, 169, 167, 167, 167, 165, 165, 165, 163, 163,
163, 161, 161, 161, 159, 159, 159, 157, 157, 157, 155, 155, 155, 153,
153, 153, 151, 151, 151, 149, 149, 149, 147, 147, 147, 145, 145, 145,
143, 143, 143, 141, 141, 141, 139, 139, 139, 137, 137, 137, 135, 135,
135, 133, 133, 133, 131, 131, 131, 129, 129, 129, 126, 126, 126, 124,
124, 124, 122, 122, 122, 120, 120, 120, 118, 118, 118, 116, 116, 116,
114, 114, 114, 112, 112, 112, 110, 110, 110, 108, 108, 108, 106, 106,
106, 104, 104, 104, 102, 102, 102, 100, 100, 100, 98, 98, 98, 96, 96,
96, 94, 94, 94, 92, 92, 92, 90, 90, 90, 88, 88, 88, 86, 86, 86, 84, 84,
84, 82, 82, 82, 80, 80, 80, 78, 78, 78, 76, 76, 76, 74, 74, 74, 72, 72,
72, 70, 70, 70, 68, 68, 68, 66, 66, 66, 64, 64, 64, 62, 62, 62, 60, 60,
60, 58, 58, 58, 56, 56, 56, 54, 54, 54, 52, 52, 52, 50, 50, 50, 48, 48,
48, 46, 46, 46, 44, 44, 44, 42, 42, 42, 40, 40, 40, 38, 38, 38, 36, 36,
36, 34, 34, 34, 32, 32, 32, 30, 30, 30, 28, 28, 28, 26, 26, 26, 24, 24,
24, 22, 22, 22, 20, 20, 20, 18, 18, 18, 16, 16, 16, 14, 14, 14, 12, 12,
12, 10, 10, 10, 8, 8, 8, 6, 6, 6, 4, 4, 4, 2, 2, 2, 0, 0, 0, 0, 0, 9,
2, 0, 16, 4, 0, 24, 6, 0, 31, 8, 0, 38, 10, 0, 45, 12, 0, 53, 14, 0,
60, 17, 0, 67, 19, 0, 74, 21, 0, 82, 23, 0, 89, 25, 0, 96, 27, 0, 103,
29, 0, 111, 31, 0, 118, 36, 0, 120, 41, 0, 121, 46, 0, 122, 51, 0, 123,
56, 0, 124, 61, 0, 125, 66, 0, 126, 71, 0, 127, 76, 1, 128, 81, 1, 129,
86, 1, 130, 91, 1, 131, 96, 1, 132, 101, 1, 133, 106, 1, 134, 111, 1,
135, 116, 1, 136, 121, 1, 136, 125, 2, 137, 130, 2, 137, 135, 3, 137,
139, 3, 138, 144, 3, 138, 149, 4, 138, 153, 4, 139, 158, 5, 139, 163,
5, 139, 167, 5, 140, 172, 6, 140, 177, 6, 140, 181, 7, 141, 186, 7,
141, 189, 10, 137, 191, 13, 132, 194, 16, 127, 196, 19, 121, 198, 22,
116, 200, 25, 111, 203, 28, 106, 205, 31, 101, 207, 34, 95, 209, 37,
90, 212, 40, 85, 214, 43, 80, 216, 46, 75, 218, 49, 69, 221, 52, 64,
223, 55, 59, 224, 57, 49, 225, 60, 47, 226, 64, 44, 227, 67, 42, 228,
71, 39, 229, 74, 37, 230, 78, 34, 231, 81, 32, 231, 85, 29, 232, 88,
27, 233, 92, 24, 234, 95, 22, 235, 99, 19, 236, 102, 17, 237, 106, 14,
238, 109, 12, 239, 112, 12, 240, 116, 12, 240, 119, 12, 241, 123, 12,
241, 127, 12, 242, 130, 12, 242, 134, 12, 243, 138, 12, 243, 141, 13,
244, 145, 13, 244, 149, 13, 245, 152, 13, 245, 156, 13, 246, 160, 13,
246, 163, 13, 247, 167, 13, 247, 171, 13, 248, 175, 14, 248, 178, 15,
249, 182, 16, 249, 185, 18, 250, 189, 19, 250, 192, 20, 251, 196, 21,
251, 199, 22, 252, 203, 23, 252, 206, 24, 253, 210, 25, 253, 213, 27,
254, 217, 28, 254, 220, 29, 255, 224, 30, 255, 227, 39, 255, 229, 53,
255, 231, 67, 255, 233, 81, 255, 234, 95, 255, 236, 109, 255, 238, 123,
255, 240, 137, 255, 242, 151, 255, 244, 165, 255, 246, 179, 255, 248,
193, 255, 249, 207, 255, 251, 221, 255, 253, 235, 255, 255, 24]

def colormapChunk(ulist, step):
return map(lambda i: ulist[i: i + step], range(0, len(ulist), step))

chunks = colormapChunk(colormapIronBlack, 3)

red = []
green = []
blue = []

for chunk in chunks:
red.append(chunk[0])
green.append(chunk[1])
blue.append(chunk[2])

lut[:, 0, 0] = blue
lut[:, 0, 1] = green
lut[:, 0, 2] = red

return lut

# Include palettes
self.colormap_rainbow = [1, 3, 74, 0, 3, 74, 0, 3, 75, 0, 3, 75, 0, 3, 76, 0, 3, 76, 0, 3, 77, 0, 3, 79, 0, 3,
82, 0,
5, 85, 0, 7, 88, 0, 10, 91, 0, 14, 94, 0, 19, 98, 0, 22, 100, 0, 25, 103, 0, 28, 106,
0, 32,
109, 0, 35, 112, 0, 38, 116, 0, 40, 119, 0, 42, 123, 0, 45, 128, 0, 49, 133, 0, 50,
134, 0,
51, 136, 0, 52, 137, 0, 53, 139, 0, 54, 142, 0, 55, 144, 0, 56, 145, 0, 58, 149, 0, 61,
154,
0, 63, 156, 0, 65, 159, 0, 66, 161, 0, 68, 164, 0, 69, 167, 0, 71, 170, 0, 73, 174, 0,
75,
179, 0, 76, 181, 0, 78, 184, 0, 79, 187, 0, 80, 188, 0, 81, 190, 0, 84, 194, 0, 87,
198, 0,
88, 200, 0, 90, 203, 0, 92, 205, 0, 94, 207, 0, 94, 208, 0, 95, 209, 0, 96, 210, 0, 97,
211,
0, 99, 214, 0, 102, 217, 0, 103, 218, 0, 104, 219, 0, 105, 220, 0, 107, 221, 0, 109,
223, 0,
111, 223, 0, 113, 223, 0, 115, 222, 0, 117, 221, 0, 118, 220, 1, 120, 219, 1, 122, 217,
2,
124, 216, 2, 126, 214, 3, 129, 212, 3, 131, 207, 4, 132, 205, 4, 133, 202, 4, 134, 197,
5,
136, 192, 6, 138, 185, 7, 141, 178, 8, 142, 172, 10, 144, 166, 10, 144, 162, 11, 145,
158,
12, 146, 153, 13, 147, 149, 15, 149, 140, 17, 151, 132, 22, 153, 120, 25, 154, 115, 28,
156,
109, 34, 158, 101, 40, 160, 94, 45, 162, 86, 51, 164, 79, 59, 167, 69, 67, 171, 60, 72,
173,
54, 78, 175, 48, 83, 177, 43, 89, 179, 39, 93, 181, 35, 98, 183, 31, 105, 185, 26, 109,
187,
23, 113, 188, 21, 118, 189, 19, 123, 191, 17, 128, 193, 14, 134, 195, 12, 138, 196, 10,
142,
197, 8, 146, 198, 6, 151, 200, 5, 155, 201, 4, 160, 203, 3, 164, 204, 2, 169, 205, 2,
173,
206, 1, 175, 207, 1, 178, 207, 1, 184, 208, 0, 190, 210, 0, 193, 211, 0, 196, 212, 0,
199,
212, 0, 202, 213, 1, 207, 214, 2, 212, 215, 3, 215, 214, 3, 218, 214, 3, 220, 213, 3,
222,
213, 4, 224, 212, 4, 225, 212, 5, 226, 212, 5, 229, 211, 5, 232, 211, 6, 232, 211, 6,
233,
211, 6, 234, 210, 6, 235, 210, 7, 236, 209, 7, 237, 208, 8, 239, 206, 8, 241, 204, 9,
242,
203, 9, 244, 202, 10, 244, 201, 10, 245, 200, 10, 245, 199, 11, 246, 198, 11, 247, 197,
12,
248, 194, 13, 249, 191, 14, 250, 189, 14, 251, 187, 15, 251, 185, 16, 252, 183, 17,
252,
178, 18, 253, 174, 19, 253, 171, 19, 254, 168, 20, 254, 165, 21, 254, 164, 21, 255,
163, 22,
255, 161, 22, 255, 159, 23, 255, 157, 23, 255, 155, 24, 255, 149, 25, 255, 143, 27,
255,
139, 28, 255, 135, 30, 255, 131, 31, 255, 127, 32, 255, 118, 34, 255, 110, 36, 255,
104, 37,
255, 101, 38, 255, 99, 39, 255, 93, 40, 255, 88, 42, 254, 82, 43, 254, 77, 45, 254, 69,
47,
254, 62, 49, 253, 57, 50, 253, 53, 52, 252, 49, 53, 252, 45, 55, 251, 39, 57, 251, 33,
59,
251, 32, 60, 251, 31, 60, 251, 30, 61, 251, 29, 61, 251, 28, 62, 250, 27, 63, 250, 27,
65,
249, 26, 66, 249, 26, 68, 248, 25, 70, 248, 24, 73, 247, 24, 75, 247, 25, 77, 247, 25,
79,
247, 26, 81, 247, 32, 83, 247, 35, 85, 247, 38, 86, 247, 42, 88, 247, 46, 90, 247, 50,
92,
248, 55, 94, 248, 59, 96, 248, 64, 98, 248, 72, 101, 249, 81, 104, 249, 87, 106, 250,
93,
108, 250, 95, 109, 250, 98, 110, 250, 100, 111, 251, 101, 112, 251, 102, 113, 251, 109,
117,
252, 116, 121, 252, 121, 123, 253, 126, 126, 253, 130, 128, 254, 135, 131, 254, 139,
133,
254, 144, 136, 254, 151, 140, 255, 158, 144, 255, 163, 146, 255, 168, 149, 255, 173,
152,
255, 176, 153, 255, 178, 155, 255, 184, 160, 255, 191, 165, 255, 195, 168, 255, 199,
172,
255, 203, 175, 255, 207, 179, 255, 211, 182, 255, 216, 185, 255, 218, 190, 255, 220,
196,
255, 222, 200, 255, 225, 202, 255, 227, 204, 255, 230, 206, 255, 233, 208]

# Colorize Palette
self.flip = []
counter = 0
for p in range(0, len(self.colormap_rainbow), 3):
a = self.colormap_rainbow[p]
b = self.colormap_rainbow[p + 1]
c = self.colormap_rainbow[p + 2]
self.flip.append(c)
self.flip.append(b)
self.flip.append(a)
self.colormap_rainbow = self.flip

How can I properly palettize? Thanks!

Kurt Kiefer

unread,
Apr 18, 2019, 5:30:27 PM4/18/19
to Flir Lepton
At first glance I think this has something to do with the way you're normalizing your image -- as if you've spread 8 bits out across a 16 bit range and then thrown away half the bits. You should normalize over the whole 16 bit range before truncating.
Message has been deleted

Phillip Class

unread,
Apr 19, 2019, 10:59:46 AM4/19/19
to Flir Lepton
Thanks for the message. I had to patch OpenCV to allow GRAY16_LE appsink and then my code works as follows:

irPipeline = 'v4l2src device=%s ! video/x-raw, width=(int)160, height=(int)120,format=GRAY16_LE ! appsink' % (self.videoDevice)

self.debugPrint("IR Pipeline: %s"%(irPipeline))
irVideo = cv2.VideoCapture(irPipeline, cv2.CAP_GSTREAMER)
irWidth = int(irVideo.get(cv2.CAP_PROP_FRAME_WIDTH))
irHeight = int(irVideo.get(cv2.CAP_PROP_FRAME_HEIGHT))
irFps = int(irVideo.get(cv2.CAP_PROP_FPS))

if irVideo.isOpened():
self.numFramesCaptured = 0
colorMap = self.generateColourMap()
startCaptureTime = time.time()
didConfigureGPU = False
while True:

try:
irCheck, irFrame = irVideo.read()
if irCheck:

                # Increment number of frames captured
self.numFramesCaptured += 1

# Create empty frame to populate with 16-bit values
emptyFrame = np.zeros(shape=(irHeight, irWidth), dtype=np.uint16)

# Make 2 8-bit channels 1 16-bit channel
irFrame = irFrame.astype(np.uint16)
emptyFrame[:,:] = ( irFrame[:,:,1] << 8 ) | irFrame[:,:,0]

# New IR FRame is empty frame populated with 16-bit values
irFrame = emptyFrame


# Change to rescale to frame size
                irFrame = cv2.resize(irFrame, (scaledIRWidth, scaledIRHeight))

# Get Min/Max values and locations
                minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(irFrame)

                # Normalize for contrast from min to max
cv2.normalize(irFrame, irFrame, 0, 65535, cv2.NORM_MINMAX) # extend contrast

# Convert to 8-bit
np.right_shift(irFrame, 8, irFrame)
irFrame = irFrame.astype(np.uint8)

# To RGB repeated values for LUT
irFrame = cv2.cvtColor(irFrame, cv2.COLOR_GRAY2BGR)

# Colorize
irFrame = cv2.LUT(irFrame, colorMap)
#irFrame = cv2.applyColorMap(irFrame, cv2.COLORMAP_JET)
# irFrame = self.palettize_frame(irFrame)

# Blue the image a bit
# irFrame = cv2.blur(irFrame, (10, 10))

else:
print("Frame not available")
print("%s" % (irVideo.isOpened()))
except Exception as err:
print("ERROR: %s" % (err))

continue

else:
self.debugPrint("Cannot open Camera!")

Sangpil Kim

unread,
May 5, 2019, 3:52:00 PM5/5/19
to Flir Lepton
I am using PureThermal 2 with Lepton3.5.
Whenever, I ran the uvc-radiometry.py I got following error:
uvc_open error
Have you got this error before?

Reply all
Reply to author
Forward
0 new messages