Skip to content

Commit

Permalink
Scale-bar and legend rendering on back-end according to #5.
Browse files Browse the repository at this point in the history
  • Loading branch information
plankter committed Jul 24, 2019
1 parent 2e0b5d4 commit 56a0f41
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 98 deletions.
104 changes: 104 additions & 0 deletions backend/app/app/core/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import logging
from typing import Tuple, List

import cv2
import numpy as np
from matplotlib.colors import to_rgb, LinearSegmentedColormap
from skimage import filters

from app.modules.channel.models import FilterModel, ScalebarModel, LegendModel

logger = logging.getLogger(__name__)


def apply_filter(image: np.ndarray, filter: FilterModel):
if filter.type == 'gaussian':
return filters.gaussian(image, 1)


def colorize(image: np.ndarray, color: str):
try:
channel_color = to_rgb(color)
except:
channel_color = to_rgb('#ffffff')
channel_colormap = LinearSegmentedColormap.from_list(None, [(0, 0, 0), channel_color])
result = channel_colormap(image)
return result * 255.0


def scale_image(image: np.ndarray, levels: Tuple[float, float]):
channel_image = image - levels[0]
channel_image /= levels[1] - levels[0]
return np.clip(channel_image, 0, 1, out=channel_image)


def draw_scalebar(image: np.ndarray, scalebar: ScalebarModel):
width, height, _ = image.shape
length = 64
cv2.line(
image,
(width - 60, height - 60),
(width - 60 - length, height - 60),
(255, 255, 255),
2,
cv2.LINE_4
)
cv2.line(
image,
(width - 60, height - 55),
(width - 60, height - 65),
(255, 255, 255),
2,
cv2.LINE_4
)
cv2.line(
image,
(width - 60 - length, height - 55),
(width - 60 - length, height - 65),
(255, 255, 255),
2,
cv2.LINE_4
)

scale_text = length
if scalebar.settings is not None and 'scale' in scalebar.settings:
scale = scalebar.settings.get('scale')
if scale is not None and scale != '':
scale_text = int(length * float(scale))
cv2.putText(
image,
f'{scale_text} um',
(width - 60 - length, height - 30),
cv2.FONT_HERSHEY_PLAIN,
1,
(255, 255, 255),
1,
cv2.LINE_4
)
return image


def draw_legend(image: np.ndarray, legend_labels: List[Tuple[str, str]], legend: LegendModel):
for i, label in enumerate(legend_labels):
cv2.rectangle(
image,
(5, 50 * (i + 1) - 30),
(15 + cv2.getTextSize(label[0], cv2.FONT_HERSHEY_DUPLEX, 1, 1)[0][0], 50 * (i + 1) - 30 + 40),
(0, 0, 0),
cv2.FILLED,
cv2.LINE_AA
)

b, g, r = tuple([255 * x for x in to_rgb(label[1])])
color = (r, g, b)
cv2.putText(
image,
label[0],
(10, 50 * (i + 1)),
cv2.FONT_HERSHEY_DUPLEX,
1,
color,
1,
cv2.LINE_AA
)
return image
33 changes: 0 additions & 33 deletions backend/app/app/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@
from pathlib import Path
from shutil import rmtree
from typing import Optional
from typing import Tuple

import jwt
import numpy as np
import sqlalchemy
from jwt.exceptions import InvalidTokenError
from matplotlib.colors import to_rgb, LinearSegmentedColormap
from skimage import filters

from app.core import config
from app.modules.channel.models import FilterModel

password_reset_jwt_subject = "preset"

Expand Down Expand Up @@ -129,29 +124,6 @@ def after_delete_callback(mapper, connection, target):
return cls


def colorize(image: np.ndarray, color: str):
try:
channel_color = to_rgb(color)
except:
channel_color = to_rgb('w')
channel_colormap = LinearSegmentedColormap.from_list(None, [(0, 0, 0), channel_color])
result = channel_colormap(image)
return result * 255.0

# image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
# if color:
# image = image * color.value
# return image


def scale_image(image: np.ndarray, levels: Tuple[float, float]):
channel_image = image - levels[0]
channel_image /= levels[1] - levels[0]
return np.clip(channel_image, 0, 1, out=channel_image)
# result = rescale_intensity(image, in_range=levels, out_range=(0, 255))
# return result


def timeit(method):
def timed(*args, **kw):
ts = time.time()
Expand Down Expand Up @@ -248,8 +220,3 @@ def verify_password_reset_token(token) -> Optional[str]:
return decoded_token["email"]
except InvalidTokenError:
return None


def apply_filter(image: np.ndarray, filter: FilterModel):
if filter.type == 'gaussian':
return filters.gaussian(image, 1)
6 changes: 6 additions & 0 deletions backend/app/app/modules/channel/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,14 @@ class LegendModel(BaseModel):
settings: Optional[dict]


class ScalebarModel(BaseModel):
apply: bool
settings: Optional[dict]


class ChannelStackModel(BaseModel):
filter: FilterModel
legend: LegendModel
scalebar: ScalebarModel
channels: List[ChannelSettingsModel]
format: Optional[str] = 'png'
73 changes: 28 additions & 45 deletions backend/app/app/modules/channel/router.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import os
from io import BytesIO
from typing import List, Tuple
Expand All @@ -7,29 +8,29 @@
import redis
import ujson
from fastapi import APIRouter, Depends
from matplotlib.colors import to_rgb, to_rgba
from sqlalchemy.orm import Session
from starlette.requests import Request
from starlette.responses import StreamingResponse, UJSONResponse

