#javascript #react-native #tensorflow #canvas
#javascript #react-native #тензорный поток #холст
Вопрос:
Я пытаюсь нарисовать прямоугольник с надписью чуть выше него на холсте. X, y, ширина и высота были сгенерированы так, чтобы быть вокруг объекта, который был обнаружен с помощью модели coco-ssd в tensorflow. Проблема в том, что координаты, сгенерированные моделью coco-ssd в tensorflow, относятся к источнику, отличному от самого холста. Более конкретно, начало координат для модели coco-ssd находится в верхнем правом углу, а начало координат для холста — в верхнем левом углу.
Я могу перемещать начало координат холста, но не происхождение модели (о котором я знаю). Чтобы переместить начало координат холста, я перевел холст вправо на 410 пикселей, на 4 пикселя меньше ширины холста, а затем отразил его горизонтально. Это рисует прямоугольник в правильном положении. Если бы я также должен был создать текст на этом этапе, он был бы перевернут и нечитаем (но в правильном положении). Если бы можно было получить положение прямоугольника по x и y после перевода холста обратно влево на 410 пикселей и еще раз отразить его по горизонтали, я мог бы легко использовать эти координаты для заполнения текста в нужном положении. Из того, что я узнал о холсте, это невозможно. (Пожалуйста, поправьте меня, если я ошибаюсь)
Другим решением, которое я рассмотрел, было бы использовать сгенерированную позицию x и применить эту формулу, -x xLim, где xLim — максимально возможное значение x. Проблема здесь в том, что получение xLim также невозможно, оно не является статичным, и оно будет меняться в зависимости от расстояния до обнаруженного объекта. Я знаю это, пытаясь получить, каким может быть xLim, просто расположив объект в крайнем левом углу экрана. (Наибольшее значение x, которое в настоящее время доступно для просмотра относительно происхождения модели coco-ssd) Имейте в виду, что если я создам расстояние от объекта, значение x в крайнем левом углу экрана увеличится. Если бы я смог каким-то образом получить наибольшее значение x, которое активно просматривается на холсте, тогда это было бы еще одним жизнеспособным решением.
Вот функция, отвечающая за рисование на холсте.
export const drawRect = (x, y, width, height, text, ctx, canvas) => {
ctx.clearRect(0, 0, canvas.current.width, canvas.current.height);
ctx.transform(-1, 0, 0, 1, 410, 0);
//draw rectangle
ctx.beginPath();
const r = ctx.rect(x,y,width,height)
ctx.stroke()
//draw text
ctx.save();
ctx.scale(-1,1);
ctx.translate(-410, 0)
//update x and y to point to where the rectangle is currently
ctx.fillText(text,x,y-5)
ctx.stroke()
ctx.restore()
})
Я чувствую себя сильно ограниченным API, доступным для react native, и я надеюсь, что есть что-то, что я просто упустил из виду. Я потратил много времени, пытаясь решить эту проблему, и нашел много связанных вопросов о переполнении стека, но ни один из них не дал представления о том, как решить эту проблему с таким количеством неизвестных переменных.
Вот несколько изображений, чтобы наглядно представить проблему.
Без восстановления исходного состояния холста
Краткие сведения:
Начало координат для модели coco-ssd находится в верхнем правом углу, а начало координат для холста — в верхнем левом углу.
Мне нужно
A.) Каким-то образом захватить наибольшее значение x, которое активно просматривается на холсте
или
B.) получите положение прямоугольника по x и y после перевода холста обратно влево на 410 пикселей и еще раз отразите его по горизонтали
Это в среде react native expo
Публичное репозиторий: https://github.com/dpaceoffice/MobileAppTeamProject
Ответ №1:
Используемые вами преобразования кажутся ненужными. Вот простое доказательство концепции с использованием кода встраивания для coco-ssd и примера по работе с видео:
- https://www.npmjs.com/package/@tensorflow-models/coco-ssd
- https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Manipulating_video_using_canvas
Следующий код не использует никаких преобразований, и все работает так, как ожидалось. Видео, холст и модель используют одну и ту же систему координат:
- (0, 0) — верхний левый угол рамки
- (ширина, 0) находится вверху справа
- (0, высота) находится внизу слева
- (ширина, высота) внизу справа
И прямоугольник (x, y, ширина, высота) также везде использует знакомые координаты:
- (x, y) находится вверху слева
- (x width, y) находится вверху справа
- (x, y высота) находится внизу слева
- (x width, y height) находится внизу справа
Единственное, что немного отличается, это метод context.fillText(text, x, y):
- (x, y) — нижний левый угол текста. Тем не менее, это на самом деле приятно, потому что мы просто можем нарисовать прямоугольник и текст с одинаковыми координатами, и текст будет располагаться прямо над прямоугольником.
- (x, y — размер шрифта) обычно находится в верхнем левом углу
- (x, y FontSize) — это координата для следующей строки.
Если вы хотите поместить текст в другую позицию, context.measureText(текст) может представлять интерес. Он возвращает объект TextMetrics. Обычно наибольший интерес представляет свойство .width этого объекта:
- (x context.measureText(text).width, y) — это нижний правый угол текста.
const processor = {}
processor.doLoad = function (model)
{
const video = document.getElementById("video")
this.video = video
this.canvas = document.getElementById("canvas")
this.context = this.canvas.getContext("2d")
this.model = model
video.addEventListener("play", () => {
this.canvas.width = video.videoWidth
this.canvas.height = video.videoHeight
this.timerCallback()
}, false)
}
processor.timerCallback = async function ()
{
if (this.video.paused || this.video.ended)
return
await this.computeFrame()
setTimeout(() => this.timerCallback(), 0)
};
processor.computeFrame = async function ()
{
// detect objects in the image.
const predictions = await this.model.detect(this.video)
const context = this.context
// draws the frame from the video at position (0, 0)
context.drawImage(this.video, 0, 0)
context.strokeStyle = "red"
context.fillStyle = "red"
context.font = "16px sans-serif"
for (const { bbox: [x, y, width, height], class: _class, score } of predictions)
{
// draws a rect with top-left corner of (x, y)
context.strokeRect(x, y, width, height)
// writes the class directly above (x, y), outside the rectangle
context.fillText(_class, x, y)
// writes the class directly below (x, y), inside the rectangle
context.fillText(score.toFixed(2), x, y 16)
}
}
// Load the model.
const model = cocoSsd.load()
model.then(model => processor.doLoad(model))
<!-- Load TensorFlow.js. This is required to use coco-ssd model. -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"> </script>
<!-- Load the coco-ssd model. -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd"> </script>
<div style="display: flex; flex-flow: column; gap: 1em; width: 200px;">
<!-- Replace this with your image. Make sure CORS settings allow reading the image! -->
<video id="video" src="https://mdn.github.io/dom-examples/canvas/chroma-keying/media/video.mp4" controls crossorigin="anonymous"></video>
<canvas id="canvas" style="border: 1px solid black;"></canvas>
</div>
Поскольку кажется, что вы хотите нарисовать изображение в какой-то части холста, вот некоторый код, который демонстрирует, как правильно использовать преобразования. Здесь есть два варианта:
- Используйте преобразования (1x translate 1x scale), чтобы вы могли использовать систему координат из изображения. Обратите внимание, что для этого не требуются отрицательные масштабы. Рисование обрабатывается браузером. Вы должны исправить шрифт и ширину строки для масштабирования.
- Также используйте преобразования (1x translate 1x scale). Затем восстановите холст и преобразуйте точки вручную. Это требует некоторой дополнительной математики для преобразования точек. Однако плюсом является то, что вам не нужно корректировать шрифт и ширину строк.
const image = document.createElement("canvas")
image.width = 1200
image.height = 600
const image_context = image.getContext("2d")
image_context.fillStyle = "#4af"
image_context.fillRect(0, 0, 1200, 600)
const circle = (x, y, r) =>
{
image_context.beginPath()
image_context.arc(x, y, r, 0, Math.PI*2)
image_context.fill()
}
image_context.fillStyle = "#800"
image_context.fillRect(500-40/2, 400, 40, 180)
image_context.fillStyle = "#080"
circle(500, 400, 100)
circle(500, 300, 70)
circle(500, 220, 50)
const prediction = { bbox: [500-100, 220-50, 100*2, (400 180)-(220-50)], class: "tree", score: 0.42 } // somehow get a prediction
const canvas = document.getElementById("canvas")
const context = canvas.getContext("2d")
// we want to draw the big image into a smaller area (x, y, width, height)
const x = 50
const y = 80
const width = 220
const height = width * (image.height / image.width)
// debug: mark the area that we want to draw the image into
{
context.save() // save context, so that stroke properties can be restored
context.lineWidth = 5
context.strokeStyle = "red"
context.setLineDash([10, 10])
context.strokeRect(x, y, width, height)
context.restore()
}
{
// save context, before doing any transforms (even before setTransform)
context.save()
// Move top left corner to (x, y)
context.translate(x, y)
// This is the scale factor, it should be less than one, because the image is bigger that the target area. The idea is to increase the scale by the target area width, and then decrease the scale by the image width.
const f = width / image.width
context.scale(f, f)
// Draws the image, note that the coordinates are just (0, 0) without scaling.
context.drawImage(image, 0, 0)
// option 1: draw the prediction using the native transforms
if (true)
{
context.strokeStyle = "red"
context.fillStyle = "red"
context.lineWidth = 1 / f // linewidth and font-size has to be adjusted by scaling
const fontSize = 16 / f
context.font = fontSize.toFixed(0) "px sans-serif"
const [p_x, p_y, p_width, p_height] = prediction.bbox
context.strokeRect(p_x, p_y, p_width, p_height) // draw the prediction
context.fillText(prediction.class, p_x, p_y) // draw the text
context.fillText(prediction.score.toFixed(2), p_x, p_y fontSize) // draw the text
}
const matrix = context.getTransform() // save transform for option 2, needs to be done before restore()
context.restore()
// option 2: draw the prediction by manually transforming the corners
if (false)
{
context.save()
context.strokeStyle = "red"
context.fillStyle = "red"
context.lineWidth = 1
const fontSize = 16
context.font = fontSize "px sans-serif"
let [p_x, p_y, p_width, p_height] = prediction.bbox
// manually transform corners
const topleft = matrix.transformPoint(new DOMPoint(p_x, p_y))
const bottomright = matrix.transformPoint(new DOMPoint(p_x p_width, p_y p_height))
p_x = topleft.x
p_y = topleft.y
p_width = bottomright.x - topleft.x
p_height = bottomright.y - topleft.y
context.strokeRect(p_x, p_y, p_width, p_height) // draw the prediction
context.fillText(prediction.class, p_x, p_y) // draw the text
context.fillText(prediction.score.toFixed(2), p_x, p_y fontSize) // draw the text
context.restore()
}
}
<canvas id="canvas" width=400 height=300 style="border: 1px solid black;"></canvas>
Теперь, если вам нужно отразить изображение с помощью преобразований, вам просто нужно обязательно отразить его обратно перед написанием текста.
const image = document.createElement("canvas")
image.width = 1200
image.height = 600
const image_context = image.getContext("2d")
image_context.fillStyle = "#4af"
image_context.fillRect(0, 0, 1200, 600)
const circle = (x, y, r) =>
{
image_context.beginPath()
image_context.arc(x, y, r, 0, Math.PI*2)
image_context.fill()
}
image_context.fillStyle = "#800"
image_context.fillRect(500-40/2, 400, 40, 180)
image_context.fillStyle = "#080"
circle(500, 400, 100)
circle(500, 300, 70)
circle(500, 220, 50)
const prediction = { bbox: [500-100, 220-50, 100*2, (400 180)-(220-50)], class: "tree", score: 0.42 } // somehow get a prediction
const canvas = document.getElementById("canvas")
const context = canvas.getContext("2d")
// we want to draw the big image into a smaller area (x, y, width, height)
const x = 50
const y = 80
const width = 220
const height = width * (image.height / image.width)
// debug: mark the area that we want to draw the image into
{
context.save() // save context, so that stroke properties can be restored
context.lineWidth = 5
context.strokeStyle = "red"
context.setLineDash([10, 10])
context.strokeRect(x, y, width, height)
context.restore()
}
{
// save context, before doing any transforms (even before setTransform)
context.save()
// Move top left corner to (x, y)
context.translate(x, y)
// This is the scale factor, it should be less than one, because the image is bigger that the target area. The idea is to increase the scale by the target area width, and then decrease the scale by the image width.
const f = width / image.width
context.scale(f, f)
// mirror the image before drawing it
context.scale(-1, 1)
context.translate(-image.width, 0)
// Draws the image, note that the coordinates are just (0, 0) without scaling.
context.drawImage(image, 0, 0)
// option 1: draw the prediction using the native transforms
if (true)
{
const [p_x, p_y, p_width, p_height] = prediction.bbox
// move to correct position and only then undo the mirroring
context.save()
context.translate(p_x p_width, p_y) // move to top "right" (that is now actually at the left, due to mirroring)
context.scale(-1, 1)
context.strokeStyle = "red"
context.fillStyle = "red"
context.lineWidth = 1 / f // linewidth and font-size has to be adjusted by scaling
const fontSize = 16 / f
context.font = fontSize.toFixed(0) "px sans-serif"
context.strokeRect(0, 0, p_width, p_height) // draw the prediction
context.fillText(prediction.class, 0, 0) // draw the text
context.fillText(prediction.score.toFixed(2), 0, 0 fontSize) // draw the text
context.restore()
}
const matrix = context.getTransform() // save transform for option 2, needs to be done before restore()
context.restore()
// option 2: draw the prediction by manually transforming the corners
if (false)
{
context.save()
context.strokeStyle = "red"
context.fillStyle = "red"
context.lineWidth = 1
const fontSize = 16
context.font = fontSize "px sans-serif"
let [p_x, p_y, p_width, p_height] = prediction.bbox
// manually transform corners, note that compared to previous snippet, topleft now uses the top right corner of the rectangle
const topleft = matrix.transformPoint(new DOMPoint(p_x p_width, p_y))
const bottomright = matrix.transformPoint(new DOMPoint(p_x, p_y p_height))
p_x = topleft.x
p_y = topleft.y
p_width = bottomright.x - topleft.x
p_height = bottomright.y - topleft.y
context.strokeRect(p_x, p_y, p_width, p_height) // draw the prediction
context.fillText(prediction.class, p_x, p_y) // draw the text
context.fillText(prediction.score.toFixed(2), p_x, p_y fontSize) // draw the text
context.restore()
}
}
<canvas id="canvas" width=400 height=300 style="border: 1px solid black;"></canvas>
Комментарии:
1. «Преобразования, которые вы используете, кажутся ненужными» Преобразования были моей попыткой переместить начало координат холста в модель coco-ssd. Который действительно поместил прямоугольник в правильное положение, без него и текст, и прямоугольник были переведены полностью по сравнению с тем, где они должны быть. В этом вопросе помечен React-native. Пожалуйста, просмотрите мои правки, чтобы было более четко известно, что это среда react native. Я прикрепил репозиторий проекта git, чтобы помочь воспроизвести проблему.