SASGIS - SAS.Планета
View Issue Details
0001203SAS.Планета[All Projects] Хотелкаpublic06-03-2012 15:1520-04-2020 21:11
Parasite 
 
normaltweakN/A
confirmedopen 
WindowsServer2003
110418 
30xxxx.Vip 
0001203: Сделать lossless склейку в JPG (с использованием libjpeg)
Было бы неплохо(с), если бы САС при сведении в ЖПЕГ когда-нибудь научился юзать широко известную в узких кругах библиотеку libjpeg
http://en.wikipedia.org/wiki/Libjpeg
одной из фич которой является _беспотерьная_ трансформация JPG-изображений (в данном случае - тайлов) при операциях на глобальном полотне. Эту фичу юзает, например, общзеизвестная тулза JPEGtran (по ссылке выше), позволяющая делать сколь угодно многочисленные операции над JPG-контентом без ре-энкодинга на каждой стадии (lossless).
PS: юзаю JpegTran долгое время при сведении из, например, Zoomify. Тулза прекрастно вызывается из кастомных скриптов даже под виндой,
http://jpegclub.org/jpegtran/

НИКАКОЙ деградации\артефактов на результирующем изображении по сравнении с исходным - не было замечено даже при _многотысячном_ вызове jpegtran при вклеивании каждого отдельного тайла (поодиночке, в циклах по Х\Y) в общее полотно.
склейка, скрипты
rar jpegtran.rar (47,135) 13-03-2012 13:30
http://bugtracker.sasgis.org/file_download.php?file_id=695&type=bug
zip jtest.zip (216,519) 23-04-2012 06:16
http://bugtracker.sasgis.org/file_download.php?file_id=751&type=bug
Issue History
06-03-2012 15:15ParasiteNew Issue
06-03-2012 15:18ParasiteAdditional Information Updatedbug_revision_view_page.php?rev_id=2968#r2968
06-03-2012 15:19zedNote Added: 0005866
06-03-2012 15:22zedNote Edited: 0005866bug_revision_view_page.php?bugnote_id=5866#r2970
06-03-2012 15:25zedNote Edited: 0005866bug_revision_view_page.php?bugnote_id=5866#r2971
06-03-2012 15:29vdemidovStatusnew => resolved
06-03-2012 15:29vdemidovResolutionopen => no change required
06-03-2012 15:29vdemidovAssigned To => vdemidov
06-03-2012 15:29vdemidovStatusresolved => closed
06-03-2012 15:40ParasiteNote Added: 0005867
06-03-2012 15:40ParasiteStatusclosed => feedback
06-03-2012 15:40ParasiteResolutionno change required => reopened
06-03-2012 15:41ParasiteNote Edited: 0005867bug_revision_view_page.php?bugnote_id=5867#r2973
07-03-2012 04:34TolikNote Added: 0005880
07-03-2012 04:35TolikStatusfeedback => resolved
07-03-2012 04:35TolikFixed in Version => 120808
07-03-2012 04:35TolikResolutionreopened => fixed
07-03-2012 04:35TolikAssigned Tovdemidov => zed
07-03-2012 04:36TolikNote Edited: 0005880bug_revision_view_page.php?bugnote_id=5880#r2985
07-03-2012 04:38TolikProduct Version.Nightly => 110418
07-03-2012 04:38TolikTarget Version => 120808
07-03-2012 04:39TolikNote Edited: 0005880bug_revision_view_page.php?bugnote_id=5880#r2986
07-03-2012 05:03zedNote Added: 0005882
07-03-2012 05:23TolikNote Added: 0005887
07-03-2012 05:29zedNote Added: 0005888
07-03-2012 07:10TolikAssigned Tozed =>
07-03-2012 07:10TolikStatusresolved => acknowledged
07-03-2012 07:10TolikResolutionfixed => open
07-03-2012 07:10TolikFixed in Version120808 =>
07-03-2012 07:10TolikTarget Version120808 =>
07-03-2012 07:10TolikSummaryЮзание libjpeg при сведении в JPG => Сделать lossless склейку в JPG (с использованием libjpeg)
07-03-2012 07:17vdemidovStatusacknowledged => confirmed
07-03-2012 07:18vdemidovTarget Version => 50xxxx
07-03-2012 08:39ParasiteNote Added: 0005901
07-03-2012 10:25zedNote Added: 0005905
07-03-2012 11:08ParasiteNote Added: 0005909
07-03-2012 11:20zedNote Added: 0005912
07-03-2012 12:30ParasiteNote Added: 0005917
08-03-2012 11:06zedNote Added: 0005953
08-03-2012 11:39ParasiteNote Added: 0005954
13-03-2012 13:30ParasiteFile Added: jpegtran.rar
13-03-2012 13:31ParasiteNote Added: 0006039
22-03-2012 08:27gpsMaxTag Attached: склейка
22-03-2012 08:28gpsMaxTag Attached: скрипты
11-04-2012 16:57zedNote Added: 0006418
11-04-2012 17:47zedNote Edited: 0006418bug_revision_view_page.php?bugnote_id=6418#r3187
20-04-2012 09:46ParasiteNote Added: 0006441
20-04-2012 10:09vdemidovNote Added: 0006442
20-04-2012 11:02ParasiteNote Edited: 0006441bug_revision_view_page.php?bugnote_id=6441#r3195
20-04-2012 14:16zedNote Added: 0006443
20-04-2012 15:34ParasiteNote Added: 0006444
20-04-2012 19:10zedNote Added: 0006445
21-04-2012 06:23ParasiteNote Added: 0006446
23-04-2012 05:02ParasiteNote Added: 0006459
23-04-2012 06:15zedNote Added: 0006460
23-04-2012 06:15zedNote Edited: 0006460bug_revision_view_page.php?bugnote_id=6460#r3197
23-04-2012 06:16zedFile Added: jtest.zip
23-04-2012 06:17zedNote Added: 0006461
23-04-2012 06:19zedNote Edited: 0006461bug_revision_view_page.php?bugnote_id=6461#r3199
23-04-2012 10:21ParasiteNote Added: 0006465
23-04-2012 12:04zedNote Added: 0006466
19-06-2012 03:14ParasiteNote Added: 0007474
19-06-2012 04:49zedNote Added: 0007478
19-06-2012 05:05ParasiteNote Added: 0007479
19-06-2012 05:09vdemidovNote Added: 0007480
19-06-2012 05:23zedNote Added: 0007482
19-06-2012 05:54vdemidovNote Added: 0007489
19-06-2012 06:00zedNote Added: 0007491
19-06-2012 06:08vdemidovNote Added: 0007497
19-06-2012 06:30ParasiteNote Added: 0007499
19-06-2012 07:48Dima2000Note Added: 0007515
19-06-2012 09:42ParasiteNote Added: 0007522
27-11-2013 14:36GlobbusNote Added: 0013342
28-11-2013 06:00zedNote Added: 0013344
12-12-2013 09:48GlobbusNote Added: 0013414
13-10-2015 08:20vdemidovTarget Version50xxxx => 30xxxx.Vip

