refact: Dependency Inversion #1
							
								
								
									
										7
									
								
								src/Interfaces/IObjectDetector.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/Interfaces/IObjectDetector.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
import PredictedObject from '../Models/PredictedObject'
 | 
			
		||||
 | 
			
		||||
interface IObjectDetector {
 | 
			
		||||
  getPredictionsFromImageData(videoImage: ImageData): Promise<PredictedObject[]>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default IObjectDetector
 | 
			
		||||
							
								
								
									
										8
									
								
								src/Interfaces/IObjectLocator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/Interfaces/IObjectLocator.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
import IOffset from "./IOffset"
 | 
			
		||||
import IPredictedObject from "./IPredictedObject"
 | 
			
		||||
 | 
			
		||||
interface IObjectLocator {
 | 
			
		||||
  getOffsetsFromPredictions(predictedObject: IPredictedObject): IOffset
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default IObjectLocator
 | 
			
		||||
							
								
								
									
										7
									
								
								src/Interfaces/IOffset.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/Interfaces/IOffset.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
interface IOffset {
 | 
			
		||||
  x: number,
 | 
			
		||||
  y: number,
 | 
			
		||||
  hypotenuse: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default IOffset
 | 
			
		||||
							
								
								
									
										8
									
								
								src/Interfaces/IUiRenderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/Interfaces/IUiRenderer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
import IOffset from "./IOffset"
 | 
			
		||||
import IPredictedObject from "./IPredictedObject"
 | 
			
		||||
 | 
			
		||||
interface IUiRenderer {
 | 
			
		||||
  render(props: { imageData: ImageData, predictedObjects: IPredictedObject[], offsets: IOffset[] }): void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default IUiRenderer
 | 
			
		||||
							
								
								
									
										5
									
								
								src/Interfaces/IVideoCapturer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/Interfaces/IVideoCapturer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
interface IVideoCapturer {
 | 
			
		||||
  imageData: ImageData | null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default IVideoCapturer
 | 
			
		||||
							
								
								
									
										14
									
								
								src/UseCases/Factories/makeObjectDetector.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/UseCases/Factories/makeObjectDetector.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
import { DetectedObject } from "@tensorflow-models/coco-ssd"
 | 
			
		||||
import ObjectDetector from "../ObjectDetector"
 | 
			
		||||
 | 
			
