add utils and its demo
Browse files- .gitignore +3 -0
- main_validate_findpeaks.ipynb +0 -0
- utils.py +297 -0
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
|
main_validate_findpeaks.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
utils.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from scipy.ndimage import convolve
|
| 3 |
+
import os
|
| 4 |
+
from numpy.lib.stride_tricks import sliding_window_view
|
| 5 |
+
from numba import njit
|
| 6 |
+
import cv2
|
| 7 |
+
|
| 8 |
+
def mipi_raw10_to_raw8_scaled(raw10_data):
|
| 9 |
+
raw10_data = np.frombuffer(raw10_data, dtype=np.uint8)
|
| 10 |
+
n_blocks = len(raw10_data) // 5
|
| 11 |
+
raw10_data = raw10_data[:n_blocks * 5].reshape(-1, 5)
|
| 12 |
+
|
| 13 |
+
p0 = (raw10_data[:, 0].astype(np.uint16) << 2) | ((raw10_data[:, 4] >> 0) & 0x03)
|
| 14 |
+
p1 = (raw10_data[:, 1].astype(np.uint16) << 2) | ((raw10_data[:, 4] >> 2) & 0x03)
|
| 15 |
+
p2 = (raw10_data[:, 2].astype(np.uint16) << 2) | ((raw10_data[:, 4] >> 4) & 0x03)
|
| 16 |
+
p3 = (raw10_data[:, 3].astype(np.uint16) << 2) | ((raw10_data[:, 4] >> 6) & 0x03)
|
| 17 |
+
|
| 18 |
+
raw8_data = np.empty((n_blocks * 4 * 2,), dtype=np.uint8)
|
| 19 |
+
raw8_data[0::8] = p0 & 0xFF
|
| 20 |
+
raw8_data[1::8] = p0 >> 8
|
| 21 |
+
raw8_data[2::8] = p1 & 0xFF
|
| 22 |
+
raw8_data[3::8] = p1 >> 8
|
| 23 |
+
raw8_data[4::8] = p2 & 0xFF
|
| 24 |
+
raw8_data[5::8] = p2 >> 8
|
| 25 |
+
raw8_data[6::8] = p3 & 0xFF
|
| 26 |
+
raw8_data[7::8] = p3 >> 8
|
| 27 |
+
|
| 28 |
+
return raw8_data.tobytes()
|
| 29 |
+
|
| 30 |
+
def readRAW(path):
|
| 31 |
+
filesize = os.path.getsize(path)
|
| 32 |
+
|
| 33 |
+
with open(path, "rb") as f:
|
| 34 |
+
raw_data = f.read()
|
| 35 |
+
|
| 36 |
+
# Case 1: 如果是 MIPI RAW10 格式,大小为 7,372,800 字节
|
| 37 |
+
if filesize == 7372800:
|
| 38 |
+
raw_data = mipi_raw10_to_raw8_scaled(raw_data)
|
| 39 |
+
|
| 40 |
+
# 转换为 int16 并 reshape
|
| 41 |
+
arr = np.frombuffer(raw_data, dtype=np.int16).reshape(96, 240, 256)
|
| 42 |
+
|
| 43 |
+
# Byte Swap: [x,y,256] → [x,y,128,2] → swap last dim → [x,y,256]
|
| 44 |
+
reshaped = arr.reshape(*arr.shape[:-1], -1, 2)
|
| 45 |
+
swapped = reshaped[..., ::-1]
|
| 46 |
+
histogram_data = swapped.reshape(arr.shape)
|
| 47 |
+
|
| 48 |
+
# Line remapping (每组8行:0,4,1,5,...)
|
| 49 |
+
mapping = [0, 4, 1, 5, 2, 6, 3, 7]
|
| 50 |
+
group_size = 8
|
| 51 |
+
num_groups = 12 # 96 / 8
|
| 52 |
+
output = np.empty_like(histogram_data)
|
| 53 |
+
|
| 54 |
+
for g in range(num_groups):
|
| 55 |
+
start = g * group_size
|
| 56 |
+
end = start + group_size
|
| 57 |
+
output[start:end, :, :] = histogram_data[start:end, :, :][mapping, :, :]
|
| 58 |
+
|
| 59 |
+
return output.astype(np.int16)
|
| 60 |
+
|
| 61 |
+
def binning_2x2_stride2(data):
|
| 62 |
+
"""
|
| 63 |
+
data: numpy array (96, 240, 256)
|
| 64 |
+
return: numpy array (48, 120, 256)
|
| 65 |
+
"""
|
| 66 |
+
# 先 reshape 再求和,效率高
|
| 67 |
+
return data.reshape(48, 2, 120, 2, 256).sum(axis=(1, 3))
|
| 68 |
+
|
| 69 |
+
def binning_2x2_stride1(data):
|
| 70 |
+
"""
|
| 71 |
+
data: numpy array (96, 240, 256)
|
| 72 |
+
return: numpy array (95, 239, 256) # 因为stride=1,边界少1行1列
|
| 73 |
+
"""
|
| 74 |
+
# 直接用切片叠加四个偏移
|
| 75 |
+
binned = (data[:-1, :-1] + data[1:, :-1] +
|
| 76 |
+
data[:-1, 1:] + data[1:, 1:])
|
| 77 |
+
# binned = np.pad(binned, ((0,1),(0,1),(0,0)), mode='constant')
|
| 78 |
+
return binned
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def ma_vectorized(data, kernel=[-2, -2, 1, 2, 2, 3, -1, -1]):
|
| 82 |
+
kernel = np.array(kernel, dtype=np.float32)
|
| 83 |
+
k = kernel.size
|
| 84 |
+
kernel_sum = kernel.sum()
|
| 85 |
+
|
| 86 |
+
# 确保 data 是 numpy array
|
| 87 |
+
data = np.asarray(data, dtype=np.float32)
|
| 88 |
+
|
| 89 |
+
# 取滑动窗口视图,shape: (96, 240, 256 - k + 1, k)
|
| 90 |
+
windows = np.lib.stride_tricks.sliding_window_view(data, window_shape=k, axis=2)
|
| 91 |
+
|
| 92 |
+
# 直接点乘kernel,然后求和,得到平滑结果 (96,240,256-k+1)
|
| 93 |
+
smoothed = np.tensordot(windows, kernel, axes=([3],[0])) / kernel_sum
|
| 94 |
+
smoothed[smoothed<0] = 0
|
| 95 |
+
|
| 96 |
+
# 为了保持和输入长度一致,可以两边补0或其他策略,这里简单在尾部补零
|
| 97 |
+
pad_width = ((0,0), (0,0), (0,k-1))
|
| 98 |
+
smoothed = np.pad(smoothed, pad_width, mode='constant', constant_values=0)
|
| 99 |
+
return smoothed
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def ma_vectorized_fast(data, kernel=[-2, -2, 1, 2, 2, 3, -1, -1]):
|
| 103 |
+
kernel = np.array(kernel, dtype=np.float32)
|
| 104 |
+
k = kernel.size
|
| 105 |
+
kernel_sum = kernel.sum()
|
| 106 |
+
|
| 107 |
+
if kernel_sum == 0:
|
| 108 |
+
kernel_sum = 1 # 避免除 0
|
| 109 |
+
|
| 110 |
+
# padding 边界,保持中心对齐
|
| 111 |
+
pad = k // 2
|
| 112 |
+
data_padded = np.pad(data, ((0,0),(0,0),(pad,pad)), mode='edge')
|
| 113 |
+
|
| 114 |
+
# 利用 np.convolve 沿最后一个轴计算
|
| 115 |
+
def conv_1d(x):
|
| 116 |
+
return np.convolve(x, kernel[::-1], mode='valid') / kernel_sum
|
| 117 |
+
|
| 118 |
+
# 按最后一维应用
|
| 119 |
+
smoothed = np.apply_along_axis(conv_1d, 2, data_padded)
|
| 120 |
+
|
| 121 |
+
smoothed = np.maximum(smoothed, 0) # 负数置零
|
| 122 |
+
return smoothed
|
| 123 |
+
|
| 124 |
+
BIN_SIZE = 180
|
| 125 |
+
MAX_PEAKS = 2 # 峰值个数
|
| 126 |
+
|
| 127 |
+
@njit
|
| 128 |
+
def sum_hist(hist, length):
|
| 129 |
+
return np.sum(hist[:length])
|
| 130 |
+
@njit
|
| 131 |
+
def max_hist(hist, length):
|
| 132 |
+
return np.max(hist[:length])
|
| 133 |
+
@njit
|
| 134 |
+
def compute_centroid(hist, start_bin, end_bin):
|
| 135 |
+
bins = np.arange(start_bin, end_bin + 1)
|
| 136 |
+
values = hist[start_bin:end_bin + 1]
|
| 137 |
+
total = np.sum(values)
|
| 138 |
+
if total == 0:
|
| 139 |
+
return (start_bin + end_bin) / 2.0
|
| 140 |
+
return np.sum(bins * values) / total
|
| 141 |
+
|
| 142 |
+
@njit
|
| 143 |
+
def find_peaks_hw(histograms, histograms_ma):
|
| 144 |
+
"""
|
| 145 |
+
输入:
|
| 146 |
+
histograms: (H, W, 256) 原始直方图
|
| 147 |
+
histograms_ma: (H, W, 256) 平滑直方图
|
| 148 |
+
输出:
|
| 149 |
+
tof_data: (H, W, MAX_PEAKS) 质心位置
|
| 150 |
+
peak_data: (H, W, MAX_PEAKS) 峰值强度
|
| 151 |
+
noise_data: (H, W) 噪声值
|
| 152 |
+
multishot_data: (H, W) 多拍信息
|
| 153 |
+
totalcount: (H, W) 总计数
|
| 154 |
+
nt_count_n: (H, W, MAX_PEAKS) NT计数
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
H, W, _ = histograms.shape
|
| 158 |
+
|
| 159 |
+
# 输出初始化
|
| 160 |
+
tof_data = np.zeros((H, W, MAX_PEAKS), dtype=np.float32)
|
| 161 |
+
peak_data = np.zeros((H, W, MAX_PEAKS), dtype=np.int32)
|
| 162 |
+
noise_data = np.zeros((H, W), dtype=np.int16)
|
| 163 |
+
multishot_data = np.zeros((H, W), dtype=np.int16)
|
| 164 |
+
totalcount = np.zeros((H, W), dtype=np.int32)
|
| 165 |
+
nt_count_n = np.zeros((H, W, MAX_PEAKS), dtype=np.int32)
|
| 166 |
+
|
| 167 |
+
for i in range(H):
|
| 168 |
+
for j in range(W):
|
| 169 |
+
hist_raw = histograms[i, j]
|
| 170 |
+
hist = histograms_ma[i, j]
|
| 171 |
+
count = 0
|
| 172 |
+
bin_idx = 1
|
| 173 |
+
|
| 174 |
+
# 多拍信息
|
| 175 |
+
multishot = (int(hist_raw[254]) << 8) + (int(hist_raw[255]) >> 2)
|
| 176 |
+
multishot_data[i, j] = multishot
|
| 177 |
+
|
| 178 |
+
totalcount[i, j] = int(sum_hist(hist_raw, BIN_SIZE) << 13) // multishot
|
| 179 |
+
|
| 180 |
+
# 噪声估计
|
| 181 |
+
noise_data[i, j] = int(sum_hist(hist, 8) + 4) >> 3
|
| 182 |
+
|
| 183 |
+
noise_i = max_hist(hist, 8)
|
| 184 |
+
# max_th = np.max(hist) * 0.01 + 25
|
| 185 |
+
max_th = 50
|
| 186 |
+
noise_i = max_th if noise_i < max_th else noise_i * 3
|
| 187 |
+
th = 2 * noise_i - 24 / (2e4 * 4) * noise_i * noise_i
|
| 188 |
+
|
| 189 |
+
# 峰值检测
|
| 190 |
+
while bin_idx < BIN_SIZE - 1 and count < MAX_PEAKS:
|
| 191 |
+
# 找到第一个 >= TH 的 bin
|
| 192 |
+
while bin_idx < BIN_SIZE - 1 and hist[bin_idx] < th:
|
| 193 |
+
bin_idx += 1
|
| 194 |
+
|
| 195 |
+
bin_idx -= 1
|
| 196 |
+
start_bin = bin_idx
|
| 197 |
+
start_peak = hist[start_bin]
|
| 198 |
+
|
| 199 |
+
# 找最大值 bin
|
| 200 |
+
while bin_idx + 1 < BIN_SIZE and (
|
| 201 |
+
hist[bin_idx] < hist[bin_idx - 1] or hist[bin_idx] < hist[bin_idx + 1]
|
| 202 |
+
):
|
| 203 |
+
bin_idx += 1
|
| 204 |
+
max_bin = bin_idx
|
| 205 |
+
max_peak = hist[max_bin]
|
| 206 |
+
|
| 207 |
+
# 找 end_bin
|
| 208 |
+
while bin_idx + 1 < BIN_SIZE and (
|
| 209 |
+
hist[bin_idx] > th or hist[bin_idx] > start_peak or hist[bin_idx] > hist[bin_idx + 1]
|
| 210 |
+
):
|
| 211 |
+
bin_idx += 1
|
| 212 |
+
end_bin = bin_idx
|
| 213 |
+
|
| 214 |
+
if (
|
| 215 |
+
start_bin == end_bin
|
| 216 |
+
or start_bin == max_bin
|
| 217 |
+
or max_bin == end_bin
|
| 218 |
+
or (max_peak - start_peak) < 50
|
| 219 |
+
):
|
| 220 |
+
bin_idx += 1
|
| 221 |
+
continue
|
| 222 |
+
|
| 223 |
+
# 质心
|
| 224 |
+
centroid = compute_centroid(hist, start_bin, end_bin)
|
| 225 |
+
tof_data[i, j, count] = centroid
|
| 226 |
+
peak_data[i, j, count] = (int(hist[max_bin]) << 13) // multishot
|
| 227 |
+
|
| 228 |
+
# NT count
|
| 229 |
+
nt_end_bin = max_bin - 10
|
| 230 |
+
nt_start_bin = nt_end_bin - 48
|
| 231 |
+
nt_start_bin = 0 if nt_start_bin < 0 else nt_start_bin
|
| 232 |
+
nt_num = nt_end_bin - nt_start_bin
|
| 233 |
+
est_nt = 48 * noise_data[i, j] if nt_num < 48 else 0
|
| 234 |
+
nt_count_n[i, j, count] = np.sum(hist[nt_start_bin:nt_start_bin + nt_num]) + est_nt
|
| 235 |
+
|
| 236 |
+
count += 1
|
| 237 |
+
|
| 238 |
+
return tof_data, peak_data, noise_data, multishot_data, totalcount, nt_count_n
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def local_threshold(img, window_size=15, C=2):
|
| 242 |
+
h, w = img.shape
|
| 243 |
+
out = np.zeros_like(img, dtype=np.uint8)
|
| 244 |
+
pad = window_size // 2
|
| 245 |
+
padded = cv2.copyMakeBorder(img, pad, pad, pad, pad, cv2.BORDER_REFLECT)
|
| 246 |
+
|
| 247 |
+
for i in range(h):
|
| 248 |
+
for j in range(w):
|
| 249 |
+
local_region = padded[i:i+window_size, j:j+window_size]
|
| 250 |
+
# thresh = np.mean(local_region) - C
|
| 251 |
+
thresh = np.mean(local_region,axis=(0,1)) - 0.1
|
| 252 |
+
out[i,j] = img[i,j] if img[i,j] > thresh else 0
|
| 253 |
+
|
| 254 |
+
return out
|
| 255 |
+
|
| 256 |
+
def select_peaks_hw(tof_data, peak_data):
|
| 257 |
+
"""
|
| 258 |
+
输入:
|
| 259 |
+
tof_data: (H, W, MAX_PEAKS) 质心位置
|
| 260 |
+
peak_data: (H, W, MAX_PEAKS) 峰值强度
|
| 261 |
+
输出:
|
| 262 |
+
tof: (H, W)
|
| 263 |
+
peak: (H, W)
|
| 264 |
+
|
| 265 |
+
"""
|
| 266 |
+
peak_data = peak_data *256/48000
|
| 267 |
+
ref_set = np.zeros_like(peak_data)
|
| 268 |
+
for i in range(MAX_PEAKS):
|
| 269 |
+
ref_set[...,i] = peak_data[...,i]*tof_data[...,i]*tof_data[...,i] /1200 * 6
|
| 270 |
+
ref = np.log2(ref_set[...,i])
|
| 271 |
+
ref[ref<0] = 0
|
| 272 |
+
_, otsu_binary = cv2.threshold(ref.astype(np.uint8), 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 273 |
+
# dynamic_ref = np.max(ref,axis=(0,1)) * 0.8
|
| 274 |
+
# ref[ref>100] = 100
|
| 275 |
+
ref[ref<3] = 0
|
| 276 |
+
# ref = local_threshold(ref,5,0)
|
| 277 |
+
ref_set[...,i] =(ref)
|
| 278 |
+
ref_set[...,1]*= 3
|
| 279 |
+
ref = np.max(ref_set,axis=2)
|
| 280 |
+
|
| 281 |
+
frame1 = ref_set[...,0]
|
| 282 |
+
frame2 = ref_set[...,1]
|
| 283 |
+
# 两个 mask 初始化为 0
|
| 284 |
+
mask1 = np.zeros_like(frame1, dtype=np.uint8)
|
| 285 |
+
mask2 = np.zeros_like(frame2, dtype=np.uint8)
|
| 286 |
+
|
| 287 |
+
# 逐像素比较:frame1 > frame2
|
| 288 |
+
mask1[(frame1 > frame2) & (frame1 > 0)] = 1
|
| 289 |
+
mask2[(frame2 > frame1) & (frame2 > 0)] = 1
|
| 290 |
+
|
| 291 |
+
# 如果相等且非零,可以任选一帧,这里给 mask1
|
| 292 |
+
mask1[(frame1 == frame2) & (frame1 > 0)] = 1
|
| 293 |
+
|
| 294 |
+
tof = tof_data[...,0] * mask1 + tof_data[...,1] * mask2
|
| 295 |
+
peak = peak_data[...,0] * mask1 + peak_data[...,1] * mask2
|
| 296 |
+
|
| 297 |
+
return tof,peak,ref,ref_set
|