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

Add backend logic for bounding box plots #5250

Merged
merged 23 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d32537c
move classes into its own collapsible row
julieg18 Jan 26, 2024
f6fb6c2
keep the same classnames for cells
julieg18 Jan 26, 2024
169384b
Store bounding box plot coordinates in single obj
julieg18 Jan 26, 2024
91c905e
Merge branch 'bounding-boxes' into stop-passing-bb-coords-as-props
julieg18 Jan 26, 2024
9045ab0
first iteration of backend code addition
julieg18 Jan 30, 2024
1e76753
Remove frontend bounding box fixture code
julieg18 Jan 30, 2024
2c85d58
fix bug in bb fixture
julieg18 Jan 30, 2024
ef0b553
fix bug in tests
julieg18 Jan 31, 2024
0692a99
Merge branch 'bounding-boxes' into stop-passing-bb-coords-as-props
julieg18 Jan 31, 2024
8cace73
fix merge conflicts
julieg18 Jan 31, 2024
ba4bdd0
fix typo
julieg18 Jan 31, 2024
b1abf5d
Merge branch 'stop-passing-bb-coords-as-props' into add-bb-backend-logic
julieg18 Jan 31, 2024
dd431e1
add toggle comparison class logic and additional tests
julieg18 Feb 1, 2024
d240272
Merge branch 'bounding-boxes' into add-bb-backend-logic
julieg18 Feb 1, 2024
4da57cb
final lookover
julieg18 Feb 1, 2024
d09e5f0
Update coordinate system
julieg18 Feb 2, 2024
646306a
apply review comments
julieg18 Feb 2, 2024
86767f6
update schema
julieg18 Feb 14, 2024
7fce713
Merge branch 'bounding-boxes' into add-bb-backend-logic
julieg18 Feb 15, 2024
0176f1a
move colors and use var when collecting plot class details
julieg18 Feb 15, 2024
ff95253
rely on collected comparison plots when grabbing selected class boxes
julieg18 Feb 15, 2024
c6e59f0
add additional optimisation
julieg18 Feb 15, 2024
06b6f5b
fix typo
julieg18 Feb 15, 2024
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
8 changes: 8 additions & 0 deletions extension/src/cli/dvc/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,18 @@ export type TemplatePlotOutput = {
type: PlotsType
}

export type BoundingBox = {
top: number
bottom: number
left: number
right: number
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I based the output off of iterative/dvclive#766 (comment) description, but this will most likely change and we will need to adjust accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to top, bottom, right, left based off the top-left corner after a discussion with Alex (see his comment):

image


export type ImagePlotOutput = {
revisions: string[]
type: PlotsType
url: string
boundingBoxes?: { label: string; box: BoundingBox }[]
}

export type PlotOutput = TemplatePlotOutput | ImagePlotOutput
Expand Down
1 change: 1 addition & 0 deletions extension/src/persistence/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum PersistenceKey {
PLOT_SELECTED_METRICS = 'plotSelectedMetrics:',
PLOTS_SMOOTH_PLOT_VALUES = 'plotSmoothPlotValues:',
PLOTS_COMPARISON_MULTI_PLOT_VALUES = 'plotComparisonMultiPlotValues:',
PLOTS_COMPARISON_CLASSES_SELECTED = 'plotComparisonClassesSelected:',
PLOT_TEMPLATE_ORDER = 'plotTemplateOrder:',
SHOW_ONLY_CHANGED = 'columnsShowOnlyChanged:'
}
Expand Down
15 changes: 14 additions & 1 deletion extension/src/plots/model/collect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,14 @@ describe('collectData', () => {
])

const heatmapPlot = join('plots', 'heatmap.png')
const boundingBoxPlot = join('plots', 'bounding_boxes.png')

expect(Object.keys(comparisonData.main)).toStrictEqual([
join('plots', 'acc.png'),
heatmapPlot,
join('plots', 'loss.png'),
join('plots', 'image'),
join('plots', 'bounding_boxes.png')
boundingBoxPlot
])

const testBranchHeatmap = comparisonData['test-branch'][heatmapPlot]
Expand All @@ -131,6 +132,18 @@ describe('collectData', () => {
sameContents(revisions, ['test-branch'])
)
])