Notes
(0005866)
zed   
06-03-2012 15:19   
(edited on: 06-03-2012 15:25)
САС и так использует libjpeg между прочим.

(0005867)
Parasite   
06-03-2012 15:40   
(edited on: 06-03-2012 15:41)
Уже?
В составе последней имеющейся у меня сборки вижу ijl15.dll, но не вижу ничего похожего на libjpg.

Сейчас скачаю последнюю ночнушку, гляну. Если токи да - то пардон.

(0005880)
Tolik   
07-03-2012 04:34   
(edited on: 07-03-2012 04:39)
Инфа 100%

(если память не изменяет, это Zed сделал, поэтому перевёл в resolved (zed))

P.S. Кстати, ijl15.dll в релизе и бете нет.

(0005882)
zed   
07-03-2012 05:03   
Использовать-то использует, но не lossless (если вообще возможна безпотерьная склейка жпегов).
(0005887)
Tolik   
07-03-2012 05:23   
Почему ж невозможна? Для такого красивого размерчика 256х256 всё должно отлично клеиться.

Рановато закрыл? Сделаете lossless слейку?
(0005888)
zed   
07-03-2012 05:29   
Ну, если Parasite покажет свои "скрипты" при помощи которых он клеит жпеги (lossless вариант), то можно подумать. Тем более, что сорцы jpegtran'а есть и "ход мыслей" можно попробовать повторить в САСе.
(0005901)
Parasite   
07-03-2012 08:39   
>P.S. Кстати, ijl15.dll в релизе и бете нет.
Ключевая фраза выше "в составе последней _имеющейся у меня_".
Про текущие бетки - см.про нераспаковываемость 7z без необходимости обновлять пакер (и полсистемы зависимостей к нему).