		||||
const defaultPredictions = [
 | 
			
		||||
  (prediction: DetectedObject) => prediction.score > 0.6,
 | 
			
		||||
  (prediction: DetectedObject) => prediction.class === 'cat',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
function makeObjectDetector (filterPredicates?: Function[]): ObjectDetector {
 | 
			
		||||
  if (!filterPredicates) filterPredicates = defaultPredictions
 | 
			
		||||
  return new ObjectDetector({ filterPredicates })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default makeObjectDetector
 | 
			
		||||
							
								
								
									
										14
									
								
								src/UseCases/Factories/makeObjectLocator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/UseCases/Factories/makeObjectLocator.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
import IVideo from "../../Interfaces/IVideo"
 | 
			
		||||
import ObjectLocator from "../ObjectLocator"
 | 
			
		||||
 | 
			
		||||
const defaultProps = {
 | 
			
		||||
  width: 640,
 | 
			
		||||
  height: 480
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function makeObjectLocator (props?: IVideo): ObjectLocator {
 | 
			
		||||
  const videoProps = props || defaultProps
 | 
			
		||||
  return new ObjectLocator(videoProps)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default makeObjectLocator
 | 
			
		||||
							
								
								
									
										15
									
								
								src/UseCases/Factories/makePredictedObject.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/UseCases/Factories/makePredictedObject.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import { DetectedObject } from "@tensorflow-models/coco-ssd"
 | 
			
		||||
import IPredictedObject from "../../Interfaces/IPredictedObject"
 | 
			
		||||
import PredictedObject from "../../Models/PredictedObject"
 | 
			
		||||
 | 
			
		||||
function makePredictedObject (p: IPredictedObject) {
 | 
			
		||||
  return new PredictedObject({
 | 
			
		||||
    xOrigin: p.xOrigin,
 | 
			
		||||
    yOrigin: p.yOrigin,
 | 
			
		||||
    width: p.width,
 | 
			
		||||
    height: p.height,
 | 
			
		||||
    class: p.class
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default makePredictedObject
 | 
			
		||||
							
								
								
									
										7
									
								
								src/UseCases/Factories/makeUiRenderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/UseCases/Factories/makeUiRenderer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
import UiRenderer from "../UiRenderer";
 | 
			
		||||
 | 
			
		||||
function makeUiRenderer (): UiRenderer {
 | 
			
		||||
  return new UiRenderer()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default makeUiRenderer
 | 
			
		||||
							
								
								
									
										14
									
								
								src/UseCases/Factories/makeVideoCatpurer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/UseCases/Factories/makeVideoCatpurer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
import IVideo from "../../Interfaces/IVideo"
 | 
			
		||||
import VideoCapturer from "../VideoCapturer"
 | 
			
		||||
 | 
			
		||||
const defaultProps = {
 | 
			
		||||
  width: 640,
 | 
			
		||||
  height: 480
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function makeVideoCapturer (props?: IVideo): VideoCapturer {
 | 
			
		||||
  const videoProps = props || defaultProps
 | 
			
		||||
  return new VideoCapturer(videoProps)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default makeVideoCapturer
 | 
			
		||||
@ -1,24 +1,20 @@
 | 
			
		||||
import * as tf from '@tensorflow/tfjs'
 | 
			
		||||
import * as cocossd from '@tensorflow-models/coco-ssd'
 | 
			
		||||
import PredictedObject from '../Models/PredictedObject'
 | 
			
		||||
import IObjectDetector from '../Interfaces/IObjectDetector'
 | 
			
		||||
import IPredictedObject from '../Interfaces/IPredictedObject'
 | 
			
		||||
import makePredictedObject from './Factories/makePredictedObject'
 | 
			
		||||
 | 
			
		||||
let instance: ObjectDetector | null = null
 | 
			
		||||
 | 
			
		||||
class ObjectDetector {
 | 
			
		||||
class ObjectDetector implements IObjectDetector {
 | 
			
		||||
  private mlModel: cocossd.ObjectDetection | null = null
 | 
			
		||||
  private filterPredicates: Function[] = []
 | 
			
		||||
 | 
			
		||||
  constructor (props?: { filterPredicates?: Function[] }) {
 | 
			
		||||
    if (!instance) instance = this
 | 
			
		||||
    
 | 
			
		||||
    if (props?.filterPredicates) this.filterPredicates = props.filterPredicates
 | 
			
		||||
    tf.getBackend()
 | 
			
		||||
 | 
			
		||||
    return instance
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private convertDetectedToPredictedObjects = (detectedObjects: cocossd.DetectedObject[]) => {
 | 
			
		||||
    const predictedObjects: PredictedObject[] = detectedObjects.map(p => new PredictedObject({
 | 
			
		||||
  private convertDetectedToPredictedObjects (detectedObjects: cocossd.DetectedObject[]) {
 | 
			
		||||
    const predictedObjects: IPredictedObject[] = detectedObjects.map(p => makePredictedObject({
 | 
			
		||||
      xOrigin: p.bbox[0],
 | 
			
		||||
      yOrigin: p.bbox[1],
 | 
			
		||||
      width: p.bbox[2],
 | 
			
		||||
@ -39,7 +35,7 @@ class ObjectDetector {
 | 
			
		||||
    else return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public predictImageStream = async (videoImage: ImageData) => {
 | 
			
		||||
  public async getPredictionsFromImageData (videoImage: ImageData): Promise<IPredictedObject[]> {
 | 
			
		||||
    const mlModel = await this.loadMlModel()
 | 
			
		||||
    const detectedObjects = await mlModel.detect(videoImage)
 | 
			
		||||
    const filteredDetections = detectedObjects.filter(p => this.doesDetectionPassFilterPredicates(p))
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,19 @@
 | 
			
		||||
import PredictedObject from "../Models/PredictedObject"
 | 
			
		||||
import Video from "../Models/Video"
 | 
			
		||||
import IObjectLocator from "../Interfaces/IObjectLocator"
 | 
			
		||||
import IOffset from "../Interfaces/IOffset"
 | 
			
		||||
import IPredictedObject from "../Interfaces/IPredictedObject"
 | 
			
		||||
import IVideo from "../Interfaces/IVideo"
 | 
			
		||||
 | 
			
		||||
interface Offset {
 | 
			
		||||
  x: number,
 | 
			
		||||
  y: number,
 | 
			
		||||
  hypotenuse: number
 | 
			
		||||
class ObjectLocator implements IObjectLocator {
 | 
			
		||||
  private videoWidth: number
 | 
			
		||||
  private videoHeight: number
 | 
			
		||||
 | 
			
		||||
  constructor (props: IVideo) {
 | 
			
		||||
    this.videoWidth = props.width
 | 
			
		||||
    this.videoHeight = props.height
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
class ObjectLocator {
 | 
			
		||||
  private video: Video
 | 
			
		||||
  constructor (video: Video) {
 | 
			
		||||
    this.video = video
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  detectPredictedObjectLocationFromVideo = (predictedObject: PredictedObject): Offset => {
 | 
			
		||||
    const videoCenter = { x: this.video.width / 2, y: this.video.height / 2 }
 | 
			
		||||
  getOffsetsFromPredictions = (predictedObject: IPredictedObject): IOffset => {
 | 
			
		||||
    const videoCenter = { x: this.videoWidth / 2, y: this.videoHeight / 2 }
 | 
			
		||||
    const objectCenter = {
 | 
			
		||||
      x: predictedObject.xOrigin + (predictedObject.width / 2),
 | 
			
		||||
      y: predictedObject.yOrigin + (predictedObject.height / 2)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										44
									
								
								src/UseCases/UiRenderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/UseCases/UiRenderer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
import IOffset from "../Interfaces/IOffset"
 | 
			
		||||
import IPredictedObject from "../Interfaces/IPredictedObject"
 | 
			
		||||
import IUiRenderer from "../Interfaces/IUiRenderer"
 | 
			
		||||
 | 
			
		||||
class UiRenderer implements IUiRenderer {
 | 
			
		||||
  render (props: { imageData: ImageData, predictedObjects: IPredictedObject[], offsets: IOffset[] }) {
 | 
			
		||||
 | 
			
		||||
    const body: HTMLBodyElement = document.querySelector('body')!
 | 
			
		||||
 | 
			
		||||
    let canvasElement: HTMLCanvasElement = document.querySelector('#videoOutput') as HTMLCanvasElement
 | 
			
		||||
    if (!canvasElement) {
 | 
			
		||||
      canvasElement = document.createElement('canvas')
 | 
			
		||||
      canvasElement.id = 'videoOutput'
 | 
			
		||||
      canvasElement.width = props.imageData.width
 | 
			
		||||
      canvasElement.height = props.imageData.height
 | 
			
		||||
      body.append(canvasElement)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const canvasContext = canvasElement.getContext('2d')!
 | 
			
		||||
    canvasContext.clearRect(0, 0, canvasElement.width, canvasElement.height)
 | 
			
		||||
    canvasContext.putImageData(props.imageData, 0, 0)
 | 
			
		||||
 | 
			
		||||
    props.predictedObjects.forEach(obj => {
 | 
			
		||||
      canvasContext.strokeStyle = 'rgb(0, 255, 0)'
 | 
			
		||||
      canvasContext.strokeRect(obj.xOrigin, obj.yOrigin, obj.width, obj.height)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const startPoint = {
 | 
			
		||||
      x: props.imageData.width / 2,
 | 
			
		||||
      y: props.imageData.height / 2
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    props.offsets.forEach(offset => {
 | 
			
		||||
      canvasContext.strokeStyle = 'rgb(255, 0, 0)'
 | 
			
		||||
      canvasContext.beginPath()
 | 
			
		||||
      canvasContext.moveTo(startPoint.x, startPoint.y)
 | 
			
		||||
      canvasContext.lineTo(startPoint.x - offset.x, startPoint.y - offset.y)
 | 
			
		||||
      canvasContext.closePath()
 | 
			
		||||
      canvasContext.stroke()
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default UiRenderer
 | 
			
		||||
							
								
								
									
										49
									
								
								src/UseCases/VideoCapturer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/UseCases/VideoCapturer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
import IVideo from '../Interfaces/IVideo'
 | 
			
		||||
import IVideoCapturer from '../Interfaces/IVideoCapturer'
 | 
			
		||||
 | 
			
		||||
class VideoCapturer implements IVideoCapturer {
 | 
			
		||||
  private videoWidth: number
 | 
			
		||||
  private videoHeight: number
 | 
			
		||||
  private videoStream: MediaStream | null = null
 | 
			
		||||
 | 
			
		||||
  constructor (props: IVideo) {
 | 
			
		||||
    this.videoWidth = props.width
 | 
			
		||||
    this.videoHeight = props.height
 | 
			
		||||
    this.enableCamera()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private enableCamera = async () => {
 | 
			
		||||
    const webCameraStream = await navigator.mediaDevices.getUserMedia({ video: true })
 | 
			
		||||
    this.videoStream = webCameraStream
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get imageData () {
 | 
			
		||||
    if (!this.videoStream) return null
 | 
			
		||||
 | 
			
		||||
    let videoElement: HTMLVideoElement = document.querySelector('#videoView') as HTMLVideoElement
 | 
			
		||||
    if (!videoElement) {
 | 
			
		||||
      videoElement = document.createElement('video')
 | 
			
		||||
      videoElement.width = this.videoWidth
 | 
			
		||||
      videoElement.height = this.videoHeight
 | 
			
		||||
      videoElement.autoplay = true
 | 
			
		||||
      videoElement.srcObject = this.videoStream
 | 
			
		||||
      videoElement.id = 'videoView'
 | 
			
		||||
      videoElement.style.display = 'none'
 | 
			
		||||
 | 
			
		||||
      const body = document.querySelector('body')!
 | 
			
		||||
      body.appendChild(videoElement)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const canvasElement: HTMLCanvasElement = document.createElement('canvas')
 | 
			
		||||
    canvasElement.width = this.videoWidth
 | 
			
		||||
    canvasElement.height = this.videoHeight
 | 
			
		||||
 | 
			
		||||
    const canvasContext = canvasElement.getContext('2d')!
 | 
			
		||||
    canvasContext.drawImage(videoElement, 0, 0, this.videoWidth, this.videoHeight)
 | 
			
		||||
 | 
			
		||||
    return canvasContext.getImageData(0, 0, this.videoWidth, this.videoHeight)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default VideoCapturer
 | 
			
		||||
							
								
								
									
										49
									
								
								src/app.ts
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								src/app.ts
									
									
									
									
									
								
							@ -1,43 +1,42 @@
 | 
			
		||||
import { DetectedObject } from "@tensorflow-models/coco-ssd"
 | 
			
		||||
import PredictedObjectCollectionController from "./Controllers/PredictedObjectCollectionController"
 | 
			
		||||
import VideoController from './Controllers/VideoController'
 | 
			
		||||
import ObjectDetector from './UseCases/ObjectDetector'
 | 
			
		||||
import ObjectLocator from "./UseCases/ObjectLocator"
 | 
			
		||||
import IObjectDetector from './Interfaces/IObjectDetector'
 | 
			
		||||
import IObjectLocator from './Interfaces/IObjectLocator'
 | 
			
		||||
import IOffset from './Interfaces/IOffset'
 | 
			
		||||
import IUiRenderer from './Interfaces/IUiRenderer'
 | 
			
		||||
import IVideoCapturer from "./Interfaces/IVideoCapturer"
 | 
			
		||||
 | 
			
		||||
const defaultPredictions = [
 | 
			
		||||
  (prediction: DetectedObject) => prediction.score > 0.6,
 | 
			
		||||
  (prediction: DetectedObject) => prediction.class === 'person', // TODO: change to cat
 | 
			
		||||
]
 | 
			
		||||
import makeObjectDetector from './UseCases/Factories/makeObjectDetector'
 | 
			
		||||
import makeObjectLocator from './UseCases/Factories/makeObjectLocator'
 | 
			
		||||
import makeUiRenderer from './UseCases/Factories/makeUiRenderer'
 | 
			
		||||
import makeVideoCapturer from './UseCases/Factories/makeVideoCatpurer'
 | 
			
		||||
 | 
			
		||||
class App {
 | 
			
		||||
  private predictedObjectCollectionController: PredictedObjectCollectionController
 | 
			
		||||
  private videoController: VideoController
 | 
			
		||||
  private objectDetector: ObjectDetector
 | 
			
		||||
  private objectLocator: ObjectLocator
 | 
			
		||||
  private objectDetector: IObjectDetector
 | 
			
		||||
  private objectLocator: IObjectLocator
 | 
			
		||||
  private videoCapturer: IVideoCapturer
 | 
			
		||||
  private uiRenderer: IUiRenderer
 | 
			
		||||
 | 
			
		||||
  constructor () {
 | 
			
		||||
    this.objectDetector = new ObjectDetector({ filterPredicates: defaultPredictions })
 | 
			
		||||
    this.predictedObjectCollectionController = new PredictedObjectCollectionController()
 | 
			
		||||
    this.videoController = new VideoController({  width: 640, height: 480 })
 | 
			
		||||
    this.objectLocator = new ObjectLocator(this.videoController.model)
 | 
			
		||||
    this.videoCapturer = makeVideoCapturer()
 | 
			
		||||
    this.objectDetector = makeObjectDetector()
 | 
			
		||||
    this.objectLocator = makeObjectLocator()
 | 
			
		||||
    this.uiRenderer = makeUiRenderer()
 | 
			
		||||
 | 
			
		||||
    const eventTarget = new EventTarget()
 | 
			
		||||
    eventTarget.addEventListener('onMediaStreamReady', this.predictImage)
 | 
			
		||||
    this.predictImage()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  predictImage = async () => {
 | 
			
		||||
    const imageData = this.videoController.imageData
 | 
			
		||||
    const imageData = this.videoCapturer.imageData
 | 
			
		||||
 | 
			
		||||
    if (!imageData) {
 | 
			
		||||
      window.requestAnimationFrame(this.predictImage)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const predictedObjects = await this.objectDetector.predictImageStream(imageData)
 | 
			
		||||
    this.predictedObjectCollectionController.predictedObjects = predictedObjects
 | 
			
		||||
    const offsets = predictedObjects.map(obj => {
 | 
			
		||||
      return this.objectLocator.detectPredictedObjectLocationFromVideo(obj)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    console.log(offsets)
 | 
			
		||||
    const predictedObjects = await this.objectDetector.getPredictionsFromImageData(imageData)
 | 
			
		||||
    const offsets: IOffset[] = predictedObjects.map(obj => this.objectLocator.getOffsetsFromPredictions(obj))
 | 
			
		||||
    this.uiRenderer.render({ imageData, predictedObjects, offsets })
 | 
			
		||||
 | 
			
		||||
    window.requestAnimationFrame(this.predictImage)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user