Sky-Kim commited on
Commit
f3c9fb2
·
1 Parent(s): e3e4c1f

Initial commit: com.sky.sentis.yolox Unity package

Browse files

YOLOX object detection for the Unity Inference Engine (Sentis), FP16.
Includes Runtime sources and model weights (Models~/, git-lfs).

.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.sentis filter=lfs diff=lfs merge=lfs -text
CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-06-22
4
+ ### Added
5
+ - Initial package: `Yolox` Unity Inference Engine (Sentis) inference wrapper and FP16 model weights under `Models~`.
CHANGELOG.md.meta ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: b98fa00cb2a2a4058b809040d3a969af
3
+ TextScriptImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
LICENSE.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # sentis-yolox License
2
+
3
+ This package's wrapper code is licensed under the Apache License 2.0.
4
+ The bundled model weights are derived from Megvii-BaseDetection/YOLOX (https://github.com/Megvii-BaseDetection/YOLOX) and are also Apache-2.0 licensed.
5
+
6
+ Copyright 2026 Sky Kim
7
+ Copyright the upstream authors of Megvii-BaseDetection/YOLOX (https://github.com/Megvii-BaseDetection/YOLOX)
8
+
9
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
10
+ these files except in compliance with the License. You may obtain a copy of the
11
+ License at:
12
+
13
+ https://www.apache.org/licenses/LICENSE-2.0
14
+
15
+ Unless required by applicable law or agreed to in writing, software distributed
16
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
17
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
18
+ specific language governing permissions and limitations under the License.
LICENSE.md.meta ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 5fad480053daa4611a1551b821c9f0f0
3
+ TextScriptImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
Models~/yolox_fp16.sentis ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:81b2a88891591fd85431fca32fbbf2337cb667fcfc157c27698be27b0f467920
3
+ size 1958976
README.md CHANGED
@@ -1,3 +1,48 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ library_name: unity-sentis
4
+ tags:
5
+ - unity
6
+ - sentis
7
+ - on-device
8
+ - object-detection
9
+ - yolox
10
  ---
11
+
12
+ # sentis-yolox
13
+
14
+ YOLOX object detection converted to **Unity Inference Engine (Sentis)** FP16.
15
+
16
+ ## Files
17
+
18
+ ```
19
+ yolox_fp16.sentis
20
+ YoloxDetector.cs # self-contained Unity Sentis inference (COCO labels)
21
+ ```
22
+
23
+ ## Model I/O
24
+
25
+ - **Input:** RGB NCHW `[1, 3, S, S]` in `[0, 1]` (read `S` from the model; the `[0,1]→[0,255]` scale is
26
+ baked into the export). YOLOX is typically trained BGR, so a R↔B swizzle is usually needed.
27
+ - **Output:** one tensor `[1, N, 5+C]` (or `[1, 5+C, N]`) where each candidate is
28
+ `[cx, cy, w, h, objectness, class_0..class_{C-1}]`, scores already sigmoid-activated and boxes in
29
+ input-pixel coordinates (export with the grid/stride decode baked in). NMS runs on the C# side.
30
+
31
+ ## Inference
32
+
33
+ A complete self-contained implementation lives in [`YoloxDetector.cs`](YoloxDetector.cs) (texture →
34
+ tensor with optional R↔B swizzle, layout auto-detection, scoring, class-wise NMS, GPU warmup, 80 COCO
35
+ labels inlined). Minimal usage:
36
+
37
+ ```csharp
38
+ var detector = new YoloxDetector(BackendType.GPUCompute);
39
+ detector.Load(modelRoot); // folder holding yolox_fp16.sentis
40
+ List<YoloxDetector.Detection> dets = await detector.DetectAsync(frameTexture); // boxes in input-pixel space
41
+ ```
42
+
43
+ > Do not substitute Ultralytics weights (`yolo*`), which are **AGPL-3.0**. This repo ships YOLOX only.
44
+
45
+ ## License & attribution
46
+
47
+ Apache-2.0. Converted from [Megvii-BaseDetection/YOLOX](https://github.com/Megvii-BaseDetection/YOLOX)
48
+ (Apache-2.0).
README.md.meta ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 4e2f69251dd1d4c95b2795cdf0ceae9e
3
+ TextScriptImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
Runtime.meta ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 8320957a855574432a15403a11cc61de
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
Runtime/Sky.Sentis.Yolox.asmdef ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Sky.Sentis.Yolox",
3
+ "rootNamespace": "SentisModels",
4
+ "references": [
5
+ "Unity.InferenceEngine"
6
+ ],
7
+ "includePlatforms": [],
8
+ "excludePlatforms": [],
9
+ "allowUnsafeCode": false,
10
+ "overrideReferences": false,
11
+ "precompiledReferences": [],
12
+ "autoReferenced": true,
13
+ "defineConstraints": [],
14
+ "versionDefines": [],
15
+ "noEngineReferences": false
16
+ }
Runtime/Sky.Sentis.Yolox.asmdef.meta ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: ec42095011d644f7a8bdca66283613de
3
+ AssemblyDefinitionImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
Runtime/YoloxDetector.cs ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // YOLOX object detector (COCO). Self-contained Unity Sentis inference.
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using System.IO;
5
+ using System.Threading;
6
+ using System.Threading.Tasks;
7
+ using Unity.InferenceEngine;
8
+ using UnityEngine;
9
+
10
+ namespace SentisModels
11
+ {
12
+ public sealed class YoloxDetector : IDisposable
13
+ {
14
+ const int DefaultInputSize = 640;
15
+ const int NumCoords = 4;
16
+ const float NmsIouThreshold = 0.45f;
17
+ const string DefaultModelFile = "yolox_fp16.sentis";
18
+
19
+ static readonly string[] s_CocoLabels =
20
+ {
21
+ "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light",
22
+ "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
23
+ "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
24
+ "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle",
25
+ "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
26
+ "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed",
27
+ "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven",
28
+ "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"
29
+ };
30
+
31
+ readonly BackendType m_BackendType;
32
+ readonly float m_ConfidenceThreshold;
33
+ readonly bool m_LogOutputShapeOnce;
34
+ readonly bool m_WarmupOnLoad;
35
+ // Swap R<->B before inference. YOLOX is typically trained on BGR while Sentis ToTensor produces RGB.
36
+ readonly bool m_SwapRedBlue;
37
+
38
+ string[] Labels => s_CocoLabels;
39
+
40
+ Worker m_Worker;
41
+ // Model input resolution (square), read from the loaded model; falls back to DefaultInputSize.
42
+ int m_InputSize = DefaultInputSize;
43
+ bool m_LoggedShape;
44
+ // Worker is not re-entrant; serializes concurrent callers.
45
+ readonly SemaphoreSlim m_InferLock = new SemaphoreSlim(1, 1);
46
+
47
+ public struct Detection
48
+ {
49
+ public int ClassId;
50
+ public string ClassName;
51
+ public float Confidence;
52
+ public Rect BoxXyxy;
53
+ }
54
+
55
+ public bool IsReady => m_Worker != null;
56
+
57
+ public YoloxDetector(
58
+ BackendType backendType = BackendType.GPUCompute,
59
+ float confidenceThreshold = 0.25f,
60
+ bool swapRedBlue = true,
61
+ bool warmupOnLoad = true,
62
+ bool logOutputShapeOnce = true)
63
+ {
64
+ m_BackendType = backendType;
65
+ m_ConfidenceThreshold = confidenceThreshold;
66
+ m_SwapRedBlue = swapRedBlue;
67
+ m_WarmupOnLoad = warmupOnLoad;
68
+ m_LogOutputShapeOnce = logOutputShapeOnce;
69
+ }
70
+
71
+ // modelRoot: absolute path to the folder holding yolox_fp16.sentis.
72
+ public void Load(string modelRoot, string modelFile = DefaultModelFile)
73
+ {
74
+ DisposeResources();
75
+ Model model;
76
+ try
77
+ {
78
+ model = ModelLoader.Load(Path.Combine(modelRoot, modelFile));
79
+ }
80
+ catch (Exception exception)
81
+ {
82
+ Debug.LogException(exception);
83
+ return;
84
+ }
85
+
86
+ m_InputSize = ResolveInputSize(model, DefaultInputSize);
87
+ m_Worker = new Worker(model, m_BackendType);
88
+
89
+ if (m_WarmupOnLoad)
90
+ WarmupAsync();
91
+ }
92
+
93
+ // YOLO inputs are square NCHW (1, 3, S, S); read S from the model so 320- and 640-input
94
+ // exports both work. Falls back when the input dim is dynamic/unknown.
95
+ static int ResolveInputSize(Model model, int fallback)
96
+ {
97
+ if (model.inputs.Count == 0)
98
+ return fallback;
99
+ var dynamicShape = model.inputs[0].shape;
100
+ if (!dynamicShape.IsStatic())
101
+ return fallback;
102
+ var shape = dynamicShape.ToTensorShape();
103
+ return shape.rank >= 4 ? Mathf.Max(1, shape[shape.rank - 1]) : fallback;
104
+ }
105
+
106
+ // One throwaway inference on a blank frame so the GPUCompute kernels compile before the first
107
+ // real detection, which would otherwise stall while every shader is built.
108
+ async void WarmupAsync()
109
+ {
110
+ if (m_Worker == null)
111
+ return;
112
+
113
+ await m_InferLock.WaitAsync();
114
+ try
115
+ {
116
+ using var input = new Tensor<float>(new TensorShape(1, 3, m_InputSize, m_InputSize));
117
+ m_Worker.Schedule(input);
118
+
119
+ var out0 = m_Worker.PeekOutput(0) as Tensor<float>;
120
+ if (out0 != null)
121
+ (await out0.ReadbackAndCloneAsync()).Dispose();
122
+ }
123
+ catch (Exception exception)
124
+ {
125
+ Debug.LogException(exception);
126
+ }
127
+ finally
128
+ {
129
+ m_InferLock.Release();
130
+ }
131
+ }
132
+
133
+ public async Task<List<Detection>> DetectAsync(Texture frame)
134
+ {
135
+ if (m_Worker == null || frame == null)
136
+ return new List<Detection>();
137
+
138
+ // No ConfigureAwait(false): must resume on Unity main thread for Schedule + readback.
139
+ await m_InferLock.WaitAsync();
140
+ try
141
+ {
142
+ using var input = new Tensor<float>(new TensorShape(1, 3, m_InputSize, m_InputSize));
143
+ // Sentis ToTensor produces RGB; YOLOX is typically BGR. m_SwapRedBlue toggles R<->B (the
144
+ // [0,1]->[0,255] scale YOLOX expects is baked into the model).
145
+ var transform = m_SwapRedBlue
146
+ ? new TextureTransform().SetChannelSwizzle(2, 1, 0, 3)
147
+ : new TextureTransform();
148
+ TextureConverter.ToTensor(frame, input, transform);
149
+ m_Worker.Schedule(input);
150
+
151
+ var out0 = m_Worker.PeekOutput(0) as Tensor<float>;
152
+ if (out0 == null)
153
+ {
154
+ if (!m_LoggedShape)
155
+ {
156
+ Debug.LogWarning("[Vision] YOLOX model produced no float output 0.");
157
+ m_LoggedShape = true;
158
+ }
159
+ return new List<Detection>();
160
+ }
161
+
162
+ using var c0 = await out0.ReadbackAndCloneAsync();
163
+
164
+ if (m_LogOutputShapeOnce && !m_LoggedShape)
165
+ {
166
+ Debug.Log("[Vision] YOLOX output: out0=" + c0.shape);
167
+ m_LoggedShape = true;
168
+ }
169
+
170
+ return Decode(c0);
171
+ }
172
+ finally
173
+ {
174
+ m_InferLock.Release();
175
+ }
176
+ }
177
+
178
+ // YOLOX output: a single tensor [1, N, 5+C] or [1, 5+C, N], where each of the N candidates is
179
+ // [cx, cy, w, h, objectness, class_0 .. class_{C-1}]. Scores are already sigmoid-activated and
180
+ // box coords are in input-pixel space (export YOLOX with the grid/stride decode baked in, i.e.
181
+ // decode_in_inference). Input is RGB NCHW in [0,1] (Sentis ToTensor default) — bake any /255 or
182
+ // mean/std normalization into the exported model.
183
+ List<Detection> Decode(Tensor<float> t)
184
+ {
185
+ var labels = Labels;
186
+ var feat = NumCoords + 1 + labels.Length; // cx,cy,w,h + objectness + classes
187
+
188
+ if (!TryResolveLayout(t, feat, out var featAxis, out var candAxis))
189
+ {
190
+ if (!m_LoggedShape)
191
+ {
192
+ Debug.LogWarning("[Vision] Could not resolve YOLOX output layout (expected a dim == " +
193
+ feat + " for " + labels.Length + " classes): " + t.shape);
194
+ m_LoggedShape = true;
195
+ }
196
+ return new List<Detection>();
197
+ }
198
+
199
+ var candidates = t.shape[candAxis];
200
+ var featMajor = featAxis < candAxis; // [1, F, N] flattens feature-major
201
+ float At(int f, int i) => featMajor ? t[f * candidates + i] : t[i * feat + f];
202
+
203
+ var dets = new List<Detection>();
204
+ for (var i = 0; i < candidates; i++)
205
+ {
206
+ var obj = At(4, i);
207
+
208
+ var bestClass = 0;
209
+ var bestCls = float.NegativeInfinity;
210
+ for (var c = 0; c < labels.Length; c++)
211
+ {
212
+ var v = At(5 + c, i);
213
+ if (v > bestCls) { bestCls = v; bestClass = c; }
214
+ }
215
+
216
+ var score = obj * bestCls;
217
+ if (score < m_ConfidenceThreshold)
218
+ continue;
219
+
220
+ float cx = At(0, i), cy = At(1, i), w = At(2, i), h = At(3, i);
221
+ var x1 = Mathf.Clamp(cx - w * 0.5f, 0f, m_InputSize);
222
+ var y1 = Mathf.Clamp(cy - h * 0.5f, 0f, m_InputSize);
223
+ var x2 = Mathf.Clamp(cx + w * 0.5f, 0f, m_InputSize);
224
+ var y2 = Mathf.Clamp(cy + h * 0.5f, 0f, m_InputSize);
225
+ if (x2 - x1 < 1f || y2 - y1 < 1f)
226
+ continue;
227
+
228
+ dets.Add(new Detection
229
+ {
230
+ ClassId = bestClass,
231
+ ClassName = labels[bestClass],
232
+ Confidence = score,
233
+ BoxXyxy = new Rect(x1, y1, x2 - x1, y2 - y1),
234
+ });
235
+ }
236
+
237
+ return NonMaxSuppression(dets, NmsIouThreshold);
238
+ }
239
+
240
+ static bool TryResolveLayout(Tensor<float> t, int targetDim, out int valueAxis, out int candidateAxis)
241
+ {
242
+ valueAxis = -1;
243
+ candidateAxis = -1;
244
+ var shape = t.shape;
245
+ if (shape.rank < 2)
246
+ return false;
247
+
248
+ for (var ax = 0; ax < shape.rank; ax++)
249
+ {
250
+ if (shape[ax] == targetDim) { valueAxis = ax; break; }
251
+ }
252
+ if (valueAxis < 0)
253
+ return false;
254
+
255
+ var maxDim = -1;
256
+ for (var ax = 0; ax < shape.rank; ax++)
257
+ {
258
+ if (ax == valueAxis) continue;
259
+ var d = shape[ax];
260
+ if (d <= 1) continue;
261
+ if (d > maxDim) { maxDim = d; candidateAxis = ax; }
262
+ }
263
+ if (candidateAxis < 0)
264
+ {
265
+ for (var ax = 0; ax < shape.rank; ax++)
266
+ if (ax != valueAxis) { candidateAxis = ax; break; }
267
+ }
268
+ return candidateAxis >= 0;
269
+ }
270
+
271
+ static List<Detection> NonMaxSuppression(List<Detection> dets, float iouThreshold)
272
+ {
273
+ dets.Sort((x, y) => y.Confidence.CompareTo(x.Confidence));
274
+ var kept = new List<Detection>();
275
+ var removed = new bool[dets.Count];
276
+ for (var i = 0; i < dets.Count; i++)
277
+ {
278
+ if (removed[i]) continue;
279
+ kept.Add(dets[i]);
280
+ for (var j = i + 1; j < dets.Count; j++)
281
+ {
282
+ if (removed[j]) continue;
283
+ if (dets[i].ClassId == dets[j].ClassId && Iou(dets[i].BoxXyxy, dets[j].BoxXyxy) > iouThreshold)
284
+ removed[j] = true;
285
+ }
286
+ }
287
+ return kept;
288
+ }
289
+
290
+ static float Iou(Rect a, Rect b)
291
+ {
292
+ var x1 = Mathf.Max(a.xMin, b.xMin);
293
+ var y1 = Mathf.Max(a.yMin, b.yMin);
294
+ var x2 = Mathf.Min(a.xMax, b.xMax);
295
+ var y2 = Mathf.Min(a.yMax, b.yMax);
296
+ var inter = Mathf.Max(0f, x2 - x1) * Mathf.Max(0f, y2 - y1);
297
+ var union = a.width * a.height + b.width * b.height - inter;
298
+ return union <= 0f ? 0f : inter / union;
299
+ }
300
+
301
+ public void Dispose()
302
+ {
303
+ DisposeResources();
304
+ }
305
+
306
+ void DisposeResources()
307
+ {
308
+ m_Worker?.Dispose();
309
+ m_Worker = null;
310
+ }
311
+ }
312
+ }
Runtime/YoloxDetector.cs.meta ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 2f82e46a779df4c52aa88078c23815fd
package.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.sky.sentis.yolox",
3
+ "version": "0.1.0",
4
+ "displayName": "Sentis YOLOX Detector",
5
+ "description": "YOLOX object detection for the Unity Inference Engine (Sentis), FP16 (80 COCO classes).",
6
+ "unity": "6000.4",
7
+ "license": "Apache-2.0",
8
+ "documentationUrl": "https://github.com/skykim/OnDeviceAgent",
9
+ "author": {
10
+ "name": "Sky Kim"
11
+ },
12
+ "keywords": [
13
+ "unity",
14
+ "sentis",
15
+ "inference-engine",
16
+ "on-device",
17
+ "ai"
18
+ ],
19
+ "dependencies": {
20
+ "com.unity.ai.inference": "2.6.1"
21
+ }
22
+ }
package.json.meta ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 34dfed7a132ee42c881b051b66826faf
3
+ PackageManifestImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant: