blob: 79ce74e9275ccdb6154eac5fd0825832cd8a90b7 [file] [log] [blame]
Philipp Schradere625ba22020-11-16 20:11:37 -08001import * as configuration from 'org_frc971/aos/configuration_generated';
Philipp Schradere625ba22020-11-16 20:11:37 -08002import {Connection} from 'org_frc971/aos/network/www/proxy';
3import * as flatbuffers_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
4import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
5import {Long} from 'org_frc971/external/com_github_google_flatbuffers/ts/long';
6import * as sift from 'org_frc971/y2020/vision/sift/sift_generated'
7import * as vision from 'org_frc971/y2020/vision/vision_generated';
James Kuszmaul71a81932020-12-15 21:08:01 -08008import * as web_proxy from 'org_frc971/aos/network/web_proxy_generated';
Philipp Schradere625ba22020-11-16 20:11:37 -08009
10import Channel = configuration.aos.Channel;
11import Configuration = configuration.aos.Configuration;
Philipp Schradere625ba22020-11-16 20:11:37 -080012import CameraImage = vision.frc971.vision.CameraImage;
13import ImageMatchResult = sift.frc971.vision.sift.ImageMatchResult;
14import Feature = sift.frc971.vision.sift.Feature;
James Kuszmaul71a81932020-12-15 21:08:01 -080015import SubscriberRequest = web_proxy.aos.web_proxy.SubscriberRequest;
16import ChannelRequest = web_proxy.aos.web_proxy.ChannelRequest;
17import TransferMethod = web_proxy.aos.web_proxy.TransferMethod;
Alex Perryd1969882020-03-06 21:19:00 -080018
19/*
20 * All the messages that are required to show an image with metadata.
21 * Messages not readable on the server node are ignored.
22 */
23const REQUIRED_CHANNELS = [
24 {
25 name: '/pi1/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080026 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080027 },
28 {
29 name: '/pi2/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080030 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080031 },
32 {
33 name: '/pi3/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080034 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080035 },
36 {
37 name: '/pi4/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080038 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080039 },
40 {
41 name: '/pi5/camera',
Philipp Schradere625ba22020-11-16 20:11:37 -080042 type: CameraImage.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080043 },
44 {
45 name: '/pi1/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080046 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080047 },
48 {
49 name: '/pi2/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080050 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080051 },
52 {
53 name: '/pi3/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080054 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080055 },
56 {
57 name: '/pi4/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080058 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080059 },
60 {
61 name: '/pi5/camera/detailed',
Philipp Schradere625ba22020-11-16 20:11:37 -080062 type: ImageMatchResult.getFullyQualifiedName(),
Alex Perryd1969882020-03-06 21:19:00 -080063 },
64];
Alex Perry5f474f22020-02-01 12:14:24 -080065
66export class ImageHandler {
67 private canvas = document.createElement('canvas');
Alex Perryfffe2e32020-02-29 19:48:17 -080068 private select = document.createElement('select');
69
Alex Perryb41d5782020-02-09 17:06:40 -080070 private imageBuffer: Uint8ClampedArray|null = null;
Philipp Schradere625ba22020-11-16 20:11:37 -080071 private image: CameraImage|null = null;
72 private imageTimestamp: Long|null = null;
73 private result: ImageMatchResult|null = null;
74 private resultTimestamp: Long|null = null;
Alex Perry22824d72020-02-29 17:11:43 -080075 private width = 0;
76 private height = 0;
Alex Perryfffe2e32020-02-29 19:48:17 -080077 private selectedIndex = 0;
Alex Perry22824d72020-02-29 17:11:43 -080078 private imageSkipCount = 3;
Alex Perry5f474f22020-02-01 12:14:24 -080079
Alex Perryd1969882020-03-06 21:19:00 -080080 constructor(private readonly connection: Connection) {
Alex Perryfffe2e32020-02-29 19:48:17 -080081 document.body.appendChild(this.select);
82 const defaultOption = document.createElement('option');
83 defaultOption.innerText = 'Show all features';
84 this.select.appendChild(defaultOption);
85 this.select.addEventListener('change', (ev) => this.handleSelect(ev));
Alex Perry5f474f22020-02-01 12:14:24 -080086 document.body.appendChild(this.canvas);
Alex Perryd1969882020-03-06 21:19:00 -080087
88 this.connection.addConfigHandler(() => {
89 this.sendConnect();
90 });
Austin Schuh7c75e582020-11-14 16:41:18 -080091 this.connection.addHandler(
Philipp Schradere625ba22020-11-16 20:11:37 -080092 ImageMatchResult.getFullyQualifiedName(), (data) => {
Austin Schuh7c75e582020-11-14 16:41:18 -080093 this.handleImageMetadata(data);
94 });
Philipp Schradere625ba22020-11-16 20:11:37 -080095 this.connection.addHandler(CameraImage.getFullyQualifiedName(), (data) => {
96 this.handleImage(data);
97 });
Alex Perryd1969882020-03-06 21:19:00 -080098 }
99
100 private sendConnect(): void {
Philipp Schradere625ba22020-11-16 20:11:37 -0800101 const builder =
102 new flatbuffers_builder.Builder(512) as unknown as flatbuffers.Builder;
Alex Perryd1969882020-03-06 21:19:00 -0800103 const channels: flatbuffers.Offset[] = [];
104 for (const channel of REQUIRED_CHANNELS) {
105 const nameFb = builder.createString(channel.name);
106 const typeFb = builder.createString(channel.type);
Philipp Schradere625ba22020-11-16 20:11:37 -0800107 Channel.startChannel(builder);
108 Channel.addName(builder, nameFb);
109 Channel.addType(builder, typeFb);
110 const channelFb = Channel.endChannel(builder);
James Kuszmaul71a81932020-12-15 21:08:01 -0800111 ChannelRequest.startChannelRequest(builder);
112 ChannelRequest.addChannel(builder, channelFb);
113 ChannelRequest.addMethod(builder, TransferMethod.SUBSAMPLE);
114 channels.push(ChannelRequest.endChannelRequest(builder));
Alex Perryd1969882020-03-06 21:19:00 -0800115 }
116
Philipp Schradere625ba22020-11-16 20:11:37 -0800117 const channelsFb =
James Kuszmaul71a81932020-12-15 21:08:01 -0800118 SubscriberRequest.createChannelsToTransferVector(builder, channels);
119 SubscriberRequest.startSubscriberRequest(builder);
120 SubscriberRequest.addChannelsToTransfer(builder, channelsFb);
121 const connect = SubscriberRequest.endSubscriberRequest(builder);
Alex Perryd1969882020-03-06 21:19:00 -0800122 builder.finish(connect);
123 this.connection.sendConnectMessage(builder);
Alex Perry5f474f22020-02-01 12:14:24 -0800124 }
125
Alex Perryfffe2e32020-02-29 19:48:17 -0800126 handleSelect(ev: Event) {
Philipp Schradera227d042020-11-14 17:33:52 -0800127 this.selectedIndex = (ev.target as HTMLSelectElement).selectedIndex;
Alex Perryfffe2e32020-02-29 19:48:17 -0800128 }
129
Alex Perryb41d5782020-02-09 17:06:40 -0800130 handleImage(data: Uint8Array): void {
Alex Perry3dfcb812020-03-04 19:32:17 -0800131 console.log('got an image to process');
Alex Perry22824d72020-02-29 17:11:43 -0800132 if (this.imageSkipCount != 0) {
133 this.imageSkipCount--;
134 return;
135 } else {
136 this.imageSkipCount = 3;
137 }
138
Philipp Schradere625ba22020-11-16 20:11:37 -0800139 const fbBuffer = new ByteBuffer(data);
140 this.image = CameraImage.getRootAsCameraImage(
141 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perry3dfcb812020-03-04 19:32:17 -0800142 this.imageTimestamp = this.image.monotonicTimestampNs();
Alex Perry5f474f22020-02-01 12:14:24 -0800143
Alex Perry3dfcb812020-03-04 19:32:17 -0800144 this.width = this.image.cols();
145 this.height = this.image.rows();
Alex Perry22824d72020-02-29 17:11:43 -0800146 if (this.width === 0 || this.height === 0) {
Alex Perry5f474f22020-02-01 12:14:24 -0800147 return;
148 }
Alex Perry5f474f22020-02-01 12:14:24 -0800149
Alex Perry3dfcb812020-03-04 19:32:17 -0800150 this.draw();
151 }
152
153 convertImage(): void {
Philipp Schradere625ba22020-11-16 20:11:37 -0800154 this.imageBuffer =
155 new Uint8ClampedArray(this.width * this.height * 4); // RGBA
Alex Perry5f474f22020-02-01 12:14:24 -0800156 // Read four bytes (YUYV) from the data and transform into two pixels of
157 // RGBA for canvas
Philipp Schradera227d042020-11-14 17:33:52 -0800158 for (let j = 0; j < this.height; j++) {
159 for (let i = 0; i < this.width; i += 2) {
Alex Perry3dfcb812020-03-04 19:32:17 -0800160 const y1 = this.image.data((j * this.width + i) * 2);
161 const u = this.image.data((j * this.width + i) * 2 + 1);
162 const y2 = this.image.data((j * this.width + i + 1) * 2);
163 const v = this.image.data((j * this.width + i + 1) * 2 + 1);
Alex Perry5f474f22020-02-01 12:14:24 -0800164
165 // Based on https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB
166 const c1 = y1 - 16;
167 const c2 = y2 - 16;
168 const d = u - 128;
169 const e = v - 128;
170
Alex Perryfffe2e32020-02-29 19:48:17 -0800171 this.imageBuffer[(j * this.width + i) * 4 + 0] =
172 (298 * c1 + 409 * e + 128) >> 8;
173 this.imageBuffer[(j * this.width + i) * 4 + 1] =
174 (298 * c1 - 100 * d - 208 * e + 128) >> 8;
175 this.imageBuffer[(j * this.width + i) * 4 + 2] =
176 (298 * c1 + 516 * d + 128) >> 8;
Alex Perry22824d72020-02-29 17:11:43 -0800177 this.imageBuffer[(j * this.width + i) * 4 + 3] = 255;
Alex Perryfffe2e32020-02-29 19:48:17 -0800178 this.imageBuffer[(j * this.width + i) * 4 + 4] =
179 (298 * c2 + 409 * e + 128) >> 8;
180 this.imageBuffer[(j * this.width + i) * 4 + 5] =
181 (298 * c2 - 100 * d - 208 * e + 128) >> 8;
182 this.imageBuffer[(j * this.width + i) * 4 + 6] =
183 (298 * c2 + 516 * d + 128) >> 8;
Alex Perry22824d72020-02-29 17:11:43 -0800184 this.imageBuffer[(j * this.width + i) * 4 + 7] = 255;
Alex Perry5f474f22020-02-01 12:14:24 -0800185 }
186 }
Alex Perryb41d5782020-02-09 17:06:40 -0800187 }
188
189 handleImageMetadata(data: Uint8Array): void {
Alex Perry3dfcb812020-03-04 19:32:17 -0800190 console.log('got an image match result to process');
Philipp Schradere625ba22020-11-16 20:11:37 -0800191 const fbBuffer = new ByteBuffer(data);
192 this.result = ImageMatchResult.getRootAsImageMatchResult(
193 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perry22824d72020-02-29 17:11:43 -0800194 this.resultTimestamp = this.result.imageMonotonicTimestampNs();
195 this.draw();
Alex Perryb41d5782020-02-09 17:06:40 -0800196 }
197
198 draw(): void {
Alex Perryfffe2e32020-02-29 19:48:17 -0800199 if (!this.imageTimestamp || !this.resultTimestamp ||
Alex Perry22824d72020-02-29 17:11:43 -0800200 this.imageTimestamp.low !== this.resultTimestamp.low ||
201 this.imageTimestamp.high !== this.resultTimestamp.high) {
Alex Perryf23c05d2020-03-07 13:52:02 -0800202 // console.log('image and result do not match');
203 // console.log(this.imageTimestamp.low, this.resultTimestamp.low);
204 // console.log(this.imageTimestamp.high, this.resultTimestamp.high);
Alex Perryb41d5782020-02-09 17:06:40 -0800205 return;
206 }
Alex Perry3dfcb812020-03-04 19:32:17 -0800207 this.convertImage();
Alex Perry5f474f22020-02-01 12:14:24 -0800208 const ctx = this.canvas.getContext('2d');
209
Alex Perry22824d72020-02-29 17:11:43 -0800210 this.canvas.width = this.width;
211 this.canvas.height = this.height;
212 const idata = ctx.createImageData(this.width, this.height);
Alex Perryb41d5782020-02-09 17:06:40 -0800213 idata.data.set(this.imageBuffer);
Alex Perry5f474f22020-02-01 12:14:24 -0800214 ctx.putImageData(idata, 0, 0);
Philipp Schradera227d042020-11-14 17:33:52 -0800215 console.log('features: ', this.result.featuresLength());
Alex Perryfffe2e32020-02-29 19:48:17 -0800216 if (this.selectedIndex === 0) {
Philipp Schradera227d042020-11-14 17:33:52 -0800217 for (let i = 0; i < this.result.featuresLength(); i++) {
Alex Perryfffe2e32020-02-29 19:48:17 -0800218 const feature = this.result.features(i);
219 this.drawFeature(feature);
220 }
221 } else {
Alex Perryf23c05d2020-03-07 13:52:02 -0800222 console.log(this.result.imageMatchesLength(), this.result.cameraPosesLength());
Alex Perryfffe2e32020-02-29 19:48:17 -0800223 const imageMatch = this.result.imageMatches(this.selectedIndex - 1);
Philipp Schradera227d042020-11-14 17:33:52 -0800224 for (let i = 0; i < imageMatch.matchesLength(); i++) {
Alex Perryfffe2e32020-02-29 19:48:17 -0800225 const featureIndex = imageMatch.matches(i).queryFeature();
226 this.drawFeature(this.result.features(featureIndex));
227 }
Alex Perryf23c05d2020-03-07 13:52:02 -0800228 // Draw center of target.
Philipp Schradera227d042020-11-14 17:33:52 -0800229 const cameraPose = this.result.cameraPoses(this.selectedIndex - 1);
Alex Perryf23c05d2020-03-07 13:52:02 -0800230 ctx.strokeStyle = 'red';
231 ctx.beginPath();
232 ctx.arc(
233 cameraPose.queryTargetPointX(), cameraPose.queryTargetPointY(),
234 cameraPose.queryTargetPointRadius(), 0, 2 * Math.PI);
235 console.log(cameraPose.queryTargetPointX(), cameraPose.queryTargetPointY(), cameraPose.queryTargetPointRadius());
236 ctx.stroke();
Alex Perryb41d5782020-02-09 17:06:40 -0800237 }
Alex Perryfffe2e32020-02-29 19:48:17 -0800238
Alex Perryfffe2e32020-02-29 19:48:17 -0800239 while (this.select.lastChild) {
240 this.select.removeChild(this.select.lastChild);
241 }
242 const defaultOption = document.createElement('option');
243 defaultOption.innerText = 'Show all features';
Philipp Schradera227d042020-11-14 17:33:52 -0800244 defaultOption.setAttribute('value', '0');
Alex Perryfffe2e32020-02-29 19:48:17 -0800245 this.select.appendChild(defaultOption);
Philipp Schradera227d042020-11-14 17:33:52 -0800246 for (let i = 0; i < this.result.imageMatchesLength(); i++) {
Alex Perryfffe2e32020-02-29 19:48:17 -0800247 const imageMatch = this.result.imageMatches(i);
248 const option = document.createElement('option');
Philipp Schradera227d042020-11-14 17:33:52 -0800249 option.setAttribute('value', (i + 1).toString());
Alex Perryfffe2e32020-02-29 19:48:17 -0800250 option.innerText =
251 `Show image ${i} features (${imageMatch.matchesLength()})`;
252 this.select.appendChild(option);
253 }
254 this.select.selectedIndex = this.selectedIndex;
255 }
256
257 // Based on OpenCV drawKeypoint.
258 private drawFeature(feature: Feature) {
259 const ctx = this.canvas.getContext('2d');
260 ctx.beginPath();
261 ctx.arc(feature.x(), feature.y(), feature.size(), 0, 2 * Math.PI);
262 ctx.stroke();
263
264 ctx.beginPath();
265 ctx.moveTo(feature.x(), feature.y());
266 const angle = feature.angle() * Math.PI / 180;
267 ctx.lineTo(
268 feature.x() + feature.size() * Math.cos(angle),
269 feature.y() + feature.size() * Math.sin(angle));
270 ctx.stroke();
Alex Perry5f474f22020-02-01 12:14:24 -0800271 }
Alex Perry5f474f22020-02-01 12:14:24 -0800272}