>если Parasite покажет свои "скрипты" при помощи которых он клеит жпеги (lossless вариант)
А скрипт-то причем? Скрипт у меня просто вызывает сторонний жпегтран в фоне столько раз, сколько нужно. Раз - значит раз. Стотыщ-значит стотыщ, благо что работает оно весьма быстро, да и форкается на ура.
Нужны были ключики вызова жпегтрана, или как?
(0005905)
zed   
07-03-2012 10:25   
Нужен алгоритм, чтобы мы пришли от состояния "у нас на винте кучка jpeg" до "у нас на винте lossless снимок". Скажем, на примере склейки 4-х тайлов на 2-ом зуме. И чем подробнее, тем лучше.
(0005909)
Parasite   
07-03-2012 11:08   
Да не вопрос.
Вот на примере последней lossless-работы (http://sasgis.org/forum/viewtopic.php?f=21&t=1905&start=0)

на Питоне. Качает тайлы (многопоточно, с очередью и контролем ерроров, по зумифай-алго), сводит используя пару темповых жпегтранов через свитч друг друга, результат - один жпег максимального уровня.
По результату на ссылке выше - отработало примерно за сутки. Ради лосслесса лично я вполне готов потерпеть.
-----------------------------
import sys
import time, os
import re
import urllib.request, urllib.parse
import optparse
import tempfile, shutil
import subprocess
import queue
import threading

from math import ceil, floor

def main():
    (opts, args) = parser.parse_args()
    if (opts.url is None):
        parser.print_help()
        exit(-1)

    if (opts.out is None):
        print("ERR: The output file '-o' must be given\n")
        parser.print_help()
        exit(-1)
    Dezoomify(opts)

def urlConcat(url1, url2):
    if url1[-1] == '/':
        url1 = url1[0:-1]

    if url2[0] == '/':
        url2 = url2[1:]

    return url1 + '/' + url2

def getFilePath(level, col, row, ext):
    name = str(level) + '-' + str(col) + '-' + str(row) + '.' + ext
    return name

def getUrl(url):
    req_headers = {
        'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13',
        'Referer': 'http://google.com'
        }
    request = urllib.request.Request(url, headers=req_headers)
    opener = urllib.request.build_opener()
    response = opener.open(request)
    code = response.code
    headers = response.headers
    contents = response.read().decode(errors='ignore')
    return code, headers, contents

class Dezoomify():

    def getImage(self, imageDir, outputDestination):

        def newDownloadThread():
            t = threading.Thread(target=downloader)
            t.daemon = True
            t.start()

        def downloader():
            if downloadQueue.empty():
                return
            url, col, row = downloadQueue.get()
            destination = tileName(col, row)
            if self.debug:
                print("\tINF: Downloading tile: " + destination)
            urllib.request.urlretrieve(url, destination)
            joinQueue.put((col, row))
            if not downloadQueue.empty():
                newDownloadThread()

        def tileName(col, row):
            return os.path.join(self.tileDir, str(col) + '_' + str(row) + '.' + self.ext)

        downloadQueue = queue.Queue()
        joinQueue = queue.Queue()
        for col in range(self.xTiles):
            for row in range(self.yTiles):

                if self.nodownload:
                    joinQueue.put((col, row))
                    continue

                tileIndex = self.getTileIndex(self.zoomLevel, col, row)
                tileGroup = tileIndex // self.tileSize

                if self.debug:
                    print(("\tINF: Adding image number (row, col) to queue: " + str(row).rjust(2) + ', ' + str(col).rjust(2) + ': Index: '+ str(tileIndex).rjust(3) + ', Tilegroup: %d'% tileGroup))

                filepath = getFilePath(self.zoomLevel, col, row, self.ext)
                url = imageDir + '/' + 'TileGroup%d' % tileGroup + '/' + filepath
                downloadQueue.put((url, col, row))
        for i in range(self.nthreads):
            newDownloadThread()

        totalTiles = self.xTiles * self.yTiles
        numJoined = 0

        tmpimgs = [None, None]
        for i in range(2):
            fhandle = tempfile.NamedTemporaryFile(suffix='.jpg', delete=False)
            tmpimgs[i] = fhandle.name
            fhandle.close()
            if self.debug:
                print( '\tINF: Created temporary image file: ' + tmpimgs[i] )
        activeTmp = 0

        while numJoined < totalTiles:
            while joinQueue.empty():
                if threading.active_count() == 1 and joinQueue.empty():
                    print("ERR: Tile downloading stopped prematurely!")
                    os.unlink(tmpimgs[0])
                    os.unlink(tmpimgs[1])
                    return
                time.sleep(0.05)

            col, row = joinQueue.get()

            if numJoined == 0:
                cmd = [self.jpegtran, '-copy', 'all', '-crop', '%dx%d+0+0' % (self.width, self.height), '-outfile', tmpimgs[activeTmp], tileName(col, row)]
                subprocess.call(cmd)

            if self.debug:
                print( '\tINF: Adding tile (row ' + str(row) +', col ' + str(col) + ') to the image' )
            cmd = [self.jpegtran, '-copy', 'all', '-drop', '+%d+%d' % (col*self.tileSize, row*self.tileSize), tileName(col, row), '-outfile', tmpimgs[(activeTmp+1)%2], tmpimgs[activeTmp]]
            subprocess.call(cmd)

            activeTmp = (activeTmp+1) % 2
            numJoined += 1

        cmd = [self.jpegtran, '-copy', 'all', '-optimize', '-outfile', outputDestination, tmpimgs[activeTmp]]
        subprocess.call(cmd)

        os.unlink(tmpimgs[0])
        os.unlink(tmpimgs[1])

    def getImageDirectory(self, url):
        try:
            content = urllib.request.urlopen(url).read().decode(errors='ignore')
        except Exception:
            print(("ERR: Specified directory not found. Check the URL.\nException: %s " % sys.exc_info()[1]))
            sys.exit()

        imagePath = None
        m = re.search('zoomifyImagePath=([^\'"&]*)[\'"&]', content)
        if m:
            imagePath = m.group(1)

        if not imagePath:
            m = re.search('ZoomifyCache/[^\'"&.]+\\.\\d+x\\d+', content)
            if m:
                imagePath = m.group(0)

        if not imagePath:
            print("ERR: Source directory not found. Ensure the given URL contains a Zoomify object.")
            sys.exit()
        else:
            print(('INF: Found zoomifyImagePath: %s' % imagePath))

            netloc = urllib.parse.urlparse(imagePath).netloc
            if not netloc:
                parsedURL = urllib.parse.urlparse(url)
                pathParts = parsedURL.path.split('/')
                m = re.search('\.', pathParts[-1])
                if m:
                    del(pathParts[-1])
                path = '/'.join(pathParts)
                url = urllib.parse.urlunparse([ parsedURL.scheme, parsedURL.netloc, path, None, None, None])

                imageDir = urlConcat(url, imagePath)
            else:
                imageDir = imagePath
            if self.debug:
                print(('INF: Found image directory: ' + imageDir))
            return imageDir

    def getMaxZoom(self):
        width = int(ceil(self.maxWidth/float(self.tileSize)))
        height = int(ceil(self.maxHeight/float(self.tileSize)))
        self.levels = []
        while True:
            self.levels.append((width, height))
            if width == 1 and height == 1:
                break
            else:
                width = int(ceil(width/2.0))
                height = int(ceil(height/2.0))
        self.levels.reverse()
    def getProperties(self, imageDir, zoomLevel):
        xmlUrl = imageDir + '/ImageProperties.xml'
        code, headers, content = getUrl(xmlUrl)
        print (xmlUrl)
        m = re.search('WIDTH="(\d+)"', content)
        if m:
            self.maxWidth = int(m.group(1))
        else:
            print('ERR: Width not found in ImageProperties.xml')
            sys.exit()
        m = re.search('HEIGHT="(\d+)"', content)
        if m:
            self.maxHeight = int(m.group(1))
        else:
            print('ERR: Height not found in ImageProperties.xml')
            sys.exit()
        m = re.search('TILESIZE="(\d+)"', content)
        if m:
            self.tileSize = int(m.group(1))
        else:
            print('ERR: Tile size not found in ImageProperties.xml')
            sys.exit()
        self.getMaxZoom()
        self.maxZoom = len(self.levels)
        if not zoomLevel:
            self.zoomLevel = self.maxZoom-1
        else:
            zoomLevel = int(zoomLevel)
            if zoomLevel < self.maxZoom and zoomLevel >= 0:
                self.zoomLevel = zoomLevel
            else:
                self.zoomLevel = self.maxZoom-1
                if self.debug:
                    print(('ERR: the requested zoom level is not available, defaulting to maximum (%d)' % self.zoomLevel ))
        self.width = self.maxWidth / 2**(self.maxZoom - self.zoomLevel - 1)
        self.height = self.maxHeight / 2**(self.maxZoom - self.zoomLevel - 1)
        self.maxxTiles = self.levels[-1][0]
        self.maxyTiles = self.levels[-1][1]
        self.xTiles = self.levels[self.zoomLevel][0]
        self.yTiles = self.levels[self.zoomLevel][1]
        if self.debug:
            print(( '\tMax zoom level: %d (working zoom level: %d)' % (self.maxZoom-1, self.zoomLevel) ))
            print(( '\tWidth (overall): %d (at given zoom level: %d)' % (self.maxWidth, self.width) ))
            print(( '\tHeight (overall): %d (at given zoom level: %d)' % (self.maxHeight, self.height )))
            print(( '\tTile size: %d' % self.tileSize ))
            print(( '\tWidth (in tiles): %d (at given level: %d)' % (self.maxxTiles, self.xTiles) ))
            print(( '\tHeight (in tiles): %d (at given level: %d)' % (self.maxyTiles, self.yTiles) ))
            print(( '\tTotal tiles: %d (to be retreived: %d)' % (self.maxxTiles * self.maxyTiles, self.xTiles * self.yTiles)))

    def getTileIndex(self, level, x, y):
        index = x + y * int(ceil( floor(self.width/pow(2, self.maxZoom - level - 1)) / self.tileSize ) )

        for i in range(1, level+1):
            index += int(ceil( floor(self.width /pow(2, self.maxZoom - i)) / self.tileSize ) ) * \
                     int(ceil( floor(self.height/pow(2, self.maxZoom - i)) / self.tileSize ) )
        return index
    def getUrls(self, opts):
        if not opts.list:
            self.imageDirs = [ opts.url ]
            self.outNames = [ self.out ]
        else:
            listFile = open( opts.url, 'r')
            self.imageDirs = []
            self.outNames = []
            i = 1
            for line in listFile:
                line = line.strip().split(' ', 1)
                if len(line) == 1:
                    root, ext = os.path.splitext(self.out)
                    self.outNames.append(root + '%03d' % i + ext)
                    i += 1
                elif len(line) == 2:
                    m = re.search('\\.' + self.ext + '$', line[1])
                    if not m:
                        line[1] += '.' + self.ext
                   self.outNames.append(os.path.join(os.path.dirname(self.out), line[1]))
                else:
                    continue

                self.imageDirs.append( line[0] )
    def setupDirectory(self, destination):
        if self.store:
            root, ext = os.path.splitext(destination)
            if not os.path.exists(root):
                if self.debug:
                    print(( 'INF: Creating image storage directory: %s' % root))
                os.makedirs(root)
            self.tileDir = root
        else:
            self.tileDir = tempfile.mkdtemp(prefix='dezoomify_')
            if self.debug:
                print(( 'INF: Created temporary image storage directory: %s' % self.tileDir))
    def __init__(self, opts):
        self.debug = opts.debug
        self.tempo = opts.tempo
        self.ext = 'jpg'
        self.store = opts.store
        self.out = opts.out
        self.jpegtran = opts.jpegtran
        self.nodownload = opts.nodownload
        self.nthreads = int(opts.nthreads)
        if self.nodownload:
            self.store = True
        if self.tempo:
            tempfile.tempdir = opts.tempo
        self.getUrls(opts)
        i = 0
        for imageDir in self.imageDirs:
            destination = self.outNames[i]
            if not opts.base:
                imageDir = self.getImageDirectory(imageDir)
            self.getProperties(imageDir, opts.zoomLevel)
            self.setupDirectory(destination)
            self.getImage(imageDir, destination)
            if self.debug:
                print( 'INF: Dezoomifed image created and saved to ' + destination )
            if not self.store:
                shutil.rmtree(self.tileDir)
                if self.debug:
                    print( 'INF: Erased the temporary directory and its contents' )

            i += 1
if __name__ == "__main__":
    try:
        main()
    finally:
        None
==============================
Если коротко:
1. качаем все запланированные тайлы в кучу (отдельными потоками, не мешающими основному).
2. Создаем ЖПЕГ полотно размера равного результирующему изображению (в виде файла).
3. Создаем его копию.
4. Последовательно клеим (юзая жпегтран) каждый тайл в полотно, а результат пихая в копию полотна. По завершении жпегтрана - полотно и копию меняем местами. Повторять пока тайлы не кончатся.
5. Результат (один, самый последний жпег) сохраняем в указанное юзером место. Смываем все темпы. Всё.

PS: работает с любым зумифаем. Надеюсь. :)
(0005912)
zed   
07-03-2012 11:20   
Зачем копия полотна и почему мы их должны менять местами, после вклеивания каждого тайла?
(0005917)
Parasite   
07-03-2012 12:30   
>Зачем копия полотна и почему мы их должны менять местами, после вклеивания каждого тайла?
Потому что standalone-JPEGtran работает как <source> <destination>. Читаем исходное полотно с первого, пишем после вклейки во второе - при следующем вызове <destination> первого должен стать <source> последующего.
(0005953)
zed   
08-03-2012 11:06   
>при следующем вызове <destination> первого должен стать <source> последующего
Да, но имена источника и приёмника могут совпадать.