from app.api.utils.db import get_db
from app.api.utils.security import get_current_active_superuser, get_current_active_user
from app.core.utils import colorize, scale_image, apply_filter
from app.core.image import scale_image, colorize, apply_filter, draw_scalebar, draw_legend
from app.modules.user.db import User
from . import crud
from .models import ChannelModel, ChannelStatsModel, ChannelStackModel

logger = logging.getLogger(__name__)
r = redis.Redis(host="redis")

router = APIRouter()


@router.get("/", response_model=List[ChannelModel])
def read_channels(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_superuser),
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_superuser),
):
"""
Retrieve channels
Expand All @@ -40,9 +41,9 @@ def read_channels(

@router.get("/{id}", response_model=ChannelModel)
def read_channel_by_id(
id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
):
"""
Get a specific channel by id
Expand All @@ -61,11 +62,11 @@ async def stream_image(record: bytes, chunk_size: int = 65536):

@router.get("/{id}/stats", response_model=ChannelStatsModel)
async def read_channel_stats(
id: int,
request: Request,
bins: int = 100,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
id: int,
request: Request,
bins: int = 100,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
):
"""
Get channel stats by id
Expand All @@ -85,12 +86,12 @@ async def read_channel_stats(

@router.get("/{id}/image", responses={200: {"content": {"image/png": {}}}})
async def read_channel_image(
id: int,
color: str = None,
min: float = None,
max: float = None,
# current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
id: int,
color: str = None,
min: float = None,
max: float = None,
# current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
):
"""
Get channel image by id
Expand All @@ -113,9 +114,9 @@ async def read_channel_image(

@router.post("/stack")
async def download_channel_stack(
params: ChannelStackModel,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
params: ChannelStackModel,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
):
"""
Download channel stack (additive) image
Expand Down Expand Up @@ -148,28 +149,10 @@ async def download_channel_stack(
additive_image = apply_filter(additive_image, params.filter)

if params.legend.apply:
for i, label in enumerate(legend_labels):
cv2.rectangle(
additive_image,
(5, 50 * (i + 1) - 30),
(15 + cv2.getTextSize(label[0], cv2.FONT_HERSHEY_DUPLEX, 1, 1)[0][0], 50 * (i + 1) - 30 + 40),
(0, 0, 0),
cv2.FILLED,
cv2.LINE_AA
)

b, g, r = tuple([255 * x for x in to_rgb(label[1])])
color = (r, g, b)
cv2.putText(
additive_image,
label[0],
(10, 50 * (i + 1)),
cv2.FONT_HERSHEY_DUPLEX,
1,
color,
1,
cv2.LINE_AA
)
additive_image = draw_legend(additive_image, legend_labels, params.legend)

if params.scalebar.apply:
additive_image = draw_scalebar(additive_image, params.scalebar)

format = params.format if params.format is not None else 'png'
status, result = cv2.imencode(f".{format}", additive_image.astype(int) if format == 'tiff' else additive_image)
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/modules/experiment/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,13 @@ export class ExperimentActions extends Actions<ExperimentState, ExperimentGetter

const filter = this.settings!.getters.filter;
const legend = this.settings!.getters.legend;
const scalebar = this.settings!.getters.scalebar;

return {
format: format,
filter: filter,
legend: legend,
scalebar: scalebar,
channels: channels,
};
}
Expand Down
14 changes: 2 additions & 12 deletions frontend/src/modules/experiment/models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IChannelSettings } from '@/modules/settings/models';
import { IChannelSettings, IImageFilter, IImageLegend, IImageScalebar } from '@/modules/settings/models';

export interface IExperiment {
id: number;
Expand Down Expand Up @@ -196,21 +196,11 @@ export interface IChannelStats {
edges: number[];
}

export interface IImageFilter {
apply: boolean;
type: string;
settings?: any;
}

export interface IImageLegend {
apply: boolean;
settings?: any;
}

export interface IChannelStack {
format?: string;
filter: IImageFilter;
legend: IImageLegend;
scalebar: IImageScalebar;
channels: Array<{
id: number;
color?: string;
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/modules/settings/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ export class SettingsGetters extends Getters<SettingsState> {
get legend() {
return this.state.legend;
}

get scalebar() {
return this.state.scalebar;
}
}
9 changes: 6 additions & 3 deletions frontend/src/modules/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { IImageFilter, IImageLegend } from '@/modules/experiment/models';
import { Module } from 'vuex-smart-module';
import { SettingsActions } from './actions';
import { SettingsGetters } from './getters';
import { IChannelSettings } from './models';
import { IChannelSettings, IImageFilter, IImageLegend, IImageScalebar } from './models';
import { SettingsMutations } from './mutations';

export class SettingsState {
Expand All @@ -16,7 +15,11 @@ export class SettingsState {
legend: IImageLegend = {
apply: false,
settings: {},
}
};
scalebar: IImageScalebar = {
apply: false,
settings: {},
};
}

export const settingsModule = new Module({
Expand Down
Loading

0 comments on commit 56a0f41

Please sign in to comment.