Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support data persistence #111

Merged
merged 4 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 78 additions & 32 deletions client/src/Annotation/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { useSettings } from "../SettingsProvider";
import {setIn} from "seamless-immutable"
import config from '../config.js';
import { useSnackbar } from '../SnackbarContext'
import { getImagesAnnotation } from "../utils/send-data-to-server"
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import colors from "../colors.js";

const extractRelevantProps = (region) => ({
cls: region.cls,
Expand Down Expand Up @@ -52,6 +56,7 @@ export default () => {
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
const [imageNames, setImageNames] = useState([])
const settingsConfig = useSettings()
const [isLoading, setIsLoading] = useState(true)
const { showSnackbar } = useSnackbar();
const [settings, setSettings] = useState({
taskDescription: "",
Expand Down Expand Up @@ -118,18 +123,46 @@ export default () => {
}
};

const mapRegionsColor = (regions) => {
if(regions === undefined) return []
return regions.map((region, index) => {
const classLabels = settingsConfig.settings.configuration.labels;
const clsIndex = classLabels.findIndex(label => label.id === region.cls);
const regionColor = clsIndex < classLabels.length ? colors[clsIndex]: colors[clsIndex % colors.length]
return {
...region,
color: regionColor
}
});
}
const fetchImages = async (imageUrls) => {
try {
const fetchPromises = imageUrls.map(url =>
fetch(url.src).then(response => response.blob())
.then(blob => ({ ...url, src: URL.createObjectURL(blob) }))
);
const images = await Promise.all(fetchPromises);
const imageURLSrcs = imageUrls.map(url => decodeURIComponent(url.src.split('/').pop()));
let image_annotations = await getImagesAnnotation({image_names: imageURLSrcs});
const imageMap = imageUrls.map((url, index) => {
const imageName = decodeURIComponent(url.src.split('/').pop());
const annotation = image_annotations.find(annotation => annotation.image_name === imageName)
const newRegions = mapRegionsColor(annotation?.regions) || []
return {
...images[index],
src: url.src,
regions: newRegions,
};
});

setSettings(prevSettings => ({
...prevSettings,
images: imageMap,
imagesBlob: images
}));
setImageNames(images);
changeSelectedImageIndex(0)
setImageNames(imageMap);
setIsLoading(false)
} catch (error) {
showSnackbar(error.message, 'error');
} finally {
Expand All @@ -146,11 +179,11 @@ export default () => {
const savedConfiguration = settingsConfig.settings|| {};
if (savedConfiguration.configuration && savedConfiguration.configuration.labels.length > 0) {
setSettings(savedConfiguration);
if (settings.images.length > 0) {
fetchImages(settings.images);
if (savedConfiguration.images.length > 0) {
fetchImages(savedConfiguration.images);
}
}
const showLab = settingsConfig.settings.showLab || false;
const showLab = settingsConfig.settings?.showLab || false;
if(!isSettingsOpen && showLab) {
setShowLabel(showLab)
}
Expand All @@ -166,33 +199,46 @@ export default () => {

return (
<>
{ !showLabel ? ( // Render loading indicator if loading is true
<SetupPage setConfiguration={setConfiguration} settings={settings} setShowLabel={setShowLabel} showAnnotationLab={showAnnotationLab}/>
{!showLabel ? (
<SetupPage
setConfiguration={setConfiguration}
settings={settings}
setShowLabel={setShowLabel}
showAnnotationLab={preloadConfiguration}
/>
) : (
<Annotator
taskDescription={settings.taskDescription || "Annotate each image according to this _markdown_ specification."}
images={settings.images || []}
enabledTools={getEnabledTools(settings.configuration.regionTypesAllowed) || []}
regionClsList={settings.configuration.labels.map(label => label.id) || []}
selectedImage={selectedImageIndex}
enabledRegionProps= {["class", "comment"]}
userReducer= {userReducer}
onExit={(output) => {
console.log("Exiting!")
}}
settings={settings}
onSelectJump={onSelectJumpHandle}
showTags={true}
selectedTool= {getToolSelectionType(settings.configuration.regions)}
openDocs={() => window.open(config.DOCS_URL, '_blank')}
hideSettings={false}
onShowSettings = {() => {
setIsSettingsOpen(!isSettingsOpen)
setShowLabel(false)
}}
selectedImageIndex={selectedImageIndex}
/>)}
<>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<CircularProgress />
</Box>
) : (
<Annotator
taskDescription={settings.taskDescription || "Annotate each image according to this _markdown_ specification."}
images={settings.images || []}
enabledTools={getEnabledTools(settings.configuration.regionTypesAllowed) || []}
regionClsList={settings.configuration.labels.map(label => label.id) || []}
selectedImage={selectedImageIndex}
enabledRegionProps={["class", "comment"]}
userReducer={userReducer}
onExit={(output) => {
console.log("Exiting!");
}}
settings={settings}
onSelectJump={onSelectJumpHandle}
showTags={true}
selectedTool={getToolSelectionType(settings.configuration.regions)}
openDocs={() => window.open(config.DOCS_URL, '_blank')}
hideSettings={false}
onShowSettings={() => {
setIsSettingsOpen(!isSettingsOpen);
setShowLabel(false);
}}
selectedImageIndex={selectedImageIndex}
/>
)}
</>
)}
</>

)
}
);
};
12 changes: 12 additions & 0 deletions client/src/utils/send-data-to-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ export const saveData = (imageData) => {
});
};

export const getImagesAnnotation = (imageData) => {
return new Promise((resolve, reject) => {
axios.post(`${config.SERVER_URL}/get_image_annotations`, imageData)
.then(response => {
resolve(response.data);
})
.catch(error => {
reject(error.response.data);
});
});
};

export const saveActiveImage = (activeImage) => {
if (activeImage === null)
return
Expand Down
101 changes: 97 additions & 4 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import tempfile
import shutil
import zipfile
import math

app = Flask(__name__)
app.config.from_object("config")
Expand Down Expand Up @@ -161,7 +162,7 @@ def create_json_response(image_name, color_map=None):
main_dict['cls'] = main_dict['cls'] if main_dict['cls'] != "nan" else ''
main_dict['processed'] = True

def add_regions(regions):
def add_regions(regions, region_type=None):
if isinstance(regions, pd.DataFrame):
regions_list = regions.to_dict(orient='records')
else:
Expand All @@ -175,16 +176,17 @@ def add_regions(regions):
points = region['points']
decoded_points = [[float(coord) for coord in point.split('-')] for point in points.split(';')]
region['points'] = decoded_points
region['type'] = region_type
main_dict['regions'].append(region)

if polygonRegions is not None:
add_regions(polygonRegions)
add_regions(polygonRegions, 'polygon')

if boxRegions is not None:
add_regions(boxRegions)
add_regions(boxRegions, 'box')

if circleRegions is not None:
add_regions(circleRegions)
add_regions(circleRegions, 'circle')

# Add the main dictionary to the list
imagesName.append(main_dict)
Expand Down Expand Up @@ -411,7 +413,98 @@ def download_image_with_annotations():
except Exception as cleanup_error:
print(f"Error cleaning up temporary directory: {cleanup_error}")

def convert_nan(value):
if isinstance(value, float) and math.isnan(value):
return None
elif isinstance(value, str) and value.lower() == 'nan':
return None
else:
return value

def hex_to_rgb_tuple(hex_color):
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

def map_region_keys(region):
mapped_region = {}
print(f"Region: {region}")
for key, value in region.items():
if key == "class":
mapped_region["cls"] = convert_nan(value)
elif key == "region-id":
mapped_region["id"] = convert_nan(value)
elif key.startswith("r") and len(key) == 2 and key[1] in ["h", "w", "x", "y"]:
mapped_region[key[1:]] = float(value[1:-1]) if isinstance(value, str) and value.startswith("[") and value.endswith("]") else convert_nan(value)
elif key in ["x", "y", "w", "h"]:
if isinstance(value, str) and value.startswith("[") and value.endswith("]"):
try:
mapped_region[key] = float(value[1:-1])
except ValueError:
mapped_region[key] = convert_nan(value)
else:
mapped_region[key] = convert_nan(value)
elif key == "color":
mapped_region['color'] = hex_to_rgb_tuple(value)
else:
mapped_region[key] = convert_nan(value)

if all(k in region for k in ["rh", "rw", "rx", "ry"]):
mapped_region["type"] = "circle"
return mapped_region


@app.route('/get_image_annotations', methods=['POST'])
@cross_origin(origin='*', headers=['Content-Type'])
def get_image_annotations():
try:
data = request.get_json()
image_names = data.get('image_names', [])
if not image_names:
raise ValueError("Invalid JSON data format: 'image_names' not found.")

image_annotations = []

for image_name in image_names:
json_bytes, download_filename = create_json_response(image_name)
json_str = json_bytes.getvalue().decode('utf-8')
# print(f"JSON String: {json_str}") # Debug: Print JSON string
images = json.loads(json_str).get("configuration", [])

for image_info in images:
regions = image_info.get("regions", [])
if not regions:
continue

region = regions[0]
image_url = region.get("image-src")
if "127.0.0.1:5001" in image_url:
image_url = image_url.replace("127.0.0.1:5001", "127.0.0.1:5000")

# Handle NaN values in regions
cleaned_regions = cleaned_regions = [map_region_keys(region) for region in regions]

image_annotations.append({
"image_name": image_info.get("image-name"),
"image_source": image_url,
"regions": cleaned_regions
})

print(f"Image Annotations: {image_annotations}")
return jsonify(image_annotations), 200

except ValueError as ve:
print('ValueError:', ve)
traceback.print_exc()
return jsonify({'error': str(ve)}), 400
except requests.exceptions.RequestException as re:
print('RequestException:', re)
traceback.print_exc()
return jsonify({'error': 'Error fetching image from URL'}), 500
except Exception as e:
print('General error:', e)
traceback.print_exc()
return jsonify({'error': str(e)}), 500

@app.route('/download_image_mask', methods=['POST'])
@cross_origin(origin=client_url, headers=['Content-Type'])
def download_image_mask():
Expand Down
5 changes: 4 additions & 1 deletion server/db/db_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ def regionType(type):
regionData['image-src'] = imageSrc
regionData['class'] = data['cls']
regionData['comment'] = data['comment'] if 'comment' in data else ''
regionData['tags'] = ';'.join(data.get('tags', []))
tags = data.get('tags')
if tags is None:
tags = []
regionData['tags'] = ';'.join(tags)

regionFunction = regionType(type)
regionFunction(regionData, data)
Expand Down
Loading