Проверь, на всякий случай?
В скрипте вместо:
cmd = [self.jpegtran, '-copy', 'all', '-drop', '+%d+%d' % (col*self.tileSize, row*self.tileSize), tileName(col, row), '-outfile', tmpimgs[(activeTmp+1)%2], tmpimgs[activeTmp]]

сделать:
cmd = [self.jpegtran, '-copy', 'all', '-drop', '+%d+%d' % (col*self.tileSize, row*self.tileSize), tileName(col, row), '-outfile', tmpimgs[activeTmp], tmpimgs[activeTmp]]

и закомментить/удалить строчку:
activeTmp = (activeTmp+1) % 2
(0005954)
Parasite   
08-03-2012 11:39   
>и закомментить/удалить строчку:
А на кой это всё вообще? Мы препарируем мой скрипт, или обучаем САС беспотерьке? Если второе - то функционал надо вводить в САС, а не звать сторонний жпегтран в данном случае. Ведь звать его придется ровно столько раз, сколько у нас тайлов на сведение - и распаковывать\запаковывать всё полотно в памяти, особенно если source с destination совпадать будут (придется еще и ждать снятия системных локов с постоянно перезаписываемого файла). В моем случае в зумифае число тайлов весьма небольшое как правило по сравнению с картами, и я зову жпегтран и ворочаю всё полотно исключительно в уме, до кучи и читая\записывая темпы на рам-диск - с зумифаем это пока что прокатывает, памяти пока что хватает, и можно открыть хоть сотню темпов, если в сотню потоков сводить...