const workspaceBoundingBoxData = comparisonData.workspace[boundingBoxPlot]

expect(workspaceBoundingBoxData[0]).toMatchObject({
boundingBoxes: {
car: [
{ bottom: 350, left: 150, right: 180, top: 320 },
{ bottom: 340, left: 200, right: 230, top: 310 }
],
'traffic light': [{ bottom: 210, left: 120, right: 195, top: 120 }]
}
})
})
})

Expand Down
191 changes: 184 additions & 7 deletions extension/src/plots/model/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import {
CustomPlotData,
CustomPlotValues,
ComparisonRevisionData,
ComparisonPlotImg
ComparisonPlotImg,
ComparisonClassDetails,
ComparisonPlotClasses,
ComparisonClassesSelected,
ComparisonPlotClass
} from '../webview/contract'
import {
AnchorDefinitions,
Expand All @@ -23,7 +27,9 @@ import {
PlotOutput,
PlotsOutput,
PlotsType,
TemplatePlotOutput
TemplatePlotOutput,
ImagePlotOutput,
BoundingBox
} from '../../cli/dvc/contract'
import { splitColumnPath } from '../../experiments/columns/paths'
import { ColumnType, Experiment } from '../../experiments/webview/contract'
Expand Down Expand Up @@ -205,10 +211,26 @@ const getMultiImageInd = (path: string) => {
return Number(fileName)
}

const collectImageBoundingBoxes = (
boundingBoxes: { label: string; box: BoundingBox }[]
): { [label: string]: BoundingBox[] } => {
const acc: { [label: string]: BoundingBox[] } = {}

for (const { label, box } of boundingBoxes) {
if (!acc[label]) {
acc[label] = []
sroy3 marked this conversation as resolved.
Show resolved Hide resolved
}

acc[label].push(box)
}

return acc
}

const collectImageData = (
acc: ComparisonData,
path: string,
plot: ImagePlot
plot: ImagePlotOutput
) => {
const isMultiImgPlot = MULTI_IMAGE_PATH_REG.test(path)
const pathLabel = isMultiImgPlot ? getMultiImagePath(path) : path
Expand All @@ -226,12 +248,20 @@ const collectImageData = (
acc[id][pathLabel] = []
}

const imgPlot: ImagePlot = { ...plot }
const imgPlot: ImagePlot = {
revisions: plot.revisions,
type: plot.type,
url: plot.url
}

if (isMultiImgPlot) {
imgPlot.ind = getMultiImageInd(path)
}

if (plot.boundingBoxes) {
imgPlot.boundingBoxes = collectImageBoundingBoxes(plot.boundingBoxes)
}

acc[id][pathLabel].push(imgPlot)
}

Expand Down Expand Up @@ -312,60 +342,120 @@ export const collectData = (output: PlotsOutput): DataAccumulator => {
return acc
}

type ComparisonPlotsAcc = { path: string; revisions: ComparisonRevisionData }[]
type ComparisonPlotsAcc = {
classDetails: ComparisonClassDetails
path: string
revisions: ComparisonRevisionData
}[]

type GetComparisonPlotImg = (
img: ImagePlot,
id: string,
path: string
) => ComparisonPlotImg

export const boundingBoxColors = [
'#ff3838',
'#ff9d97',
'#ff701f',
'#ffb21d',
'#cfd231',
'#48f90a',
'#92cc17',
'#3ddb86',
'#1a9334',
'#00d4bb',
'#2c99a8',
'#00c2ff',
'#344593',
'#6473ff',
'#0018ec',
'#8438ff',
'#520085',
'#cb38ff',
'#ff95c8',
'#ff37c7'
]
julieg18 marked this conversation as resolved.
Show resolved Hide resolved

const collectSelectedPlotImgClassLabels = (
boundingBoxClassLabels: Set<string>,
imgs: ImagePlot[] = []
) => {
for (const img of imgs) {
if (!img.boundingBoxes) {
continue
}

for (const label of Object.keys(img.boundingBoxes)) {
boundingBoxClassLabels.add(label)
}
}
}

const collectSelectedPathComparisonPlots = ({
acc,
comparisonData,
comparisonClassesSelected,
path,
selectedRevisionIds,
getComparisonPlotImg
}: {
acc: ComparisonPlotsAcc
comparisonData: ComparisonData
comparisonClassesSelected: ComparisonClassesSelected
path: string
selectedRevisionIds: string[]
getComparisonPlotImg: GetComparisonPlotImg
}) => {
const boundingBoxClassLabels = new Set<string>()
const pathRevisions = {
classDetails: {} as ComparisonClassDetails,
path,
revisions: {} as ComparisonRevisionData
}

for (const id of selectedRevisionIds) {
const imgs = comparisonData[id]?.[path]
const imgs: ImagePlot[] | undefined = comparisonData[id]?.[path]

pathRevisions.revisions[id] = {
id,
imgs: imgs
? imgs.map(img => getComparisonPlotImg(img, id, path))
: [{ errors: undefined, loading: false, url: undefined }]
}
collectSelectedPlotImgClassLabels(boundingBoxClassLabels, imgs)
}

for (const [ind, label] of [...boundingBoxClassLabels].entries()) {
julieg18 marked this conversation as resolved.
Show resolved Hide resolved
const selectedState = comparisonClassesSelected[path]?.[label]
pathRevisions.classDetails[label] = {
color: boundingBoxColors[ind % boundingBoxColors.length],
selected: selectedState === undefined ? true : selectedState
}
}

acc.push(pathRevisions)
}

export const collectSelectedComparisonPlots = ({
comparisonData,
comparisonClassesSelected,
paths,
selectedRevisionIds,
getComparisonPlotImg
}: {
comparisonData: ComparisonData
comparisonClassesSelected: ComparisonClassesSelected
paths: string[]
selectedRevisionIds: string[]
getComparisonPlotImg: GetComparisonPlotImg
}) => {
}): ComparisonPlotsAcc => {
const acc: ComparisonPlotsAcc = []

for (const path of paths) {
collectSelectedPathComparisonPlots({
acc,
comparisonClassesSelected,
comparisonData,
getComparisonPlotImg,
path,
Expand All @@ -376,6 +466,93 @@ export const collectSelectedComparisonPlots = ({
return acc
}

const getSelectedImgComparisonPlotClasses = ({
comparisonClassesSelected,
img,
path
}: {
comparisonClassesSelected: ComparisonClassesSelected
img: ImagePlot
path: string
}) => {
const imgClasses: ComparisonPlotClass[] = []

for (const [label, boxes] of Object.entries(img.boundingBoxes || {})) {
const selectedState = comparisonClassesSelected[path]?.[label]

if (selectedState === false) {
continue
}

imgClasses.push({
boxes,
label
})
}

return imgClasses
}

const collectedSelectedPathComparisonPlotClasses = ({
acc,
comparisonClassesSelected,
id,
comparisonData,
path
}: {
comparisonClassesSelected: ComparisonClassesSelected
acc: ComparisonPlotClasses
comparisonData: ComparisonData
path: string
id: string
}) => {
for (const img of comparisonData[id][path]) {
const imgClasses = getSelectedImgComparisonPlotClasses({
comparisonClassesSelected,
img,
path
})

if (imgClasses.length === 0) {
return
}

if (!acc[id]) {
acc[id] = {}
}

acc[id][path] = imgClasses
}
}

export const collectSelectedComparisonPlotClasses = ({
comparisonClassesSelected,
comparisonData,
paths,
selectedRevisionIds
}: {
comparisonClassesSelected: ComparisonClassesSelected
comparisonData: ComparisonData
paths: string[]
selectedRevisionIds: string[]
}) => {
const acc: ComparisonPlotClasses = {}

for (const id of selectedRevisionIds) {
for (const path of paths) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant by removing nested loops was not moving one inside of another function (I know realize we have 4 nested loops total), but can we change the code to drop some or at least make them more performant.

I see that in the fourth nested loop, we check for selected state. Maybe for a start, we could loop only through paths that are also in comparisonClassesSelected, since we only care about labels in comparisonClassesSelected[path].

There might be other optimizations to be made as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant by removing nested loops was not moving one inside of another function (I know realize we have 4 nested loops total), but can we change the code to drop some or at least make them more performant.

Thanks for the clarification!

I see that in the fourth nested loop, we check for selected state. Maybe for a start, we could loop only through paths that are also in comparisonClassesSelected, since we only care about labels in comparisonClassesSelected[path].

comparisonClassesSelected only holds classes that the user has toggled directly, it doesn't hold every visible class. Unless I'm missing something, we can't rely on it to detect which paths need to be looped over to collect classes.

The four loops are:

  1. looping over each revision (up to 7)
  2. looping over each path (could be a lot depending on how many plots the user has)
  3. looping over each path img (will usually be just one, unless it's an image step plot)
  4. looping over each class, gathering only the selected class boxes

I don't see a way to get rid of the first three loops, since for us to gather a plot's classes, we need to check for classes in each single image and collect the data. Atleast the third loop will likely just be a single item array.

We could get rid of the fourth loop by passing all image box data to the frontend and have ComparisonTableBoundingBoxImg filter out the boxes, but that means passing larger arrays that the component needs to loop over. Seems like we would want to limit the data passed to the frontend as much as possible but please correct me if I'm wrong.

Another alternative is having collectSelectedComparisonPlots gather class information along with the selected comparison plot data. I chose not to do this since that would make the function more complex, but if we think it's worth it for performance, I'm alright with doing that.

TL;DR

I'm not seeing a good way to get rid of any of our four loops. I'll review the loops and see what we can do to optimize them for now.

@sroy3, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed optimizing the loops! Instead of looping over every revision/path in comparisonData, we could start by looping over the already formed plots array. The plots already have a collected classDetails that tells you if a path has selected classes or not, lettings us skip early. Aka:

  1. looping over each plots path (skip over if classDetails is empty)
  2. looping over each revision
  3. looping over each path img (will usually be just one, unless it's an image step plot)
  4. looping over each class, gathering only the selected class boxes

Not seeing any other optimizations besides the two ideas I mentioned in my previous comment (have collectSelectedComparisonPlots collect box data, have ComparisonTableBoundingBoxImg do the box filtering).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm seeing is this:

if (selectedState === false) {
  continue
}
if (imgClasses.length === 0) {
  return
}

Why do we have to go through four loops to return nothing when we know it before hand? I would make this the first loop (the selected classes).

If there are no other possible optimizations, then I guess it's fine to keep as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (selectedState === false) {
continue
}
Why do we have to go through four loops to return nothing when we know it before hand? I would make this the first loop (the selected classes).

Aren't we already getting the selected classes in the first loop? In fact the line if (selectedState === false) {... no longer exists in the code. Did Github direct you the latest changes or is there something I'm missing here?

One thing I'm seeing is this:
if (imgClasses.length === 0) {
return
}

Good catch on if (imgClasses.length === 0) {...! I didn't spot the fact that we might be needlessy looping over selected classes if an img has no attached boxes to begin with. I'll update the code to check for img.boxes before running getSelectedImgComparisonPlotClasses.

collectedSelectedPathComparisonPlotClasses({
acc,
comparisonClassesSelected,
comparisonData,
id,
path
})
}
}

return acc
}

export type TemplateDetailsAccumulator = {
[path: string]: {
content: TopLevelSpec
Expand Down
Loading
Loading