blob: 121ab755eb6349a89cdde25bb42e3b3d876fef20 [file] [log] [blame]
Philipp Schradere625ba22020-11-16 20:11:37 -08001import * as configuration from 'org_frc971/aos/configuration_generated';
2import * as connect from 'org_frc971/aos/network/connect_generated';
3import {Connection} from 'org_frc971/aos/network/www/proxy';
4import * as flatbuffers_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
5import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
6import {Long} from 'org_frc971/external/com_github_google_flatbuffers/ts/long';
7import * as sift from 'org_frc971/y2020/vision/sift/sift_generated'
8import * as vision from 'org_frc971/y2020/vision/vision_generated';
9
10import Channel = configuration.aos.Channel;
11import Configuration = configuration.aos.Configuration;
12import Connect = connect.aos.message_bridge.Connect;
13import CameraImage = vision.frc971.vision.CameraImage;
14import ImageMatchResult = sift.frc971.vision.sift.ImageMatchResult;
15import Feature = sift.frc971.vision.sift.Feature;
Alex Perryd1969882020-03-06 21:19:00 -080016
17/*
18 * All the messages that are required to show an image with metadata.
19 * Messages not readable on the server node are ignored.
20 */
21const REQUIRED_CHANNELS = [
22 {
23 name: '/pi1/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080024 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080025 },
26 {
27 name: '/pi2/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080028 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080029 },
30 {
31 name: '/pi3/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080032 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080033 },
34 {
35 name: '/pi4/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080036 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080037 },
38 {
39 name: '/pi5/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080040 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080041 },
42 {
43 name: '/pi1/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080044 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080045 },
46 {
47 name: '/pi2/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080048 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080049 },
50 {
51 name: '/pi3/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080052 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080053 },
54 {
55 name: '/pi4/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080056 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080057 },
58 {
59 name: '/pi5/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080060 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080061 },
62];
Alex Perry5f474f22020-02-01 12:14:24 -080063
64export class ImageHandler {
65 private canvas = document.createElement('canvas');
Alex Perryfffe2e32020-02-29 19:48:17 -080066 private select = document.createElement('select');
67
Alex Perryb41d5782020-02-09 17:06:40 -080068 private imageBuffer: Uint8ClampedArray|null = null;
Philipp Schradere625ba22020-11-16 20:11:37 -080069 private image: CameraImage|null = null;
70 private imageTimestamp: Long|null = null;
71 private result: ImageMatchResult|null = null;
72 private resultTimestamp: Long|null = null;
Alex Perry22824d72020-02-29 17:11:43 -080073 private width = 0;
74 private height = 0;
Alex Perryfffe2e32020-02-29 19:48:17 -080075 private selectedIndex = 0;
Alex Perry22824d72020-02-29 17:11:43 -080076 private imageSkipCount = 3;
Alex Perry5f474f22020-02-01 12:14:24 -080077
Alex Perryd1969882020-03-06 21:19:00 -080078 constructor(private readonly connection: Connection) {
Alex Perryfffe2e32020-02-29 19:48:17 -080079 document.body.appendChild(this.select);
80 const defaultOption = document.createElement('option');
81 defaultOption.innerText = 'Show all features';
82 this.select.appendChild(defaultOption);
83 this.select.addEventListener('change', (ev) => this.handleSelect(ev));
Alex Perry5f474f22020-02-01 12:14:24 -080084 document.body.appendChild(this.canvas);
Alex Perryd1969882020-03-06 21:19:00 -080085
86 this.connection.addConfigHandler(() => {
87 this.sendConnect();
88 });
Austin Schuh7c75e582020-11-14 16:41:18 -080089 this.connection.addHandler(
Philipp Schradere625ba22020-11-16 20:11:37 -080090 ImageMatchResult.getFullyQualifiedName(), (data) => {
Austin Schuh7c75e582020-11-14 16:41:18 -080091 this.handleImageMetadata(data);
92 });
Philipp Schradere625ba22020-11-16 20:11:37 -080093 this.connection.addHandler(CameraImage.getFullyQualifiedName(), (data) => {
94 this.handleImage(data);
95 });
Alex Perryd1969882020-03-06 21:19:00 -080096 }
97
98 private sendConnect(): void {
Philipp Schradere625ba22020-11-16 20:11:37 -080099 const builder =
100 new flatbuffers_builder.Builder(512) as unknown as flatbuffers.Builder;
Alex Perryd1969882020-03-06 21:19:00 -0800101 const channels: flatbuffers.Offset[] = [];
102 for (const channel of REQUIRED_CHANNELS) {
103 const nameFb = builder.createString(channel.name);
104 const typeFb = builder.createString(channel.type);
Philipp Schradere625ba22020-11-16 20:11:37 -0800105 Channel.startChannel(builder);
106 Channel.addName(builder, nameFb);
107 Channel.addType(builder, typeFb);
108 const channelFb = Channel.endChannel(builder);
Alex Perryd1969882020-03-06 21:19:00 -0800109 channels.push(channelFb);
110 }
111
Philipp Schradere625ba22020-11-16 20:11:37 -0800112 const channelsFb =
113 Connect.createChannelsToTransferVector(builder, channels);
114 Connect.startConnect(builder);
115 Connect.addChannelsToTransfer(builder, channelsFb);
116 const connect = Connect.endConnect(builder);
Alex Perryd1969882020-03-06 21:19:00 -0800117 builder.finish(connect);
118 this.connection.sendConnectMessage(builder);
Alex Perry5f474f22020-02-01 12:14:24 -0800119 }
120
Alex Perryfffe2e32020-02-29 19:48:17 -0800121 handleSelect(ev: Event) {
Philipp Schradera227d042020-11-14 17:33:52 -0800122 this.selectedIndex = (ev.target as HTMLSelectElement).selectedIndex;
Alex Perryfffe2e32020-02-29 19:48:17 -0800123 }
124
Alex Perryb41d5782020-02-09 17:06:40 -0800125 handleImage(data: Uint8Array): void {
Alex Perry3dfcb812020-03-04 19:32:17 -0800126 console.log('got an image to process');
Alex Perry22824d72020-02-29 17:11:43 -0800127 if (this.imageSkipCount != 0) {
128 this.imageSkipCount--;
129 return;
130 } else {
131 this.imageSkipCount = 3;
132 }
133
Philipp Schradere625ba22020-11-16 20:11:37 -0800134 const fbBuffer = new ByteBuffer(data);
135 this.image = CameraImage.getRootAsCameraImage(
136 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perry3dfcb812020-03-04 19:32:17 -0800137 this.imageTimestamp = this.image.monotonicTimestampNs();
Alex Perry5f474f22020-02-01 12:14:24 -0800138
Alex Perry3dfcb812020-03-04 19:32:17 -0800139 this.width = this.image.cols();
140 this.height = this.image.rows();
Alex Perry22824d72020-02-29 17:11:43 -0800141 if (this.width === 0 || this.height === 0) {
Alex Perry5f474f22020-02-01 12:14:24 -0800142 return;
143 }
Alex Perry5f474f22020-02-01 12:14:24 -0800144
Alex Perry3dfcb812020-03-04 19:32:17 -0800145 this.draw();
146 }
147
148 convertImage(): void {
Philipp Schradere625ba22020-11-16 20:11:37 -0800149 this.imageBuffer =
150 new Uint8ClampedArray(this.width * this.height * 4); // RGBA
Alex Perry5f474f22020-02-01 12:14:24 -0800151 // Read four bytes (YUYV) from the data and transform into two pixels of
152 // RGBA for canvas
Philipp Schradera227d042020-11-14 17:33:52 -0800153 for (let j = 0; j < this.height; j++) {
154 for (let i = 0; i < this.width; i += 2) {
Alex Perry3dfcb812020-03-04 19:32:17 -0800155 const y1 = this.image.data((j * this.width + i) * 2);
156 const u = this.image.data((j * this.width + i) * 2 + 1);
157 const y2 = this.image.data((j * this.width + i + 1) * 2);
158 const v = this.image.data((j * this.width + i + 1) * 2 + 1);
Alex Perry5f474f22020-02-01 12:14:24 -0800159
160 // Based on https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB
161 const c1 = y1 - 16;
162 const c2 = y2 - 16;
163 const d = u - 128;
164 const e = v - 128;
165
Alex Perryfffe2e32020-02-29 19:48:17 -0800166 this.imageBuffer[(j * this.width + i) * 4 + 0] =
167 (298 * c1 + 409 * e + 128) >> 8;
168 this.imageBuffer[(j * this.width + i) * 4 + 1] =
169 (298 * c1 - 100 * d - 208 * e + 128) >> 8;
170 this.imageBuffer[(j * this.width + i) * 4 + 2] =
171 (298 * c1 + 516 * d + 128) >> 8;
Alex Perry22824d72020-02-29 17:11:43 -0800172 this.imageBuffer[(j * this.width + i) * 4 + 3] = 255;
Alex Perryfffe2e32020-02-29 19:48:17 -0800173 this.imageBuffer[(j * this.width + i) * 4 + 4] =
174 (298 * c2 + 409 * e + 128) >> 8;
175 this.imageBuffer[(j * this.width + i) * 4 + 5] =
176 (298 * c2 - 100 * d - 208 * e + 128) >> 8;
177 this.imageBuffer[(j * this.width + i) * 4 + 6] =
178 (298 * c2 + 516 * d + 128) >> 8;
Alex Perry22824d72020-02-29 17:11:43 -0800179 this.imageBuffer[(j * this.width + i) * 4 + 7] = 255;
Alex Perry5f474f22020-02-01 12:14:24 -0800180 }
181 }
Alex Perryb41d5782020-02-09 17:06:40 -0800182 }
183
184 handleImageMetadata(data: Uint8Array): void {
Alex Perry3dfcb812020-03-04 19:32:17 -0800185 console.log('got an image match result to process');
Philipp Schradere625ba22020-11-16 20:11:37 -0800186 const fbBuffer = new ByteBuffer(data);
187 this.result = ImageMatchResult.getRootAsImageMatchResult(
188 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perry22824d72020-02-29 17:11:43 -0800189 this.resultTimestamp = this.result.imageMonotonicTimestampNs();
190 this.draw();
Alex Perryb41d5782020-02-09 17:06:40 -0800191 }
192
193 draw(): void {
Alex Perryfffe2e32020-02-29 19:48:17 -0800194 if (!this.imageTimestamp || !this.resultTimestamp ||
Alex Perry22824d72020-02-29 17:11:43 -0800195 this.imageTimestamp.low !== this.resultTimestamp.low ||
196 this.imageTimestamp.high !== this.resultTimestamp.high) {
Alex Perryf23c05d2020-03-07 13:52:02 -0800197 // console.log('image and result do not match');
198 // console.log(this.imageTimestamp.low, this.resultTimestamp.low);
199 // console.log(this.imageTimestamp.high, this.resultTimestamp.high);
Alex Perryb41d5782020-02-09 17:06:40 -0800200 return;
201 }
Alex Perry3dfcb812020-03-04 19:32:17 -0800202 this.convertImage();
Alex Perry5f474f22020-02-01 12:14:24 -0800203 const ctx = this.canvas.getContext('2d');
204
Alex Perry22824d72020-02-29 17:11:43 -0800205 this.canvas.width = this.width;
206 this.canvas.height = this.height;
207 const idata = ctx.createImageData(this.width, this.height);
Alex Perryb41d5782020-02-09 17:06:40 -0800208 idata.data.set(this.imageBuffer);
Alex Perry5f474f22020-02-01 12:14:24 -0800209 ctx.putImageData(idata, 0, 0);
Philipp Schradera227d042020-11-14 17:33:52 -0800210 console.log('features: ', this.result.featuresLength());
Alex Perryfffe2e32020-02-29 19:48:17 -0800211 if (this.selectedIndex === 0) {
Philipp Schradera227d042020-11-14 17:33:52 -0800212 for (let i = 0; i < this.result.featuresLength(); i++) {
Alex Perryfffe2e32020-02-29 19:48:17 -0800213 const feature = this.result.features(i);
214 this.drawFeature(feature);
215 }
216 } else {
Alex Perryf23c05d2020-03-07 13:52:02 -0800217 console.log(this.result.imageMatchesLength(), this.result.cameraPosesLength());
Alex Perryfffe2e32020-02-29 19:48:17 -0800218 const imageMatch = this.result.imageMatches(this.selectedIndex - 1);
Philipp Schradera227d042020-11-14 17:33:52 -0800219 for (let i = 0; i < imageMatch.matchesLength(); i++) {
Alex Perryfffe2e32020-02-29 19:48:17 -0800220 const featureIndex = imageMatch.matches(i).queryFeature();
221 this.drawFeature(this.result.features(featureIndex));
222 }
Alex Perryf23c05d2020-03-07 13:52:02 -0800223 // Draw center of target.
Philipp Schradera227d042020-11-14 17:33:52 -0800224 const cameraPose = this.result.cameraPoses(this.selectedIndex - 1);
Alex Perryf23c05d2020-03-07 13:52:02 -0800225 ctx.strokeStyle = 'red';
226 ctx.beginPath();
227 ctx.arc(
228 cameraPose.queryTargetPointX(), cameraPose.queryTargetPointY(),
229 cameraPose.queryTargetPointRadius(), 0, 2 * Math.PI);
230 console.log(cameraPose.queryTargetPointX(), cameraPose.queryTargetPointY(), cameraPose.queryTargetPointRadius());
231 ctx.stroke();
Alex Perryb41d5782020-02-09 17:06:40 -0800232 }
Alex Perryfffe2e32020-02-29 19:48:17 -0800233
Alex Perryfffe2e32020-02-29 19:48:17 -0800234 while (this.select.lastChild) {
235 this.select.removeChild(this.select.lastChild);
236 }
237 const defaultOption = document.createElement('option');
238 defaultOption.innerText = 'Show all features';
Philipp Schradera227d042020-11-14 17:33:52 -0800239 defaultOption.setAttribute('value', '0');
Alex Perryfffe2e32020-02-29 19:48:17 -0800240 this.select.appendChild(defaultOption);
Philipp Schradera227d042020-11-14 17:33:52 -0800241 for (let i = 0; i < this.result.imageMatchesLength(); i++) {
Alex Perryfffe2e32020-02-29 19:48:17 -0800242 const imageMatch = this.result.imageMatches(i);
243 const option = document.createElement('option');
Philipp Schradera227d042020-11-14 17:33:52 -0800244 option.setAttribute('value', (i + 1).toString());
Alex Perryfffe2e32020-02-29 19:48:17 -0800245 option.innerText =
246 `Show image ${i} features (${imageMatch.matchesLength()})`;
247 this.select.appendChild(option);
248 }
249 this.select.selectedIndex = this.selectedIndex;
250 }
251
252 // Based on OpenCV drawKeypoint.
253 private drawFeature(feature: Feature) {
254 const ctx = this.canvas.getContext('2d');
255 ctx.beginPath();
256 ctx.arc(feature.x(), feature.y(), feature.size(), 0, 2 * Math.PI);
257 ctx.stroke();
258
259 ctx.beginPath();
260 ctx.moveTo(feature.x(), feature.y());
261 const angle = feature.angle() * Math.PI / 180;
262 ctx.lineTo(
263 feature.x() + feature.size() * Math.cos(angle),
264 feature.y() + feature.size() * Math.sin(angle));
265 ctx.stroke();
Alex Perry5f474f22020-02-01 12:14:24 -0800266 }
Alex Perry5f474f22020-02-01 12:14:24 -0800267}