В случае же САСа - и звать упаримся, и поворочать на среднестатистическом железе тоже не всегда сумеем. Мы же планируем чтобы работало у всех секретулек тоже, а не только у меня в памяти, так? Этот функционал имхо надо пихать в сводилку САСа, подсмотрев фичу в сорцах жпегтрана (например). Имхо.
(0006039)
Parasite   
13-03-2012 13:31   
>Проверь, на всякий случай?
Приложил в шапку скомпиленный под винду жпегтран.
(0006418)
zed   
11-04-2012 16:57   
(edited on: 11-04-2012 17:47)
Облом. Функции crop и drop работают с жпегами в памяти, соответственно и клеить снимки больших разрешений не выйдет:

>c:\jtest>jpegtran -crop 32000x32000+0+0 tile.jpg crop.jpg
>Insufficient memory (case 4)

Для 32-х битной системы максимальное разрешение снимка будет где-то в районе 24k*24k pix (естественно, при достаточном размере памяти. Если будет нехватка памяти, то и того меньше).

Заставить их работать по другому, не представляется возможным.

Ввиду вышесказанного фича ещё интересна?

(0006441)
Parasite   
20-04-2012 09:46   
(edited on: 20-04-2012 11:02)
>Insufficient memory (case 4)
>Для 32-х битной системы

