From aecf3bc86b07fdd2de9abe0d61edccf466bff20c Mon Sep 17 00:00:00 2001 From: knguyenrise8 <159168836+knguyenrise8@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:07:54 -0500 Subject: [PATCH] feat(185): Add extraction page with part 1 of integration (#231) * feat(185): Add extraction page with part 1 of integration * feat(185): Add Cors, update shape type feat(185): Add Cors, update shape type * feat(185): Add Cors, update shape type --- OCR/frontend/api/api.ts | 24 ++++++ OCR/frontend/api/types/types.ts | 32 ++++++++ OCR/frontend/package-lock.json | 33 +++++--- OCR/frontend/package.json | 3 + .../src/components/ImageAnnotator.tsx | 4 +- .../src/components/LoadingWrapper.scss | 51 +++++++++++++ .../src/components/LoadingWrapper.tsx | 33 ++++++++ .../src/contexts/AnnotationContext.tsx | 13 +++- OCR/frontend/src/contexts/FilesContext.tsx | 23 ++++-- OCR/frontend/src/pages/AnnotateTemplate.tsx | 9 ++- OCR/frontend/src/pages/ExtractProcess.tsx | 76 ++++++++++++++++++- OCR/frontend/src/pages/SaveTemplate.test.tsx | 67 ++++++++++++---- OCR/frontend/src/pages/SaveTemplate.tsx | 62 +++++++++------ OCR/frontend/src/utils/utils.ts | 42 ++++++++++ OCR/ocr/api.py | 13 ++++ 15 files changed, 420 insertions(+), 65 deletions(-) create mode 100644 OCR/frontend/api/api.ts create mode 100644 OCR/frontend/api/types/types.ts create mode 100644 OCR/frontend/src/components/LoadingWrapper.scss create mode 100644 OCR/frontend/src/components/LoadingWrapper.tsx create mode 100644 OCR/frontend/src/utils/utils.ts diff --git a/OCR/frontend/api/api.ts b/OCR/frontend/api/api.ts new file mode 100644 index 00000000..3aa84e12 --- /dev/null +++ b/OCR/frontend/api/api.ts @@ -0,0 +1,24 @@ +import { Page } from "../src/contexts/FilesContext"; +import { ImageToTextResponse } from "./types/types"; + +export const AddFormData = async (args: Page): Promise => { + + const { sourceImage, templateImage, fieldNames } = args; + const form = new FormData(); + form.append("source_image", sourceImage); + form.append("segmentation_template", templateImage); + form.append("labels", JSON.stringify(fieldNames)); + form.append("", ""); + console.log(args) + try { + const response = await fetch("http://localhost:8000/image_to_text/", { + "method": "POST", + body: form + }) + return await response.json() as ImageToTextResponse; + } catch (error) { + console.error(error); + return null; + } + +} diff --git a/OCR/frontend/api/types/types.ts b/OCR/frontend/api/types/types.ts new file mode 100644 index 00000000..b5b55737 --- /dev/null +++ b/OCR/frontend/api/types/types.ts @@ -0,0 +1,32 @@ +export interface Label { + color: string; + label: string; + type: string; +} + +export type Labels = Label[]; + +export interface ImageToTextArgs { + fieldNames: Labels; + sourceImage: string; + templateImage: string; +} + +export type ImageToTextResponse = { + [key: string]: [string, number]; +}; + +export interface ResultItem { + text: string; + confidence: number; + } + + export interface Submission { + template_name: string; + template_image: string; // Base64-encoded image string + file_name: string; + file_image: string; // Base64-encoded image string + results: { + [key: string]: ResultItem; // Allows any key with a ResultItem value + }; + } \ No newline at end of file diff --git a/OCR/frontend/package-lock.json b/OCR/frontend/package-lock.json index c421f92e..3493a8fe 100644 --- a/OCR/frontend/package-lock.json +++ b/OCR/frontend/package-lock.json @@ -9,8 +9,11 @@ "version": "0.0.0", "dependencies": { "@trussworks/react-uswds": "^9.0.0", + "@types/hex-rgb": "^3.0.0", + "@uswds/uswds": "^3.8.1", "classnames": "^2.5.1", "focus-trap-react": "^10.2.3", + "hex-rgb": "^5.0.0", "html2canvas": "^1.4.1", "pdfjs-dist": "^4.5.136", "prop-types": "^15.8.1", @@ -1476,6 +1479,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hex-rgb": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/hex-rgb/-/hex-rgb-3.0.0.tgz", + "integrity": "sha512-HrlzEM9VEzui+B9Jx+Fxb5IEkW6p2SIzUOpbvRtoEez1Yi80y/ZomfEFjd6r9OO4X3pZtMygv3hrU8BIbUjUBQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.4.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.2.tgz", @@ -1748,7 +1757,6 @@ "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.8.2.tgz", "integrity": "sha512-8sTx/GqlbTwSIK+0AFOGrYdaW1rKVB7Bp0+v9AMVt3I5vPK7CL0+I6vlclSf3U7ysJZeTTdkNS8q89sIAeL+AA==", "license": "SEE LICENSE IN LICENSE.md", - "peer": true, "dependencies": { "object-assign": "4.1.1", "receptor": "1.0.0", @@ -2466,7 +2474,6 @@ "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz", "integrity": "sha512-QCqAWP3kwj8Gz9UXncVXQGdrhnWxD8SQBSeZp5pOsyCcQ6RpL738L1/tfuwBiMi6F1fYkxqPnBrFBR4L+f49Cg==", "license": "CC0-1.0", - "peer": true, "engines": { "node": ">=4.0.0" } @@ -3117,6 +3124,18 @@ "license": "ISC", "optional": true }, + "node_modules/hex-rgb": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-5.0.0.tgz", + "integrity": "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -3445,8 +3464,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/keyboardevent-key-polyfill/-/keyboardevent-key-polyfill-1.1.0.tgz", "integrity": "sha512-NTDqo7XhzL1fqmUzYroiyK2qGua7sOMzLav35BfNA/mPUSCtw8pZghHFMTYR9JdnJ23IQz695FcaM6EE6bpbFQ==", - "license": "CC0-1.0", - "peer": true + "license": "CC0-1.0" }, "node_modules/keyv": { "version": "4.5.4", @@ -3574,8 +3592,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/matches-selector/-/matches-selector-1.2.0.tgz", "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", @@ -4387,7 +4404,6 @@ "resolved": "https://registry.npmjs.org/receptor/-/receptor-1.0.0.tgz", "integrity": "sha512-yvVEqVQDNzEmGkluCkEdbKSXqZb3WGxotI/VukXIQ+4/BXEeXVjWtmC6jWaR1BIsmEAGYQy3OTaNgDj2Svr01w==", "license": "CC0-1.0", - "peer": true, "dependencies": { "element-closest": "^2.0.1", "keyboardevent-key-polyfill": "^1.0.2", @@ -4437,8 +4453,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/resolve-id-refs/-/resolve-id-refs-0.1.0.tgz", "integrity": "sha512-hNS03NEmVpJheF7yfyagNh57XuKc0z+NkSO0oBbeO67o6IJKoqlDfnNIxhjp7aTWwjmSWZQhtiGrOgZXVyM90w==", - "license": "CC0-1.0", - "peer": true + "license": "CC0-1.0" }, "node_modules/reusify": { "version": "1.0.4", diff --git a/OCR/frontend/package.json b/OCR/frontend/package.json index 0e4f34bb..20cfb4a9 100644 --- a/OCR/frontend/package.json +++ b/OCR/frontend/package.json @@ -16,8 +16,11 @@ }, "dependencies": { "@trussworks/react-uswds": "^9.0.0", + "@types/hex-rgb": "^3.0.0", + "@uswds/uswds": "^3.8.1", "classnames": "^2.5.1", "focus-trap-react": "^10.2.3", + "hex-rgb": "^5.0.0", "html2canvas": "^1.4.1", "pdfjs-dist": "^4.5.136", "prop-types": "^15.8.1", diff --git a/OCR/frontend/src/components/ImageAnnotator.tsx b/OCR/frontend/src/components/ImageAnnotator.tsx index a438dc28..274a5f88 100644 --- a/OCR/frontend/src/components/ImageAnnotator.tsx +++ b/OCR/frontend/src/components/ImageAnnotator.tsx @@ -34,8 +34,8 @@ export const MultiImageAnnotator: FC = ({ images, cate const fields = [...LABELS.patientInformation.items, ...LABELS.organizationInformation.items]; const field = fields.find(field => field.name === selectedField?.name); const updatedShapes = [...shapes]; - // todo fix typing but for now this is fine - updatedShapes[index] = [...(updatedShapes[index] || []), {...shape, field: selectedField?.name as string, color: field?.color}]; + // for field?.color.slice(0,7) to remove the alpha channel from the hexcode + updatedShapes[index] = [...(updatedShapes[index] || []), {...shape, field: selectedField?.name as string, color: field?.color.slice(0,7)}]; setShapes(updatedShapes); localStorage.setItem('shapes', JSON.stringify(updatedShapes)); annotator?.updateCategories(shape.id, [], field?.color); diff --git a/OCR/frontend/src/components/LoadingWrapper.scss b/OCR/frontend/src/components/LoadingWrapper.scss new file mode 100644 index 00000000..1ed98ff8 --- /dev/null +++ b/OCR/frontend/src/components/LoadingWrapper.scss @@ -0,0 +1,51 @@ +.loading-wrapper { + position: relative; + } + + .loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); /* Semi-transparent white */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 10; + } + + .loading-spinner { + border: 8px solid rgba(0, 0, 0, 0.1); + border-left-color: #000; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + .loading-text { + text-align: center; + margin-top: 20px; + color: #333; /* Color for the text */ + } + + .loading-text h2 { + font-size: 24px; + margin-bottom: 10px; + } + + .loading-text p { + font-size: 16px; + color: #666; /* Lighter color for subtitle */ + } \ No newline at end of file diff --git a/OCR/frontend/src/components/LoadingWrapper.tsx b/OCR/frontend/src/components/LoadingWrapper.tsx new file mode 100644 index 00000000..349ab793 --- /dev/null +++ b/OCR/frontend/src/components/LoadingWrapper.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import './LoadingWrapper.scss'; // You'll define some styles here + +interface LoadingWrapperProps { + isLoading: boolean; + children: React.ReactNode; + title?: string; + subtitle?: string; + } + +const LoadingWrapper: React.FC = ({ + isLoading, + children, + title = 'Loading', + subtitle = 'Please wait a moment...', + }) => { + return ( +
+ {children} + {isLoading && ( +
+
+
+

{title}

+

{subtitle}

+
+
+ )} +
+ ); + }; + + export default LoadingWrapper; \ No newline at end of file diff --git a/OCR/frontend/src/contexts/AnnotationContext.tsx b/OCR/frontend/src/contexts/AnnotationContext.tsx index 73f15fef..c2b73d0a 100644 --- a/OCR/frontend/src/contexts/AnnotationContext.tsx +++ b/OCR/frontend/src/contexts/AnnotationContext.tsx @@ -1,8 +1,15 @@ import { createContext, useState, useContext, ReactNode } from 'react'; -import { Shape, useImageAnnotator } from 'react-image-label'; +import { useImageAnnotator } from 'react-image-label'; -interface CustomShape extends Shape { - field: string; +export interface Shape { + categories: string[]; + phi: number; + color?: string | undefined; + id: number; +} + +export interface CustomShape extends Shape { + field: string; } interface Field { diff --git a/OCR/frontend/src/contexts/FilesContext.tsx b/OCR/frontend/src/contexts/FilesContext.tsx index a655e12d..5ac431dd 100644 --- a/OCR/frontend/src/contexts/FilesContext.tsx +++ b/OCR/frontend/src/contexts/FilesContext.tsx @@ -1,20 +1,29 @@ import { createContext, useContext, useState, ReactNode } from 'react'; +import { Shape } from './AnnotationContext'; + +export interface Field { + color: string; + label: string; + type: string; +} export interface Page { // base 64 encoded image - image: string ; - fieldNames: string[]; + sourceImage: string; + templateImage: string; + fieldNames: Field[]; + shapes: Shape[]; } -export interface File { +export interface FileType { name: string; description: string; pages: Page[]; } interface FileContextType { - files: File[]; - addFile: (file: File) => void; + files: FileType[]; + addFile: (file: FileType) => void; removeFile: (fileName: string) => void; clearFiles: () => void; } @@ -22,9 +31,9 @@ interface FileContextType { const FilesContext = createContext(undefined); export const FilesProvider = ({ children }: { children: ReactNode }) => { - const [files, setFiles] = useState([]); + const [files, setFiles] = useState([]); - const addFile = (file: File) => { + const addFile = (file: FileType) => { setFiles(() => [file]); }; diff --git a/OCR/frontend/src/pages/AnnotateTemplate.tsx b/OCR/frontend/src/pages/AnnotateTemplate.tsx index a40af8c4..6ab33aa2 100644 --- a/OCR/frontend/src/pages/AnnotateTemplate.tsx +++ b/OCR/frontend/src/pages/AnnotateTemplate.tsx @@ -38,7 +38,6 @@ const AnnotateTemplate: React.FC = () => { setFields, fields, setAnnotatedImages, - annotatedImages, index, setIndex, } = useAnnotationContext(); @@ -97,9 +96,10 @@ const AnnotateTemplate: React.FC = () => { className="display-flex flex-justify space-between flex-align-center padding-y-1 label-container margin-0" onClick={() => { setSelectedField({ - name: item.name, - id: String(idx + 1), - color: item.color, + + name: item.name, + id: String(idx + 1), + color: item.color.slice(0, 7), }); let tempFields = [...fields]; if (fields.length === 0) { @@ -143,6 +143,7 @@ const AnnotateTemplate: React.FC = () => { ); const handleSubmit = async () => { + annotator!.stop(); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/OCR/frontend/src/pages/ExtractProcess.tsx b/OCR/frontend/src/pages/ExtractProcess.tsx index bcf67565..13e1030c 100644 --- a/OCR/frontend/src/pages/ExtractProcess.tsx +++ b/OCR/frontend/src/pages/ExtractProcess.tsx @@ -3,15 +3,84 @@ import ExtractDataHeader from "../components/ExtractDataHeader"; import { Divider } from "../components/Divider"; import { ExtractStepper } from "../components/ExtractStepper"; import { ExtractStep } from "../utils/constants"; +import LoadingWrapper from "../components/LoadingWrapper"; +import { AddFormData } from "../../api/api" +import { useState } from "react"; +import { FileType } from "../contexts/FilesContext"; +import { ImageToTextResponse, Submission } from "../../api/types/types"; + +interface IResults { + [key: string]: { + text: string; + confidence: number; + }; +} + + const ExtractProcess = () => { const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + + + const handleSubmit = async () => { + const templates: FileType[] = JSON.parse(localStorage.getItem("templates") || "[]"); + // Check if templates exist + if (templates.length === 0) { + setIsLoading(false); + return; + } + + try { + // Map through each template and page to create a promise for each query + const queries = templates.map(template => + template.pages.map(page => AddFormData(page)) + ).flat(); // Flatten the array if you have nested pages + + const responses: (ImageToTextResponse | null)[] = await Promise.all(queries); + + + const results: IResults = {} + + responses.forEach((response) => { + if (response) { + Object.keys(response).forEach(key => { + results[key] = { + text: response[key][0], + confidence: response[key][1], + }; + }); + } + }) + + const transformedResponses: Submission = { + template_name: templates[0].name, + template_image: templates[0].pages[0].templateImage, + file_name: 'image name', + file_image: templates[0].pages[0].sourceImage, + results, + } + + + + localStorage.setItem("extractedData", JSON.stringify(transformedResponses)); + + // Update loading state and navigate + setIsLoading(false); + navigate("/"); + + } catch (error) { + console.error("Error processing templates:", error); + setIsLoading(false); + } + } + return ( - <> +
navigate("extract/upload")} - onSubmit={() => navigate("/")} + onSubmit={handleSubmit} isUploadComplete={true} /> @@ -20,7 +89,8 @@ const ExtractProcess = () => {
- +
+ ); }; diff --git a/OCR/frontend/src/pages/SaveTemplate.test.tsx b/OCR/frontend/src/pages/SaveTemplate.test.tsx index 96f69a2f..ead95ce6 100644 --- a/OCR/frontend/src/pages/SaveTemplate.test.tsx +++ b/OCR/frontend/src/pages/SaveTemplate.test.tsx @@ -15,22 +15,47 @@ vi.mock('react-router-dom', async (importOriginal) => { }; }); -const wrapper = ({ children }: { children: React.ReactNode }) => { - return ( - - - - {children} - - - -); -} - describe('SaveTemplate Component', () => { + beforeEach(() => { + // Mock localStorage + const localStorageMock = (() => { + let store: Record = {}; + + return { + getItem(key: string) { + return store[key] || null; + }, + setItem(key: string, value: string) { + store[key] = value; + }, + clear() { + store = {}; + }, + removeItem(key: string) { + delete store[key]; + }, + }; + })(); + + Object.defineProperty(window, "localStorage", { + value: localStorageMock, + writable: true, + }); + }); + + afterEach(() => { + window.localStorage.clear(); + vi.clearAllMocks(); + }); it('should render the SaveTemplate page with all elements', () => { render( - , { wrapper } + + + + + + + ); expect(screen.getByTestId('save-template-title')).toBeInTheDocument(); @@ -42,7 +67,13 @@ describe('SaveTemplate Component', () => { it('should navigate back when the back button is clicked', () => { render( - , { wrapper } + + + + + + + ); fireEvent.click(screen.getByText(/back/i)); @@ -52,7 +83,13 @@ describe('SaveTemplate Component', () => { it('should navigate to the home page when the form is submitted', () => { render( - , { wrapper } + + + + + + + ); fireEvent.click(screen.getByText(/submit/i)); diff --git a/OCR/frontend/src/pages/SaveTemplate.tsx b/OCR/frontend/src/pages/SaveTemplate.tsx index 9687e9be..2b208e20 100644 --- a/OCR/frontend/src/pages/SaveTemplate.tsx +++ b/OCR/frontend/src/pages/SaveTemplate.tsx @@ -1,31 +1,47 @@ -import {Label, TextInput} from "@trussworks/react-uswds"; -import {Divider} from "../components/Divider"; -import {UploadHeader} from "../components/Header"; -import {Stepper} from "../components/Stepper"; -import {AnnotateStep} from "../utils/constants"; -import {useNavigate} from "react-router-dom"; -import {useAnnotationContext} from "../contexts/AnnotationContext"; -import {useFiles, File, Page} from "../contexts/FilesContext"; +import { Label, TextInput } from "@trussworks/react-uswds"; +import { Divider } from "../components/Divider"; +import { UploadHeader } from "../components/Header"; +import { Stepper } from "../components/Stepper"; +import { AnnotateStep } from "../utils/constants"; +import { useNavigate } from "react-router-dom"; +import { useAnnotationContext } from "../contexts/AnnotationContext"; +import { useFiles, FileType, Page } from "../contexts/FilesContext"; +import hexRgb from "hex-rgb"; + + export const SaveTemplate = () => { const navigate = useNavigate(); - const {fields, setDescription, setName, name, description, annotatedImages} = useAnnotationContext() - const {addFile} = useFiles(); - + const { fields, setDescription, setName, name, description, annotatedImages, shapes} = useAnnotationContext() + const { addFile } = useFiles(); + const handleSubmit = () => { - const pages: Page[] = fields.map((field, index) => { - return { - fieldNames: [...field.keys()], - image: annotatedImages[index] + const images: string[] = localStorage.getItem('images') ? JSON.parse(localStorage.getItem('images') as string) : []; + let pages: Page[] = []; + if (images.length > 0) { + pages = fields.map((_, index) => { + const shape = shapes[index] + return { + fieldNames: shape.map(s => { + const { red, green, blue } = hexRgb(s.color as string); + return { + type: 'text', + color: `${red},${green},${blue}`, + label: s.field + } + }), + sourceImage: images[index], + templateImage: annotatedImages[index], + shapes: shape + } + }); + const tempFile: FileType = { + name, + description, + pages: pages + } - }); - const tempFile: File = { - name, - description, - pages: pages - } - addFile(tempFile) let existingTemplates = [] try { const data = localStorage.getItem('templates'); @@ -37,6 +53,8 @@ export const SaveTemplate = () => { console.error("Invalid information found in templates, it will be overwritten") } localStorage.setItem('templates', JSON.stringify([...existingTemplates, tempFile])) + addFile(tempFile) + } navigate('/') } return ( diff --git a/OCR/frontend/src/utils/utils.ts b/OCR/frontend/src/utils/utils.ts new file mode 100644 index 00000000..323c5f94 --- /dev/null +++ b/OCR/frontend/src/utils/utils.ts @@ -0,0 +1,42 @@ +export function base64ToBinaryFile(base64Data: string, fileName: string): File { + // Remove the data URL part if it exists + const base64String = base64Data.split(',')[1]; + + // Decode base64 string into binary data + const binaryString = atob(base64String); + + // Create a Uint8Array from the binary string + const binaryData = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + binaryData[i] = binaryString.charCodeAt(i); + } + + // Create a Blob from the binary data + const blob = new Blob([binaryData], { type: 'image/png' }); + + // Create a file from the Blob if needed + const file = new File([blob], fileName, { type: 'image/png' }); + console.log(file); + saveFileLocally(file, fileName); + return file; + } + + function saveFileLocally(file: File, fileName: string) { + // Create a URL for the file + const url = URL.createObjectURL(file); + + // Create an anchor element and trigger download + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + + // Clean up by revoking the object URL and removing the anchor element + setTimeout(() => { + URL.revokeObjectURL(url); + document.body.removeChild(a); + }, 0); + } + + diff --git a/OCR/ocr/api.py b/OCR/ocr/api.py index 0a063dcd..d32bbe0f 100644 --- a/OCR/ocr/api.py +++ b/OCR/ocr/api.py @@ -4,11 +4,24 @@ import numpy as np from fastapi import FastAPI, UploadFile, Form +from fastapi.middleware.cors import CORSMiddleware from ocr.services.image_ocr import ImageOCR from ocr.services.image_segmenter import ImageSegmenter, segment_by_color_bounding_box app = FastAPI() +origins = [ + "http://localhost:8000", # Allow requests from this origin + "http://localhost:5173", # Add your front-end domain here +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, # You can allow specific origins or ["*"] for all + allow_credentials=True, + allow_methods=["*"], # Allow all HTTP methods (GET, POST, etc.) + allow_headers=["*"], # Allow all headers +) segmenter = ImageSegmenter( segmentation_function=segment_by_color_bounding_box, )