forked from violetljj/blind-assist
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerate_qnn_preprocess_candidate.py
More file actions
154 lines (132 loc) · 4.86 KB
/
Copy pathgenerate_qnn_preprocess_candidate.py
File metadata and controls
154 lines (132 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/env python3
"""Generate and verify the isolated QNN preprocessing candidate model."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import numpy as np
import tensorflow as tf
SOURCE_WIDTH = 640
SOURCE_HEIGHT = 480
INPUT_SIZE = 320
RESIZED_WIDTH = 240
ROTATION_DEGREES = 90
MODEL_FILENAME = "rgba640x480_rot90_letterbox320.tflite"
CONTRACT_FILENAME = "contract.json"
class PreprocessModule(tf.Module):
@tf.function(
input_signature=[
tf.TensorSpec(
shape=[1, SOURCE_HEIGHT, SOURCE_WIDTH, 4],
dtype=tf.uint8,
name="rgba",
)
]
)
def preprocess(self, rgba: tf.Tensor) -> dict[str, tf.Tensor]:
rotated = tf.image.rot90(rgba, k=3)
rgb = tf.cast(rotated[:, :, :, :3], tf.float32)
resized = tf.raw_ops.ResizeNearestNeighbor(
images=rgb,
size=tf.constant([INPUT_SIZE, RESIZED_WIDTH], tf.int32),
align_corners=False,
half_pixel_centers=False,
)
padded = tf.pad(
resized,
paddings=[[0, 0], [0, 0], [40, 40], [0, 0]],
mode="CONSTANT",
constant_values=0,
)
normalized = tf.math.divide(padded, tf.constant(255.0, tf.float32))
return {"normalized_rgb": normalized}
def synthetic_rgba() -> np.ndarray:
y, x = np.indices((SOURCE_HEIGHT, SOURCE_WIDTH), dtype=np.int32)
rgba = np.empty((1, SOURCE_HEIGHT, SOURCE_WIDTH, 4), dtype=np.uint8)
rgba[0, :, :, 0] = (x * 31 + y * 7) & 0xFF
rgba[0, :, :, 1] = (x * 13 + y * 29) & 0xFF
rgba[0, :, :, 2] = (x * 19 + y * 17) & 0xFF
rgba[0, :, :, 3] = 255
return rgba
def reference_preprocess(rgba: np.ndarray) -> np.ndarray:
output = np.zeros((1, INPUT_SIZE, INPUT_SIZE, 3), dtype=np.float32)
for target_y in range(INPUT_SIZE):
source_x = target_y * 2
for resized_x in range(RESIZED_WIDTH):
source_y = SOURCE_HEIGHT - 1 - resized_x * 2
output[0, target_y, 40 + resized_x, :] = (
rgba[0, source_y, source_x, :3].astype(np.float32) / 255.0
)
return output
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest().upper()
def generate(output_dir: Path) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
module = PreprocessModule()
concrete = module.preprocess.get_concrete_function()
converter = tf.lite.TFLiteConverter.from_concrete_functions([concrete], module)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
model = converter.convert()
model_path = output_dir / MODEL_FILENAME
model_path.write_bytes(model)
interpreter = tf.lite.Interpreter(model_content=model)
interpreter.allocate_tensors()
input_detail = interpreter.get_input_details()[0]
output_detail = interpreter.get_output_details()[0]
rgba = synthetic_rgba()
interpreter.set_tensor(input_detail["index"], rgba)
interpreter.invoke()
actual = interpreter.get_tensor(output_detail["index"])
expected = reference_preprocess(rgba)
difference = np.abs(actual.astype(np.float64) - expected.astype(np.float64))
max_abs = float(difference.max(initial=0.0))
mean_abs = float(difference.mean())
if max_abs > 1e-7:
raise RuntimeError(
f"generated preprocessing graph exceeds tolerance: max_abs={max_abs}"
)
contract = {
"schema": "blindassist_qnn_preprocess_candidate_contract_v1",
"candidate_only": True,
"source": {
"shape": [1, SOURCE_HEIGHT, SOURCE_WIDTH, 4],
"dtype": "UINT8",
"rotation_degrees": ROTATION_DEGREES,
"row_stride": SOURCE_WIDTH * 4,
"pixel_stride": 4,
},
"output": {
"shape": [1, INPUT_SIZE, INPUT_SIZE, 3],
"dtype": "FLOAT32",
"letterbox_left": 40,
"letterbox_right": 40,
"normalization": "value / 255.0",
},
"model": {
"filename": MODEL_FILENAME,
"sha256": sha256(model),
"bytes": len(model),
},
"host_reference": {
"max_abs": max_abs,
"mean_abs": mean_abs,
"acceptance_max_abs": 1e-7,
"output_sha256": sha256(actual.tobytes()),
},
}
(output_dir / CONTRACT_FILENAME).write_text(
json.dumps(contract, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(json.dumps(contract, ensure_ascii=False, indent=2))
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--output-dir",
type=Path,
default=Path("artifacts.local/experiments/qnn-preprocess-fusion-v1"),
)
return parser.parse_args()
if __name__ == "__main__":
generate(parse_args().output_dir.resolve())