Ну и нормально. 24Кх24К жпега с лослессом все же лучше чем 24Кх24К без оного - и будет достаточно для 95% задач (все равно ТАКОЙ жпег потом открывать будет проблематично, даже если и создастся), а кому надо действительно БОЛЬШИЕ losless полотна - будет подождать решения 295-го тикета вместе со мной. :)

(0006442)
vdemidov   
20-04-2012 10:09   
Кстати. Сейчас 295-й тикет сможет реализовать почти любой чайник где-то за пару часов. Там самое сложно нарисовать формочку выбора параметров склейки.
(0006443)
zed   
20-04-2012 14:16   
Хм, хоть это и склейка по сути, но фактически получается что мы экспортируем jpeg тайлы в файл, на подобии экспорта в zip и проч. Поэтому даже не знаю как это лучше сделать в сасе.
Выходит, что нужно делать класс-наследник от TThreadExportAbstract, а не от TThreadMapCombineBase. И соответственно, на страничке Экспорт, появится пункт JPEG (Lossless). При этом будут недоступны опции создания привязок и разбиения на части. Полная чехорда выходит.
(0006444)
Parasite   
20-04-2012 15:34   
>Сейчас 295-й тикет сможет реализовать почти любой чайник
Подождем, пока он туда придет и реализует. :)

>Хм, хоть это и склейка по сути, но фактически получается что мы экспортируем jpeg тайлы в файл, на подобии экспорта в zip и проч.
Предлагаю на время отладки просто опцию в инишнике типа "JPEGlossless=true", при наличии которой - выводить ЛЮБОЙ сводимый сасом жпег через жпегтран. При отсутствии, соответственно - как обычно.
Потом, если все срастется и всем понравится - вывод через жпегтран можно оставить единственным. Я думаю - никто же против более качественных жпегов не будет? :)
(0006445)
zed   
20-04-2012 19:10   
Так-с, с безпотерьной склейкой разобрался. Кода оказалось с гулькин нос, правда обнаружилось, что libjpeg-turbo не в полной мере поддерживает losslless, так что пришлось заюзать официальный libjpeg. В итоге, добавятся 2 dll-ки: jpeg8.dll (libjpeg, 190k) и transupp.dll (API для losslless трансформаций, 20k). Осталось набраться мужества и прикрутить это всё к сасу.

>выводить ЛЮБОЙ сводимый сасом жпег через жпегтран
Ага, разогнался. Без потерь можно сводить жпеги с очень большими оговорками:
- тайлы в кэше должны быть в jpeg формате
- наложение слоёв/меток невозможно
- цветокоррекция недоступна
- операция ресурсоёмкая и отжирает много ОЗУ
Так что - только опционально, по галочке. И по дефолту галочка будет снята.
(0006446)
Parasite   
21-04-2012 06:23   
>- тайлы в кэше должны быть в jpeg формате
Ну разумеется. Lossless такой lossless, что если изначально не жпег - без репака не обойтись.

