I am a new to Angular and I'm playing with it to make a simple prototype app. The application is about checking if an image perfectly fits for some pattern. The pattern is always black and white, and the image is transparent with some colored fields. I have made a functionality for user to resize/reshape the image so it fits for the pattern in Angular. That's because both images differ in size and center position. The problem is, when I POST both images from Angular to my FastAPI endpoint the image to check is differently positioned as it was in Angular canvas element. The only way to make it not change position is not resizing it, which isn't acceptable. I have tested the endpoint with the images edited with Gimp and it works perfectly as expected. So I think the problem is located in Angular component. #resize-image.component.ts export class ResizeImageComponent { @ViewChild('baseImage') baseImage!: ElementRef<HTMLImageElement>; @ViewChild('resizableImage', { static: true }) resizableImage!: ElementRef<HTMLImageElement>; @ViewChild('resizableContainer', { static: true }) resizableContainer!: ElementRef<HTMLDivElement>; @ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>; @ViewChild('invisibleCanvas') invisibleCanvas!: ElementRef<HTMLCanvasElement>; @Input() patternSrc: string | ArrayBuffer | null = null; @Input() toCheckSrc: string | ArrayBuffer | null = null; @Output() isResizeImageVisible = new EventEmitter<boolean>(); @Output() overlayUrl = new EventEmitter<string | ArrayBuffer | null>(); @Output() differencesUrl = new EventEmitter<string | ArrayBuffer | null>(); visible: boolean = false; resizingDirection: string | null = null; originalWidth!: number; originalHeight!: number; originalMouseX!: number; originalMouseY!: number; newWidth!: number; newHeight!: number; startResizing(event: MouseEvent, direction: string) { event.preventDefault(); this.resizingDirection = direction; const rect = this.resizableImage.nativeElement.getBoundingClientRect(); this.originalWidth = rect.width; this.originalHeight = rect.height; this.originalMouseX = event.clientX; this.originalMouseY = event.clientY; } @HostListener('document:mousemove', ['$event']) onMouseMove(event: MouseEvent) { if (!this.resizingDirection) return; let newWidth = this.originalWidth; let newHeight = this.originalHeight; if (this.resizingDirection.includes('r')) { newWidth = this.originalWidth + (event.clientX - this.originalMouseX); } if (this.resizingDirection.includes('l')) { newWidth = this.originalWidth - (event.clientX - this.originalMouseX); this.resizableImage.nativeElement.style.left = `${event.clientX - this.originalMouseX}px`; } if (this.resizingDirection.includes('b')) { newHeight = this.originalHeight + (event.clientY - this.originalMouseY); } if (this.resizingDirection.includes('t')) { newHeight = this.originalHeight - (event.clientY - this.originalMouseY); this.resizableImage.nativeElement.style.top = `${event.clientY - this.originalMouseY}px`; } this.resizableImage.nativeElement.style.width = `${newWidth}px`; this.resizableImage.nativeElement.style.height = `${newHeight}px`; // Update the invisible canvas this.updateInvisibleCanvas(); } @HostListener('document:mouseup') stopResizing() { this.resizingDirection = null; } async saveImage() { const baseImageElement = this.baseImage.nativeElement; const resizableImageElement = this.resizableImage.nativeElement; const canvasElement = this.canvas.nativeElement; const invisibleCanvasElement = this.invisibleCanvas.nativeElement; const ctx = canvasElement.getContext('2d'); const invisibleCtx = invisibleCanvasElement.getContext('2d'); canvasElement.width = baseImageElement.width; canvasElement.height = baseImageElement.height; if (ctx) { const screenWidth = window.innerWidth; console.log('Current screen width:', screenWidth); const size = (screenWidth >= 2100 ? 1100 : screenWidth >= 1600 && screenWidth < 2100 ? 850 : 500); ctx.drawImage(baseImageElement, 0, 0, baseImageElement.width, baseImageElement.height); const resizableRect = resizableImageElement.getBoundingClientRect(); const containerRect = this.resizableContainer.nativeElement.getBoundingClientRect(); const x = resizableRect.left - containerRect.left; const y = resizableRect.top - containerRect.top; const width = resizableRect.width; const height = resizableRect.height; ctx.drawImage(resizableImageElement, x, y, width, height); const image = canvasElement.toDataURL('image/png'); this.overlayUrl.emit(image); if (invisibleCtx) { invisibleCanvasElement.toBlob(async (toCheckBlob: Blob | null) => { if (toCheckBlob) { const baseCanvas = document.createElement('canvas'); baseCanvas.width = 1200; baseCanvas.height = 1200; const baseCtx = baseCanvas.getContext('2d'); if (baseCtx) { baseCtx.drawImage(baseImageElement, 0, 0, baseCanvas.width, baseCanvas.height); baseCanvas.toBlob(async (patternBlob: Blob | null) => { if (patternBlob) { const formData = new FormData(); formData.append('pattern', patternBlob, 'pattern-image.png'); formData.append('toCheck', toCheckBlob, 'to-check-image.png'); formData.append('width', this.newWidth.toString()); formData.append('height', this.newHeight.toString()); const response = await fetch('http://localhost:8000/api/get_differences/', { method: 'POST', body: formData }); if (response.ok) { const returnedBlob = await response.blob(); const imageUrl = URL.createObjectURL(returnedBlob); this.differencesUrl.emit(imageUrl); } } }, 'image/png'); } } }, 'image/png'); } } this.isResizeImageVisible.emit(false); } updateInvisibleCanvas() { const resizableImageElement = this.resizableImage.nativeElement; const invisibleCanvasElement = this.invisibleCanvas.nativeElement; const ctx = invisibleCanvasElement.getContext('2d'); invisibleCanvasElement.width = resizableImageElement.width; invisibleCanvasElement.height = resizableImageElement.height; const screenWidth = window.innerWidth; const size = (screenWidth >= 2100 ? 1100 : screenWidth >= 1600 && screenWidth < 2100 ? 850 : 500); const actualWidth = resizableImageElement.width; const actualHeight = resizableImageElement.height; const widthGrow = actualWidth / size; const heightGrow = actualHeight / size; this.newWidth = Math.round(1200 * widthGrow); this.newHeight = Math.round(1200 * heightGrow); console.log(widthGrow, heightGrow, size, this.newWidth, this.newHeight); if (ctx) { ctx.clearRect(0, 0, invisibleCanvasElement.width, invisibleCanvasElement.height); ctx.drawImage(resizableImageElement, 0, 0, resizableImageElement.width, resizableImageElement.height); } } } #resize-image.component.html <div class="resize-component dark-mode" > <div class="bg"> <div class="image-container" #imageContainer> <img src="assets/pattern.png" alt="Cut" class="base-image" #baseImage/> <div class="resizable-container" #resizableContainer> <img [src]="toCheckSrc" class="resizable-image" #resizableImage/> <div class="resize-handle top-left" direction="tl" #resizeHandle (mousedown)="startResizing($event, 'tl')"></div> <div class="resize-handle top" direction="t" #resizeHandle (mousedown)="startResizing($event, 't')"></div> <div class="resize-handle top-right" direction="tr" #resizeHandle (mousedown)="startResizing($event, 'tr')"></div> <div class="resize-handle right" direction="r" #resizeHandle (mousedown)="startResizing($event, 'r')"></div> <div class="resize-handle bottom-right" direction="br" #resizeHandle (mousedown)="startResizing($event, 'br')"></div> <div class="resize-handle bottom" direction="b" #resizeHandle (mousedown)="startResizing($event, 'b')"></div> <div class="resize-handle bottom-left" direction="bl" #resizeHandle (mousedown)="startResizing($event, 'bl')"></div> <div class="resize-handle left" direction="l" #resizeHandle (mousedown)="startResizing($event, 'l')"></div> </div> </div> <canvas #canvas style="display: none;"></canvas> <canvas #invisibleCanvas style="display: none;"></canvas> <button mat-flat-button color="primary" class="mic-btn-sm" (click)="saveImage()">Zapisz obraz</button> </div> </div> #FastAPI main.py @app.post('/api/get_differences/') async def get_differences(pattern: UploadFile = File(...), toCheck: UploadFile = File(...), width: str = Form(...), height: str = Form(...)): width = int(width) height = int(height) pattern = cv2.imread(pattern.file, cv2.IMREAD_GRAYSCALE) toCheck = cv2.imread(toCheck.file, cv2.IMREAD_GRAYSCALE) toCheck = cv2.resize(toCheck, (width, height)) if width > pattern.shape[1] or height > pattern.shape[0]: background = np.zeros((height, width), dtype=np.uint8) background.fill(255) pattern_height, pattern_width = pattern.shape background_y = (height - pattern_height) // 2 background_x = (width - pattern_width) // 2 background[background_y:background_y + pattern_height, background_x:background_x + pattern_width] = pattern pattern = background elif width < pattern.shape[1] or height < pattern.shape[0]: background = np.zeros((pattern.shape[1], pattern.shape[0]), dtype=np.uint8) eng_height, eng_width = toCheck.shape background_y = (pattern.shape[0] - eng_height) // 2 background_x = (pattern.shape[1] - eng_width) // 2 background[background_y:background_y + eng_height, background_x:background_x + eng_width] = toCheck toCheck = background else: pass pattern = cv2.bitwise_not(pattern) _, pattern = cv2.threshold(pattern, 200, 255, cv2.THRESH_BINARY) pattern_contours = cv2.Canny(pattern, 300, 500) toCheck_contours = cv2.Canny(toCheck, 300, 500) KERNEL = (7, 7) bold_pattern_contours = cv2.GaussianBlur(pattern_contours, KERNEL, sigmaX=100) _, pattern = cv2.threshold(bold_pattern_contours, 10, 255, cv2.THRESH_BINARY) subtracted_toCheck_pattern = cv2.subtract(toCheck_contours, pattern) bold_toCheck_contours = cv2.GaussianBlur(toCheck_contours, KERNEL, sigmaX=100) _, toCheck = cv2.threshold(bold_toCheck_contours, 10, 255, cv2.THRESH_BINARY) subtracted_pattern_toCheck = cv2.subtract(pattern_contours, toCheck) subtracted_picture = cv2.bitwise_or(subtracted_pattern_toCheck, subtracted_toCheck_pattern) cv2.imwrite('subtraction.png', subtracted_picture) subtracted_picture = Image.fromarray(subtracted_picture) subtracted_picture.show() #### img_array = np.array(subtracted_picture.convert("RGB")) pattern_bgr = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR) results = MODEL(pattern_bgr) boxes = results.xyxy[0].numpy() image_with_boxes = draw_boxes(subtracted_picture.convert("RGB"), boxes) image_with_boxes.show() #### img_byte_arr = BytesIO() subtracted_picture.save(img_byte_arr, format='PNG') img_byte_arr.seek(0) return Response(content=img_byte_arr.getvalue(), media_type="image/png") I've tried placing the toCheck image on a transparent background with constant size. I've expected it wouldn't reposition the image, but unfortunately nothing like that happened. I have also tried some smaller attempts like additional resizing or repositioning the image with no effects. I have spent 3 days with no good results. Continue reading...