Conversation
| Comparator<String> cameraIdByResolutionComparator = new Comparator<String>() { | ||
| private long convertIdToResolution(String id) { | ||
| Size pixelArraySize = cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); | ||
| return (long) pixelArraySize.getHeight() * pixelArraySize.getWidth(); |
There was a problem hiding this comment.
Как я понимаю, цель введения long - избавиться от переполнения. В таком случае надо явно расставить скобки (читатель может не помнить, что приведение типов имеет больший приоритет, чем умножение).
| long result = convertIdToResolution(o1) - convertIdToResolution(o2); | ||
| if (result > 0) { | ||
| return 1; | ||
| } else if (result < 0) { | ||
| return -1; | ||
| } |
There was a problem hiding this comment.
return Long.compare(convertIdToResolution(o1), convertIdToResolution(o2));
| if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK | ||
| && cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT) != CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_MONO) { | ||
|
|
||
| if (bestCameraId == null) { |
There was a problem hiding this comment.
Лучше во всём таком коде сокращать вложенность if-ов, отсекая случаи по одному:
if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) != CameraCharacteristics.LENS_FACING_BACK) {
continue;
}
if (cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT) == CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_MONO) {
continue;
}
if (bestCameraId == null) {
bestCameraId = cameraId;
continue;
}
// дальше код с компаратором
| if (cameraIdByResolutionComparator.compare(bestCameraId, cameraId) > 0) { | ||
| bestCameraId = cameraId; | ||
| } |
There was a problem hiding this comment.
Я бы стал его вводить компаратор если хочется использовать метод Collections.max(Collection, Comparator), сейчас же мы всё равно идём в цикле по-старинке, и он только затрудняет чтение, так как можно просто вычислить два разрешения и сравнить, больше одно другого или нет.
Size pixelArraySize = cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
long resolution = ((long) pixelArraySize.getHeight()) * pixelArraySize.getWidth();
if (resolution > bestResolution) {
bestCameraId = cameraId;
bestResolution = resolution;
}
Это во много раз проще читается (не надо помнить, какая конвенция у компаратора -- когда +1, когда -1, нет лишнего кода, итд), у него лучше производительность (не надо вытаскивать камеру по id), итд
| } | ||
|
|
||
| // returns a camera that 1) is not monochrome 2) is of largest sensor area | ||
| private String chooseCamera() { |
There was a problem hiding this comment.
По сути здесь ничего не выбирается ("choose" можно трактовать как будто мы выбираем камеру и включаем, например), лучше назвать getBestCameraId например.
Кстати, возвращать лучше Optional<String>, null это большая проблема в программировании в целом
| // CaptureRequest request = builder.build(); | ||
|
|
||
| // submit a capture request by creating a session | ||
| ArrayList<Surface> surfacesList = new ArrayList<>(); |
There was a problem hiding this comment.
В левой части просто List<Surface> (это конвенция в Java - использовать наиболее общий из достаточных интерфейсов)
| e.printStackTrace(); | ||
| throw new RuntimeException("camera access exception"); | ||
| } |
| } else if (scaleByHeight * size.getWidth() < screenSizeRect.width()) { | ||
| previewView.setLayoutParams(new FrameLayout.LayoutParams((int) (size.getWidth() * scaleByHeight), (int) (size.getHeight() * scaleByHeight))); | ||
| } else { | ||
| throw new AssertionError("unscalable size?"); |
There was a problem hiding this comment.
Если мы вычислим a = b/c, то результат a*c может быть немного больше, немного меньше, или равен b. Это же арифметика с плавающей точкой, могут быть погрешности округления. Так что это исключение может начать падать "случайно" (например, и размер и экран квадратные, но результат деления неточный).
|
|
||
| public void closeCamera() { | ||
| if (cameraDevice != null) { | ||
| isCameraOpen.set(false); |
There was a problem hiding this comment.
Вообще, мне кажется, что весь этот флаг здесь лишний. Он всегда true, когда cameraId выставлен, и всегда false, когда нет.
| public static ImageStreamHandler getInstance() { | ||
| if (instance == null) { | ||
| instance = new ImageStreamHandler(); | ||
| } | ||
| return instance; | ||
| } | ||
|
|
||
| private ImageStreamHandler() { |
There was a problem hiding this comment.
Синглетоны это плохо (ухудшают тестируемость), но это можно обсудить.
| private ScriptIntrinsicYuvToRGB script; | ||
| private Allocation in, out; | ||
|
|
||
| private boolean isInit = false; |
| image.close(); // asap | ||
| ImageProcessor.process(new ImageProcessor.Task(nv21, width, height, ReceiverCameraActivity.getReadingStatusHandler())); | ||
| image.close(); // ASAP | ||
| ImageProcessor.getInstance().EXECUTOR.execute(new ImageProcessor.Task(nv21, width, height, ReceiverCameraActivity.getReadingStatusHandler())); |
There was a problem hiding this comment.
Старый способ мне, кстати, больше нравится по стилю. Метод process может сам сделать getInstance().EXECUTOR.execute.
| double resultH = effectiveH; | ||
| double marginX = margin; | ||
| double marginY = margin; | ||
| if (effectiveH / effectiveW > ImageProcessor.RATIO) { |
There was a problem hiding this comment.
Маловероятно, но потенциально может быть деление на ноль (очень маленький экран?). В этом случае лучше что-нибудь специальное сделать, показать ошибку например. Вряд ли такое будет конечно, но проверить, что оба числа положительны, не мешает.
| if (!ImageProcessor.getInstance().isInit) { | ||
| ImageProcessor.getInstance().init(width, height, imageBytes.length); |
There was a problem hiding this comment.
Учитывая, что init() проверяет isInit, и при этом init - synchronized, всё это выглядит как-то не очень надежно. Может быть здесь тоже должна быть синхронизация на мониторе объекта ImageProcessor, а при этом ImageProcessor не должен проверять, что он уже isInit, раз мы тут проверяем?
Кажется, можно сделать более надежный API у ImageProcessor-a, например просто передавать ему в метод yuv420ToBitmap и ширину, и высоту, и байты, и пусть он сам разбирается, надо ли ему делать init; изменяется ли ширина и высота от картинки к картинке. Сейчас, по идее, то, что должно быть приватным делом ImageProcessor-a, проверяет Task - то есть API не очень хорошо абстрагирует внутренности класса.
There was a problem hiding this comment.
Ровно так, как Вы говорите, у меня и было сделано! Но потом я подумал какую-то мысль, а сейчас уже не помню, какую, и переделал... Технически изменение размеров кадра может только затормозить работу, но ломаться от этого, кажется, ничего не должно. С другой стороны, меняться эти размеры не должны, и если они будут меняться - что-то идет не так.
| executor.shutdown(); | ||
| } | ||
| private final int poolSize = 5; // ! needs testing | ||
| public final ExecutorService EXECUTOR = Executors.newFixedThreadPool(poolSize); |
There was a problem hiding this comment.
Обычно заглавными буквами называют то, что "полностью константно" - строки, числа, может быть объекты иммутабельных классов, но не более. Т.е. это не просто константная ссылка, но и состояние объекта не меняется. У ExecutorService же внутри полно изменяемого состояния.
| Log.d("BBW", "width=" + delegate.getWidth() + " height=" + delegate.getHeight()); | ||
| } | ||
|
|
||
| // false for white, true for black |
There was a problem hiding this comment.
Кажется логичным, но не потеряет ли от этого скорость?
There was a problem hiding this comment.
В целом не должно: Enum-значения существуют в единственном экземпляре (это, кстати, один из способов написать синглтон -- объявить Enum с единственным значением INSTANCE), поэтому всякие сравнения Enum-ов это сравнения указателей, а возвращение Enum-a - это возвращение уже готового объекта (то есть, по сути, тоже возвращение указателя без каких-либо аллокаций).
| private final static int prevCenterCheckingEps = 10; | ||
| private final static int prevCenterUnitEps = 3; | ||
|
|
||
| private final static int checkingEps = 15; | ||
| private final static int minUnit = 3; | ||
| private final static int maxUnit = 100; // ! probably too much | ||
|
|
||
| private final static int iSkip = 1; | ||
| private final static int jSkip = 1; |
There was a problem hiding this comment.
Тут, кстати, написать всё заглавными буквами как раз будет по Java-конвенциям.
| new Check(0, 0, true), // center black area | ||
| new Check(1, -1, false), // white area | ||
| new Check(1, 0, false), | ||
| new Check(1, 1, false), | ||
| new Check(-1, -1, false), | ||
| new Check(-1, 0, false), | ||
| new Check(-1, 1, false), | ||
| new Check(0, -1, false), | ||
| new Check(0, 1, false), | ||
| new Check(2, -2, true), // outer black area | ||
| new Check(2, 0, true), | ||
| new Check(2, 2, true), | ||
| new Check(-2, -2, true), | ||
| new Check(-2, 0, true), | ||
| new Check(-2, 2, true), | ||
| new Check(0, -2, true), | ||
| new Check(0, 2, true) |
There was a problem hiding this comment.
Да, точно ENUM нужен для цвета. Сейчас плохо читается.
| for (int i = Math.max(0, pcx - prevCenterCheckingEps); i < Math.min(img.getWidth(), pcx + prevCenterCheckingEps); i += iSkip) { | ||
| for (int j = Math.max(0, pcy - prevCenterCheckingEps); j < Math.min(img.getHeight(), pcy + prevCenterCheckingEps); j += jSkip) { | ||
| for (int unit = Math.max(1, prevCenterUnit - prevCenterUnitEps); unit < prevCenterUnit + prevCenterUnitEps; unit++) { |
There was a problem hiding this comment.
Можно, наверное, ещё для оптимальности начинать с прошлых координат искать поочередно в обе стороны, и делать что-то типа поиска в ширину вместо поиска в глубину (т.е. искать в порядке удаления тройки (i,j,unit) от прошлой тройки), но это может быть не существенно. Если и так работает надежно и быстро, оставляйте как есть. Вообще, +1 за идею переиспользовать прошлые координаты.
| for (double x : values) { | ||
| sum += x; | ||
| } | ||
| return sum / values.length; |
There was a problem hiding this comment.
Если values.length ноль, можно вернуть ноль, а не падать.
| && cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT) != CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_MONO) { | ||
|
|
||
| boolean isFacingBack = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK; | ||
| boolean isNotMono = true; |
There was a problem hiding this comment.
Старайтесь избавляться от not в именах логических переменных. Я понимаю, чего хочется достичь, но not всегда очень увеличивает когнитивную нагрузку в сложных логических выражениях. В какой-то момент, вам понадобится написать isXXX and !isNotMono и это будет тяжело читать. Можно либо использовать антоним (isColor), либо использовать isMono с противоположным значением и добавлять отрицание, где надо. !isMono читается не особо хуже isNotMono, а вот !isNotMono хуже isMono существенно.
| } | ||
|
|
||
| public static boolean roundColorBW(int color) { | ||
| return Color.red(color) + Color.green(color) + Color.blue(color) <= 128 * 3; |
There was a problem hiding this comment.
Видимо на данном этапе не сильно критично, но вообще кажется, что при изменении яркости освещения значения будут очень сильно "плавать" - и эта проверка может быть не очень надежной (белый будет иногда считаться черным, а черный белым). Может, как-то нормировать на среднюю яркость картинки вместо константы 128?
| for (int value : values) { | ||
| sum += value; | ||
| } | ||
| int avgRounded = round(sum / 4.0); |
There was a problem hiding this comment.
Не очень понятно из кода, почему делим на 4. Может, делать на values.length? Если в эту функцию всегда передаются массивы длины 4, то тогда наверное лучше сделать четыре int-аргумента?
| if ((x <= 5 && y <= 5) || (width - x <= 6 && y <= 5) || (width - x <= 6 && height - y <= 6) || (x <= 5 && height - y <= 6)) { | ||
| // simplified version is unreadable | ||
| return false; | ||
| } | ||
| return true; |
There was a problem hiding this comment.
Может будет более читаемо, если обработать случаи отдельно:
if (x <= 5 && y <= 5) return true;
if (x >= width - 6 && y <= 5) return true;
...
return false;
There was a problem hiding this comment.
Вообще, правда, что это выражение эквивалентно
if (5 < x && x < width - 6) return true;
if (5 < y && y < height - 6) return true;
return false;
? Т.е true получаем в "крестообразной" области?
|
|
||
| public static boolean roundColorBW(int color) { | ||
| return Color.red(color) + Color.green(color) + Color.blue(color) <= 128 * 3; | ||
| return Color.red(color) + Color.green(color) + Color.blue(color) <= 200; // ! 128 * 3 sometimes fails |
There was a problem hiding this comment.
О, понятно, я как раз про это только что написал на прошлый коммит. Видимо да, надо учитывать, что обычное наше восприятие яркости, оно, во-первых, скорее логарифмическое чем линейное, а во-вторых очень сильно зависит от яркости окружающей области. Но если просто 200 работает, то ок)
| // for (int x = Math.max(0, pcx - PREV_CENTER_CHECKING_EPS); x < Math.min(img.getWidth(), pcx + PREV_CENTER_CHECKING_EPS); x += xSkip) { | ||
| // for (int y = Math.max(0, pcy - PREV_CENTER_CHECKING_EPS); y < Math.min(img.getHeight(), pcy + PREV_CENTER_CHECKING_EPS); y += ySkip) { | ||
| // for (int unit = Math.max(1, prevCenterUnit - PREV_CENTER_UNIT_EPS); unit < prevCenterUnit + PREV_CENTER_UNIT_EPS; unit++) { | ||
| // if (checkCenter(img, x, y, unit)) { | ||
| // return specifyCenter(img, x, y, unit); | ||
| // } | ||
| // } | ||
| // } |
There was a problem hiding this comment.
Редко необходимо оставлять закомментированный код в репозитории. Если хочется просто иметь возможность посмотреть старые версии кода, то есть история, а если хочется что-то сообщить, то комментарии будут обычно более понятны.
| private static final int maxUnitDifference = 4; | ||
| int magic = 3 + round(Math.max(0, img.getHeight() - 800) / 2000.0 * 8); | ||
|
|
||
| return new LocatorResult(round(average(right, left)), round(average(top, bottom)), |
There was a problem hiding this comment.
А как вы сейчас это отлаживаете / как показывается пользователю, что в данный момент происходит? Сейчас смотрю, как работает обычный сканер QR-кодов на айфоне, и он подсвечивает, где он думает, что находятся опорные точки; может что-нибудь такое же сделать?
| } | ||
| Bitmap bitmap = ImageProcessor.getInstance().yuv420ToBitmap(imageBytes); | ||
| imageBytes = null; // help GC! | ||
| // System.gc(); // ! this might slow everything down |
There was a problem hiding this comment.
Да, обычно gc сам знает, когда ему приходить.. В любом случае, System.gc() это только рекомендация. imageBytes = null -- разумно.
| lock.lock(); | ||
| super.put(e); | ||
| lock.unlock(); |
There was a problem hiding this comment.
Вообще, объявление каждого метода как synchronized, на первый взгляд, должно делать то же самое, что и эти локи.
Должно быть возможно (и совершенно нормально) добавлять synchronized в классах-наследниках (но не удалять имеющийся в предках synchronized - это нарушит принцип подстановки Лисков).
|
|
||
| getInstance().executor.execute(t); | ||
| } | ||
|
|
There was a problem hiding this comment.
Вообще вся логика довольно интересная, но хотелось бы понять и обсудить (не уверен, что всё в ней понимаю).
krasko
left a comment
There was a problem hiding this comment.
С lastResult надо точно хорошо подумать - это первое, что я нашёл, но обычно в многопоточном приложении бывает ещё много всяких скрытых и неочевидных проблем.
| private static LocatorResult[] lastResult; | ||
| // found out that threads actually interfere with each other and break sometimes | ||
|
|
||
| private volatile static LocatorResult[] lastResult; // different threads make this 'null' when unnecessary |
There was a problem hiding this comment.
Ломается это дело скорее всего ниже, в коде:
if (lastResult != null) .... new Point(lastResult[i].x, lastResult[i].y)
Так как код исполняется во многих потоках, то к тому моменту, как вы вычисляете lastResult[i].x, lastResult уже может быть null, даже если раньше он был не-null. Надо быть очень аккуратным с доступом к общим данным из многих потоков.
Проблему можно решить, если сделать так:
LocatorResult[] lastResultLocal = lastResult;
if (lastResultLocal != null) .... new Point(lastResultLocal[i].x, lastResultLocal[i].y)
Локальную переменную никто перезаписывать не будет - если получилось схватить не-null последний результат, то он таковым точно останется. Но здесь есть ещё более тонкие моменты, например: если переменная lastResult не объявлена как volatile, то и это может дальше падать (т.к. не гарантируется, что если 2й поток "увидел", что в lastResult что-то записали, то он увидит результаты работы конструкторов отдельных элементов массива). То, как ведут себя чтения и записи переменных в случае многих потоков не подчиняется ожидаемой логике, при которой события происходят в каком-то порядке в "линейном" времени, там скорее частично упорядоченное множество. Кажется с volatile всё работает, но на всякий случай я бы всё равно вместо volatile брал lock на время извлечения общих данных в lastResultLocal. Взять один lock на кадр - не принесет никаких заметных проблем с перформансом.
| try { | ||
| super.take(); | ||
| } catch (InterruptedException e) { | ||
| // ignored |
There was a problem hiding this comment.
Можно выйти из цикла. Хотя я не знаю, что сейчас ожидается в случае многих потоков - когда один достает, а второй кладёт. Должен ли первый попробовать достать ещё в случае первой неудачи? Сейчас эта логика, видимо, довольно плохо определена.
No description provided.