>- наложение слоёв/меток невозможно
>- цветокоррекция недоступна
Кстати, да. Слона-то я и не приметил...в САСе оно вроде есть, но я ни разу не пользовался - потому и не обращал внимания на эти пункты.

>- операция ресурсоёмкая и отжирает много ОЗУ
Угу. В основном из-за ворочанья глобального полотна в памяти * число вклеек тайлов.

>Так что - только опционально, по галочке. И по дефолту галочка будет снята.
Как скажешь, начальнег. Не вопрос.
(0006459)
Parasite   
23-04-2012 05:02   
Меня, собственно, беспокоит вопрос быстродействия этого всего на относительно больших склеиваемых листах.
Так как [стандартные] алгоритмы ворочают все полотно в памяти, а при выходе - пишут на диск (в жпег разумеется), то время всей склейки будет (постоянное подчитывание глобального растра+растягивание в памяти+вклейка+запись на диск) * число_тайлов_для_склейки. При использовании стандартных алгоритмов и стандартного же жпегтрана (того что в шапке) - весь процесс весьма и весьма печален даже при наличии 32Гб памяти и рамдиска. Например вот это изображение http://sasgis.org/forum/viewtopic.php?f=21&t=1905 из тайлов вышеуказанный скрипт+жпегтран сводили около суток непрерывной работы (правда включая и выкачку тайлов с интернетов, ну да там всего 105Мб...).

Если САС будет точно так же сводить сутками - то смысл фичи теряется к сожалению, с тем же успехом оно все сводится и сторонними скриптами еще вчера. То есть, придется таки переработать сам жпегтран хотя бы для того, чтобы он постоянно не читал\писал весь растр при обработке каждого тайла - а держал в памяти пока вся задача не будет выполнена и все тайлы не вклеятся...
Zed, ты уже проводил тесты быстродействия в связке с САСом?
(0006460)
zed   
23-04-2012 06:15   
Чисто синтетически: время склейки 22k*22k = ~ 7 часов (~ 3 сек. на тайл).
Убийственно медленно, по сравнению с обычной склейкой.

Ща приаттачу тестовую утиль, сможешь оценить на своей системе.

(0006461)
zed   
23-04-2012 06:17   
(edited on: 23-04-2012 06:19)
В аттаче, кстати, и сорцы есть, на случай если кто решит поучаствовать.

(0006465)
Parasite   
23-04-2012 10:21   
>время склейки 22k*22k = ~ 7 часов (~ 3 сек. на тайл)
Охх, щщи....... :((( Не думал, что всё будет настолько печально....

Есть ли возможность изменить логику работы всего алгоритма - чтобы он не переписывал все полотно после к.тайла, а держал бы в памяти до окончания вклейки всех тайлов - и только тогда уже сливал в файл? Имхо это единственное, что глобально повлияет на быстродействие. Все же жпегтран сам по себе - коммандлайновая утиль и ей сам Аллах велел работать читать\отрабатывать\записывать\отваливаться, а у нас-то тут практически "потоковая вклейка от обеда и до пока_кэш_не_кончится"...:)

Если возможности поднять быстродействие не найдется - имхо, хотелка становится бесполезной и ее можно будет закрыть. Ведь за то же время оно отрабатывается и простым батником с кучей натравлений штатного жпегтрана на имеющийся кэш, без потерь человеко\часов на САСовый кодинг...

>Ща приаттачу тестовую утиль
Вечерком поюзаю. Отпишусь.
(0006466)
zed   
23-04-2012 12:04   
>Есть ли возможность изменить логику работы всего алгоритма
Есть пара мыслей. Попробую.
(0007474)
Parasite   
19-06-2012 03:14   
>>Есть ли возможность изменить логику работы всего алгоритма
>Есть пара мыслей. Попробую.
Есть ли новости?
(0007478)
zed   
19-06-2012 04:49   
Нет. Видимо быстрее не получится.
(0007479)
Parasite   
19-06-2012 05:05   
>Нет. Видимо быстрее не получится.
Та не к спеху, собссно. Не тороплю.
Просто roadmap дай, хоть приблизительно? Месяц - знач месяц, три - знач три...
(0007480)
vdemidov   
19-06-2012 05:09   
Попробуй клеить сначала полоски с размерами кратными 8 пикселам в отдельный файл. А потом уже собирать эти полоски в большой файл. То есть, склеил полоску - добавил в результирующий файл и тд.
Должно быть гораздо быстрее за счет отсутствия рендомных записей на винчестер.
(0007482)
zed   
19-06-2012 05:23   
>roadmap дай
В том виде, как оно есть сейчас, сделать склейку можно за день максимум. Но не оптимизированный вариант, я так понимаю, нафиг кому нужен.

>Должно быть гораздо быстрее за счет отсутствия рендомных записей на винчестер
Там не из-за винта тормоза, а из-за необходимости распаковки/запаковки жпега (служебных данных) для записи/чтени каждого тайла. К слову, если загрузить жпег целиком в память (исключив винт, как возможное узкое место) и делать из него безпотерьный crop, то всё происходит так же медленно и печально.
(0007489)
vdemidov   
19-06-2012 05:54   
Ну если в памяти все так же медленно и печально, то можно закрывать.
>22k*22k = ~ 7 часов (~ 3 сек. на тайл).
Это явный перебор.
(0007491)
zed   
19-06-2012 06:00   
Перебор, но с другой стороны, фича полезная и найдёт своих маньяков. А в TODO записать: Оптимизировать losslless, и год поставить - 2020 :)
(0007497)
vdemidov   
19-06-2012 06:08   
Ну, как хотите :) Для меня фитча бесполезная. Я склейку использую, только если мне нужно кусок карты распечатать и отдать знакомым.
(0007499)
Parasite   
19-06-2012 06:30   
А что если просто вклеивать body от каждого тайла напрямик в результирующее большое полотно, и потом к нему генерить хидер? То есть, вообще ничего никуда не перепаковывать, а обойтись сугубо файловыми операциями?

Насколько я помню, боди от жпега вполне себе самостоятельно и может быть распаковано даже со сторонним хидером (более того - некоторые карторесурсы отдают вообще только боди, а хидер там одинаков для всех и приклеивается "на ходу" уже на стороне клиента). Грубо говоря, взять два смежных jpeg-тайла, оторвать у обоих хидеры, два боди вклеить в результирующий файл, и приписать ему зановосгенеренный хидер уже согласно новой картинки. По идее - должно открыться как родное, и будет вполне себе lossless....
(0007515)
Dima2000   
19-06-2012 07:48   
Я хотел такой способ предложить ещё с самого начала, не перепаковывать тайлы и вообще не использовать библиотеки работы с jpeg, а юзать чисто пересылки бинарных данных. Но посмотрев внимательнее на формат jpeg-а, образумился: очень похоже что какие-то таблицы, жизненно важные для распаковки, сидят в хидере и в общем случае могут быть разными для разных файлов. Конкретно, кажется таблица квантования такова. Когда jpeg файлы создаём сами, то всегда можем выбрать одинаковую таблицу для всех файлов, но вот если файлы получены из сторонних источников, это может и не соблюдаться. А тогда лишь перепаковка.
Хотя, никто не мешает один раз выбрать таблицу для всего большого jpeg-а и потом перепаковывать тайлы по ней, дозаписывая в результирующий файл уже готовое body (но перепакованое!). От перепаковки не уйти, lossless не будет, но хотя бы памяти не надо будет очень много и винту легче. А, немного не прав, если выходной формат lossless, то будет вполне себе безпотерьно, лишь от распаковки тайлов не уйти.
(0007522)
Parasite   
19-06-2012 09:42   
>Конкретно, кажется таблица квантования такова. Когда jpeg файлы создаём сами, то всегда можем выбрать одинаковую таблицу для всех файлов, но вот если файлы получены из сторонних источников, это может и не соблюдаться. А тогда лишь перепаковка.
Угу. Но, как правило, картосервисы в пределах себя любимых таким не балуются (да и на тайлы обычно нарезается автоматом - с одинаковыми настройками к каждому тайлу) - и все тайлы отличаются только боди, а остальное стандартно. Как правило - но возможно есть и исключения. Теоретически можно в процессе склейки проверять каждый тайл на это дело, и по нахождении расхождений - либо еррорить, либо переквантовать этот злобный тайл к общей картинке и далее вклеивать как обычно.

Либо же перепахивать сорцы жпегтрана, отучая его перепаковывать все полотно после к.тайла - а делать это только раз, в финале...

Либо же вообще забить на этот тикет и сосредоточиться на заведомо более вкусном lossless\limitless сведении в RAW, предоставив хомякам потом делать с ним что угодно но уже своими силами. Но там свои тараканы (например куча дисковых операций + место на винте будет весьма сильно уходить, со жпегом ни в какие ворота не сравнится).
(0013342)
Globbus   
27-11-2013 14:36   
Тема заглохла? Уж больно печально, когда 9 кБ тайл z19 в склейке раздувается в 30 кБайт при худшем визуальном качестве (даже на 100%). Думаю, даже многочасовая обработка не являлась бы помехой - можно запускать на ночь.
(0013344)
zed   
28-11-2013 06:00   
Ну, вы как бы первый, кто проявил заинтересованность в фиче, не смотря на её недостатки.
(0013414)
Globbus   
12-12-2013 09:48   
Думаю ситуация здесь, как и в ряде других случаев в жизни - "а парни-то и не знают..."

Не знают о багтрекере, не знают lossless-операциях с jpeg.